-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Low level API support for RCW and CCW management #1845
Comments
Would it be possible to make the return value of |
There is no way to pin byrefs via GCHandles (it would be expensive for the GC to allow it). If you believe that |
This assumes that the vtables won't be ever unloaded. I think it is ok for v1, but I wondering whether we need to do any tweaks in the design now to potentially allow unloading these in future. |
That is just an example. I hope the preface for the Example section wasn't missed. That example is for conceptual understanding for the API review. It is not how I would expect source generators to consume the API.
I don't think so. The source generator here can drive that decision. Once all the CCWs the source generated are unused, that memory can be reclaimed. I would argue that is something the source generator code can track themselves. |
Completely forgot about the implicit conversion. That seems bad actually. I think I am inclined to change that to one of the original designs and return a tuple with a pointer and count - similar to what @jkotas is suggesting above. |
What is the code that the source generator would generate to track this? |
Boo. I just re-realized the whole point of this is for WinUI and in that scenario the caller isn't going to be implementing any part of This is something we should put thought into now. I am loathe to request a callback for when an object is destroyed. Perhaps some kind of enumerating mechanism for the CCW caches? Open to other suggestions as well. |
Thinking more about this, I believe I got distracted from my example and @jkotas's very specific question. @jkotas Users should be able to track this information by returning their own version of I hope that no one will use This means that when the CCW is freed, the managed object can be collected, which means the assembly and all associated memory can be collected. Is that sound or am I missing something? |
How does this coupling happen? Does this mean that all structures involved in this have to be allocated on GC heap? Also, note that infinite pinning is not good for GC performance - it prevents GC from compacting the heap and getting rid of the fragmentation. A different way to solve this would be to introduce method that allocates unmanaged piece of memory that has lifetime attached to lifetime of given Type. E.g. |
Since the data would be the same for all instances of a type, I assume it would something along the lines of the definition below. Then the static class LongLivedData
{
public static ComInterfaceEntry[] Type1= ...
} If there isn't any such efficient mechanism then your allocator API seems reasonable. |
Chatting offline with various people the following updates will be made to the API to reflect the lifetime issues mentioned in #1845 (comment). /cc @jkotas namespace System.Runtime
{
public static partial class RuntimeHelpers
{
/// <summary>
/// Allocate memory that is associated with the <paramref name="type"/> and
/// will be freed if and when the <see cref="System.Type"/> is unloaded.
/// </summary>
/// <param name="type">Type associated with the allocated memory.</param>
/// <param name="size">Amount of memory in bytes to allocate.</param>
/// <returns>The allocated memory</returns>
public static IntPtr AllocateTypeAssociatedMemory(Type type, int size);
}
} In order to mitigate issues mentioned in #1845 (comment), the the /// <summary>
/// Compute the desired VTables for <paramref name="obj"/> respecting the values of <paramref name="flags"/>.
/// </summary>
/// <param name="obj">Target of the returned VTables.</param>
/// <param name="flags">Flags used to compute VTables.</param>
/// <param name="count">The number of elements contained in the returned memory.</param>
/// <returns><see cref="ComInterfaceEntry*" /> containing memory for all COM interface entries.</returns>
/// <remarks>
/// All memory returned from this function must either be unmanaged memory, pinned managed memory, or have been
/// allocated with the <see cref="System.Runtime.RuntimeHelpers.AllocateTypeAssociatedMemory(Type, int)"/> API.
/// </remarks>
protected unsafe abstract ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count); |
Will we support this xplat? That would be awesome. |
@mjsabby That is a good question. For right now the implementation is being written to target only Windows. My plan is to ensure the API exists on Windows for future WinUI/WinRT scenario tooling. If it proves to be valuable outside of that scenario it would be a small amount of work to enable this xplat if demand exists. Edit: The COM Apartment API usage has been removed. |
@AaronRobinsonMSFT Right, if we can cull that it would be a very useful addition to cross platform COM porting. I've seen many libraries benefit from the conventions of COM that don't need registry based activation or COM and would be a breeze to support with the right tooling support, and I say that even if the tooling continues to be windows-based. Consider a +1 from me :) |
APInamespace System.Runtime
{
public static partial class RuntimeHelpers
{
/// <summary>
/// Allocate memory that is associated with the <paramref name="type"/> and
/// will be freed if and when the <see cref="System.Type"/> is unloaded.
/// </summary>
/// <param name="type">Type associated with the allocated memory.</param>
/// <param name="size">Amount of memory in bytes to allocate.</param>
/// <returns>The allocated memory</returns>
public static IntPtr AllocateTypeAssociatedMemory(Type type, int size);
}
}
namespace System.Runtime.InteropServices
{
/// <summary>
/// Enumeration of flags for <see cref="ComWrappers.GetOrCreateComInterfaceForObject(object, CreateComInterfaceFlags)"/>.
/// </summary>
[Flags]
public enum CreateComInterfaceFlags
{
None = 0,
/// <summary>
/// The caller will provide an IUnknown Vtable.
/// </summary>
/// <remarks>
/// This is useful in scenarios when the caller has no need to rely on an IUnknown instance
/// that is used when running managed code is not possible (i.e. during a GC). In traditional
/// COM scenarios this is common, but scenarios involving <see href="https://docs.microsoft.com/windows/win32/api/windows.ui.xaml.hosting.referencetracker/nn-windows-ui-xaml-hosting-referencetracker-ireferencetrackertarget">Reference Tracker hosting</see>
/// calling of the IUnknown API during a GC is possible.
/// </remarks>
CallerDefinedIUnknown = 1,
/// <summary>
/// Flag used to indicate the COM interface should implement <see href="https://docs.microsoft.com/windows/win32/api/windows.ui.xaml.hosting.referencetracker/nn-windows-ui-xaml-hosting-referencetracker-ireferencetrackertarget">IReferenceTrackerTarget</see>.
/// When this flag is passed, the resulting COM interface will have an internal implementation of IUnknown
/// and as such none should be supplied by the caller.
/// </summary>
TrackerSupport = 2,
}
/// <summary>
/// Enumeration of flags for <see cref="ComWrappers.GetOrCreateObjectForComInstance(IntPtr, CreateObjectFlags, object?)"/>.
/// </summary>
[Flags]
public enum CreateObjectFlags
{
None = 0,
/// <summary>
/// Indicate if the supplied external COM object implements the <see href="https://docs.microsoft.com/windows/win32/api/windows.ui.xaml.hosting.referencetracker/nn-windows-ui-xaml-hosting-referencetracker-ireferencetracker">IReferenceTracker</see>.
/// </summary>
TrackerObject = 1,
/// <summary>
/// Ignore any internal caching and always create a unique instance.
/// </summary>
UniqueInstance = 2,
}
/// <summary>
/// Class for managing wrappers of COM IUnknown types.
/// </summary>
[CLSCompliant(false)]
public abstract partial class ComWrappers
{
/// <summary>
/// Interface type and pointer to targeted VTable.
/// </summary>
public struct ComInterfaceEntry
{
/// <summary>
/// Interface IID.
/// </summary>
public Guid IID;
/// <summary>
/// Memory must have the same lifetime as the memory returned from the call to <see cref="ComputeVtables(object, CreateComInterfaceFlags, out int)"/>.
/// </summary>
public IntPtr Vtable;
}
/// <summary>
/// ABI for function dispatch of a COM interface.
/// </summary>
public struct ComInterfaceDispatch
{
public IntPtr vftbl;
/// <summary>
/// Given a <see cref="System.IntPtr"/> from a generated VTable, convert to the target type.
/// </summary>
/// <typeparam name="T">Desired type.</typeparam>
/// <param name="dispatchPtr">Pointer supplied to VTable function entry.</param>
/// <returns>Instance of type associated with dispatched function call.</returns>
public static unsafe T GetInstance<T>(ComInterfaceDispatch* dispatchPtr) where T : class;
}
/// <summary>
/// Create an COM representation of the supplied object that can be passed to an non-managed environment.
/// </summary>
/// <param name="instance">A GC Handle to the managed object to expose outside the .NET runtime.</param>
/// <param name="flags">Flags used to configure the generated interface.</param>
/// <returns>The generated COM interface that can be passed outside the .NET runtime.</returns>
public IntPtr GetOrCreateComInterfaceForObject(object instance, CreateComInterfaceFlags flags);
/// <summary>
/// Compute the desired VTables for <paramref name="obj"/> respecting the values of <paramref name="flags"/>.
/// </summary>
/// <param name="obj">Target of the returned VTables.</param>
/// <param name="flags">Flags used to compute VTables.</param>
/// <param name="count">The number of elements contained in the returned memory.</param>
/// <returns><see cref="ComInterfaceEntry" /> pointer containing memory for all COM interface entries.</returns>
/// <remarks>
/// All memory returned from this function must either be unmanaged memory, pinned managed memory, or have been
/// allocated with the <see cref="System.Runtime.CompilerServices.RuntimeHelpers.AllocateTypeAssociatedMemory(Type, int)"/> API.
///
/// If the interface entries cannot be created and <code>null</code> is returned, the call to <see cref="ComWrappers.GetOrCreateComInterfaceForObject(object, CreateComInterfaceFlags)"/> will throw a <see cref="System.ArgumentNullException"/>.
/// </remarks>
protected unsafe abstract ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count);
/// <summary>
/// Get the currently registered managed object or creates a new managed object and registers it.
/// </summary>
/// <param name="externalComObject">Object to import for usage into the .NET runtime.</param>
/// <param name="flags">Flags used to describe the external object.</param>
/// <param name="wrapper">An optional <see cref="object"/> to be used as the wrapper for the external object</param>
/// <returns>Returns a managed object associated with the supplied external COM object.</returns>
/// <remarks>
/// Providing a <paramref name="wrapper"/> instance means <see cref="ComWrappers.GetOrCreateObjectForComInstance(IntPtr, CreateObjectFlags, object?)"/>
/// will not be called.
///
/// If the <paramref name="wrapper"/> instance already has an associated external object a <see cref="System.NotSupportedException"/> will be thrown.
/// </remarks>
public object GetOrCreateObjectForComInstance(IntPtr externalComObject, CreateObjectFlags flags, object? wrapper = null);
/// <summary>
/// Create a managed object for the object pointed at by <paramref name="externalComObject"/> respecting the values of <paramref name="flags"/>.
/// </summary>
/// <param name="externalComObject">Object to import for usage into the .NET runtime.</param>
/// <param name="flags">Flags used to describe the external object.</param>
/// <returns>Returns a managed object associated with the supplied external COM object.</returns>
/// <remarks>
/// If the object cannot be created and <code>null</code> is returned, the call to <see cref="ComWrappers.GetOrCreateObjectForComInstance(IntPtr, CreateObjectFlags, object?)"/> will throw a <see cref="System.ArgumentNullException"/>.
/// </remarks>
protected abstract object? CreateObject(IntPtr externalComObject, CreateObjectFlags flags);
/// <summary>
/// Called when a request is made for a collection of objects to be released.
/// </summary>
/// <param name="objects">Collection of objects to release.</param>
/// <remarks>
/// The default implementation of this function throws <see cref="System.NotImplementedException"/>.
/// </remarks>
protected virtual void ReleaseObjects(IEnumerable objects);
/// <summary>
/// Register this class's implementation to be used as the single global instance.
/// </summary>
/// <remarks>
/// This function can only be called a single time. Subsequent calls to this function will result
/// in a <see cref="System.InvalidOperationException"/> being thrown.
///
/// Scenarios where the global instance may be used are:
/// * Object tracking via the <see cref="CreateComInterfaceFlags.TrackerSupport" /> and <see cref="CreateObjectFlags.TrackerObject" /> flags.
/// * Usage of COM related Marshal APIs.
/// </remarks>
public void RegisterAsGlobalInstance();
/// <summary>
/// Get the runtime provided IUnknown implementation.
/// </summary>
/// <param name="fpQueryInterface">Function pointer to QueryInterface.</param>
/// <param name="fpAddRef">Function pointer to AddRef.</param>
/// <param name="fpRelease">Function pointer to Release.</param>
protected static void GetIUnknownImpl(out IntPtr fpQueryInterface, out IntPtr fpAddRef, out IntPtr fpRelease);
}
} |
Design has been validated by the CsWinRT team. |
@AaronRobinsonMSFT Would this API allow for converting an IntPtr into a managed ComImport interface? I'm thinking about the following use case as an example [ComImport]
[Guid("0000010C-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public unsafe interface IPersist
{
[PreserveSig]
int GetClassID(
Guid* pClassID);
}
public unsafe void Method()
{
var control = new Control();
IntPtr pUnk = Marshal.GetIUnknownForObject(control);
Guid iid = typeof(IPersist).GUID;
int hr = Marshal.QueryInterface(pUnk, ref iid, out IntPtr pPersist);
Debug.Assert(hr == 0);
IPersist persist = ???
// Do something with persist. E.g.
Guid classId;
hr = persist.GetClassID(&classId);
Debug.Assert(hr == 0);
} The class However, what I actually want to do is to call methods on the I can't directly cast Any ideas? |
@hughbe I am going to assume we are talking about the WinForms This API could perform the desired actions, but that would be a decent amount of work and isn't scalable. VTable layouts would need to be defined and allocated and most of that code is monotonous and easy to get wrong. There is a test that shows an example of consuming a trivial Instead I would recommend using the public unsafe void Method()
{
dynamic control = new Control();
Guid classId;
hr = control.GetClassID(&classId);
Debug.Assert(hr == 0);
} If the The |
Work has begun to provide support for WinUI 3.0. This support is expected to manifest in a way similar to the CppWinRT tool by way of a new source generation tool (e.g. CsWinRT). In order to support this new tool, APIs for integrating and coordinating with the runtime object lifetime are necessary.
Rationale and Usage
The below API surface provides a way for a third party tool to generate what are colloquially known as Runtime Callable Wrappers (RCW) and COM Callable Wrappers (CCW) in a way that allows safe interaction with managed object lifetime and identity.
A specific example of the need for lifetime coordination is in WinRT scenarios involving UI (e.g. WinUI 3.0) via the
IReferenceTrackerManager
interface.Goals:
Non-Goals:
Outstanding questions:
Proposed API
Example usage
The below example is merely for illustrative purposes. In a production ready consumption of the API many of the
Marshal
APIs would not be used and the VTable layouts should be done in a static manner for optimal efficiency./cc @jkotas @Scottj1s @dunhor @jkoritzinsky @davidwrighton @terrajobst @tannergooding @jeffschwMSFT
The text was updated successfully, but these errors were encountered: