microsoft / CsWinRT

C# language projection for the Windows Runtime
MIT License
554 stars 105 forks source link

With AOT support, authoring a COM class that derives from WinRT interfaces doesn't work (or causes System.ExecutionEngineException) #1722

Open smourier opened 2 months ago

smourier commented 2 months ago

Describe the bug With AOT, GeneratedComClasses and GeneratedComInterfaces and with runtime marshaling disabled, I'm trying to create a COM class that derives from a C#/WinRT generated interface, for example IGeometrySource2D (it's the same for all interfaces but I tried to choose a simple one) and it doesn't work, it throws:

Unhandled exception. System.ArgumentException: The parameter is incorrect.
Invalid argument to parameter source. Object parameter must not be null.
   at WinRT.ExceptionHelpers.<ThrowExceptionForHR>g__Throw|38_0(Int32 hr)
   at WinRT.ExceptionHelpers.ThrowExceptionForHR(Int32 hr)

I've tried more complex implementations, for example with ICustomQueryInterface (see Geo2class below) since it's based on IntPtr instead of .NET objects, but I can't find a way that works.

Note that I need this class to also implement a non WinRT, IUnknown-derived class (here IGeometrySource2DInterop) declared manually.

Is there a way to make this work?

To Reproduce Here's my .csproj:

Exe net8.0-windows10.0.26100.0 enable true true 10.0.26100.42

Here's my Progam.cs:

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using Windows.Graphics;
using Windows.UI.Composition;
using WinRT;

[assembly: DisableRuntimeMarshalling]

namespace ConsoleAotAuthor;

internal class Program
{
    static void Main()
    {
        var geo = new Geo(); // or Geo2 (it's worse)
        var compositor = new CompositionPath(geo);
    }
}

[GeneratedComClass]
public partial class Geo : IGeometrySource2D, IGeometrySource2DInterop
{
    // these are never called anyway
    public int GetGeometry(out nint value) => throw new NotImplementedException();
    public int TryGetGeometryUsingFactory(nint factory, out nint value) => throw new NotImplementedException();
}

[GeneratedComClass]
public partial class Geo2 : IGeometrySource2D, IGeometrySource2DInterop, ICustomQueryInterface
{
    // these are never called anyway
    public int GetGeometry(out nint value) => throw new NotImplementedException();
    public int TryGetGeometryUsingFactory(nint factory, out nint value) => throw new NotImplementedException();

    CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out nint ppv)
    {
        if (iid == typeof(IGeometrySource2D).GUID)
        {
            ppv = MarshalInspectable<IGeometrySource2D>.FromManaged(this);
            return CustomQueryInterfaceResult.Handled;
        }
        // what to return here???
        else if (iid == typeof(IGeometrySource2DInterop).GUID)
        {
            ComWrappers.TryGetComInstance(this, out var unk); // unk is 0

            // returning this causes System.ExecutionEngineException "Fatal error. Internal CLR error. (0x80131506)" later on
            ppv = new StrategyBasedComWrappers().GetOrCreateComInterfaceForObject(this, CreateComInterfaceFlags.None);
            return CustomQueryInterfaceResult.Handled;
        }

        ppv = 0;
        return CustomQueryInterfaceResult.NotHandled;
    }
}

// from %ProgramFiles(x86)%\Windows Kits\10\Include\10.0.26100.0\winrt\windows.graphics.interop.h
[GeneratedComInterface, Guid("0657af73-53fd-47cf-84ff-c8492d2a80a3")]
public partial interface IGeometrySource2DInterop
{
    [PreserveSig]
    int GetGeometry(out nint value);

    [PreserveSig]
    int TryGetGeometryUsingFactory(nint factory, out nint value);
}

Expected behavior It works with older C#/WinRT and w/o AOT, I can do something like

var unk = Marshal.GetIUnknownForObject(this);
return WinRT.MarshalInspectable<Windows.Graphics.IGeometrySource2D>.FromAbi(unk);

But Marshal.GetIUnknownForObject cannot be used with disabled runtime marshaling, that I also require for proper AOT support.

dongle-the-gadget commented 2 months ago

cc @Sergio0694, I think you've tried this before?

Sergio0694 commented 2 months ago

I don't think we support scenarios with a [GenerateComClass] type that also implements WinRT interfaces. That said, we do support mixed WinRT/COM scenarios (I added support for that as it's needed by ComputeSharp for Win2D interop), but it's a bit clunky to setup and not really documented. You can see an example here. You basically need to:

Then you can just implement this interface in your class, and things should work as expected. That is, if you get a CCW from your object, that will also have the correct vtable slot entry for your COM interface, so native callers can use it.

smourier commented 2 months ago

Thanks, it's not super simple but at least there's a way. IMHO, you could document this, it's interesting to know we can use C#/WinRT this "manual" way.

FWIW, here is my Program.cs modified this way and GetGeometry is now called:

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using MyNamespace;
using Windows.Graphics;
using Windows.UI.Composition;
using WinRT;
using WinRT.Interop;

[assembly: DisableRuntimeMarshalling]

namespace ConsoleAotAuthor
{
  internal class Program
  {
      static void Main()
      {
          var geo = new Geo();
          var compositor = new CompositionPath(geo);
      }
  }

  public partial class Geo : IGeometrySource2D, IGeometrySource2DInterop
  {
      public unsafe int GetGeometry(out nint value) => throw new NotImplementedException();
      public unsafe int TryGetGeometryUsingFactory(nint factory, out nint value) => throw new NotImplementedException();
  }
}

namespace ABI.MyNamespace
{
  // all names here are hardcoded in WinRT's generator (ABI., Methods, IID, AbiToProjectionVftablePtr)
  [EditorBrowsable(EditorBrowsableState.Never)]
  public static class IGeometrySource2DInteropMethods
  {
      public static Guid IID { get; } = typeof(IGeometrySource2DInterop).GUID;
      public static nint AbiToProjectionVftablePtr { get; } = IGeometrySource2DInterop.Vftbl.InitVtbl();
  }
}

namespace MyNamespace
{
  [Guid("0657af73-53fd-47cf-84ff-c8492d2a80a3")]
  [WindowsRuntimeType]
  //[WindowsRuntimeHelperType(typeof(IGeometrySource2DInterop))] // this seems optional
  public interface IGeometrySource2DInterop
  {
      // interface (public) declaration
      // from %ProgramFiles(x86)%\Windows Kits\10\Include\10.0.26100.0\winrt\windows.graphics.interop.h
      int GetGeometry(out nint value);
      int TryGetGeometryUsingFactory(nint factory, out nint value);

      // v-table
      internal unsafe struct Vftbl
      {
          public static nint InitVtbl()
          {
              var lpVtbl = (Vftbl*)ComWrappersSupport.AllocateVtableMemory(typeof(Vftbl), sizeof(Vftbl));

              lpVtbl->IUnknownVftbl = IUnknownVftbl.AbiToProjectionVftbl;
              lpVtbl->GetGeometry = &GetGeometryFromAbi;
              lpVtbl->TryGetGeometryUsingFactory = &TryGetGeometryUsingFactoryFromAbi;

              return (nint)lpVtbl;
          }

          private IUnknownVftbl IUnknownVftbl;

          // interface delegates
          private delegate* unmanaged[MemberFunction]<nint, nint*, int> GetGeometry;
          private delegate* unmanaged[MemberFunction]<nint, nint, nint*, int> TryGetGeometryUsingFactory;

          // interface implementation
          [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
          private static int GetGeometryFromAbi(nint thisPtr, nint* value)
          {
              try
              {
                  var hr = ComWrappersSupport.FindObject<IGeometrySource2DInterop>(thisPtr).GetGeometry(out var v);
                  *value = v;
                  return hr;
              }
              catch (Exception e)
              {
                  ExceptionHelpers.SetErrorInfo(e);
                  return Marshal.GetHRForException(e);
              }
          }

          [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
          private static int TryGetGeometryUsingFactoryFromAbi(nint thisPtr, nint factory, nint* value)
          {
              try
              {
                  var hr = ComWrappersSupport.FindObject<IGeometrySource2DInterop>(thisPtr).TryGetGeometryUsingFactory(factory, out var v);
                  *value = v;
                  return hr;
              }
              catch (Exception e)
              {
                  ExceptionHelpers.SetErrorInfo(e);
                  return Marshal.GetHRForException(e);
              }
          }
      }
  }
}
Gaoyifei1011 commented 1 week ago

Describe the bug With AOT, GeneratedComClasses and GeneratedComInterfaces and with runtime marshaling disabled, I'm trying to create a COM class that derives from a C#/WinRT generated interface, for example IGeometrySource2D (it's the same for all interfaces but I tried to choose a simple one) and it doesn't work, it throws:

Unhandled exception. System.ArgumentException: The parameter is incorrect.
Invalid argument to parameter source. Object parameter must not be null.
   at WinRT.ExceptionHelpers.<ThrowExceptionForHR>g__Throw|38_0(Int32 hr)
   at WinRT.ExceptionHelpers.ThrowExceptionForHR(Int32 hr)

I've tried more complex implementations, for example with ICustomQueryInterface (see Geo2class below) since it's based on IntPtr instead of .NET objects, but I can't find a way that works.

Note that I need this class to also implement a non WinRT, IUnknown-derived class (here IGeometrySource2DInterop) declared manually.

Is there a way to make this work?

To Reproduce Here's my .csproj:

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
    <WindowsSdkPackageVersion>10.0.26100.42</WindowsSdkPackageVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.1.1" />
  </ItemGroup>

</Project>

Here's my Progam.cs:

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using Windows.Graphics;
using Windows.UI.Composition;
using WinRT;

[assembly: DisableRuntimeMarshalling]

namespace ConsoleAotAuthor;

internal class Program
{
    static void Main()
    {
        var geo = new Geo(); // or Geo2 (it's worse)
        var compositor = new CompositionPath(geo);
    }
}

[GeneratedComClass]
public partial class Geo : IGeometrySource2D, IGeometrySource2DInterop
{
    // these are never called anyway
    public int GetGeometry(out nint value) => throw new NotImplementedException();
    public int TryGetGeometryUsingFactory(nint factory, out nint value) => throw new NotImplementedException();
}

[GeneratedComClass]
public partial class Geo2 : IGeometrySource2D, IGeometrySource2DInterop, ICustomQueryInterface
{
    // these are never called anyway
    public int GetGeometry(out nint value) => throw new NotImplementedException();
    public int TryGetGeometryUsingFactory(nint factory, out nint value) => throw new NotImplementedException();

    CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out nint ppv)
    {
        if (iid == typeof(IGeometrySource2D).GUID)
        {
            ppv = MarshalInspectable<IGeometrySource2D>.FromManaged(this);
            return CustomQueryInterfaceResult.Handled;
        }
        // what to return here???
        else if (iid == typeof(IGeometrySource2DInterop).GUID)
        {
            ComWrappers.TryGetComInstance(this, out var unk); // unk is 0

            // returning this causes System.ExecutionEngineException "Fatal error. Internal CLR error. (0x80131506)" later on
            ppv = new StrategyBasedComWrappers().GetOrCreateComInterfaceForObject(this, CreateComInterfaceFlags.None);
            return CustomQueryInterfaceResult.Handled;
        }

        ppv = 0;
        return CustomQueryInterfaceResult.NotHandled;
    }
}

// from %ProgramFiles(x86)%\Windows Kits\10\Include\10.0.26100.0\winrt\windows.graphics.interop.h
[GeneratedComInterface, Guid("0657af73-53fd-47cf-84ff-c8492d2a80a3")]
public partial interface IGeometrySource2DInterop
{
    [PreserveSig]
    int GetGeometry(out nint value);

    [PreserveSig]
    int TryGetGeometryUsingFactory(nint factory, out nint value);
}

Expected behavior It works with older C#/WinRT and w/o AOT, I can do something like

var unk = Marshal.GetIUnknownForObject(this);
return WinRT.MarshalInspectable<Windows.Graphics.IGeometrySource2D>.FromAbi(unk);

But Marshal.GetIUnknownForObject cannot be used with disabled runtime marshaling, that I also require for proper AOT support.

Here's an example code I'm trying to use with GeneratedComInterface and WindowsRuntimeType, which works with IGraphicsEffectD2D1Interop and is primarily used to display the mica and DesktopAcrylic background colors

-------------------------------------

这是我尝试将 GeneratedComInterface 和 WindowsRuntimeType 配合使用的一个示例代码,该代码作用于 IGraphicsEffectD2D1Interop,主要用于显示 Mica 和 DesktopAcrylic 背景色

    public static unsafe class IGraphicsEffectD2D1InteropMethods
    {
        public static Guid IID { get; } = typeof(IGraphicsEffectD2D1Interop).GUID;

        public static IntPtr AbiToProjectionVftablePtr { get; } = (nint)StrategyBasedComWrappers.DefaultIUnknownInterfaceDetailsStrategy.GetIUnknownDerivedDetails(typeof(IGraphicsEffectD2D1Interop).TypeHandle).ManagedVirtualMethodTable;
    }
   [GeneratedComInterface, WindowsRuntimeType, Guid("2FC57384-A068-44D7-A331-30982FCF7177")]
   public partial interface IGraphicsEffectD2D1Interop
   {
       [PreserveSig]
       int GetEffectId(out Guid id);

       [PreserveSig]
       int GetNamedPropertyMapping(IntPtr name, out uint index, out GRAPHICS_EFFECT_PROPERTY_MAPPING mapping);

       [PreserveSig]
       int GetPropertyCount(out uint count);

       [PreserveSig]
       int GetProperty(uint index, out IntPtr value);

       [PreserveSig]
       int GetSource(uint index, out IntPtr source);

       [PreserveSig]
       int GetSourceCount(out uint count);
   }

Image

Gaoyifei1011 commented 1 week ago

I don't think we support scenarios with a [GenerateComClass] type that also implements WinRT interfaces. That said, we do support mixed WinRT/COM scenarios (I added support for that as it's needed by ComputeSharp for Win2D interop), but it's a bit clunky to setup and not really documented. You can see an example here. You basically need to:

  • Declare the correct <TYPE_NAME>Methods type in the right ABI.* namespace (see link)
  • Implement your interface with [WindowsRuntimeType] and [WindowsRuntimeHelperType] (see here)
  • Implement a managed vftable for it (see the same link)
  • Return that from the AbiToProjectionVftablePtr property of the *Methods type from above

Then you can just implement this interface in your class, and things should work as expected. That is, if you get a CCW from your object, that will also have the correct vtable slot entry for your COM interface, so native callers can use it.

I tried a new way to use the GeneratedComInterface and WindowsRuntimeType properties in a single COM interface. You'll need to manually modify the ManagedVirtualMethodTable generated by AbiToProjectionVftablePtr for the Com source in IXXXMethods

-----------------------------------

我尝试了一种新的方法可以将 GeneratedComInterface 和 WindowsRuntimeType 这两个属性在一个 COM 接口中使用。需要在 IXXXMethods 中手动修改 AbiToProjectionVftablePtr 为 Com 源生成的 ManagedVirtualMethodTable。


效果图 / Results Image