-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Add opt-in support for GeneratedComInterface/ComImport RCW interop #87583
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
Changes from all commits
4c34ffb
34f4120
5500bde
bb808ac
9d09747
971a2a5
dcd4402
5406981
b058b40
d0c0b3e
9e915fc
670799a
af91b01
3124755
cc068b3
e680980
7f36032
c42589d
682ba71
35f25a8
3b5ae8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Semantic Compatibility | ||
|
||
Documentation on compatibility guidance and the current state. The version headings act as a rolling delta between the previous version. | ||
|
||
## .NET 8 | ||
|
||
### Interface base types | ||
|
||
IUnknown-derived interfaces are supported. IDispatch-based interfaces are disallowed. The default is IUnknown-derived (in comparison to the built-in support's default of IDispatch-derived). | ||
|
||
### Marshalling rules | ||
|
||
The marshalling rules are identical to LibraryImportGenerator's support. | ||
|
||
### Interface inheritance | ||
|
||
Interface inheritance is supported for up to one COM-based interface type. Unlike the built-in COM interop system, base interface methods do **NOT** need to be redefined. The source generator discovers the members from the base interface and generates the derived interface members at appropriate offsets. | ||
|
||
The generator also generates shadow members in the derived interface for each base interface member. The shadow members have default implementations that call the base interface member, but the emitted code for the "COM Object Wrapper" implementation will override the shadow members with a call to the underlying COM interface member on the current interface. This shadow member support helps reduce `QueryInterface` overhead in interface inheritance scenarios. | ||
|
||
### Interop with `ComImport` | ||
|
||
Source-generated COM will provide limited opt-in interop with `ComImport`-based COM interop. In particular, the following scenarios are supported: | ||
|
||
- Casting a "Com Object Wrapper" created using `StrategyBasedComWrappers` to a `ComImport`-based interface type. | ||
|
||
This support is achieved through some internal interfaces and reflection-emit to shim a `DynamicInterfaceCastableImplementation` of a `ComImport` interface to use the built-in runtime interop marshalling support. The core of this experience is implemented by the `System.Runtime.InteropServices.Marshalling.ComImportInteropInterfaceDetailsStrategy` class. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<linker> | ||
<assembly fullname="System.Runtime.InteropServices"> | ||
<type fullname="System.Runtime.InteropServices.Marshalling.ComObject" feature="System.Runtime.InteropServices.Marshalling.EnableGeneratedComInterfaceComImportInterop" featurevalue="false"> | ||
<method signature="System.Boolean get_ComImportInteropEnabled()" body="stub" value="false" /> | ||
</type> | ||
<type fullname="System.Runtime.InteropServices.Marshalling.ComObject" feature="System.Runtime.InteropServices.BuiltInComInterop.IsSupported" featurevalue="false"> | ||
<method signature="System.Boolean get_BuiltInComSupported()" body="stub" value="false" /> | ||
</type> | ||
</assembly> | ||
</linker> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Diagnostics.CodeAnalysis; | ||
using System.Reflection; | ||
using System.Reflection.Emit; | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace System.Runtime.InteropServices.Marshalling | ||
{ | ||
/// <summary> | ||
/// An interface details strategy that enables discovering both interfaces defined with source-generated COM (i.e. <see cref="GeneratedComInterfaceAttribute"/> and <see cref="IUnknownDerivedAttribute{T, TImpl}"/>) and built-in COM (i.e. <see cref="ComImportAttribute"/>). | ||
/// </summary> | ||
/// <remarks> | ||
/// This strategy is meant for intermediary adoption scenarios and is not compatible with trimming or NativeAOT by design. Since built-in COM is not trim friendly or AOT-compatible, these restrictions are okay. | ||
/// This strategy only supports "COM Object Wrapper" scenarios, so casting a COM object wrapper to a <see cref="ComImportAttribute"/>-attributed type. It does not support exposing a <see cref="ComImportAttribute"/>-attributed type as an additional interface on a managed object wrapper. | ||
/// The strategy provides <see cref="DynamicInterfaceCastableImplementationAttribute"/>-based implementations of <see cref="ComImportAttribute"/>-attributed interfaces by dynamically generating an interface using <see cref="System.Reflection.Emit"/> that has the following shape: | ||
/// <code> | ||
/// [assembly:IgnoresAccessChecksTo("AssemblyContainingIComInterface")] | ||
/// [assembly:IgnoresAccessChecksTo("AssemblyContainingRetType")] | ||
/// [assembly:IgnoresAccessChecksTo("AssemblyContainingArgType1")] | ||
/// [assembly:IgnoresAccessChecksTo("AssemblyContainingArgType2")] | ||
/// // One attribute per containing assembly of each type used in each method signature of the interface. | ||
/// | ||
/// namespace System.Runtime.CompilerServices | ||
/// { | ||
/// [AssemblyUsage(AttributeTargets.Assembly, AllowMultiple = true)] | ||
/// internal class IgnoresAccessChecksToAttribute : Attribute | ||
/// { | ||
/// public IgnoresAccessChecksToAttribute(string assemblyName) { } | ||
/// } | ||
/// } | ||
/// | ||
/// [DynamicInterfaceCastableImplementation] | ||
/// interface InterfaceForwarder : IComInterface | ||
/// { | ||
/// RetType IComInterface.Method1(ArgType1 arg1, ArgType2 arg2, ...) | ||
/// { | ||
/// return ((IComInterface)((IComImportAdapter)this).GetRuntimeCallableWrapper())(arg1, arg2, ...); | ||
/// } | ||
/// } | ||
/// </code> | ||
/// | ||
/// This mechanism allows source-generated COM interop to allow using built-in COM interfaces with runtime-defined marshalling behavior with minimal work on the source-generated COM interop side. | ||
/// Additionally, by scoping the majority of the logic to this class, we make this logic more easily trimmable. | ||
/// | ||
/// We emit the <c>IgnoresAccessChecksToAttribute</c> to enable casting to internal <see cref="ComImportAttribute"/> types, which is a very common scenario (most <see cref="ComImportAttribute"/> types are internal). | ||
/// </remarks> | ||
[RequiresDynamicCode("Enabling interop between source-generated and built-in COM is not supported when trimming is enabled.")] | ||
[RequiresUnreferencedCode("Enabling interop between source-generated and built-in COM requires dynamic code generation.")] | ||
internal sealed class ComImportInteropInterfaceDetailsStrategy : IIUnknownInterfaceDetailsStrategy | ||
{ | ||
public static readonly IIUnknownInterfaceDetailsStrategy Instance = new ComImportInteropInterfaceDetailsStrategy(); | ||
|
||
private readonly ConditionalWeakTable<Type, Type> _forwarderInterfaceCache = new(); | ||
|
||
// TODO: Support exposing ComImport interfaces through StrategyBasedComWrappers? | ||
public IComExposedDetails? GetComExposedTypeDetails(RuntimeTypeHandle type) => DefaultIUnknownInterfaceDetailsStrategy.Instance.GetComExposedTypeDetails(type); | ||
|
||
public IIUnknownDerivedDetails? GetIUnknownDerivedDetails(RuntimeTypeHandle type) | ||
{ | ||
Type runtimeType = Type.GetTypeFromHandle(type)!; | ||
if (!runtimeType.IsImport) | ||
{ | ||
return DefaultIUnknownInterfaceDetailsStrategy.Instance.GetIUnknownDerivedDetails(type); | ||
} | ||
|
||
Type implementationType = _forwarderInterfaceCache.GetValue(runtimeType, runtimeType => | ||
{ | ||
AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("ComImportForwarder"), runtimeType.IsCollectible ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run); | ||
ModuleBuilder module = assembly.DefineDynamicModule("ComImportForwarder"); | ||
|
||
ConstructorInfo ignoresAccessChecksToAttributeConstructor = GetIgnoresAccessChecksToAttributeConstructor(module); | ||
|
||
assembly.SetCustomAttribute(new CustomAttributeBuilder(ignoresAccessChecksToAttributeConstructor, new object[] { typeof(IComImportAdapter).Assembly.GetName().Name! })); | ||
|
||
TypeBuilder implementation = module.DefineType("InterfaceForwarder", TypeAttributes.Interface | TypeAttributes.Abstract, parent: null, interfaces: runtimeType.GetInterfaces()); | ||
implementation.AddInterfaceImplementation(runtimeType); | ||
implementation.SetCustomAttribute(new CustomAttributeBuilder(typeof(DynamicInterfaceCastableImplementationAttribute).GetConstructor(Array.Empty<Type>())!, Array.Empty<object>())); | ||
|
||
foreach (Type iface in implementation.GetInterfaces()) | ||
{ | ||
assembly.SetCustomAttribute(new CustomAttributeBuilder(ignoresAccessChecksToAttributeConstructor, new object[] { iface.Assembly.GetName().Name! })); | ||
foreach (MethodInfo method in iface.GetMethods()) | ||
{ | ||
Type[] returnTypeOptionalModifiers = method.ReturnParameter.GetOptionalCustomModifiers(); | ||
Type[] returnTypeRequiredModifiers = method.ReturnParameter.GetRequiredCustomModifiers(); | ||
ParameterInfo[] parameters = method.GetParameters(); | ||
var parameterTypes = new Type[parameters.Length]; | ||
var parameterOptionalModifiers = new Type[parameters.Length][]; | ||
var parameterRequiredModifiers = new Type[parameters.Length][]; | ||
for (int i = 0; i < parameters.Length; i++) | ||
{ | ||
parameterTypes[i] = parameters[i].ParameterType; | ||
parameterOptionalModifiers[i] = parameters[i].GetOptionalCustomModifiers(); | ||
parameterRequiredModifiers[i] = parameters[i].GetRequiredCustomModifiers(); | ||
} | ||
MethodBuilder builder = implementation.DefineMethod(method.Name, MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.HasThis, method.ReturnType, returnTypeRequiredModifiers, returnTypeOptionalModifiers, parameterTypes, parameterRequiredModifiers, parameterOptionalModifiers); | ||
ILGenerator il = builder.GetILGenerator(); | ||
il.Emit(OpCodes.Ldarg_0); | ||
il.Emit(OpCodes.Castclass, typeof(IComImportAdapter)); | ||
il.Emit(OpCodes.Callvirt, IComImportAdapter.GetRuntimeCallableWrapperMethod); | ||
il.Emit(OpCodes.Castclass, iface); | ||
for (int i = 0; i < parameters.Length; i++) | ||
{ | ||
il.Emit(OpCodes.Ldarg, i + 1); | ||
} | ||
il.Emit(OpCodes.Callvirt, method); | ||
il.Emit(OpCodes.Ret); | ||
implementation.DefineMethodOverride(builder, method); | ||
} | ||
} | ||
|
||
return implementation.CreateType(); | ||
}); | ||
|
||
return new ComImportDetails(runtimeType.GUID, implementationType); | ||
} | ||
|
||
private static ConstructorInfo GetIgnoresAccessChecksToAttributeConstructor(ModuleBuilder moduleBuilder) | ||
{ | ||
Type attributeType = EmitIgnoresAccessChecksToAttribute(moduleBuilder); | ||
return attributeType.GetConstructor(new Type[] { typeof(string) })!; | ||
} | ||
|
||
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] | ||
private static Type EmitIgnoresAccessChecksToAttribute(ModuleBuilder moduleBuilder) | ||
{ | ||
var tb = moduleBuilder.DefineType( | ||
"System.Runtime.CompilerServices.IgnoresAccessChecksToAttribute", | ||
TypeAttributes.NotPublic, | ||
typeof(Attribute)); | ||
|
||
var attributeUsage = new CustomAttributeBuilder( | ||
s_attributeUsageCtor, | ||
new object[] { AttributeTargets.Assembly }, | ||
new PropertyInfo[] { s_attributeUsageAllowMultipleProperty }, | ||
new object[] { true }); | ||
tb.SetCustomAttribute(attributeUsage); | ||
|
||
var cb = tb.DefineConstructor( | ||
MethodAttributes.Public | | ||
MethodAttributes.HideBySig | | ||
MethodAttributes.SpecialName | | ||
MethodAttributes.RTSpecialName, | ||
CallingConventions.Standard, | ||
new Type[] { typeof(string) }); | ||
cb.DefineParameter(1, ParameterAttributes.None, "assemblyName"); | ||
|
||
var il = cb.GetILGenerator(); | ||
il.Emit(OpCodes.Ldarg_0); | ||
il.Emit(OpCodes.Call, s_attributeBaseClassCtor); | ||
il.Emit(OpCodes.Ret); | ||
|
||
return tb.CreateType()!; | ||
} | ||
|
||
/// <summary> | ||
/// The <see cref="Attribute()"/> constructor. | ||
/// </summary> | ||
private static readonly ConstructorInfo s_attributeBaseClassCtor = typeof(Attribute).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0]; | ||
|
||
/// <summary> | ||
/// The <see cref="AttributeUsageAttribute(AttributeTargets)"/> constructor. | ||
/// </summary> | ||
private static readonly ConstructorInfo s_attributeUsageCtor = typeof(AttributeUsageAttribute).GetConstructor(new Type[] { typeof(AttributeTargets) })!; | ||
|
||
/// <summary> | ||
/// The <see cref="AttributeUsageAttribute.AllowMultiple"/> property. | ||
/// </summary> | ||
private static readonly PropertyInfo s_attributeUsageAllowMultipleProperty = typeof(AttributeUsageAttribute).GetProperty(nameof(AttributeUsageAttribute.AllowMultiple))!; | ||
|
||
private sealed class ComImportDetails(Guid iid, Type implementation) : IIUnknownDerivedDetails | ||
{ | ||
public Guid Iid { get; } = iid; | ||
|
||
public Type Implementation { get; } = implementation; | ||
|
||
public unsafe void** ManagedVirtualMethodTable => null; | ||
} | ||
|
||
/// <summary> | ||
/// This interface enables a COM Object Wrapper (such as <see cref="ComObject"/>) to provide a built-in COM object to enable integration between built-in COM objects and | ||
/// other COM interop systems like source-generated COM. | ||
/// </summary> | ||
internal interface IComImportAdapter | ||
jkoritzinsky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
internal static readonly MethodInfo GetRuntimeCallableWrapperMethod = typeof(IComImportAdapter).GetMethod(nameof(GetRuntimeCallableWrapper))!; | ||
|
||
/// <summary> | ||
/// Gets the built-in COM object that corresponds to the same underlying COM object as this wrapper. | ||
/// </summary> | ||
/// <returns>The built-in RCW</returns> | ||
/// <remarks>The returned object must be an object such that a call to <see cref="Marshal.IsComObject(object)"/> would return true.</remarks> | ||
object GetRuntimeCallableWrapper(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,16 +5,23 @@ | |
// This API need to be exposed to implement the COM source generator in one form or another. | ||
|
||
using System.Diagnostics; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Runtime.Versioning; | ||
|
||
namespace System.Runtime.InteropServices.Marshalling | ||
{ | ||
/// <summary> | ||
/// Base class for all COM source generated Runtime Callable Wrapper (RCWs). | ||
/// </summary> | ||
public sealed unsafe class ComObject : IDynamicInterfaceCastable, IUnmanagedVirtualMethodTableProvider | ||
public sealed unsafe class ComObject : IDynamicInterfaceCastable, IUnmanagedVirtualMethodTableProvider, ComImportInteropInterfaceDetailsStrategy.IComImportAdapter | ||
{ | ||
internal static bool BuiltInComSupported { get; } = AppContext.TryGetSwitch("System.Runtime.InteropServices.BuiltInComInterop.IsSupported", out bool supported) ? supported : true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That API isn't exposed outside of CoreLib (we never made it public). I should be able to make it exposed enough for System.Runtime.InteropServices to see it I think without making it public if we would prefer. |
||
internal static bool ComImportInteropEnabled { get; } = AppContext.TryGetSwitch("System.Runtime.InteropServices.Marshalling.EnableGeneratedComInterfaceComImportInterop", out bool enabled) ? enabled : false; | ||
jkoritzinsky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private readonly void* _instancePointer; | ||
|
||
private readonly object? _runtimeCallableWrapper; | ||
jkoritzinsky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// Initialize ComObject instance. | ||
/// </summary> | ||
|
@@ -28,6 +35,11 @@ internal ComObject(IIUnknownInterfaceDetailsStrategy interfaceDetailsStrategy, I | |
IUnknownStrategy = iunknownStrategy; | ||
CacheStrategy = cacheStrategy; | ||
_instancePointer = IUnknownStrategy.CreateInstancePointer(thisPointer); | ||
if (OperatingSystem.IsWindows() && BuiltInComSupported && ComImportInteropEnabled) | ||
{ | ||
_runtimeCallableWrapper = Marshal.GetObjectForIUnknown((nint)thisPointer); | ||
Debug.Assert(Marshal.IsComObject(_runtimeCallableWrapper)); | ||
} | ||
} | ||
|
||
~ComObject() | ||
|
@@ -135,5 +147,11 @@ VirtualMethodTableInfo IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTabl | |
|
||
return new(result.ThisPtr, result.Table); | ||
} | ||
|
||
object ComImportInteropInterfaceDetailsStrategy.IComImportAdapter.GetRuntimeCallableWrapper() | ||
jkoritzinsky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
Debug.Assert(_runtimeCallableWrapper != null); | ||
return _runtimeCallableWrapper; | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.