icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

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:
Acmd/shell-extension/go.mod | 11+++++++++++
Acmd/shell-extension/go.sum | 4++++
Acmd/shell-extension/internal/com/com.go | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/shell-extension/internal/com/factory.go | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 + }), +}