go-toast

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

ARCHITECTURE.md (9155B)


      1 # Architecture
      2 
      3 This document will attempt to explain how this code works. 
      4 
      5 ## Windows COM 
      6 
      7 Windows makes heavy use of it's [COM api](https://en.wikipedia.org/wiki/Component_Object_Model), 
      8 (Component Object Model) which is a binary interface - allowing programs that agree on a memory 
      9 layout in order to communicate. 
     10 
     11 COM apis are typically Object Oriented, and based on interfaces. This should be familiar to Go
     12 programmers, since Go includes interfaces as a core part of its language design and type system.  
     13 
     14 The difference being that in COM we don't get any runtime help, nice syntax or type safety. We
     15 get raw [VTables](https://en.wikipedia.org/wiki/Virtual_method_table) and deal with raw memory. 
     16 
     17 You can think of COM as like working with a Go api that uses `any` (empty interface) _everywhere_
     18 and typeswitching is required to access methods `file, ok := obj.(File)`.  
     19 
     20 Some languages like C++ have extensions that support COM and provide convenient wrappers for
     21 generating and using COM apis. Go does not. C also, does not. 
     22 
     23 However there is a package `go-ole` that allows us to _call_ COM apis with some level of 
     24 convenience - which we will use where possible. What go-ole does not expose is a way to 
     25 implement a COM object in Go. 
     26 
     27 ## Interacting with COM objects in pure Go
     28 
     29 In order to interact with COM objects we need to:
     30 
     31 1. locate headers containing the VTable definitions
     32 2. define vtables in Go that are compatable with those definitions
     33 3. invoke the appropriate COM objects using our vtables and the syscall package  
     34 
     35 ### 1. locate headers 
     36 
     37 Download Windows SDK via the [Visual Studio installer](https://visualstudio.microsoft.com/downloads). 
     38 You will need to check "Desktop development with C++".
     39 
     40 Once complete you can navigate to the SDK include directory.
     41 
     42 In our case we needed `Windows.ui.notifications.h`, which contains the definitions of
     43 the types we want to call, and `NotificationActivationCallback.h` which contains the definition of
     44 `INotificationActivationCallback` which is the interface we need to _implement_.
     45 
     46 ### 2. define vtables in Go
     47 
     48 The VTables are defined in C (mired in macros). We need to define compatible vtables in Go syntax
     49 so we can call the ones defined in the header. 
     50 
     51 COM objects are structured in a such a way that we want a parent struct who's first field is a pointer
     52 to the vtable struct. A full example is provided later, for now it we need something like this:
     53 
     54 ```go
     55 type Object struct {
     56   lpvtbl *ObjectVtbl
     57 }
     58 type ObjectVtbl struct {
     59   MethodOne uintptr
     60   MethdoTwo uintptr
     61   //...
     62 }
     63 ```
     64 
     65 ### 3. invoke methods in Go 
     66 
     67 Using package `syscall` we can invoke these methods (provided the uintptr are valid) using
     68 `syscal.SyscallN`. Paramters and return values are defined in the C headers. 
     69 
     70 ```go
     71 func (v *Object) One() error {
     72   hr, _, _ := syscall.SyscallN(uintptr(v))
     73   if hr != ole.S_OK {
     74     return ole.NewError(hr)
     75   }
     76   return nil
     77 }
     78 ```
     79 
     80 With that we can inoke methods on a COM object. This is how `go-ole` works. 
     81 
     82 ## Implementing a COM object in pure Go (no cgo!)
     83 
     84 To do this we will need to allocate raw memory for the VTables (so that Go garbage collector
     85 doesn't interfere) and write our function pointers to the VTables. 
     86 
     87 Since these are not safe Go capabilities we will need the help of package `syscall` (on Windows). 
     88 
     89 Package `syscall` provides two very important functions:
     90 
     91 1. `NewProc` - which loads a function from a DLL 
     92 2. `NewCallback` - which allocates a C-callable function pointer from a Go function
     93 
     94 For the first part, we can load the Windows kernel api via `kernel32.dll` system dll, and
     95 pull out `GlobalAlloc` and `GlobalFree` using `syscall.NewProc`. 
     96 
     97 For the second part, we can use `syscall.NewCallback` to build a C-callable function pointer 
     98 from a Go function and instantiate the VtTables with it. Caveat emptor: memory allocated by
     99 `NewCallback` is never released, and only 1024 callbacks are guaranteed to be allowed. This
    100 is why we only allocate the callbacks once on init.
    101 
    102 Thus we can implement a COM object (invokable from C) like this:
    103 
    104 ```go
    105 
    106 // Initialize our kernel functions. 
    107 var (
    108   kernel32   = windows.NewLazySystemDLL("kernel32.dll")
    109   procMalloc = kernel32.NewProc("GlobalAlloc")
    110   procFree   = kernel32.NewProc("GlobalFree")
    111 )
    112 
    113 // malloc allocates raw memory using the Windows kernel.
    114 // In case of out of memory, the returned pointer will be nil.
    115 // The memory is zeroed out to make sure we don't get garbage that looks like
    116 // valid Go data types.
    117 func malloc(size uintptr) unsafe.Pointer {
    118 	hr, _, _ := procMalloc.Call(uintptr(GMEM_FIXED|GMEM_ZEROINIT), uintptr(size))
    119 	if hr == 0 {
    120 		return nil
    121 	}
    122 	return unsafe.Pointer(hr)
    123 }
    124 
    125 // free deallocates raw memory allocated by malloc.
    126 func free(object unsafe.Pointer) {
    127 	procFree.Call(uintptr(object))
    128 }
    129 
    130 // Object defines our object. 
    131 // This is how COM objects are laid out in memory, where the first field is a pointer
    132 // to a vtable, and the vtable's fields are pointers to functions. 
    133 type Object struct {
    134   lpvtbl *ObjectVtbl // lpvtbl is a COM conventional name for this field. 
    135 }
    136 
    137 // ObjectVtbl defines the Vtable of our object. 
    138 type ObjectVtbl struct {
    139   MethodOne   uintptr
    140   MethodTwo   uintptr
    141   MethodThree uintptr
    142 }
    143 
    144 // These methods are allocated once as package globals because Go will never reclaim the
    145 // memory allocated for such callbacks. 
    146 // 
    147 // All arguments must be uintptr sized, and the return must be a uintptr as well. 
    148 // 
    149 // By convention, the first parameter is a pointer to the parent object. 
    150 var (
    151   methodOne = syscall.NewCallback(func(this *Object) uintptr {
    152     fmt.Printf("methodOne invoked\n")
    153     return uintptr(0)
    154   })
    155   
    156   methodTwo = syscall.NewCallback(func(this *Object) uintptr {
    157     fmt.Printf("methodTwo invoked\n")
    158     return uintptr(0)
    159   })
    160   
    161   methodThree = syscall.NewCallback(func(this *Object) uintptr {
    162     fmt.Printf("methodThree invoked\n")
    163     return uintptr(0)
    164   })
    165 )
    166 
    167 
    168 func NewObject() *Object {
    169   // Allocate the parent object and the vtable. 
    170   obj := (*Object)(malloc(unsafe.Sizeof(Object{})))
    171   vtbl := (*ObjectVtbl)(malloc(unsafe.Sizeof(ObjectVtbl{})))
    172 
    173   // Initialize the vtable with our static callback implementations. 
    174   vtbl.MethodOne = methodOne
    175   vtbl.MethodTwo = methodTwo
    176   vtbl.MethodThree = methodThree
    177   
    178   // The returned object must be freed by GlobalFree. 
    179   object.lpvtbl = vtbl
    180   return obj
    181 }
    182 ```
    183 
    184 ## WinRT and Toast Notifications
    185 
    186 For this package the vtables we need are located in various headers `Windows.ui.notifications.h` and
    187 `NotificationActivationCallback.h` and `combase.h`.
    188 
    189 With all of the vtables replicated in Go as explained above we now need to interact with the Windows
    190 Runtime. 
    191 
    192 First we need to initialize the Windows Runtime with `RoInitialize`. 
    193 
    194 ```go
    195 ole.RoInitialize(0)
    196 ```
    197 
    198 Traditional COM uses GUIDs to identify objects and interfaces. WinRT uses strings (mapped to GUIDS 
    199 at runtime).
    200 
    201 To instantiate a WinRT COM object we invoke `RoGetActivationFactory` with the class string along with
    202 the interface GUID we expect to use. 
    203 
    204 
    205 ```go
    206 CLSID_ToastNotification := "Windows.UI.Notifications.ToastNotification"
    207 IID_IToastNotificationFactory := ole.NewGUID("{50AC103F-D235-4598-BBEF-98FE4D1A3AD4}")
    208 
    209 factoryObject, err := ole.RoGetActivationFactory(CLSID_ToastNotification, IID_ToastNotificationFactory)
    210 if err != nil {
    211 	return nil, fmt.Errorf("getting activation factory: %w", err)
    212 }
    213 ```
    214 
    215 From there we can unsafe cast to our callback definition (ole doesn't provide direct access to the methods).
    216 
    217 ```go
    218 factory := (*IToastNotificationFactory)(unsafe.Pointer(factoryObject))
    219 notification, err := factory.CreateToastNotification(xml)
    220 ```
    221 
    222 Repeat this process per object we need to instantiate. 
    223 
    224 To generate a toast notification from XML with a callback we need to instantiate several COM objects:
    225 
    226 1. `INotificationActivationCallback` our implementation to be invoked by the runtime 
    227 1. `ClassFactory` which can instantiate our `INotificationActivationCallback` implementation
    228 1. `XmlDocument` to contain the xml content of the notification
    229 1. `XmlDocumentIO` to provide an IO interface to the xml document (so we can write the xml to it)
    230 1. `Notification` specifying the content of the notification
    231 1. `Notifier` for showing notifications
    232 
    233 Finally we register our class factory using `CoRegisterClassObject` so the runtime can call us back
    234 and then we invoke `Notifier.Show` passing in the `Notification` object to display the notification. 
    235 
    236 In addition to calling and implementing COM objects we need to manipulate registry state to tell the 
    237 Windows Runtime metadata about our application. 
    238 
    239 1. register a CLSID (GUID) for our INotificationActivationCallback; this is how the runtime knows
    240 what object to ask for
    241 2. optionally provide an icon and an activation executable to be invoked when our application is not running
    242 
    243 
    244 With all of that correctly configured we can generate toast notifications on Windows in pure Go!