From 9023e7292416f5da591a736959992fa298cef43e Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Fri, 4 Oct 2024 13:57:53 -0700 Subject: [PATCH] Add Lifetime from WInterop Add a basic test looking at the behavior of the runtime's RCW. --- src/thirtytwo/NativeMethods.txt | 2 + src/thirtytwo/Win32/System/Com/Lifetime.cs | 80 ++++++++++ .../Win32/System/Com/ComTests.cs | 145 ++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 src/thirtytwo/Win32/System/Com/Lifetime.cs diff --git a/src/thirtytwo/NativeMethods.txt b/src/thirtytwo/NativeMethods.txt index dd5ba7c..9d0d814 100644 --- a/src/thirtytwo/NativeMethods.txt +++ b/src/thirtytwo/NativeMethods.txt @@ -22,6 +22,7 @@ CoGetClassObject CombineRgn COMBOBOXINFO_BUTTON_STATE CopyImage +CoTaskMemAlloc CoTaskMemFree CountClipboardFormats CreateActCtx @@ -224,6 +225,7 @@ IMarshal IModalWindow InitCommonControlsEx InitVariantFromDoubleArray +INoMarshal INTERFACEDATA INVALID_HANDLE_VALUE InvalidateRect diff --git a/src/thirtytwo/Win32/System/Com/Lifetime.cs b/src/thirtytwo/Win32/System/Com/Lifetime.cs new file mode 100644 index 0000000..cf132c7 --- /dev/null +++ b/src/thirtytwo/Win32/System/Com/Lifetime.cs @@ -0,0 +1,80 @@ +// Copyright (c) Jeremy W. Kuhne. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace Windows.Win32.System.Com; + +/// +/// Lifetime management helper for a COM callable wrapper. It holds the created +/// wrapper with he given . +/// +/// +/// +/// This should not be created directly. Instead use . +/// +/// +/// A COM object's memory layout is a virtual function table (vtable) pointer followed by instance data. We're +/// effectively manually creating a COM object here that contains instance data of a GCHandle to the related +/// managed object and a ref count. +/// +/// +public unsafe struct Lifetime where TVTable : unmanaged +{ + public TVTable* VTable; + public IUnknown* Handle; + public uint RefCount; + + public static unsafe uint AddRef(IUnknown* @this) + => Interlocked.Increment(ref ((Lifetime*)@this)->RefCount); + + public static unsafe uint Release(IUnknown* @this) + { + var lifetime = (Lifetime*)@this; + Debug.Assert(lifetime->RefCount > 0); + uint count = Interlocked.Decrement(ref lifetime->RefCount); + if (count == 0) + { + GCHandle.FromIntPtr((nint)lifetime->Handle).Free(); + Interop.CoTaskMemFree(lifetime); + } + + return count; + } + + /// + /// Allocate a lifetime wrapper for the given with the given + /// . + /// + /// + /// + /// This creates a to root the until ref + /// counting has gone to zero. + /// + /// + /// The should be fixed, typically as a static. Com calls always + /// include the "this" pointer as the first argument. + /// + /// + public static unsafe Lifetime* Allocate(TObject @object, TVTable* vtable) + { + // Manually allocate a native instance of this struct. + var wrapper = (Lifetime*)Interop.CoTaskMemAlloc((nuint)sizeof(Lifetime)); + + // Assign a pointer to the vtable, allocate a GCHandle for the related object, and set the initial ref count. + wrapper->VTable = vtable; + wrapper->Handle = (IUnknown*)GCHandle.ToIntPtr(GCHandle.Alloc(@object)); + wrapper->RefCount = 1; + + return wrapper; + } + + /// + /// Gets the object wrapped by a lifetime wrapper. + /// + public static TObject? GetObject(IUnknown* @this) + { + var lifetime = (Lifetime*)@this; + return (TObject?)GCHandle.FromIntPtr((nint)lifetime->Handle).Target; + } +} \ No newline at end of file diff --git a/src/thirtytwo_tests/Win32/System/Com/ComTests.cs b/src/thirtytwo_tests/Win32/System/Com/ComTests.cs index b2ff04a..7d5ab72 100644 --- a/src/thirtytwo_tests/Win32/System/Com/ComTests.cs +++ b/src/thirtytwo_tests/Win32/System/Com/ComTests.cs @@ -1,8 +1,13 @@ // Copyright (c) Jeremy W. Kuhne. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Windows.Dialogs; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com.Marshal; using Windows.Win32.UI.Shell; +using InteropMarshal = global::System.Runtime.InteropServices.Marshal; namespace Windows.Win32.System.Com; @@ -27,4 +32,144 @@ public void Com_GetComPointer_SameInterfaceInstance() Assert.True(iEvents1.Pointer == iEvents2.Pointer); } + + [Fact] + public void Com_BuiltInCom_RCW_Behavior() + { + UnknownTest unknown = new(); + using ComScope iUnknown = new(UnknownCCW.CreateInstance(unknown)); + + object rcw = InteropMarshal.GetObjectForIUnknown((IntPtr)iUnknown.Pointer); + + unknown.AddRefCount.Should().Be(1); + unknown.ReleaseCount.Should().Be(1); + unknown.LastRefCount.Should().Be(2); + unknown.QueryInterfaceGuids.Should().BeEquivalentTo([ + IUnknown.IID_Guid, + INoMarshal.IID_Guid, + IAgileObject.IID_Guid, + IMarshal.IID_Guid]); + + // Release and FinalRelease look the same from our IUnknown's perspective + InteropMarshal.FinalReleaseComObject(rcw); + + unknown.AddRefCount.Should().Be(1); + unknown.ReleaseCount.Should().Be(2); + unknown.LastRefCount.Should().Be(1); + unknown.QueryInterfaceGuids.Should().BeEquivalentTo([ + IUnknown.IID_Guid, + INoMarshal.IID_Guid, + IAgileObject.IID_Guid, + IMarshal.IID_Guid]); + } + + public interface IUnkownTest + { + public void QueryInterface(Guid riid); + public void AddRef(uint current); + public void Release(uint current); + } + + public class UnknownTest : IUnkownTest + { + public int AddRefCount { get; private set; } + public int ReleaseCount { get; private set; } + public List QueryInterfaceGuids { get; } = []; + public int LastRefCount { get; private set; } + + void IUnkownTest.AddRef(uint current) + { + AddRefCount++; + LastRefCount = (int)current; + } + + void IUnkownTest.QueryInterface(Guid riid) + { + QueryInterfaceGuids.Add(riid); + } + + void IUnkownTest.Release(uint current) + { + ReleaseCount++; + LastRefCount = (int)current; + } + } + + public static class UnknownCCW + { + public static unsafe IUnknown* CreateInstance(IUnkownTest @object) + => (IUnknown*)Lifetime.Allocate(@object, CCWVTable); + + private static readonly IUnknown.Vtbl* CCWVTable = AllocateVTable(); + + private static unsafe IUnknown.Vtbl* AllocateVTable() + { + // Allocate and create a static VTable for this type projection. + var vtable = (IUnknown.Vtbl*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(UnknownCCW), sizeof(IUnknown.Vtbl)); + + // IUnknown + vtable->QueryInterface_1 = &QueryInterface; + vtable->AddRef_2 = &AddRef; + vtable->Release_3 = &Release; + return vtable; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])] + private static HRESULT QueryInterface(IUnknown* @this, Guid* riid, void** ppvObject) + { + if (ppvObject is null) + { + return HRESULT.E_POINTER; + } + + var unknown = Lifetime.GetObject(@this); + if (unknown is null) + { + return HRESULT.COR_E_OBJECTDISPOSED; + } + + unknown.QueryInterface(*riid); + + if (*riid == typeof(IUnknown).GUID) + { + *ppvObject = @this; + } + else + { + *ppvObject = null; + return HRESULT.E_NOINTERFACE; + } + + Lifetime.AddRef(@this); + return HRESULT.S_OK; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])] + private static uint AddRef(IUnknown* @this) + { + var unknown = Lifetime.GetObject(@this); + if (unknown is null) + { + return HRESULT.COR_E_OBJECTDISPOSED; + } + + uint current = Lifetime.AddRef(@this); + unknown.AddRef(current); + return current; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])] + private static uint Release(IUnknown* @this) + { + var unknown = Lifetime.GetObject(@this); + if (unknown is null) + { + return HRESULT.COR_E_OBJECTDISPOSED; + } + + uint current = Lifetime.Release(@this); + unknown.Release(current); + return current; + } + } }