diff --git a/core/interop/source-generation/ComWrappersGeneration/.gitignore b/core/interop/source-generation/ComWrappersGeneration/.gitignore new file mode 100644 index 00000000000..3195f839657 --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/.gitignore @@ -0,0 +1 @@ +OutputFiles diff --git a/core/interop/source-generation/ComWrappersGeneration/Client/Client.csproj b/core/interop/source-generation/ComWrappersGeneration/Client/Client.csproj new file mode 100644 index 00000000000..f35c6e9ddde --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Client/Client.csproj @@ -0,0 +1,13 @@ + + + + Exe + net8.0 + enable + enable + + + + + + diff --git a/core/interop/source-generation/ComWrappersGeneration/Client/Program.cs b/core/interop/source-generation/ComWrappersGeneration/Client/Program.cs new file mode 100644 index 00000000000..11fcf8a6b72 --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Client/Program.cs @@ -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))] + 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, + } +} diff --git a/core/interop/source-generation/ComWrappersGeneration/ComWrappersGeneration.sln b/core/interop/source-generation/ComWrappersGeneration/ComWrappersGeneration.sln new file mode 100644 index 00000000000..902bb554635 --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/ComWrappersGeneration.sln @@ -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 diff --git a/core/interop/source-generation/ComWrappersGeneration/Directory.Build.props b/core/interop/source-generation/ComWrappersGeneration/Directory.Build.props new file mode 100644 index 00000000000..225cf8b5742 --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Directory.Build.props @@ -0,0 +1,14 @@ + + + + net8.0 + true + true + bin\Generated + $(MSBuildThisFileDirectory)\OutputFiles\$(MSBuildProjectName)\ + + + + + + diff --git a/core/interop/source-generation/ComWrappersGeneration/README.md b/core/interop/source-generation/ComWrappersGeneration/README.md new file mode 100644 index 00000000000..1be774bb52f --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/README.md @@ -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 ` where `` 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) diff --git a/core/interop/source-generation/ComWrappersGeneration/Server/Calculator.cs b/core/interop/source-generation/ComWrappersGeneration/Server/Calculator.cs new file mode 100644 index 00000000000..9c57e2474cd --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Server/Calculator.cs @@ -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; +} diff --git a/core/interop/source-generation/ComWrappersGeneration/Server/ClassFactory.cs b/core/interop/source-generation/ComWrappersGeneration/Server/ClassFactory.cs new file mode 100644 index 00000000000..8637c0b786b --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Server/ClassFactory.cs @@ -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) { } +} diff --git a/core/interop/source-generation/ComWrappersGeneration/Server/DNNE.cs b/core/interop/source-generation/ComWrappersGeneration/Server/DNNE.cs new file mode 100644 index 00000000000..20b4482294e --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Server/DNNE.cs @@ -0,0 +1,27 @@ +namespace DNNE; + +/// +/// Provide C code to be defined early in the generated C header file. +/// +/// +/// 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 +/// +internal class C99DeclCodeAttribute : System.Attribute +{ + public C99DeclCodeAttribute(string code) { } +} + +/// +/// Define the C type to be used. +/// +/// +/// The level of indirection should be included in the supplied string. +/// +internal class C99TypeAttribute : System.Attribute +{ + public C99TypeAttribute(string code) { } +} diff --git a/core/interop/source-generation/ComWrappersGeneration/Server/Exports.cs b/core/interop/source-generation/ComWrappersGeneration/Server/Exports.cs new file mode 100644 index 00000000000..b66c87c3ab8 --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Server/Exports.cs @@ -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 +{ + /// + /// Returns a pointer to an IClassFactory instance that corresponds to the requested . + /// 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. + /// + /// + [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.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; + } + + /// + /// Registers the server with the COM system. + /// Called by regsvr32.exe when run with this .dll as the argument. + /// + /// + [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); + } + } + + /// + /// Unregisters the server from the COM system. + /// Called by regsvr32.exe when run with the -u flag and this .dll as the argument + /// + /// + [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"; +} diff --git a/core/interop/source-generation/ComWrappersGeneration/Server/FileUtils.cs b/core/interop/source-generation/ComWrappersGeneration/Server/FileUtils.cs new file mode 100644 index 00000000000..7ef0b9838da --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Server/FileUtils.cs @@ -0,0 +1,52 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Tutorial; + +public static class FileUtils +{ + /// + /// Gets the path to the NativeDll that the COM system should use to call the native exports + /// + [UnconditionalSuppressMessage(category: "SingleFile", checkId: "IL3000" /* Avoid accessing Assembly file path when publishing as a single file */, Justification = "The code is aware the `Assembly.Location` may be an empty string and falls back to Win32 APIs")] + public static unsafe bool TryGetDllPath([NotNullWhen(true)] out string? dllPath) + { + // Try using reflection to get the path to the native COM entry points + dllPath = typeof(FileUtils).Assembly.Location; + // Assembly.Location is an empty string in single file and NativeAOT + // Fall back to Windows APIs if Location is empty string + if (dllPath == "" && !TryGetDllPathWin32(out dllPath)) + return false; + // Check if DNNE binary exists and return path to DNNE binary if it exists + const string dnneSuffix = "NE.dll"; + string fileName = Path.GetFileNameWithoutExtension(dllPath); + string directory = Path.GetDirectoryName(dllPath) ?? ""; + string dnnePath = Path.Combine(directory, fileName + dnneSuffix); + if (File.Exists(dnnePath)) + dllPath = dnnePath; + return true; + } + + /// + /// Gets the path to the dll that has the DllRegisterServer address using the Windows APIs + /// + public static unsafe bool TryGetDllPathWin32([NotNullWhen(true)] out string? dllPath) + { + dllPath = null; + bool receivedHandle = Kernel32.GetModuleHandleExW( + Kernel32.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | Kernel32.GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (nint)(delegate* unmanaged)&Exports.DllRegisterServer, + out nint moduleHandle); + if (!receivedHandle) + return false; + + char[] filePath = new char[256]; + int pathSize = Kernel32.GetModuleFileNameW(moduleHandle, filePath, filePath.Length); + if (pathSize == 0) + return false; + + dllPath = new string(filePath); + return true; + } +} diff --git a/core/interop/source-generation/ComWrappersGeneration/Server/IClassFactory.cs b/core/interop/source-generation/ComWrappersGeneration/Server/IClassFactory.cs new file mode 100644 index 00000000000..9d73feab049 --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Server/IClassFactory.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +namespace Tutorial; + +/// +/// The interface that the COM system uses to create new objects when clients use CoCreateInstance. +/// +/// +[GeneratedComInterface] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid(IID)] +internal unsafe partial interface IClassFactory +{ + void CreateInstance(nint outer, in Guid id, [MarshalAs(UnmanagedType.Interface)] out object? iface); + void LockServer([MarshalAs(UnmanagedType.Bool)] bool fLock); + public const string IID = "00000001-0000-0000-C000-000000000046"; +} diff --git a/core/interop/source-generation/ComWrappersGeneration/Server/Kernel32.cs b/core/interop/source-generation/ComWrappersGeneration/Server/Kernel32.cs new file mode 100644 index 00000000000..aba76d1d55f --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Server/Kernel32.cs @@ -0,0 +1,33 @@ +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +internal static partial class Kernel32 +{ + /// + /// Gets a module handle given an address from that module. + /// + /// + [LibraryImport(nameof(Kernel32))] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool GetModuleHandleExW(int flags, nint pAddressInModule, out nint moduleHandle); + + /// + /// Makes the second argument of interpreted as an address in the module rather than a string. + /// + public static int GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 0x00000004; + + /// + /// Makes GetModuleHandleExW not increment the reference count, so we don't need to decrement it ourselves. + /// + public const int GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002; + + /// + /// Gets the path to a module given the module handle. + /// + /// + [LibraryImport(nameof(Kernel32), StringMarshalling = StringMarshalling.Utf16)] + public static partial int GetModuleFileNameW( + nint moduleHandle, + [MarshalUsing(CountElementName = nameof(nSize)), Out] char[] FileName, + int nSize); +} diff --git a/core/interop/source-generation/ComWrappersGeneration/Server/Server.csproj b/core/interop/source-generation/ComWrappersGeneration/Server/Server.csproj new file mode 100644 index 00000000000..ed30283e295 --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Server/Server.csproj @@ -0,0 +1,23 @@ + + + + Library + net8.0 + enable + enable + + + + + + true + + + + + + + + + + diff --git a/core/interop/source-generation/ComWrappersGeneration/Server/server.def b/core/interop/source-generation/ComWrappersGeneration/Server/server.def new file mode 100644 index 00000000000..fad93f81976 --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Server/server.def @@ -0,0 +1,4 @@ +EXPORTS + DllGetClassObject PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE diff --git a/core/interop/source-generation/ComWrappersGeneration/Shared/ClsIds.cs b/core/interop/source-generation/ComWrappersGeneration/Shared/ClsIds.cs new file mode 100644 index 00000000000..a3423b64b2d --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Shared/ClsIds.cs @@ -0,0 +1,6 @@ +namespace Tutorial; + +public static class Clsids +{ + public const string Calculator = "99899901-bb69-460c-bf4c-ba122044030e"; +} diff --git a/core/interop/source-generation/ComWrappersGeneration/Shared/ICalculator.cs b/core/interop/source-generation/ComWrappersGeneration/Shared/ICalculator.cs new file mode 100644 index 00000000000..69e8ea6e67e --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/Shared/ICalculator.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +namespace Tutorial; + +[GeneratedComInterface] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid(IID)] +internal partial interface ICalculator +{ + int Add(int a, int b); + int Subtract(int a, int b); + public const string IID = "c67121c6-cf26-431f-adc7-d12fe2448841"; +} diff --git a/core/interop/source-generation/ComWrappersGeneration/global.json b/core/interop/source-generation/ComWrappersGeneration/global.json new file mode 100644 index 00000000000..e5716884662 --- /dev/null +++ b/core/interop/source-generation/ComWrappersGeneration/global.json @@ -0,0 +1,5 @@ +{ + "msbuild-sdks": { + "Microsoft.Build.Traversal": "2.0.2" + } +} \ No newline at end of file