Last active
November 18, 2024 20:02
-
-
Save jagobagascon/fd807800ee5c658955a81f0dd1c40bb8 to your computer and use it in GitHub Desktop.
WinRT Go IIterable & IIterator implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package winrt | |
import ( | |
"reflect" | |
"sync" | |
"syscall" | |
"unsafe" | |
"github.com/go-ole/go-ole" | |
"github.com/saltosystems/winrt-go/internal/iunknown" | |
"github.com/saltosystems/winrt-go/internal/kernel32" | |
"github.com/saltosystems/winrt-go/windows/foundation/collections" | |
) | |
var ( | |
firstCallback = syscall.NewCallback(first) | |
) | |
// We cannot pass a pointer that includes Go pointers to WinRT | |
// so we either copy the arrays manually into the Heap | |
// or store them somewhere in Go (which is simpler). | |
// | |
// This struct stores each array in a map that uses the Pointer passed | |
// to WinRT as a key. This pointer is Heap allocated and will never change. | |
// Once the pointer is Released, the array is also removed. | |
type syncArrayIterables struct { | |
mu *sync.Mutex | |
iterables map[unsafe.Pointer][]any | |
} | |
var arrayItems = &syncArrayIterables{ | |
mu: &sync.Mutex{}, | |
iterables: make(map[unsafe.Pointer][]any), | |
} | |
func (i *syncArrayIterables) add(instPtr unsafe.Pointer, items []any) { | |
arrayItems.mu.Lock() | |
defer arrayItems.mu.Unlock() | |
i.iterables[instPtr] = items | |
} | |
func (i *syncArrayIterables) get(instPtr unsafe.Pointer) ([]any, bool) { | |
arrayItems.mu.Lock() | |
defer arrayItems.mu.Unlock() | |
it, ok := arrayItems.iterables[instPtr] | |
return it, ok | |
} | |
func (i *syncArrayIterables) remove(instPtr unsafe.Pointer) { | |
arrayItems.mu.Lock() | |
defer arrayItems.mu.Unlock() | |
delete(i.iterables, instPtr) | |
} | |
type arrayIterable struct { | |
collections.IIterable | |
sync.Mutex | |
refs uintptr | |
IID ole.GUID | |
itemSignature string | |
} | |
func NewArrayIterable(items []any, itemSignature string) *collections.IIterable { | |
// create type instance | |
size := unsafe.Sizeof(*(*arrayIterable)(nil)) | |
instPtr := kernel32.Malloc(size) | |
inst := (*arrayIterable)(instPtr) | |
// get the callbacks for the VTable | |
callbacks := iunknown.RegisterInstance(instPtr, inst) | |
// the VTable should also be allocated in the heap | |
sizeVTable := unsafe.Sizeof(*(*collections.IIterableVtbl)(nil)) | |
vTablePtr := kernel32.Malloc(sizeVTable) | |
inst.RawVTable = (*interface{})(vTablePtr) | |
vTable := (*collections.IIterableVtbl)(vTablePtr) | |
vTable.IUnknownVtbl = ole.IUnknownVtbl{ | |
QueryInterface: callbacks.QueryInterface, | |
AddRef: callbacks.AddRef, | |
Release: callbacks.Release, | |
} | |
vTable.First = firstCallback | |
arrayItems.add(instPtr, items) | |
// Initialize all properties: the malloc may contain garbage | |
iid := ole.NewGUID(ParameterizedInstanceGUID(collections.GUIDIIterable, itemSignature)) | |
inst.IID = *iid // copy contents | |
inst.Mutex = sync.Mutex{} | |
inst.refs = 0 | |
inst.itemSignature = itemSignature | |
inst.AddRef() | |
return &inst.IIterable // ugly but works | |
} | |
func (r *arrayIterable) GetIID() *ole.GUID { | |
return &r.IID | |
} | |
// addRef increments the reference counter by one | |
func (r *arrayIterable) AddRef() uintptr { | |
r.Lock() | |
defer r.Unlock() | |
r.refs++ | |
return r.refs | |
} | |
func (r *arrayIterable) removeRef() uintptr { | |
r.Lock() | |
defer r.Unlock() | |
if r.refs > 0 { | |
r.refs-- | |
} | |
return r.refs | |
} | |
// removeRef decrements the reference counter by one. If it was already zero, it will just return zero. | |
func (r *arrayIterable) Release() uintptr { | |
rem := r.removeRef() | |
if rem == 0 { | |
// We're done. | |
instancePtr := unsafe.Pointer(r) | |
arrayItems.remove(instancePtr) | |
kernel32.Free(unsafe.Pointer(r.RawVTable)) | |
kernel32.Free(instancePtr) | |
} | |
return rem | |
} | |
func first(inst unsafe.Pointer, out **collections.IIterator) uintptr { | |
offset := unsafe.Offsetof(arrayIterable{}.IIterable) | |
i := (*arrayIterable)(unsafe.Pointer(uintptr(inst) - offset)) | |
arrIt, ok := arrayItems.get(inst) | |
if !ok { | |
return ole.E_FAIL | |
} | |
it := NewArrayIterator(arrIt, i.itemSignature) | |
*out = it | |
return ole.S_OK | |
} | |
var ( | |
getCurrentCallback = syscall.NewCallback(getCurrent) | |
getHasCurrentCallback = syscall.NewCallback(getHasCurrent) | |
moveNextCallback = syscall.NewCallback(moveNext) | |
getManyCallback = syscall.NewCallback(getMany) | |
) | |
type collectionsIterator struct { | |
collections.IIterator | |
sync.Mutex | |
refs uintptr | |
IID ole.GUID | |
index int | |
itemSignature string | |
} | |
func NewArrayIterator(items []any, itemSignature string) *collections.IIterator { | |
// create type instance | |
size := unsafe.Sizeof(*(*collectionsIterator)(nil)) | |
instPtr := kernel32.Malloc(size) | |
inst := (*collectionsIterator)(instPtr) | |
// get the callbacks for the VTable | |
callbacks := iunknown.RegisterInstance(instPtr, inst) | |
// the VTable should also be allocated in the heap | |
sizeVTable := unsafe.Sizeof(*(*collections.IIterableVtbl)(nil)) | |
vTablePtr := kernel32.Malloc(sizeVTable) | |
inst.RawVTable = (*interface{})(vTablePtr) | |
vTable := (*collections.IIteratorVtbl)(vTablePtr) | |
vTable.IUnknownVtbl = ole.IUnknownVtbl{ | |
QueryInterface: callbacks.QueryInterface, | |
AddRef: callbacks.AddRef, | |
Release: callbacks.Release, | |
} | |
vTable.GetCurrent = getCurrentCallback | |
vTable.GetHasCurrent = getHasCurrentCallback | |
vTable.GetMany = getManyCallback | |
vTable.MoveNext = moveNextCallback | |
arrayItems.add(instPtr, items) | |
// Initialize all properties: the malloc may contain garbage | |
iid := ole.NewGUID(ParameterizedInstanceGUID(collections.GUIDIIterator, itemSignature)) | |
inst.IID = *iid // copy contents | |
inst.Mutex = sync.Mutex{} | |
inst.refs = 0 | |
inst.itemSignature = itemSignature | |
inst.index = -1 // not initialized | |
inst.AddRef() | |
return &inst.IIterator // ugly but works | |
} | |
func (r *collectionsIterator) GetIID() *ole.GUID { | |
return &r.IID | |
} | |
// addRef increments the reference counter by one | |
func (r *collectionsIterator) AddRef() uintptr { | |
r.Lock() | |
defer r.Unlock() | |
r.refs++ | |
return r.refs | |
} | |
func (r *collectionsIterator) removeRef() uintptr { | |
r.Lock() | |
defer r.Unlock() | |
if r.refs > 0 { | |
r.refs-- | |
} | |
return r.refs | |
} | |
// removeRef decrements the reference counter by one. If it was already zero, it will just return zero. | |
func (r *collectionsIterator) Release() uintptr { | |
rem := r.removeRef() | |
if rem == 0 { | |
// We're done. | |
instancePtr := unsafe.Pointer(r) | |
arrayItems.remove(instancePtr) | |
kernel32.Free(unsafe.Pointer(r.RawVTable)) | |
kernel32.Free(instancePtr) | |
} | |
return rem | |
} | |
func getCurrent(inst, out unsafe.Pointer) uintptr { | |
// good old C pointer magic tricks | |
offset := unsafe.Offsetof(collectionsIterator{}.IIterator) | |
it := (*collectionsIterator)(unsafe.Pointer(uintptr(inst) - offset)) | |
items, ok := arrayItems.get(unsafe.Pointer(it)) | |
if !ok { | |
return ole.E_FAIL | |
} | |
current := items[it.index] | |
copyItemToPointer(current, out) | |
return ole.S_OK | |
} | |
func getHasCurrent(inst, out unsafe.Pointer) uintptr { | |
// good old C pointer magic tricks | |
offset := unsafe.Offsetof(collectionsIterator{}.IIterator) | |
it := (*collectionsIterator)(unsafe.Pointer(uintptr(inst) - offset)) | |
items, ok := arrayItems.get(unsafe.Pointer(it)) | |
if !ok { | |
return ole.E_FAIL | |
} | |
if it.index > -1 && it.index < len(items) { | |
// index is within items length | |
*(*uintptr)(out) = uintptr(1) | |
} else { | |
*(*uintptr)(out) = uintptr(0) | |
} | |
return ole.S_OK | |
} | |
func moveNext(inst, out unsafe.Pointer) uintptr { | |
// good old C pointer magic tricks | |
offset := unsafe.Offsetof(collectionsIterator{}.IIterator) | |
it := (*collectionsIterator)(unsafe.Pointer(uintptr(inst) - offset)) | |
it.index++ | |
return getHasCurrent(inst, out) | |
} | |
func getMany(inst, itemsAmount, outItems, outItemsSize unsafe.Pointer) uintptr { | |
// good old C pointer magic tricks | |
offset := unsafe.Offsetof(collectionsIterator{}.IIterator) | |
it := (*collectionsIterator)(unsafe.Pointer(uintptr(inst) - offset)) | |
items, ok := arrayItems.get(unsafe.Pointer(it)) | |
if !ok { | |
return ole.E_FAIL | |
} | |
// requested itemsAmount | |
requestedItems := int(uintptr(itemsAmount)) | |
availableItems := len(items) - it.index - 1 | |
returnItems := requestedItems | |
if returnItems > availableItems { | |
// not enough items available | |
returnItems = availableItems | |
} | |
// copy items | |
n := uintptr(0) | |
for i := 0; i < returnItems; i++ { | |
it.index++ | |
n += copyItemToPointer(items[it.index], unsafe.Pointer(uintptr(outItems)+n)) | |
} | |
// output size | |
*(*uint32)(outItemsSize) = uint32(returnItems) /*the amount of items*/ | |
return ole.S_OK | |
} | |
func copyItemToPointer(item any, out unsafe.Pointer) uintptr { | |
var size uintptr = 0 | |
switch t := item.(type) { | |
case uint: | |
*(*uint)(out) = t | |
size = unsafe.Sizeof(uint(0)) | |
case uint8: | |
*(*uint8)(out) = t | |
size = unsafe.Sizeof(uint8(0)) | |
case uint16: | |
*(*uint16)(out) = t | |
size = unsafe.Sizeof(uint16(0)) | |
case uint32: | |
*(*uint32)(out) = t | |
size = unsafe.Sizeof(uint32(0)) | |
case uint64: | |
*(*uint64)(out) = t | |
size = unsafe.Sizeof(uint64(0)) | |
case int: | |
*(*int)(out) = t | |
size = unsafe.Sizeof(int(0)) | |
case int8: | |
*(*int8)(out) = t | |
size = unsafe.Sizeof(int8(0)) | |
case int16: | |
*(*int16)(out) = t | |
size = unsafe.Sizeof(int16(0)) | |
case int32: | |
*(*int32)(out) = t | |
size = unsafe.Sizeof(int32(0)) | |
case int64: | |
*(*int64)(out) = t | |
size = unsafe.Sizeof(int64(0)) | |
default: //pointer type | |
*(*unsafe.Pointer)(out) = reflect.ValueOf(t).UnsafePointer() | |
size = unsafe.Sizeof(unsafe.Pointer(nil)) | |
} | |
return size | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"github.com/saltosystems/winrt-go" | |
) | |
func main() { | |
if err := testGetCurrent(); err != nil { | |
panic(err) | |
} | |
if err := testGetMany(); err != nil { | |
panic(err) | |
} | |
} | |
func testGetCurrent() error { | |
a := winrt.NewArrayIterable([]any{1, 2, 3}, winrt.SignatureInt32) | |
it, err := a.First() | |
if err != nil { | |
return err | |
} | |
var ok bool = false | |
for ok, err = it.MoveNext(); err == nil && ok; ok, err = it.MoveNext() { | |
b, err := it.GetHasCurrent() | |
if err != nil { | |
return err | |
} | |
if !b { | |
return fmt.Errorf("expected b to be true") | |
} | |
ptr, err := it.GetCurrent() | |
if err != nil { | |
return err | |
} | |
println(int(uintptr(ptr))) | |
} | |
if err != nil { | |
return err | |
} | |
return nil | |
} | |
func testGetMany() error { | |
a := winrt.NewArrayIterable([]any{1, 2, 3}, winrt.SignatureInt32) | |
it, err := a.First() | |
if err != nil { | |
return err | |
} | |
resp, n, err := it.GetMany(12) | |
if err != nil { | |
return err | |
} | |
println("RESP", n, resp, len(resp)) | |
for i := uint32(0); i < n; i++ { | |
println(resp[i]) | |
} | |
return nil | |
} |
i think I found a bug here: https://gist.github.com/jagobagascon/fd807800ee5c658955a81f0dd1c40bb8#file-array-go-L316 this
outItemsSize
should be set before we iterate and copy over the value tooutItems
; otherwise, it returns 0.
The order of the assignments should not matter, the mistake must be somewhere else. There's quite a bit of pointer arithmetics involved and any mistake can lead to unexpected behaviour. Could you share a code sample so I can test it?
in your existing test at this line: https://gist.github.com/jagobagascon/fd807800ee5c658955a81f0dd1c40bb8#file-main-go-L66
n
always return 0 for me where i expect it to be 3; the number of availableItems
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
i think I found a bug here: https://gist.github.com/jagobagascon/fd807800ee5c658955a81f0dd1c40bb8#file-array-go-L316 this
outItemsSize
should be set before we iterate and copy over the value tooutItems
; otherwise, it returns 0.