Skip to content
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

Invoking method of .NET 5 COM object (inproc server) from .NET 5 client app using the 'dynamic' keyword throws 'NotSupportedException' #47329

Closed
lauxjpn opened this issue Jan 22, 2021 · 6 comments · Fixed by #48037

Comments

@lauxjpn
Copy link

lauxjpn commented Jan 22, 2021

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}");
        }
    }
}
@dotnet-issue-labeler dotnet-issue-labeler bot added area-Interop-coreclr untriaged New issue has not been triaged by the area owner labels Jan 22, 2021
@lauxjpn lauxjpn changed the title Invoking .NET 5 COM object (inproc server) from .NET 5 client app using the 'dynamic' keyword throws 'NotSupportedException' Invoking method of .NET 5 COM object (inproc server) from .NET 5 client app using the 'dynamic' keyword throws 'NotSupportedException' Jan 22, 2021
@lauxjpn
Copy link
Author

lauxjpn commented Jan 23, 2021

I later debugged the CoreCLR with the COMClient.dll loaded:

The code that returns the COR_E_NOTSUPPORTED that gets then translated in the NotSupportedException is the following from GetITypeLibForAssembly:

// If the module wasn't imported from COM, fail. In .NET Framework the runtime
// would generate a ITypeLib instance, but .NET Core doesn't support that.
if (!pAssembly->IsImportedFromTypeLib())
return COR_E_NOTSUPPORTED;

When I just skip the check, my type lib gets loaded successfully and my method gets successfully invoked dynamically as well.

To not manually keep skipping the check, I now added the ImportedFromTypeLibAttribute to my COMServer code:

[assembly: Guid(ContractGuids.TypeLibrary)]
[assembly: ImportedFromTypeLib("COMServer.tlb")] // <-- added

So the project version, where I let a type library being generated from an IDL file via MIDL.exe, manually register it in a [ComRegisterFunctionAttribute] method, manually load the type library in the constructor of the COM class that is being instantiated, let the class explicitly implement ITypeInfo and then delegate all related method calls to the default implementation provided by the type library parser, does now work.

However, discovering the ImportedFromTypeLibAttribute was only possible for me by debugging the CoreCLR itself.

@janvorli
Copy link
Member

cc: @AaronRobinsonMSFT

@AaronRobinsonMSFT
Copy link
Member

@lauxjpn Thank you for the analysis. This is annoying and we should fix this. I believe all that would be needed is removing the following.

// If the module wasn't imported from COM, fail. In .NET Framework the runtime
// would generate a ITypeLib instance, but .NET Core doesn't support that.
if (!pAssembly->IsImportedFromTypeLib())
return COR_E_NOTSUPPORTED;

I don't believe we could get this through .NET 5 servicing given there is a workaround. Would a .NET 6+ fix be acceptable for you?

/cc @elinor-fung @jkoritzinsky

@AaronRobinsonMSFT AaronRobinsonMSFT removed the untriaged New issue has not been triaged by the area owner label Jan 25, 2021
@AaronRobinsonMSFT AaronRobinsonMSFT added this to the 6.0.0 milestone Jan 25, 2021
@lauxjpn
Copy link
Author

lauxjpn commented Jan 25, 2021

Would a .NET 6+ fix be acceptable for you?

I think its fine, as long as the impact of using (or not using) the ImportedFromTypeLibAttribute is explicitly mentioned in the docs at least at three points:

It might also be a good idea to add a dynamic call to the COMServerDemo. While this would also apply to in-proc COM servers, this sample is the only one that demonstrates an approach to build a type library, and it should be good enough to extend it with a bit of code and comments for context.

Finally, in the spirit of James Newton-King's recent tweet, it's definitely a good idea to make the exception message more explicit:

Answer a GitHub issue and you fix an app for a day.
Improve the API error message and you fix every app for a lifetime."
-Ancient JNK proverb


I don't believe we could get this through .NET 5 servicing given there is a workaround.

What's the reasoning (e.g. guidelines) or possible risk for implementing this as a feature in .NET 6 instead of as a bugfix in .NET 5, or is it about consistency within a major release?

@AaronRobinsonMSFT
Copy link
Member

What's the reasoning (e.g. guidelines) or possible risk for implementing this as a feature in .NET 6 instead of as a bugfix in .NET 5, or is it about consistency within a major release?

Risk, always risk. In this case there is a reasonable workaround (i.e. no one is technically blocked) and it isn't worth even the remote possibility of causing issues in some other non-obvious scenario.

@lauxjpn
Copy link
Author

lauxjpn commented Jan 26, 2021

@AaronRobinsonMSFT Thanks for clarifying. In that case, making the exception message more explicit will not be an option for now, because there might exist obscure cases, where apps explicitly check for the current behavior and not (or not only) use the exception type for that, but its message.

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Feb 9, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Feb 17, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Mar 19, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants