-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ComInterfaceGenerator sample with in proc registered COM (#6137)
* 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
1 parent
ec72cc1
commit 88d42b1
Showing
18 changed files
with
502 additions
and
0 deletions.
There are no files selected for viewing
1 change: 1 addition & 0 deletions
1
core/interop/source-generation/ComWrappersGeneration/.gitignore
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
OutputFiles |
13 changes: 13 additions & 0 deletions
13
core/interop/source-generation/ComWrappersGeneration/Client/Client.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
79 changes: 79 additions & 0 deletions
79
core/interop/source-generation/ComWrappersGeneration/Client/Program.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
core/interop/source-generation/ComWrappersGeneration/ComWrappersGeneration.sln
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
14 changes: 14 additions & 0 deletions
14
core/interop/source-generation/ComWrappersGeneration/Directory.Build.props
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
38
core/interop/source-generation/ComWrappersGeneration/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
15 changes: 15 additions & 0 deletions
15
core/interop/source-generation/ComWrappersGeneration/Server/Calculator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
24 changes: 24 additions & 0 deletions
24
core/interop/source-generation/ComWrappersGeneration/Server/ClassFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { } | ||
} |
27 changes: 27 additions & 0 deletions
27
core/interop/source-generation/ComWrappersGeneration/Server/DNNE.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
108
core/interop/source-generation/ComWrappersGeneration/Server/Exports.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} |
Oops, something went wrong.