commit d84747fae5b1d0479b9658a38744530e99e257d8
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Fri, 10 Mar 2023 16:34:16 +0800
go-toast: initial commit
This setup can succesfully use the MSVC compiled DLL
to generate toast notifications on Windows.
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Diffstat:
10 files changed, 972 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+*.exe
+\ No newline at end of file
diff --git a/README.md b/README.md
@@ -0,0 +1,8 @@
+# Build
+
+`env CC="gcc" go build .`
+
+## Pre-requisites
+
+- `mingw-w64-gcc` to build the Go code
+- MSVC build tools to recompile the .dll
diff --git a/gen.go b/gen.go
@@ -0,0 +1,2 @@
+//go:generate gosh ./internal/c-lib/build.sh
+package main
diff --git a/go.mod b/go.mod
@@ -0,0 +1,8 @@
+module toast
+
+go 1.20
+
+require (
+ github.com/go-ole/go-ole v1.2.6
+ golang.org/x/sys v0.6.0
+)
diff --git a/go.sum b/go.sum
@@ -0,0 +1,5 @@
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/internal/c-lib/Makefile b/internal/c-lib/Makefile
@@ -0,0 +1,14 @@
+.PHONY: clean
+.PHONY: gosh
+
+toast.dll: toast.c
+ gosh build.sh
+
+gosh:
+ # Grab the gosh shell interpreter. Requires a Go toolchain.
+ go install github.com/mvdan/sh/cmd/gosh@latest
+
+clean:
+ rm *.exe ; rm *.dll ; rm *.exp ; rm *.lib ; rm *.obj
+
+
diff --git a/internal/c-lib/build.sh b/internal/c-lib/build.sh
@@ -0,0 +1,79 @@
+# This build script captures the method used to build the MSVC compiled DLL.
+#
+# Make sure you have installed the Widows C++ build tools using the Visual
+# Studio Installer (https://visualstudio.microsoft.com/downloads). Activate
+# the "Desktop development with C++" component and check "MSVC C++ build tools".
+#
+# The latest version at the time of this script (10/03/2023) is MSVC v143.
+#
+# Finally, make sure you install the Windows SDK. This script is specialized
+# to version 10.0.22000.0, however that is probably an unnecessary constraint.
+#
+# After installation make sure you add the MSVC binaries to your system path.
+# In this case, that path is:
+#
+# "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x64"
+#
+# Keep in mind the exact path will be determined by which version of the build
+# you have installed.
+#
+# On Windows, this script can be invoked via https://github.com/mvdan/sh.
+#
+# go install github.com/mvdan/sh/cmd/gosh@latest && gosh build.sh
+#
+# I'm not an expert in Windows build systems by any means, so there might be
+# more correct ways to do things in this script.
+#
+# This script could probably be parameterized by SDK version, OS version,
+# build tools version and CPU architecture.
+#
+# It may also be possible to extract the .lib files and bundle them directly
+# to avoid differences between versions of the various components.
+
+# Host Details
+DRIVE="C:"
+WINDOWS_VERSION="10"
+SDK_VERSION="10.0.22000.0"
+BUILD_TOOLS_VERSION="14.35.32215"
+ARCH="x64"
+
+# Windows SDK
+SDK_ROOT="$DRIVE\\Program Files (x86)\\Windows Kits\\$WINDOWS_VERSION"
+SDK_LIB="$SDK_ROOT\\Lib\\$SDK_VERSION"
+SDK_INCLUDE="$SDK_ROOT\\Include\\$SDK_VERSION"
+
+# MSVC Build Tools
+BUILD_TOOLS_ROOT="$DRIVE\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\$BUILD_TOOLS_VERSION"
+BUILD_TOOLS_LIB="$BUILD_TOOLS_ROOT\\lib"
+BUILD_TOOLS_INCLUDE="$BUILD_TOOLS_ROOT\\include"
+
+# Make this script invocable from parent directories by resolving
+# the source against the script path.
+SOURCE="$0/../toast.c"
+
+# `cl` must be available on system path.
+cl.exe \
+ /I "$SDK_INCLUDE\\um" \
+ /I "$SDK_INCLUDE\\ucrt" \
+ /I "$SDK_INCLUDE\\winrt" \
+ /I "$SDK_INCLUDE\\shared" \
+ /I "$BUILD_TOOLS_INCLUDE" \
+ /LD $SOURCE \
+ /link \
+ /LIBPATH "$SDK_LIB\\um\\$ARCH\\Uuid.Lib" \
+ /LIBPATH "$SDK_LIB\\um\\$ARCH\\Ole32.lib" \
+ /LIBPATH "$SDK_LIB\\um\\$ARCH\\oleaut32.lib" \
+ /LIBPATH "$SDK_LIB\\um\\$ARCH\\Advapi32.lib" \
+ /LIBPATH "$SDK_LIB\\um\\$ARCH\\User32.lib" \
+ /LIBPATH "$SDK_LIB\\um\\$ARCH\\kernel32.lib" \
+ /LIBPATH "$SDK_LIB\\um\\$ARCH\\runtimeobject.lib" \
+ /LIBPATH "$SDK_LIB\\ucrt\\$ARCH\\libucrt.lib" \
+ /LIBPATH "$BUILD_TOOLS_LIB\\$ARCH\\libcmt.lib" \
+ /LIBPATH "$BUILD_TOOLS_LIB\\$ARCH\\oldnames.lib" \
+ /LIBPATH "$BUILD_TOOLS_LIB\\$ARCH\\libvcruntime.lib"
+
+# These artefacts are noisy and not needed for our purposes; so we
+# can clean them up.
+rm toast.exp
+rm toast.lib
+rm toast.obj
+\ No newline at end of file
diff --git a/internal/c-lib/toast.c b/internal/c-lib/toast.c
@@ -0,0 +1,816 @@
+#include <Windows.h>
+#include <Windows.ui.notifications.h>
+#include <notificationactivationcallback.h>
+#include <initguid.h>
+#include <roapi.h>
+#include <tchar.h>
+#include <stdio.h>
+#include <string.h>
+
+#pragma comment(lib, "runtimeobject.lib")
+#pragma comment(lib, "ole32.lib")
+#pragma comment(lib, "oleaut32.lib")
+#pragma comment(lib, "Advapi32.lib")
+#pragma comment(lib, "User32.lib")
+
+DWORD dwMainThreadId = 0;
+
+/*
+ * Our AUMID and argument that tells our app when it was launched by interacting with a toast notification.
+ */
+#define APP_ID L"ToastActivatorPureC"
+#define TOAST_ACTIVATED_LAUNCH_ARG "-ToastActivated"
+
+/*
+ * The GUID that we associate with our factory that produces our INotificationActivationCallback interface.
+ */
+#define GUID_Impl_INotificationActivationCallback_Textual "0F82E845-CB89-4039-BDBF-67CA33254C76"
+DEFINE_GUID(GUID_Impl_INotificationActivationCallback,
+ 0xf82e845, 0xcb89, 0x4039, 0xbd, 0xbf, 0x67, 0xca, 0x33, 0x25, 0x4c, 0x76);
+
+/*
+ * The XML that describes the notification that will be shown. Of course, this can be built at runtime,
+ * and more can be done with it, but for this basic example, this will suffice.
+ */
+const wchar_t wszBannerText[] =
+L"<toast scenario=\"reminder\" "
+L"activationType=\"foreground\" launch=\"action=mainContent\" duration=\"short\">\r\n"
+L" <visual>\r\n"
+L" <binding template=\"ToastGeneric\">\r\n"
+L" <text><![CDATA[This is a demo notification]]></text>\r\n"
+L" <text><![CDATA[It contains 2 lines of text]]></text>\r\n"
+L" </binding>\r\n"
+L" </visual>\r\n"
+L" <actions>\r\n"
+L" <input id=\"tbReply\" type=\"text\" placeHolderContent=\"Send a message to the app\"/>\r\n"
+L" <action content=\"Send\" activationType=\"foreground\" arguments=\"action=reply\"/>\r\n"
+L" <action content=\"Close app\" activationType=\"foreground\" arguments=\"action=closeApp\"/>\r\n"
+L" </actions>\r\n"
+L" <audio src=\"ms-winsoundevent:Notification.Default\" loop=\"false\" silent=\"false\"/>\r\n"
+L"</toast>\r\n";
+
+/*
+ * IIDs of other interfaces we use throughout this example.
+ */
+DEFINE_GUID(IID_IToastNotificationManagerStatics,
+ 0x50ac103f, 0xd235, 0x4598, 0xbb, 0xef, 0x98, 0xfe, 0x4d, 0x1a, 0x3a, 0xd4);
+
+DEFINE_GUID(IID_IToastNotificationFactory,
+ 0x04124b20, 0x82c6, 0x4229, 0xb1, 0x09, 0xfd, 0x9e, 0xd4, 0x66, 0x2b, 0x53);
+
+DEFINE_GUID(IID_IXmlDocument,
+ 0xf7f3a506, 0x1e87, 0x42d6, 0xbc, 0xfb, 0xb8, 0xc8, 0x09, 0xfa, 0x54, 0x94);
+
+DEFINE_GUID(IID_IXmlDocumentIO,
+ 0x6cd0e74e, 0xee65, 0x4489, 0x9e, 0xbf, 0xca, 0x43, 0xe8, 0x7b, 0xa6, 0x37);
+
+/*
+ * All the objects we allocate in this example (our class factory and our INotificationActivationCallback
+ * implementation) have this memory layout, and all inherit from IUnknown.
+ */
+typedef struct Impl_IGeneric
+{
+ IUnknownVtbl* lpVtbl;
+ LONG64 dwRefCount;
+} Impl_IGeneric;
+
+static ULONG STDMETHODCALLTYPE Impl_IGeneric_AddRef(Impl_IGeneric* _this)
+{
+ return InterlockedIncrement64(&(_this->dwRefCount));
+}
+
+static ULONG STDMETHODCALLTYPE Impl_IGeneric_Release(Impl_IGeneric* _this)
+{
+ LONG64 dwNewRefCount = InterlockedDecrement64(&(_this->dwRefCount));
+ if (!dwNewRefCount) free(_this);
+ return dwNewRefCount;
+}
+
+/*
+ * Our INotificationActivationCallback implementation.
+ */
+static
+HRESULT
+STDMETHODCALLTYPE
+Impl_INotificationActivationCallback_QueryInterface(
+ Impl_IGeneric* _this,
+ REFIID riid,
+ void** ppvObject
+) {
+ if (!IsEqualIID(riid, &IID_INotificationActivationCallback) && !IsEqualIID(riid, &IID_IUnknown))
+ {
+ *ppvObject = NULL;
+ return E_NOINTERFACE;
+ }
+ *ppvObject = _this;
+ _this->lpVtbl->AddRef(_this);
+ return S_OK;
+}
+
+/*
+ * This is where the magic happens when someone interacts with our notification: this method will be called
+ * (on another thread !!!).
+ */
+static
+HRESULT
+STDMETHODCALLTYPE
+Impl_INotificationActivationCallback_Activate(
+ INotificationActivationCallback* _this,
+ LPCWSTR appUserModelId,
+ LPCWSTR invokedArgs,
+ const NOTIFICATION_USER_INPUT_DATA* data,
+ ULONG count
+) {
+ wprintf(L"Interacted with notification from AUMID \"%s\" with arguments: \"%s\". User input count: %d.\n", appUserModelId, invokedArgs, count);
+ if (!_wcsicmp(invokedArgs, L"action=closeApp"))
+ {
+ PostThreadMessageW(dwMainThreadId, WM_QUIT, 0, 0);
+ }
+ else if (!_wcsicmp(invokedArgs, L"action=reply"))
+ {
+ for (unsigned int i = 0; i < count; ++i)
+ {
+ if (!_wcsicmp(data[i].Key, L"tbReply"))
+ {
+ wprintf(L"Reply was \"%s\".\n", data[i].Value);
+ }
+ }
+ }
+ return S_OK;
+}
+
+static const INotificationActivationCallbackVtbl Impl_INotificationActivationCallback_Vtbl = {
+ .QueryInterface = Impl_INotificationActivationCallback_QueryInterface,
+ .AddRef = Impl_IGeneric_AddRef,
+ .Release = Impl_IGeneric_Release,
+ .Activate = Impl_INotificationActivationCallback_Activate
+};
+
+/*
+ * Our IClassFactory implementation.
+ */
+static
+HRESULT
+STDMETHODCALLTYPE
+Impl_IClassFactory_QueryInterface(
+ Impl_IGeneric* _this,
+ REFIID riid,
+ void** ppvObject
+) {
+ if (!IsEqualIID(riid, &IID_IClassFactory) && !IsEqualIID(riid, &IID_IUnknown))
+ {
+ *ppvObject = NULL;
+ return E_NOINTERFACE;
+ }
+ *ppvObject = _this;
+ _this->lpVtbl->AddRef(_this);
+ return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE Impl_IClassFactory_LockServer(IClassFactory* _this, BOOL flock)
+{
+ return S_OK;
+}
+
+static
+HRESULT
+STDMETHODCALLTYPE
+Impl_IClassFactory_CreateInstance(
+ IClassFactory* _this,
+ IUnknown* punkOuter,
+ REFIID vTableGuid,
+ void** ppv
+) {
+ HRESULT hr = E_NOINTERFACE;
+ Impl_IGeneric* thisobj = NULL;
+ *ppv = 0;
+
+ if (punkOuter) hr = CLASS_E_NOAGGREGATION;
+ else
+ {
+ BOOL bOk = FALSE;
+ if (!(thisobj = malloc(sizeof(Impl_IGeneric)))) hr = E_OUTOFMEMORY;
+ else
+ {
+ thisobj->lpVtbl = &Impl_INotificationActivationCallback_Vtbl;
+ bOk = TRUE;
+ }
+ if (bOk)
+ {
+ thisobj->dwRefCount = 1;
+ hr = thisobj->lpVtbl->QueryInterface(thisobj, vTableGuid, ppv);
+ thisobj->lpVtbl->Release(thisobj);
+ }
+ else
+ {
+ return hr;
+ }
+ }
+
+ return hr;
+}
+
+static const IClassFactoryVtbl Impl_IClassFactory_Vtbl = {
+ .QueryInterface = Impl_IClassFactory_QueryInterface,
+ .AddRef = Impl_IGeneric_AddRef,
+ .Release = Impl_IGeneric_Release,
+ .LockServer = Impl_IClassFactory_LockServer,
+ .CreateInstance = Impl_IClassFactory_CreateInstance
+};
+
+__declspec(dllexport)
+void
+GenerateToast()
+{
+ HRESULT hr = S_OK;
+ Impl_IGeneric* pClassFactory = NULL;
+ BOOL bOk = FALSE;
+ dwMainThreadId = GetCurrentThreadId();
+ BOOL bInvokedFromToast = FALSE;
+
+ /*
+ * Initialize COM and Windows Runtime on this thread. Make sure that the threading models of the two match.
+ */
+ if (SUCCEEDED(hr))
+ {
+ hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
+ } else {
+ printf("failed to");
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ hr = RoInitialize(RO_INIT_MULTITHREADED);
+ } else {
+ printf("failed to roinitialize\n");
+ }
+
+ /*
+ * Allocate class factory. This factory produces our implementation of the INotificationActivationCallback interface.
+ * This interface has an ::Activate member method that gets called when someone interacts with the toast notification.
+ */
+ if (SUCCEEDED(hr))
+ {
+ if (!(pClassFactory = malloc(sizeof(Impl_IGeneric)))) hr = E_OUTOFMEMORY;
+ else
+ {
+ pClassFactory->lpVtbl = &Impl_IClassFactory_Vtbl;
+ pClassFactory->dwRefCount = 1;
+ }
+ } else {
+ printf("failed to allocate our IGeneric class factory\n");
+ }
+
+ /*
+ * Instead of having to register our COM class in the registry beforehand, we opt to registering it at runtime;
+ * we associate our GUID with the class factory that provides our INotificationActivationCallback interface.
+ */
+ DWORD dwCookie = 0;
+ if (SUCCEEDED(hr))
+ {
+ hr = CoRegisterClassObject(&GUID_Impl_INotificationActivationCallback, pClassFactory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dwCookie);
+ } else {
+ printf("failed to register notification activation callback\n");
+ }
+
+ /*
+ * Construct the path to our EXE that will be used to launch it when something requests our interface.
+ * As said above, registration is dynamic - as long as this app runs, COM knows about the fact that this
+ * app implements our INotificationActivationCallback interface. The info here is used when this app has
+ * closed and someone clicks the toast notification for example; in that case, since our app is not
+ * running, thus CoRegisterClassObject was not called, COM needs info on what EXE contains the implementation
+ * of the interface, and we specify that here; without setting this, clicking on notifications will do nothing
+ */
+ wchar_t wszExePath[MAX_PATH + 100];
+ ZeroMemory(wszExePath, MAX_PATH + 100);
+ if (SUCCEEDED(hr))
+ {
+ hr = (GetModuleFileNameW(NULL, wszExePath + 1, MAX_PATH) != 0 ? S_OK : E_FAIL);
+ } else {
+ printf("failed to get module file\n");
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ wszExePath[0] = L'"';
+ wcscat_s(wszExePath, MAX_PATH + 100, L"\" " _T(TOAST_ACTIVATED_LAUNCH_ARG));
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ hr = HRESULT_FROM_WIN32(RegSetValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\CLSID\\{" _T(GUID_Impl_INotificationActivationCallback_Textual) L"}\\LocalServer32", REG_SZ, wszExePath, wcslen(wszExePath) + 1));
+ } else {
+ printf("failed to set registery value CLSID\n");
+ }
+
+ /*
+ * Here we set some info about our app and associate our AUMID with the GUID from above
+ * (the one that is associated with our class factory which produces our INotificationActivationCallback interface)
+ */
+ if (SUCCEEDED(hr))
+ {
+ hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\AppUserModelId\\" APP_ID, L"DisplayName", REG_SZ, L"Toast Activator Pure C Example", 31 * sizeof(wchar_t)));
+ } else {
+ printf("failed to set registery value AppUserModelId DisplayName\n");
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\AppUserModelId\\" APP_ID, L"IconBackgroundColor", REG_SZ, L"FF00FF00", 9 * sizeof(wchar_t)));
+ } else {
+ printf("failed to set registery value AppUserModelId IconBackgroundColor\n");
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\AppUserModelId\\" APP_ID, L"CustomActivator", REG_SZ, L"{" _T(GUID_Impl_INotificationActivationCallback_Textual) L"}", 39 * sizeof(wchar_t)));
+ } else {
+ printf("failed to set registery value AppUserModelId CustomActivator\n");
+ }
+
+ /*
+ * We will display a notification only when this app is launched standalone (not by interacting with a notification)
+ */
+ HSTRING_HEADER hshAppId;
+ HSTRING hsAppId = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(APP_ID, wcslen(APP_ID), &hshAppId, &hsAppId);
+ } else {
+ printf("failed to create string reference for app id\n");
+ }
+
+ HSTRING_HEADER hshToastNotificationManager;
+ HSTRING hsToastNotificationManager = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(
+ RuntimeClass_Windows_UI_Notifications_ToastNotificationManager,
+ (UINT32)wcslen(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager),
+ &hshToastNotificationManager, &hsToastNotificationManager
+ );
+ } else {
+ printf("failed to create string reference for ToastNotificationManager runtime class: %x\n", hr);
+ }
+
+ __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics* pToastNotificationManager = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = RoGetActivationFactory(hsToastNotificationManager, &IID_IToastNotificationManagerStatics, (LPVOID*)&pToastNotificationManager);
+ } else {
+ printf("failed to RoGetActivcationFactory for ToastNotificationManagerStatics\n");
+ }
+
+ __x_ABI_CWindows_CUI_CNotifications_CIToastNotifier* pToastNotifier = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pToastNotificationManager->lpVtbl->CreateToastNotifierWithId(pToastNotificationManager, hsAppId, &pToastNotifier);
+ } else {
+ printf("failed to CreateToastNotifierWithId\n");
+ }
+
+ HSTRING_HEADER hshToastNotification;
+ HSTRING hsToastNotification = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification, (UINT32)wcslen(RuntimeClass_Windows_UI_Notifications_ToastNotification), &hshToastNotification, &hsToastNotification);
+ } else {
+ printf("failed to create string reference for ToastNotification\n");
+ }
+
+ __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationFactory* pNotificationFactory = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = RoGetActivationFactory(hsToastNotification, &IID_IToastNotificationFactory, (LPVOID*)&pNotificationFactory);
+ } else {
+ printf("failed to RoGetActivationFactory ToastNotification\n");
+ }
+
+ HSTRING_HEADER hshXmlDocument;
+ HSTRING hsXmlDocument = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument, (UINT32)wcslen(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument), &hshXmlDocument, &hsXmlDocument);
+ }
+
+ HSTRING_HEADER hshBanner;
+ HSTRING hsBanner = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(wszBannerText, (UINT32)wcslen(wszBannerText), &hshBanner, &hsBanner);
+ }
+
+ IInspectable* pInspectable = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = RoActivateInstance(hsXmlDocument, &pInspectable);
+ }
+
+ __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocument* pXmlDocument = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pInspectable->lpVtbl->QueryInterface(pInspectable, &IID_IXmlDocument, &pXmlDocument);
+ }
+
+ __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocumentIO* pXmlDocumentIO = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pXmlDocument->lpVtbl->QueryInterface(pXmlDocument, &IID_IXmlDocumentIO, &pXmlDocumentIO);
+ }
+
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pXmlDocumentIO->lpVtbl->LoadXml(pXmlDocumentIO, hsBanner);
+ }
+
+ __x_ABI_CWindows_CUI_CNotifications_CIToastNotification* pToastNotification = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pNotificationFactory->lpVtbl->CreateToastNotification(pNotificationFactory, pXmlDocument, &pToastNotification);
+ }
+
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pToastNotifier->lpVtbl->Show(pToastNotifier, pToastNotification);
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ MSG msg;
+ while (GetMessageW(&msg, NULL, 0, 0) > 0)
+ {
+ TranslateMessage(&msg);
+ DispatchMessageW(&msg);
+ }
+ }
+
+ if (pToastNotification)
+ {
+ pToastNotification->lpVtbl->Release(pToastNotification);
+ }
+ if (pXmlDocumentIO)
+ {
+ pXmlDocumentIO->lpVtbl->Release(pXmlDocumentIO);
+ }
+ if (pXmlDocument)
+ {
+ pXmlDocument->lpVtbl->Release(pXmlDocument);
+ }
+ if (pInspectable)
+ {
+ pInspectable->lpVtbl->Release(pInspectable);
+ }
+ if (hsBanner)
+ {
+ WindowsDeleteString(hsBanner);
+ }
+ if (hsXmlDocument)
+ {
+ WindowsDeleteString(hsXmlDocument);
+ }
+ if (pNotificationFactory)
+ {
+ pNotificationFactory->lpVtbl->Release(pNotificationFactory);
+ }
+ if (hsToastNotification)
+ {
+ WindowsDeleteString(hsToastNotification);
+ }
+ if (pToastNotifier)
+ {
+ pToastNotifier->lpVtbl->Release(pToastNotifier);
+ }
+ if (pToastNotificationManager)
+ {
+ pToastNotificationManager->lpVtbl->Release(pToastNotificationManager);
+ }
+ if (hsToastNotificationManager)
+ {
+ WindowsDeleteString(hsToastNotificationManager);
+ }
+ if (hsAppId)
+ {
+ WindowsDeleteString(hsAppId);
+ }
+ if (dwCookie)
+ {
+ CoRevokeClassObject(dwCookie);
+ }
+ if (pClassFactory)
+ {
+ pClassFactory->lpVtbl->Release(pClassFactory);
+ }
+ RoUninitialize();
+ CoUninitialize();
+
+ return;
+}
+
+int main(int argc, char** argv)
+{
+ HRESULT hr = S_OK;
+ Impl_IGeneric* pClassFactory = NULL;
+ BOOL bOk = FALSE;
+ dwMainThreadId = GetCurrentThreadId();
+ BOOL bInvokedFromToast = (argc > 1);
+
+ /*
+ * Initialize COM and Windows Runtime on this thread. Make sure that the threading models of the two match.
+ */
+ if (SUCCEEDED(hr))
+ {
+ hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
+ } else {
+ printf("failed to");
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ hr = RoInitialize(RO_INIT_MULTITHREADED);
+ } else {
+ printf("failed to roinitialize\n");
+ }
+
+ /*
+ * Allocate class factory. This factory produces our implementation of the INotificationActivationCallback interface.
+ * This interface has an ::Activate member method that gets called when someone interacts with the toast notification.
+ */
+ if (SUCCEEDED(hr))
+ {
+ if (!(pClassFactory = malloc(sizeof(Impl_IGeneric)))) hr = E_OUTOFMEMORY;
+ else
+ {
+ pClassFactory->lpVtbl = &Impl_IClassFactory_Vtbl;
+ pClassFactory->dwRefCount = 1;
+ }
+ } else {
+ printf("failed to allocate our IGeneric class factory\n");
+ }
+
+ /*
+ * Instead of having to register our COM class in the registry beforehand, we opt to registering it at runtime;
+ * we associate our GUID with the class factory that provides our INotificationActivationCallback interface.
+ */
+ DWORD dwCookie = 0;
+ if (SUCCEEDED(hr))
+ {
+ hr = CoRegisterClassObject(&GUID_Impl_INotificationActivationCallback, pClassFactory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dwCookie);
+ } else {
+ printf("failed to register notification activation callback\n");
+ }
+
+ /*
+ * Construct the path to our EXE that will be used to launch it when something requests our interface.
+ * As said above, registration is dynamic - as long as this app runs, COM knows about the fact that this
+ * app implements our INotificationActivationCallback interface. The info here is used when this app has
+ * closed and someone clicks the toast notification for example; in that case, since our app is not
+ * running, thus CoRegisterClassObject was not called, COM needs info on what EXE contains the implementation
+ * of the interface, and we specify that here; without setting this, clicking on notifications will do nothing
+ */
+ wchar_t wszExePath[MAX_PATH + 100];
+ ZeroMemory(wszExePath, MAX_PATH + 100);
+ if (SUCCEEDED(hr))
+ {
+ hr = (GetModuleFileNameW(NULL, wszExePath + 1, MAX_PATH) != 0 ? S_OK : E_FAIL);
+ } else {
+ printf("failed to get module file\n");
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ wszExePath[0] = L'"';
+ wcscat_s(wszExePath, MAX_PATH + 100, L"\" " _T(TOAST_ACTIVATED_LAUNCH_ARG));
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ hr = HRESULT_FROM_WIN32(RegSetValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\CLSID\\{" _T(GUID_Impl_INotificationActivationCallback_Textual) L"}\\LocalServer32", REG_SZ, wszExePath, wcslen(wszExePath) + 1));
+ } else {
+ printf("failed to set registery value CLSID\n");
+ }
+
+ /*
+ * Here we set some info about our app and associate our AUMID with the GUID from above
+ * (the one that is associated with our class factory which produces our INotificationActivationCallback interface)
+ */
+ if (SUCCEEDED(hr))
+ {
+ hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\AppUserModelId\\" APP_ID, L"DisplayName", REG_SZ, L"Toast Activator Pure C Example", 31 * sizeof(wchar_t)));
+ } else {
+ printf("failed to set registery value AppUserModelId DisplayName\n");
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\AppUserModelId\\" APP_ID, L"IconBackgroundColor", REG_SZ, L"FF00FF00", 9 * sizeof(wchar_t)));
+ } else {
+ printf("failed to set registery value AppUserModelId IconBackgroundColor\n");
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\AppUserModelId\\" APP_ID, L"CustomActivator", REG_SZ, L"{" _T(GUID_Impl_INotificationActivationCallback_Textual) L"}", 39 * sizeof(wchar_t)));
+ } else {
+ printf("failed to set registery value AppUserModelId CustomActivator\n");
+ }
+
+ /*
+ * We will display a notification only when this app is launched standalone (not by interacting with a notification)
+ */
+ HSTRING_HEADER hshAppId;
+ HSTRING hsAppId = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(APP_ID, wcslen(APP_ID), &hshAppId, &hsAppId);
+ } else {
+ printf("failed to create string reference for app id\n");
+ }
+
+ HSTRING_HEADER hshToastNotificationManager;
+ HSTRING hsToastNotificationManager = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(
+ RuntimeClass_Windows_UI_Notifications_ToastNotificationManager,
+ (UINT32)wcslen(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager),
+ &hshToastNotificationManager, &hsToastNotificationManager
+ );
+ } else {
+ printf("failed to create string reference for ToastNotificationManager runtime class: %x\n", hr);
+ }
+
+ __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics* pToastNotificationManager = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = RoGetActivationFactory(hsToastNotificationManager, &IID_IToastNotificationManagerStatics, (LPVOID*)&pToastNotificationManager);
+ } else {
+ printf("failed to RoGetActivcationFactory for ToastNotificationManagerStatics\n");
+ }
+
+ __x_ABI_CWindows_CUI_CNotifications_CIToastNotifier* pToastNotifier = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pToastNotificationManager->lpVtbl->CreateToastNotifierWithId(pToastNotificationManager, hsAppId, &pToastNotifier);
+ } else {
+ printf("failed to CreateToastNotifierWithId\n");
+ }
+
+ HSTRING_HEADER hshToastNotification;
+ HSTRING hsToastNotification = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification, (UINT32)wcslen(RuntimeClass_Windows_UI_Notifications_ToastNotification), &hshToastNotification, &hsToastNotification);
+ } else {
+ printf("failed to create string reference for ToastNotification\n");
+ }
+
+ __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationFactory* pNotificationFactory = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = RoGetActivationFactory(hsToastNotification, &IID_IToastNotificationFactory, (LPVOID*)&pNotificationFactory);
+ } else {
+ printf("failed to RoGetActivationFactory ToastNotification\n");
+ }
+
+ HSTRING_HEADER hshXmlDocument;
+ HSTRING hsXmlDocument = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument, (UINT32)wcslen(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument), &hshXmlDocument, &hsXmlDocument);
+ }
+
+ HSTRING_HEADER hshBanner;
+ HSTRING hsBanner = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = WindowsCreateStringReference(wszBannerText, (UINT32)wcslen(wszBannerText), &hshBanner, &hsBanner);
+ }
+
+ IInspectable* pInspectable = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = RoActivateInstance(hsXmlDocument, &pInspectable);
+ }
+
+ __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocument* pXmlDocument = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pInspectable->lpVtbl->QueryInterface(pInspectable, &IID_IXmlDocument, &pXmlDocument);
+ }
+
+ __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocumentIO* pXmlDocumentIO = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pXmlDocument->lpVtbl->QueryInterface(pXmlDocument, &IID_IXmlDocumentIO, &pXmlDocumentIO);
+ }
+
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pXmlDocumentIO->lpVtbl->LoadXml(pXmlDocumentIO, hsBanner);
+ }
+
+ __x_ABI_CWindows_CUI_CNotifications_CIToastNotification* pToastNotification = NULL;
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pNotificationFactory->lpVtbl->CreateToastNotification(pNotificationFactory, pXmlDocument, &pToastNotification);
+ }
+
+ if (SUCCEEDED(hr) && !bInvokedFromToast)
+ {
+ hr = pToastNotifier->lpVtbl->Show(pToastNotifier, pToastNotification);
+ }
+
+ if (SUCCEEDED(hr))
+ {
+ MSG msg;
+ while (GetMessageW(&msg, NULL, 0, 0) > 0)
+ {
+ TranslateMessage(&msg);
+ DispatchMessageW(&msg);
+ }
+ }
+
+ if (pToastNotification)
+ {
+ pToastNotification->lpVtbl->Release(pToastNotification);
+ }
+ if (pXmlDocumentIO)
+ {
+ pXmlDocumentIO->lpVtbl->Release(pXmlDocumentIO);
+ }
+ if (pXmlDocument)
+ {
+ pXmlDocument->lpVtbl->Release(pXmlDocument);
+ }
+ if (pInspectable)
+ {
+ pInspectable->lpVtbl->Release(pInspectable);
+ }
+ if (hsBanner)
+ {
+ WindowsDeleteString(hsBanner);
+ }
+ if (hsXmlDocument)
+ {
+ WindowsDeleteString(hsXmlDocument);
+ }
+ if (pNotificationFactory)
+ {
+ pNotificationFactory->lpVtbl->Release(pNotificationFactory);
+ }
+ if (hsToastNotification)
+ {
+ WindowsDeleteString(hsToastNotification);
+ }
+ if (pToastNotifier)
+ {
+ pToastNotifier->lpVtbl->Release(pToastNotifier);
+ }
+ if (pToastNotificationManager)
+ {
+ pToastNotificationManager->lpVtbl->Release(pToastNotificationManager);
+ }
+ if (hsToastNotificationManager)
+ {
+ WindowsDeleteString(hsToastNotificationManager);
+ }
+ if (hsAppId)
+ {
+ WindowsDeleteString(hsAppId);
+ }
+ if (dwCookie)
+ {
+ CoRevokeClassObject(dwCookie);
+ }
+ if (pClassFactory)
+ {
+ pClassFactory->lpVtbl->Release(pClassFactory);
+ }
+ RoUninitialize();
+ CoUninitialize();
+
+ return 0;
+}
+
+// These are just testing the callback functionality.
+// We can provide go callbacks to the DLL!
+
+typedef int (__stdcall *GoCallback)(int data);
+
+GoCallback _cb;
+
+__declspec(dllexport)
+void Set(GoCallback cb) {
+ _cb = cb;
+}
+
+__declspec(dllexport)
+void Fire(void) {
+ _cb(1);
+}
+
+__declspec(dllexport)
+HRESULT Wrap_RoInitialize(RO_INIT_TYPE roInit) {
+ return RoInitialize(roInit);
+}
+
diff --git a/main.go b/main.go
@@ -0,0 +1,37 @@
+package main
+
+import "C"
+
+import (
+ "fmt"
+ "syscall"
+
+ "golang.org/x/sys/windows"
+)
+
+var (
+ toast = windows.NewLazyDLL("toast.dll")
+ generateToast = toast.NewProc("GenerateToast")
+ setCallback = toast.NewProc("Set")
+ fireCallback = toast.NewProc("Fire")
+ // This is testing whether we can use the RoInitialize wrapper when the ole.RoInitialize function doesn't work.
+ // And it appears to work just fine!
+ roinit = toast.NewProc("Wrap_RoInitialize")
+)
+
+func main() {
+ generateToast.Call(uintptr(0))
+
+ fmt.Printf("building callback\n")
+ callback := syscall.NewCallback(func(data int) (ret uintptr) {
+ fmt.Printf("from callback: %v\n", data)
+ return
+ })
+
+ fmt.Printf("setting callback\n")
+ setCallback.Call(callback)
+
+ fmt.Printf("firing callback\n")
+ fireCallback.Call()
+
+}
diff --git a/toast.dll b/toast.dll
Binary files differ.