commit 9b2bf0b1d578a355302161fc86d67b96149a041a
parent 08918e35ac6788d52ac4c5c2b0a579ddae837c53
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Thu, 16 Mar 2023 12:18:13 +0800
toast: migrate orchestration to Go code
This commit focuses on moving the content of GenerateToast to the Go
side.
This entails describing all the Vtables using structs with uintptr and
calling them with the syscall package.
While performing the migration, I've broken the code out in the bind
package in attempt to be more organized.
The C code surface is significantly reduced.
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Diffstat:
6 files changed, 336 insertions(+), 193 deletions(-)
diff --git a/internal/bind/bind.go b/internal/bind/bind.go
@@ -9,7 +9,6 @@ import (
"unsafe"
"github.com/go-ole/go-ole"
- "golang.org/x/sys/windows"
)
// This GUID matches the one defined in the C code. This coincidentally matches that
@@ -21,12 +20,6 @@ import (
// it from the app ID.
const iNotificationActivationCallbackGUID = "{0F82E845-CB89-4039-BDBF-67CA33254C76}"
-var (
- toast = windows.NewLazyDLL("toast.dll")
- procGenerateToast = toast.NewProc("GenerateToast")
- procSetActivationCallback = toast.NewProc("SetActivationCallback")
-)
-
// IsDLLAvailable returns true if the DLL can be loaded.
func IsDLLAvailable() bool {
return toast.Load() == nil
@@ -41,25 +34,72 @@ func GenerateToast(appID, xml string) error {
return err
}
- xmlStr, err := syscall.UTF16PtrFromString(xml)
+ // 1. Allocate ClassFactory implementation.
+ // 2. register callback implementation.
+ // 3. load noti statics
+ // 4. load noti factory
+ // 5. create xml
+ // 6. create xmlIO
+ // 7. load xml
+ // 8. create noti
+
+ classFactory := newClassFactory()
+ if classFactory == nil {
+ return fmt.Errorf("could not allocate class factory")
+ }
+ defer free(classFactory)
+
+ if err := registerClassFactory(classFactory); err != nil {
+ return fmt.Errorf("registering class factory: %w", err)
+ }
+
+ inspectable, err := ole.RoGetActivationFactory(CLSID_ToastNotificationManager, IID_ToastNotificationManagerStatics)
if err != nil {
- return fmt.Errorf("allocating xml string: %w", err)
+ return fmt.Errorf("getting activation factory: %w", err)
}
- appIDStr, err := syscall.UTF16PtrFromString(appID)
+
+ itns := (*IToastNotificationManager)(unsafe.Pointer(inspectable))
+
+ notifier, err := itns.CreateToastNotifierWithID(appID)
if err != nil {
- return fmt.Errorf("allocating appID string: %w", err)
- }
-
- // NOTE(jfm): I tried to verify whether is safe to pass strings directly to the proc
- // and it appears to be, and I couldn't find advice to the contrary.
- // However I would have expected the string to still be garbage collected? It's possible
- // the Go runtime knows how to handle this case? Need to look into this more.
- hr, _, _ := procGenerateToast.Call(
- uintptr(unsafe.Pointer(appIDStr)),
- uintptr(unsafe.Pointer(xmlStr)),
- )
- if hr != ole.S_OK {
- return ole.NewError(hr)
+ return fmt.Errorf("creating toast notifier: %w", err)
+ }
+
+ inspectable, err = ole.RoGetActivationFactory(CLSID_ToastNotification, IID_ToastNotificationFactory)
+ if err != nil {
+ return fmt.Errorf("getting activation factory: %w", err)
+ }
+
+ factory := (*IToastNotificationFactory)(unsafe.Pointer(inspectable))
+
+ inspectable, err = ole.RoActivateInstance(CLSID_XMLDocument)
+ if err != nil {
+ return err
+ }
+
+ xmlDispatch, err := inspectable.QueryInterface(IID_IXmlDocument)
+ if err != nil {
+ return err
+ }
+
+ xmlIODispatch, err := xmlDispatch.QueryInterface(IID_IXmlDocumentIO)
+ if err != nil {
+ return err
+ }
+
+ xmlIO := (*IXMLDocumentIO)(unsafe.Pointer(xmlIODispatch))
+
+ if err := xmlIO.LoadXml(xml); err != nil {
+ return err
+ }
+
+ noti, err := factory.CreateToastNotification(xmlDispatch)
+ if err != nil {
+ return fmt.Errorf("creating toast notification: %w", err)
+ }
+
+ if err := notifier.Show(noti); err != nil {
+ return fmt.Errorf("showing notification: %w", err)
}
return nil
diff --git a/internal/bind/objects.go b/internal/bind/objects.go
@@ -0,0 +1,147 @@
+// This file contains the implementations of the various COM objects we need to call.
+// THESE TYPES ARE NOT COMPLETE. Only the behaviour that is needed is implemented.
+// The definitions are derived from `<windows.ui.notifications.h>`.
+package bind
+
+import (
+ "fmt"
+ "syscall"
+ "unsafe"
+
+ "github.com/go-ole/go-ole"
+)
+
+// Runtime class names. These correspond to normal COM GUIDs, however the Windows Runtime likes
+// to use string identifiers and derives the GUIDs under the hood.
+var (
+ CLSID_ToastNotificationManager = "Windows.UI.Notifications.ToastNotificationManager"
+ CLSID_ToastNotification = "Windows.UI.Notifications.ToastNotification"
+ CLSID_XMLDocument = "Windows.Data.Xml.Dom.XmlDocument"
+)
+
+// Interface GUIDS. These GUIDS are predefined by the Windows Runtime, identifying the various
+// interfaces we want to make use of.
+var (
+ IID_ToastNotificationManagerStatics = ole.NewGUID("{50AC103F-D235-4598-BBEF-98FE4D1A3AD4}")
+ IID_ToastNotificationFactory = ole.NewGUID("{04124B20-82C6-4229-B109-FD9ED4662B53}")
+ IID_IXmlDocument = ole.NewGUID("{F7F3A506-1E87-42D6-BCFB-B8C809FA5494}")
+ IID_IXmlDocumentIO = ole.NewGUID("{6CD0E74E-EE65-4489-9EBF-CA43E87BA637}")
+)
+
+type IToastNotification struct {
+ lpVtbl *IToastNotificationVtbl
+}
+
+// IToastNotificationVtbl is empty because we don't care, as of now, about calling its methods.
+type IToastNotificationVtbl struct {
+ ole.IInspectableVtbl
+}
+
+type IToastNotificationManager struct {
+ lpVtbl *IToastNotificationManagerVtbl
+}
+
+type IToastNotificationManagerVtbl struct {
+ ole.IInspectableVtbl
+
+ CreateToastNotifier uintptr
+ CreateToastNotifierWithID uintptr
+ GetTemplateContent uintptr
+}
+
+func (v *IToastNotificationManager) CreateToastNotifierWithID(appID string) (ret *IToastNotifier, err error) {
+ hsAppID, err := ole.NewHString(appID)
+ if err != nil {
+ return nil, fmt.Errorf("allocating string: %w", err)
+ }
+ defer ole.DeleteHString(hsAppID)
+ hr, _, _ := syscall.SyscallN(
+ v.lpVtbl.CreateToastNotifierWithID,
+ uintptr(unsafe.Pointer(v)),
+ uintptr(hsAppID),
+ uintptr(unsafe.Pointer(&ret)),
+ )
+ if hr != ole.S_OK {
+ return nil, ole.NewError(hr)
+ }
+ return ret, nil
+}
+
+type IToastNotifier struct {
+ lpVtbl *IToastNotifierVtbl
+}
+
+type IToastNotifierVtbl struct {
+ ole.IInspectableVtbl
+
+ Show uintptr
+ Hide uintptr
+ GetSetting uintptr
+ AddToSchedule uintptr
+ RemoveFromSchedule uintptr
+ GetScheduledToastNotifications uintptr
+}
+
+func (v *IToastNotifier) Show(noti *IToastNotification) (err error) {
+ hr, _, _ := syscall.SyscallN(
+ v.lpVtbl.Show,
+ uintptr(unsafe.Pointer(v)),
+ uintptr(unsafe.Pointer(noti)),
+ )
+ if hr != ole.S_OK {
+ return ole.NewError(hr)
+ }
+ return nil
+}
+
+type IToastNotificationFactory struct {
+ lpVtbl *IToastNotificationFactoryVtbl
+}
+
+type IToastNotificationFactoryVtbl struct {
+ ole.IInspectableVtbl
+
+ CreateToastNotification uintptr
+}
+
+func (v *IToastNotificationFactory) CreateToastNotification(xmlDispatch *ole.IDispatch) (ret *IToastNotification, err error) {
+ hr, _, _ := syscall.SyscallN(
+ v.lpVtbl.CreateToastNotification,
+ uintptr(unsafe.Pointer(v)),
+ uintptr(unsafe.Pointer(xmlDispatch)),
+ uintptr(unsafe.Pointer(&ret)),
+ )
+ if hr != ole.S_OK {
+ return nil, ole.NewError(hr)
+ }
+ return ret, nil
+}
+
+type IXMLDocumentIO struct {
+ lpVtbl *IXMLDocumentIOVtbl
+}
+
+type IXMLDocumentIOVtbl struct {
+ ole.IInspectableVtbl
+
+ LoadXml uintptr
+ LoadXmlWithSettings uintptr
+ SaveToFileAsync uintptr
+}
+
+func (v *IXMLDocumentIO) LoadXml(xml string) (err error) {
+ hsXML, err := ole.NewHString(xml)
+ if err != nil {
+ return err
+ }
+ defer ole.DeleteHString(hsXML)
+ hr, _, _ := syscall.SyscallN(
+ v.lpVtbl.LoadXml,
+ uintptr(unsafe.Pointer(v)),
+ uintptr(hsXML),
+ )
+ if hr != ole.S_OK {
+ return ole.NewError(hr)
+ }
+ return nil
+}
diff --git a/internal/bind/proc.go b/internal/bind/proc.go
@@ -0,0 +1,56 @@
+package bind
+
+import (
+ "unsafe"
+
+ "github.com/go-ole/go-ole"
+ "golang.org/x/sys/windows"
+)
+
+var (
+ toast = windows.NewLazyDLL("toast.dll")
+ procSetActivationCallback = toast.NewProc("SetActivationCallback")
+ procRegisterClassObject = toast.NewProc("RegisterClassObject")
+ procMalloc = toast.NewProc("Malloc")
+ procFree = toast.NewProc("Free")
+ procNewClassFactory = toast.NewProc("NewClassFactory")
+ procRegisterClassFactory = toast.NewProc("RegisterClassFactory")
+)
+
+// malloc allocates raw memory.
+func malloc(size int) unsafe.Pointer {
+ hr, _, _ := procMalloc.Call(uintptr(size))
+ if hr == 0 {
+ return nil
+ }
+ return unsafe.Pointer(hr)
+}
+
+// free deallocates raw memory allocated by malloc.
+func free(object unsafe.Pointer) {
+ procFree.Call(uintptr(object))
+}
+
+// registerClassObject associates an in memory object with a GUID at runtime.
+func registerClassObject(clsid *ole.GUID, factory unsafe.Pointer) error {
+ hr, _, _ := procRegisterClassObject.Call(uintptr(unsafe.Pointer(clsid)), uintptr(factory))
+ if hr != ole.S_OK {
+ return ole.NewError(hr)
+ }
+ return nil
+}
+
+// newClassFactory allocates our ClassFactory that can build our NotificationActivationCallback object.
+// Deallocate with free.
+func newClassFactory() unsafe.Pointer {
+ object, _, _ := procNewClassFactory.Call()
+ return unsafe.Pointer(object)
+}
+
+func registerClassFactory(factory unsafe.Pointer) error {
+ hr, _, _ := procRegisterClassFactory.Call(uintptr(factory))
+ if hr != ole.S_OK {
+ return ole.NewError(hr)
+ }
+ return nil
+}
diff --git a/internal/bind/registry.go b/internal/bind/registry.go
@@ -1,3 +1,6 @@
+// This file contains registry manipulation code.
+// This logic is orthogonal to, but works in tandem with the COM code; since the
+// Windows Runtime uses the registry as it's primary source of state.
package bind
import (
diff --git a/internal/c-lib/toast.c b/internal/c-lib/toast.c
@@ -23,21 +23,6 @@ DEFINE_GUID(GUID_Impl_INotificationActivationCallback,
0xf82e845, 0xcb89, 0x4039, 0xbd, 0xbf, 0x67, 0xca, 0x33, 0x25, 0x4c, 0x76);
/*
- * 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.
*/
@@ -95,15 +80,9 @@ typedef void (__stdcall *ActivationCallback)(
const NOTIFICATION_USER_INPUT_DATA* data,
ULONG count
);
-
+
ActivationCallback _activation_callback;
-// SetActivationCallback to use when the notification
-__declspec(dllexport)
-void SetActivationCallback(ActivationCallback cb) {
- _activation_callback = cb;
-}
-
/*
* This is where the magic happens when someone interacts with our notification: this method will be called
* (on another thread !!!).
@@ -124,7 +103,7 @@ Impl_INotificationActivationCallback_Activate(
return S_OK;
}
-static const INotificationActivationCallbackVtbl Impl_INotificationActivationCallback_Vtbl = {
+static INotificationActivationCallbackVtbl Impl_INotificationActivationCallback_Vtbl = {
.QueryInterface = Impl_INotificationActivationCallback_QueryInterface,
.AddRef = Impl_IGeneric_AddRef,
.Release = Impl_IGeneric_Release,
@@ -195,7 +174,7 @@ Impl_IClassFactory_CreateInstance(
return hr;
}
-static const IClassFactoryVtbl Impl_IClassFactory_Vtbl = {
+static IClassFactoryVtbl Impl_IClassFactory_Vtbl = {
.QueryInterface = Impl_IClassFactory_QueryInterface,
.AddRef = Impl_IGeneric_AddRef,
.Release = Impl_IGeneric_Release,
@@ -225,163 +204,80 @@ static const IClassFactoryVtbl Impl_IClassFactory_Vtbl = {
WindowsDeleteString(ident); \
}
-// Generate a Toast notification with the provided xml content.
-// Register a callback with SetActivationCallback to handle notificaton interactions.
-//
-// TODO(jfm): We could move all the orchestration to Go and just have the Vtables (statically)
-// defined in C.
+// Init exports RoInitialize. For go-ole we can use the ole provided function instead.
+// This allows the consumer to handle initialization without relying on a third-party.
+__declspec(dllexport)
+HRESULT Init(RO_INIT_TYPE roInit) {
+ return RoInitialize(roInit);
+}
+
+// Deinit exports RoUninitialize. For go-ole we can use the ole provided function instead.
+// This allows the consumer to handle uninitialization without relying on a third-party.
+__declspec(dllexport)
+void Deinit() {
+ RoUninitialize();
+}
+
+// Malloc exports memory allocation. This allows the caller to get C memory without importing C.
+__declspec(dllexport)
+void*
+Malloc(size_t size) {
+ return malloc(size);
+}
+
+// Free exports memory deallocation. This allows the caller to free C memory without importing C.
+__declspec(dllexport)
+void
+Free(void* object) {
+ free(object);
+}
+
+// RegisterClassObject exports CoRegisterClassObject since package ole does not
+// offer this function.
__declspec(dllexport)
HRESULT
-GenerateToast(
- wchar_t* app_id,
- wchar_t* xml
+RegisterClassObject(
+ REFCLSID rclsid,
+ LPUNKNOWN pUnk,
+ DWORD dwClsContext,
+ DWORD flags,
+ LPDWORD lpdwRegister
) {
- HRESULT hr = S_OK;
- Impl_IGeneric* pClassFactory = NULL;
- BOOL bOk = FALSE;
- dwMainThreadId = GetCurrentThreadId();
-
- /*
- * 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 {
- goto cleanup;
+ return CoRegisterClassObject(rclsid, pUnk, dwClsContext, flags, lpdwRegister);
+}
+
+// NewClassFactory allocates a ClassFactory.
+// If the pointer is NULL we are out of memory.
+__declspec(dllexport)
+Impl_IGeneric*
+NewClassFactory() {
+ Impl_IGeneric* pClassFactory;
+ pClassFactory = malloc(sizeof(Impl_IGeneric));
+ if (pClassFactory == NULL) {
+ return NULL;
}
+ pClassFactory->lpVtbl = &Impl_IClassFactory_Vtbl;
+ pClassFactory->dwRefCount = 1;
+ return pClassFactory;
+}
- /*
- * 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.
- */
+// RegisterClassFactory associates the object that can build our INotificationActivationCallback
+// with that interface so the runtime can build our objects.
+__declspec(dllexport)
+HRESULT
+RegisterClassFactory(Impl_IGeneric* pClassFactory) {
DWORD dwCookie = 0;
- Must(CoRegisterClassObject(
+ return CoRegisterClassObject(
&GUID_Impl_INotificationActivationCallback,
pClassFactory,
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE,
&dwCookie
- ));
-
- HSTRING_HEADER hshAppId;
- HSTRING hsAppId = NULL;
-
- Must(WindowsCreateStringReference(app_id, wcslen(app_id), &hshAppId, &hsAppId));
-
- HSTRING_HEADER hshToastNotificationManager;
- HSTRING hsToastNotificationManager = NULL;
-
- Must(WindowsCreateStringReference(
- RuntimeClass_Windows_UI_Notifications_ToastNotificationManager,
- (UINT32)wcslen(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager),
- &hshToastNotificationManager, &hsToastNotificationManager
- ));
-
- __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics* pToastNotificationManager = NULL;
-
- Must(RoGetActivationFactory(
- hsToastNotificationManager,
- &IID_IToastNotificationManagerStatics,
- (LPVOID*)&pToastNotificationManager
- ));
-
- __x_ABI_CWindows_CUI_CNotifications_CIToastNotifier* pToastNotifier = NULL;
-
- Must(pToastNotificationManager->lpVtbl->CreateToastNotifierWithId(
- pToastNotificationManager,
- hsAppId,
- &pToastNotifier
- ));
-
- HSTRING_HEADER hshToastNotification;
- HSTRING hsToastNotification = NULL;
-
- Must(WindowsCreateStringReference(
- RuntimeClass_Windows_UI_Notifications_ToastNotification,
- (UINT32)wcslen(RuntimeClass_Windows_UI_Notifications_ToastNotification),
- &hshToastNotification,
- &hsToastNotification
- ));
-
- __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationFactory* pNotificationFactory = NULL;
-
- Must(RoGetActivationFactory(
- hsToastNotification,
- &IID_IToastNotificationFactory,
- (LPVOID*)&pNotificationFactory
- ));
-
- HSTRING_HEADER hshXmlDocument;
- HSTRING hsXmlDocument = NULL;
-
- Must(WindowsCreateStringReference(
- RuntimeClass_Windows_Data_Xml_Dom_XmlDocument,
- (UINT32)wcslen(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument),
- &hshXmlDocument,
- &hsXmlDocument
- ));
-
- HSTRING_HEADER hshBanner;
- HSTRING hsBanner = NULL;
-
- Must(WindowsCreateStringReference(
- xml,
- (UINT32)wcslen(xml),
- &hshBanner,
- &hsBanner
- ));
-
- IInspectable* pInspectable = NULL;
- Must(RoActivateInstance(hsXmlDocument, &pInspectable));
-
- __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocument* pXmlDocument = NULL;
- Must(pInspectable->lpVtbl->QueryInterface(pInspectable, &IID_IXmlDocument, &pXmlDocument));
-
- __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocumentIO* pXmlDocumentIO = NULL;
- Must(pXmlDocument->lpVtbl->QueryInterface(pXmlDocument, &IID_IXmlDocumentIO, &pXmlDocumentIO));
-
- Must(pXmlDocumentIO->lpVtbl->LoadXml(pXmlDocumentIO, hsBanner));
-
- __x_ABI_CWindows_CUI_CNotifications_CIToastNotification* pToastNotification = NULL;
- Must(pNotificationFactory->lpVtbl->CreateToastNotification(pNotificationFactory, pXmlDocument, &pToastNotification));
-
- Must(pToastNotifier->lpVtbl->Show(pToastNotifier, pToastNotification));
-
-cleanup:
-
- Release(pToastNotification);
- Release(pXmlDocumentIO);
- Release(pXmlDocument);
- Release(pInspectable);
- DeleteString(hsBanner);
- DeleteString(hsXmlDocument);
- Release(pNotificationFactory);
- DeleteString(hsToastNotification);
- Release(pToastNotifier);
- Release(pToastNotificationManager);
- DeleteString(hsToastNotificationManager);
- DeleteString(hsAppId);
-
- return hr;
-}
-
-// Init exports RoInitialize. For go-ole we can use the ole provided function instead.
-// This allows the consumer to handle initialization without relying on a third-party.
-__declspec(dllexport)
-HRESULT Init(RO_INIT_TYPE roInit) {
- return RoInitialize(roInit);
+ );
}
-// Deinit exports RoUninitialize. For go-ole we can use the ole provided function instead.
-// This allows the consumer to handle uninitialization without relying on a third-party.
+// SetActivationCallback to use when the notification
__declspec(dllexport)
-void Deinit() {
- return RoUninitialize();
-}
+void SetActivationCallback(ActivationCallback cb) {
+ _activation_callback = cb;
+}
+\ No newline at end of file
diff --git a/toast.dll b/toast.dll
Binary files differ.