commit 75c992cdb1b087fe9ca0d6aa62ec435fcf05eacc
parent 32962dfc3ed6f6df86fe891db422252f2e53ceb6
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Wed, 16 Sep 2026 21:08:00 -0400
shell-extension: add COM primitives for a Go in-process server
Explorer thumbnail handlers are COM objects loaded into a host process, and the
goal is a handler with no C code. Nothing in the standard library describes
vtables, class factories or IStream, so this package defines the minimum an
in-process Go COM server needs.
Diffstat:
4 files changed, 267 insertions(+), 0 deletions(-)
diff --git a/cmd/shell-extension/go.mod b/cmd/shell-extension/go.mod
@@ -0,0 +1,11 @@
+module github.com/jackmordaunt/icns/cmd/shell-extension
+
+go 1.21.5
+
+require (
+ github.com/jackmordaunt/icns/v3 v3.0.1
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
+ golang.org/x/sys v0.25.0
+)
+
+replace github.com/jackmordaunt/icns/v3 => ../..
diff --git a/cmd/shell-extension/go.sum b/cmd/shell-extension/go.sum
@@ -0,0 +1,4 @@
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/cmd/shell-extension/internal/com/com.go b/cmd/shell-extension/internal/com/com.go
@@ -0,0 +1,172 @@
+//go:build windows
+
+// Package com contains the minimal set of COM definitions needed to implement
+// an in-process COM server in pure Go: GUIDs, HRESULT codes, the IUnknown and
+// IClassFactory vtables, and an [io.Reader] adapter over IStream.
+//
+// A COM interface pointer points at a struct whose first word is a pointer to
+// a vtable: a struct of function pointers. To implement an interface in Go we
+// allocate a vtable populated with [syscall.NewCallback] trampolines and hand
+// COM a pointer to a Go struct whose first field is that vtable pointer. Each
+// trampoline receives the struct pointer back as its `this` argument.
+//
+// Callbacks created by [syscall.NewCallback] are never freed, so every vtable
+// is a package-level singleton shared by all instances of an interface.
+package com
+
+import (
+ "fmt"
+ "io"
+ "math"
+ "syscall"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+)
+
+// GUID identifies COM classes (CLSID) and interfaces (IID).
+type GUID = windows.GUID
+
+// MustGUID parses a GUID of the form "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}",
+// panicking on malformed input. Intended for package-level constants.
+func MustGUID(s string) *GUID {
+ g, err := windows.GUIDFromString(s)
+ if err != nil {
+ panic(fmt.Sprintf("com: invalid GUID %q: %v", s, err))
+ }
+ return &g
+}
+
+// IsEqualGUID reports whether two GUIDs are equal. Nil never equals anything.
+func IsEqualGUID(a, b *GUID) bool {
+ return a != nil && b != nil && *a == *b
+}
+
+// HRESULT is a COM status code.
+//
+// It is sized as uintptr rather than the 32-bit LONG that COM defines because
+// [syscall.NewCallback] requires callbacks to return exactly one uintptr-sized
+// value. Callers only read the low 32 bits.
+type HRESULT = uintptr
+
+// Well-known HRESULT values.
+const (
+ S_OK HRESULT = 0x00000000
+ S_FALSE HRESULT = 0x00000001
+
+ E_NOTIMPL HRESULT = 0x80004001
+ E_NOINTERFACE HRESULT = 0x80004002
+ E_POINTER HRESULT = 0x80004003
+ E_FAIL HRESULT = 0x80004005
+ E_UNEXPECTED HRESULT = 0x8000FFFF
+ E_OUTOFMEMORY HRESULT = 0x8007000E
+ E_INVALIDARG HRESULT = 0x80070057
+
+ CLASS_E_NOAGGREGATION HRESULT = 0x80040110
+ CLASS_E_CLASSNOTAVAILABLE HRESULT = 0x80040111
+
+ // HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED): the documented result of
+ // IInitializeWithStream::Initialize when called a second time.
+ HRESULT_ALREADY_INITIALIZED HRESULT = 0x800704DF
+
+ // WTS_E_FAILEDEXTRACTION signals that a thumbnail could not be produced.
+ WTS_E_FAILEDEXTRACTION HRESULT = 0x8004B200
+)
+
+// Failed reports whether hr denotes failure (the sign bit of the 32-bit code).
+func Failed(hr HRESULT) bool {
+ return int32(uint32(hr)) < 0
+}
+
+// Error wraps a failing HRESULT as a Go error.
+type Error HRESULT
+
+func (e Error) Error() string {
+ return fmt.Sprintf("HRESULT 0x%08X", uint32(e))
+}
+
+// Well-known interface identifiers.
+var (
+ IID_IUnknown = MustGUID("{00000000-0000-0000-C000-000000000046}")
+ IID_IClassFactory = MustGUID("{00000001-0000-0000-C000-000000000046}")
+ IID_IStream = MustGUID("{0000000C-0000-0000-C000-000000000046}")
+)
+
+// IUnknownVtbl is the vtable every COM interface begins with.
+type IUnknownVtbl struct {
+ QueryInterface uintptr // HRESULT (*)(This, REFIID riid, void **ppvObject)
+ AddRef uintptr // ULONG (*)(This)
+ Release uintptr // ULONG (*)(This)
+}
+
+// IClassFactoryVtbl is the vtable of IClassFactory.
+type IClassFactoryVtbl struct {
+ IUnknownVtbl
+ CreateInstance uintptr // HRESULT (*)(This, IUnknown *pUnkOuter, REFIID riid, void **ppvObject)
+ LockServer uintptr // HRESULT (*)(This, BOOL fLock)
+}
+
+// IStreamVtbl is the vtable of IStream (which extends ISequentialStream).
+type IStreamVtbl struct {
+ IUnknownVtbl
+ Read uintptr // HRESULT (*)(This, void *pv, ULONG cb, ULONG *pcbRead)
+ Write uintptr
+ Seek uintptr
+ SetSize uintptr
+ CopyTo uintptr
+ Commit uintptr
+ Revert uintptr
+ LockRegion uintptr
+ UnlockRegion uintptr
+ Stat uintptr
+ Clone uintptr
+}
+
+// IStream is a COM stream owned by the caller. It implements [io.Reader] so
+// COM-provided data can be consumed by ordinary Go decoders.
+type IStream struct {
+ vtbl *IStreamVtbl
+}
+
+var _ io.Reader = (*IStream)(nil)
+
+// Read implements [io.Reader] over IStream::Read.
+func (s *IStream) Read(p []byte) (int, error) {
+ if len(p) == 0 {
+ return 0, nil
+ }
+ if len(p) > math.MaxUint32 {
+ p = p[:math.MaxUint32]
+ }
+ var n uint32
+ r, _, _ := syscall.SyscallN(
+ s.vtbl.Read,
+ uintptr(unsafe.Pointer(s)),
+ uintptr(unsafe.Pointer(unsafe.SliceData(p))),
+ uintptr(len(p)),
+ uintptr(unsafe.Pointer(&n)),
+ )
+ // Only the low 32 bits of the return register hold the HRESULT.
+ hr := HRESULT(uint32(r))
+ if Failed(hr) {
+ return int(n), Error(hr)
+ }
+ // IStream::Read reports end of stream either with S_FALSE or with S_OK
+ // and zero bytes read, depending on the implementation.
+ if n == 0 {
+ return 0, io.EOF
+ }
+ return int(n), nil
+}
+
+// AddRef increments the stream's reference count.
+func (s *IStream) AddRef() uint32 {
+ r, _, _ := syscall.SyscallN(s.vtbl.AddRef, uintptr(unsafe.Pointer(s)))
+ return uint32(r)
+}
+
+// Release decrements the stream's reference count.
+func (s *IStream) Release() uint32 {
+ r, _, _ := syscall.SyscallN(s.vtbl.Release, uintptr(unsafe.Pointer(s)))
+ return uint32(r)
+}
diff --git a/cmd/shell-extension/internal/com/factory.go b/cmd/shell-extension/internal/com/factory.go
@@ -0,0 +1,80 @@
+//go:build windows
+
+package com
+
+import (
+ "runtime"
+ "syscall"
+ "unsafe"
+)
+
+// Constructor creates a new object and stores the interface identified by riid
+// in *ppv, returning E_NOINTERFACE when the object does not implement riid.
+type Constructor func(riid *GUID, ppv *unsafe.Pointer) HRESULT
+
+// ClassFactory implements IClassFactory for a single COM class.
+//
+// A ClassFactory is a process-lifetime singleton: AddRef and Release are
+// no-ops and the object is pinned so COM may hold its pointer indefinitely.
+// The vtable pointer must remain the first field.
+type ClassFactory struct {
+ vtbl *IClassFactoryVtbl
+ create Constructor
+ pin runtime.Pinner
+}
+
+// NewClassFactory returns a factory whose CreateInstance defers to create.
+func NewClassFactory(create Constructor) *ClassFactory {
+ f := &ClassFactory{vtbl: classFactoryVtbl, create: create}
+ f.pin.Pin(f)
+ return f
+}
+
+// QueryInterface implements IUnknown::QueryInterface for the factory.
+func (f *ClassFactory) QueryInterface(riid *GUID, ppv *unsafe.Pointer) HRESULT {
+ if ppv == nil {
+ return E_POINTER
+ }
+ if !IsEqualGUID(riid, IID_IUnknown) && !IsEqualGUID(riid, IID_IClassFactory) {
+ *ppv = nil
+ return E_NOINTERFACE
+ }
+ *ppv = unsafe.Pointer(f)
+ return S_OK
+}
+
+// CreateInstance implements IClassFactory::CreateInstance.
+func (f *ClassFactory) CreateInstance(outer unsafe.Pointer, riid *GUID, ppv *unsafe.Pointer) HRESULT {
+ if ppv == nil {
+ return E_POINTER
+ }
+ *ppv = nil
+ if outer != nil {
+ return CLASS_E_NOAGGREGATION
+ }
+ return f.create(riid, ppv)
+}
+
+// classFactoryVtbl is shared by all factories. The trampolines recover the
+// concrete factory from the `this` pointer COM passes back to us.
+var classFactoryVtbl = &IClassFactoryVtbl{
+ IUnknownVtbl: IUnknownVtbl{
+ QueryInterface: syscall.NewCallback(func(this *ClassFactory, riid *GUID, ppv *unsafe.Pointer) uintptr {
+ return this.QueryInterface(riid, ppv)
+ }),
+ AddRef: syscall.NewCallback(func(this *ClassFactory) uintptr {
+ return 1
+ }),
+ Release: syscall.NewCallback(func(this *ClassFactory) uintptr {
+ return 1
+ }),
+ },
+ CreateInstance: syscall.NewCallback(func(this *ClassFactory, outer unsafe.Pointer, riid *GUID, ppv *unsafe.Pointer) uintptr {
+ return this.CreateInstance(outer, riid, ppv)
+ }),
+ LockServer: syscall.NewCallback(func(this *ClassFactory, lock uintptr) uintptr {
+ // The Go runtime can never be unloaded from a host process (see
+ // DllCanUnloadNow), so there is nothing to lock.
+ return S_OK
+ }),
+}