go-toast

Send toast notifications in Windows
Log | Files | Refs | README | LICENSE

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:
A.gitignore | 2++
AREADME.md | 8++++++++
Agen.go | 2++
Ago.mod | 8++++++++
Ago.sum | 5+++++
Ainternal/c-lib/Makefile | 14++++++++++++++
Ainternal/c-lib/build.sh | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/c-lib/toast.c | 816+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain.go | 37+++++++++++++++++++++++++++++++++++++
Atoast.dll | 0
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.