Skip to content

Commit

Permalink
Add ComInterfaceGenerator sample with in proc registered COM (#6137)
Browse files Browse the repository at this point in the history
* Start COM interface generator tutorial

* Add files before I lose them

* Stuck with exception in server

* working

* Cleaned up sample

* Update gitignore

* Remove BuiltInCom for now

* Use Kernel32 methods for getting Dll path

* Update README

* Update core/interop/source-generation/ComWrappersGeneration/README.md

Co-authored-by: Aaron Robinson <arobins@microsoft.com>

* Apply suggestions from code review

Co-authored-by: Aaron Robinson <arobins@microsoft.com>

* PR feedback

* PR feedback

* PR feedback

* Get DNNE set up. Pushing to share to remote machine, not ready for review

* Add more descriptions, rework nativeaot detection

* use backslash for folders

* Update build, use more custom marshalling, update readme

* Clean up and suppress warnings

* Use build.proj instead of sln

* Go back to sln and fix readme typo

---------

Co-authored-by: Aaron Robinson <arobins@microsoft.com>
  • Loading branch information
jtschuster and AaronRobinsonMSFT authored Nov 20, 2023
1 parent ec72cc1 commit 88d42b1
Show file tree
Hide file tree
Showing 18 changed files with 502 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OutputFiles
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<!-- Uncomment to publish as a native executable -->
<!-- <PublishAOT>true</PublishAOT> -->
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

[assembly: DisableRuntimeMarshalling]

namespace Tutorial;

public class Program
{
public static unsafe void Main(string[] args)
{
var clsid = new Guid(Clsids.Calculator);
var iid = new Guid(ICalculator.IID);
Console.WriteLine($"Client: Requesting a Calculator (CLSID {clsid}) with ICalculator (IID {iid})");
int hr = Ole32.CoCreateInstance(ref clsid, /* No aggregation */ 0, (uint)Ole32.CLSCTX.CLSCTX_INPROC_SERVER, ref iid, out object comObject);
Marshal.ThrowExceptionForHR(hr);
ICalculator calculator = (ICalculator) comObject;

int a = 5;
int b = 3;
int c;
c = calculator.Add(a, b);
Console.WriteLine($"Client: {a} + {b} = {c}");
c = calculator.Subtract(a, b);
Console.WriteLine($"Client: {a} - {b} = {c}");
}
}

internal static unsafe partial class Ole32
{
// https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance
[LibraryImport(nameof(Ole32))]
public static partial int CoCreateInstance(
ref Guid rclsid,
nint pUnkOuter,
uint dwClsContext,
ref Guid riid,
// The default ComInterfaceMarshaller will unwrap a .NET object if it can tell the COM instance is a ComWrapper.
// This causes issues when casting to ICalculator, since the Server's Calculator class doesn't implement the Client's interface.
// UniqueComInterfaceMarshaller doesn't try to unwrap the object and always creates a new COM object.
[MarshalUsing(typeof(UniqueComInterfaceMarshaller<object>))]
out object ppv);

// https://learn.microsoft.com/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx
public enum CLSCTX : uint
{
CLSCTX_INPROC_SERVER = 0x1,
CLSCTX_INPROC_HANDLER = 0x2,
CLSCTX_LOCAL_SERVER = 0x4,
CLSCTX_INPROC_SERVER16 = 0x8,
CLSCTX_REMOTE_SERVER = 0x10,
CLSCTX_INPROC_HANDLER16 = 0x20,
CLSCTX_RESERVED1 = 0x40,
CLSCTX_RESERVED2 = 0x80,
CLSCTX_RESERVED3 = 0x100,
CLSCTX_RESERVED4 = 0x200,
CLSCTX_NO_CODE_DOWNLOAD = 0x400,
CLSCTX_RESERVED5 = 0x800,
CLSCTX_NO_CUSTOM_MARSHAL = 0x1000,
CLSCTX_ENABLE_CODE_DOWNLOAD = 0x2000,
CLSCTX_NO_FAILURE_LOG = 0x4000,
CLSCTX_DISABLE_AAA = 0x8000,
CLSCTX_ENABLE_AAA = 0x10000,
CLSCTX_FROM_DEFAULT_CONTEXT = 0x20000,
CLSCTX_ACTIVATE_X86_SERVER = 0x40000,
CLSCTX_ACTIVATE_32_BIT_SERVER,
CLSCTX_ACTIVATE_64_BIT_SERVER = 0x80000,
CLSCTX_ENABLE_CLOAKING = 0x100000,
CLSCTX_APPCONTAINER = 0x400000,
CLSCTX_ACTIVATE_AAA_AS_IU = 0x800000,
CLSCTX_RESERVED6 = 0x1000000,
CLSCTX_ACTIVATE_ARM32_SERVER = 0x2000000,
CLSCTX_ALLOW_LOWER_TRUST_REGISTRATION,
CLSCTX_PS_DLL = 0x80000000,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{065F345B-901A-4BCD-8CAF-ACBB6B6910F8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{DCA455A0-57CF-467F-9D5D-10C95EAA9441}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{065F345B-901A-4BCD-8CAF-ACBB6B6910F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{065F345B-901A-4BCD-8CAF-ACBB6B6910F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{065F345B-901A-4BCD-8CAF-ACBB6B6910F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{065F345B-901A-4BCD-8CAF-ACBB6B6910F8}.Release|Any CPU.Build.0 = Release|Any CPU
{DCA455A0-57CF-467F-9D5D-10C95EAA9441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DCA455A0-57CF-467F-9D5D-10C95EAA9441}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCA455A0-57CF-467F-9D5D-10C95EAA9441}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCA455A0-57CF-467F-9D5D-10C95EAA9441}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project>
<!-- Shared properties -->
<PropertyGroup>
<DefaultTargetFramework>net8.0</DefaultTargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>bin\Generated</CompilerGeneratedFilesOutputPath>
<PublishDir>$(MSBuildThisFileDirectory)\OutputFiles\$(MSBuildProjectName)\</PublishDir>
</PropertyGroup>

<ItemGroup>
<Compile Include="../Shared/*.cs" />
</ItemGroup>
</Project>
38 changes: 38 additions & 0 deletions core/interop/source-generation/ComWrappersGeneration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
languages:
- csharp
products:
- dotnet
page_type: sample
name: "Source-Generated COM Sample"
urlFragment: "generated-comwrappers"
description: "A .NET codebase that uses source-generated COM in .NET"
---
# .NET Source-Generated COM Sample

This tutorial demonstrates how to use COM source generators in .NET 8+ to create a COM server and client for in-process COM.

This example defines an interface `ICalculator` that provides `Add` and `Subtract` methods. The server provides an implementation of `ICalculator` for the client to use after the server has been registered. The client project creates an instance of the object using the [`CoCreateInstance`](https://learn.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance) Win32 method, and calls methods on the object.

This sample supports NativeAOT and standard CoreCLR deployments. The native methods that the Windows COM system requires are exported automatically with the `[UnmanagedCallersOnly]` attribute when publishing with NativeAOT. For CoreCLR, the [DNNE](https://github.com/AaronRobinsonMSFT/DNNE) package is used to provide the exported functions.

## Prerequisites

- .NET 8+ SDK
- Windows 10+ OS

## Build and Run

### NativeAOT

Build the Native AOT binaries by running `dotnet publish -r <RID>` where `<RID>` is the RuntimeIdentifier for your OS, for example `win-x64`. The projects will copy the binaries to the `OutputFiles\` directory. After publishing, use `regsvr32.exe` to register `Server.dll` with COM by running run `regsvr.exe .\OutputFiles\Server\Server.dll`. Then, run the client application `.\OutputFiles\Client\Client.exe` and observe the output as it activates and uses a COM instance from `Server.dll`.

### CoreCLR

Build the projects by running `dotnet publish`. The projects will copy the binaries to the `OutputFiles\` directory. After publishing, use `regsvr32.exe` to register the native binary produced by DNNE, `ServerNE.dll` by running `regsvr.exe .\OutputFiles\Server\ServerNE.dll`. `ServerNE.dll` will start the .NET runtime and call the exported methods in the managed `Server.dll` which register the server with COM. Then, run the client application `.\OutputFiles\Client\Client.exe` and observe the output as it activates and uses a COM instance from `ServerNE.dll`.

## See also

- [ComWrappers source generation](https://learn.microsoft.com/dotnet/standard/native-interop/comwrappers-source-generation)
- [Native exports in NativeAOT](https://learn.microsoft.com/dotnet/core/deploying/native-aot/interop#native-exports)
- [COM interop in .NET](https://learn.microsoft.com/dotnet/standard/native-interop/cominterop)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using Microsoft.Win32;

namespace Tutorial;

[GeneratedComClass]
[Guid(Clsid)]
internal partial class Calculator : ICalculator
{
public int Add(int a, int b) => a + b;
public int Subtract(int a, int b) => a - b;
internal const string Clsid = Clsids.Calculator;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

namespace Tutorial;

[GeneratedComClass]
public unsafe partial class ClassFactory : IClassFactory
{
public static ClassFactory Instance { get; } = new ClassFactory();
public void CreateInstance(nint pOuter, in Guid iid, out object? ppInterface)
{
Console.WriteLine($"Server: IID requested from ClassFactory.CreateInstance: {iid}");
if (pOuter != 0)
{
ppInterface = null;
const int CLASS_E_NOAGGREGATION = unchecked((int)0x80040110);
throw new COMException("Class does not support aggregation", CLASS_E_NOAGGREGATION);
}
Calculator calculator = new();
ppInterface = calculator;
}

public void LockServer(bool fLock) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace DNNE;

/// <summary>
/// Provide C code to be defined early in the generated C header file.
/// </summary>
/// <remarks>
/// This attribute is respected on an exported method declaration or on a parameter for the method.
/// The following header files will be included prior to the code being defined.
/// - stddef.h
/// - stdint.h
/// - dnne.h
/// </remarks>
internal class C99DeclCodeAttribute : System.Attribute
{
public C99DeclCodeAttribute(string code) { }
}

/// <summary>
/// Define the C type to be used.
/// </summary>
/// <remarks>
/// The level of indirection should be included in the supplied string.
/// </remarks>
internal class C99TypeAttribute : System.Attribute
{
public C99TypeAttribute(string code) { }
}
108 changes: 108 additions & 0 deletions core/interop/source-generation/ComWrappersGeneration/Server/Exports.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using Microsoft.Win32;

namespace Tutorial;

public static unsafe class Exports
{
/// <summary>
/// Returns a pointer to an IClassFactory instance that corresponds to the requested <paramref name="classId"/>.
/// <paramref name="interfaceId"/> is expected to be the IID of IClassFactory.
/// This method is called by the COM system when a client requests an object that this server has registered.
/// <see href="https://learn.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-dllgetclassobject"/>
/// </summary>
[UnmanagedCallersOnly(EntryPoint = nameof(DllGetClassObject))]
public static int DllGetClassObject([DNNE.C99Type("void*")] Guid* classId, [DNNE.C99Type("void*")] Guid* interfaceId, nint* ppIClassFactory)
{
Console.WriteLine($"Server: Class ID requested from DllGetClassObject: {*classId}");
Console.WriteLine($"Server: Interface ID requested from DllGetClassObject: {*interfaceId}");
if (*classId != new Guid(Clsids.Calculator)
|| *interfaceId != new Guid(IClassFactory.IID))
{
*ppIClassFactory = 0;
const int CLASS_E_CLASSNOTAVAILABLE = unchecked((int)0x80040111);
return CLASS_E_CLASSNOTAVAILABLE;
}
ClassFactory factory = ClassFactory.Instance;
nint pIUnknown = (nint)ComInterfaceMarshaller<ClassFactory>.ConvertToUnmanaged(factory);
// Call QI on the COM ptr from COM wrappers to get the requested interface pointer
// This is IClassFactory for CoCreateInstance
int hr = Marshal.QueryInterface(pIUnknown, in *interfaceId, out *ppIClassFactory);
Marshal.Release(pIUnknown);
if (hr != 0)
{
Console.WriteLine($"Server: QueryInterface in DllGetClassObject failed: {hr:x}");
return hr;
}
return 0;
}

/// <summary>
/// Registers the server with the COM system.
/// Called by <c>regsvr32.exe</c> when run with this .dll as the argument.
/// <see href="https://learn.microsoft.com/windows/win32/api/olectl/nf-olectl-dllregisterserver"/>
/// </summary>
[UnmanagedCallersOnly(EntryPoint = nameof(DllRegisterServer))]
public static int DllRegisterServer()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return -1;

if (!FileUtils.TryGetDllPath(out string? dllPath))
{
const int SELFREG_E_CLASS = unchecked((int)0x80040201);
return SELFREG_E_CLASS;
}
CreateComRegistryEntryForClass(Calculator.Clsid, nameof(Calculator), dllPath!);
return 0;
}

static void CreateComRegistryEntryForClass(string clsid, string className, string dllPath)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new InvalidOperationException();

string progId = GetProgId(className);

using (RegistryKey key = Registry.CurrentUser.CreateSubKey($$"""SOFTWARE\Classes\CLSID\{{{clsid}}}"""))
{
key.SetValue(null, className, RegistryValueKind.String);
key.SetValue("ProgId", progId, RegistryValueKind.String);
}
using (RegistryKey key = Registry.CurrentUser.CreateSubKey($$"""SOFTWARE\Classes\CLSID\{{{clsid}}}\InprocServer32"""))
{
key.SetValue(null, dllPath, RegistryValueKind.String);
key.SetValue("ThreadingModel", "Both", RegistryValueKind.String);
}
using (RegistryKey key = Registry.CurrentUser.CreateSubKey($$"""SOFTWARE\Classes\{{{progId}}}"""))
{
key.SetValue(null, className, RegistryValueKind.String);
key.SetValue("CLSID", clsid, RegistryValueKind.String);
}
}

/// <summary>
/// Unregisters the server from the COM system.
/// Called by <c>regsvr32.exe</c> when run with the -u flag and this .dll as the argument
/// <see href="https://learn.microsoft.com/windows/win32/api/olectl/nf-olectl-dllunregisterserver"/>
/// </summary>
[UnmanagedCallersOnly(EntryPoint = nameof(DllUnregisterServer))]
public static int DllUnregisterServer()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new InvalidOperationException();

string clsid = Calculator.Clsid;
string progId = GetProgId(nameof(Calculator));

Registry.CurrentUser.DeleteSubKeyTree($$"""SOFTWARE\Classes\CLSID\{{{clsid}}}""");
Registry.CurrentUser.DeleteSubKeyTree($$"""SOFTWARE\Classes\{{{progId}}}""");
return 0;
}

public static string GetProgId(string className) => $"Tutorial.{className}.0";
}
Loading

0 comments on commit 88d42b1

Please sign in to comment.