Description
Dynamic support for COM objects has been added in .NET 5 (see #12587).
However, I am unable call methods of a .NET 5 COM class (inproc server) from a .NET 5 client app using the dynamic
keyword.
I used the COM Server Demo sample as a base and altered it in a way that I would expect to work when invoked dynamically.
The COMClient\WscriptClient.js
script executes correctly and demonstrates, that IDispatch
is implemented by the CCW and works as expected:
PS E:\Sources\COM\COMClient> cscript.exe .\WScriptClient.js
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.
PI: 3.140616091322624
The script is simple:
// Works as expected:
var server = new ActiveXObject("ComServerVbs.ServerVbs");
var pi = server.ComputePi();
WScript.Echo("PI: " + pi);
However, when running the COMClient.exe
, I get the following exception when dynamically invoking the ComputePi()
method:
Exception thrown: 'System.NotSupportedException' in System.Private.CoreLib.dll
An unhandled exception of type 'System.NotSupportedException' occurred in System.Private.CoreLib.dll
Specified method is not supported.
Full stack trace
at System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.cs:line 601
at Microsoft.CSharp.RuntimeBinder.ComInterop.ComRuntimeHelpers.GetITypeInfoFromIDispatch(IDispatch dispatch) in /_/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/ComInterop/ComRuntimeHelpers.cs:line 120
at Microsoft.CSharp.RuntimeBinder.ComInterop.IDispatchComObject.EnsureScanDefinedMethods() in /_/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/ComInterop/IDispatchComObject.cs:line 633
at Microsoft.CSharp.RuntimeBinder.ComInterop.IDispatchComObject.System.Dynamic.IDynamicMetaObjectProvider.GetMetaObject(Expression parameter) in /_/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/ComInterop/IDispatchComObject.cs:line 319
at System.Dynamic.DynamicMetaObject.Create(Object value, Expression expression) in /_/src/libraries/System.Linq.Expressions/src/System/Dynamic/DynamicMetaObject.cs:line 287
at System.Dynamic.DynamicMetaObjectBinder.Bind(Object[] args, ReadOnlyCollection`1 parameters, LabelTarget returnLabel) in /_/src/libraries/System.Linq.Expressions/src/System/Dynamic/DynamicMetaObjectBinder.cs:line 87
at System.Runtime.CompilerServices.CallSiteBinder.BindCore[T](CallSite`1 site, Object[] args) in /_/src/libraries/System.Linq.Expressions/src/System/Runtime/CompilerServices/CallSiteBinder.cs:line 128
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0) in /_/src/libraries/System.Linq.Expressions/src/System/Dynamic/UpdateDelegates.Generated.cs:line 115
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0) in /_/src/libraries/System.Linq.Expressions/src/System/Dynamic/UpdateDelegates.Generated.cs:line 124
at COMClient.Program.Main(String[] args) in E:\Sources\COM_Vbs\COMClient\Program.cs:line 18
The script is simple as well:
using System;
namespace COMClient
{
class Program
{
static void Main(string[] args)
{
var serverType = Type.GetTypeFromCLSID(new Guid(ContractGuids.ServerClass));
var serverObject = Activator.CreateInstance(serverType);
// This works:
// var server = (IServer) serverObject;
// This does not work:
dynamic server = serverObject;
var pi = server.ComputePi(); // <-- throws System.NotSupportedException (for dynamic call)
Console.WriteLine($"\u03C0 = {pi}");
}
}
}
According to the full stack trace, the exception is being thrown in ComRuntimeHelpers.GetITypeInfoFromIDispatch()
, where it is being thrown for the HRESULT
of dispatch.TryGetTypeInfoCount(out uint typeCount)
.
This might be a bug. The remarks section states:
Some COM objects just dont expose typeinfo. In these cases, this method will return null.
Some COM objects do intend to expose typeinfo, but may not be able to do so if the type-library is not properly
registered. This will be considered as acceptable or as an error condition depending on throwIfMissingExpectedTypeInfo
The docs at COM Callable Wrapper: Simulating COM interfaces state, that ITypeInfo
is not implemented by the CCW for .NET Core:
Interface Description ITypeInfo (.NET Framework only) Provides type information for a class that is exactly the same as the type information produced by Tlbexp.exe.
Which might be indirectly backed up by #3740.
However, if a scripting host like WScript is able to dynamically call a dual interface via IDispatch
, I would expect the same to be true for a .NET 5 client (at least for common cases).
I also created another project version, that explicitly generates a type library from an IDL file using the MIDL.exe tool (I changed the GUIDs for this project version).
Contract.idl
[
uuid(B2EE0DB3-972B-4AE3-95DD-9DB2AD5B6CDB),
version(1.0)
]
library COMServer
{
importlib("stdole2.tlb");
[
object,
oleautomation,
dual,
uuid(402FB956-E484-4C25-8A89-8E26C5B588CA),
version(1.0)
]
interface IServer : IDispatch {
[id(1)]
HRESULT ComputePi([out, retval] double* pRetVal);
};
[
uuid(65012759-8F78-40EC-8BB7-48741B178251),
version(1.0)
]
coclass Server {
[default] interface IServer;
};
};
When the COM class is registered, the project also registers the type library (via [ComRegisterFunction]
).
When my Server
COM class is instantiated, I load the type library and retrieve the default ITypeInfo
interface (provided by the type library parser) in the class constructor (see Essential COM page 353 by @donbox):
[ComVisible(true)]
[Guid(ContractGuids.ServerClass)]
[ProgId("ComServerTlb.ServerTlb")]
[ComDefaultInterface(typeof(IServer))]
public class Server : IServer, ITypeInfo
{
private ITypeInfo _typeInfo;
public Server()
{
var libid = new Guid(ContractGuids.TypeLibrary);
Marshal.ThrowExceptionForHR(
TypeLib.OleAut32.LoadRegTypeLib(ref libid, 1, 0, 0, out var typeLib));
var iidIServer = new Guid(ContractGuids.ServerInterface);
typeLib.GetTypeInfoOfGuid(ref iidIServer, out _typeInfo);
}
// ...
#region ITypeInfo
void ITypeInfo.AddressOfMember(int memid, INVOKEKIND invKind, out IntPtr ppv)
=> _typeInfo.AddressOfMember(memid, invKind, out ppv);
// ...
My Server
COM class explicitly implements ITypeInfo
and forwards all calls to the retrieved default implementation. According to COM Callable Wrapper: Simulating COM interfaces, my explicit ITypeInfo
implementation should be honored:
A .NET class can override the default behavior by providing its own implementation of these interfaces.
The COMClient project demonstrates, that the type information is available via the interface, but the internal ComRuntimeHelpers.GetITypeInfoFromIDispatch()
call by .NET when dynamically invoking ComputePi()
throws the same NotSupportedException
as before:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices.ComTypes;
namespace COMClient
{
class Program
{
static void Main(string[] args)
{
var serverType = Type.GetTypeFromCLSID(new Guid(ContractGuids.ServerClass));
var serverObject = Activator.CreateInstance(serverType);
// ITypeInfo is available and returns expected data.
var typeInfo = (ITypeInfo)serverObject;
string[] names = new string[256];
typeInfo.GetNames(1, names, 256, out var namesCount);
Trace.Assert(namesCount == 1);
Trace.Assert(names[0] == "ComputePi");
// This works:
// var server = (IServer) serverObject;
// This does not work:
dynamic server = serverObject;
var pi = server.ComputePi(); // <-- throws System.NotSupportedException (for dynamic call)
Console.WriteLine($"\u03C0 = {pi}");
}
}
}