Skip to content

Integrate Shmueli's ComServer into CsWinRT #1995

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

Closed
wants to merge 18 commits into from
Closed
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion build/AzurePipelineTemplates/CsWinRT-Build-Steps.yml
Original file line number Diff line number Diff line change
@@ -171,7 +171,7 @@ steps:
displayName: Build cswinrt.sln
inputs:
solution: $(Build.SourcesDirectory)\src\cswinrt.sln
msbuildArgs: /restore /p:CIBuildReason=CI,VersionNumber=$(VersionNumber),VersionString=$(Build.BuildNumber),AssemblyVersionNumber=$(WinRT.Runtime.AssemblyVersion),GenerateTestProjection=true,AllowedReferenceRelatedFileExtensions=".xml;.pri;.dll.config;.exe.config" /bl:$(Build.SourcesDirectory)\cswinrt.binlog
msbuildArgs: /restore /p:CIBuildReason=CI,VersionNumber=$(VersionNumber),VersionString=$(Build.BuildNumber),AssemblyVersionNumber=$(WinRT.Runtime.AssemblyVersion),ComServerHelpersAssemblyVersionNumber=$(ComServerHelpers.AssemblyVersion),GenerateTestProjection=true,AllowedReferenceRelatedFileExtensions=".xml;.pri;.dll.config;.exe.config" /bl:$(Build.SourcesDirectory)\cswinrt.binlog
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)

@@ -287,6 +287,16 @@ steps:
WinRT.Runtime.dll
WinRT.Runtime.pdb
TargetFolder: $(StagingFolder)\net8.0

- task: CopyFiles@2
displayName: Stage Net8.0
condition: and(succeeded(), eq(variables['BuildPlatform'], 'x86'), eq(variables['BuildConfiguration'], 'release'))
inputs:
SourceFolder: $(Build.SourcesDirectory)\src\ComServerHelpers\bin\$(BuildConfiguration)\net8.0
Contents: |
ComServerHelpers.dll
ComServerHelpers.pdb
TargetFolder: $(StagingFolder)\net8.0

# Stage Net9.0
- task: CopyFiles@2
Original file line number Diff line number Diff line change
@@ -71,7 +71,7 @@ steps:
command: pack
searchPatternPack: nuget/Microsoft.Windows.CsWinRT.nuspec
configurationToPack: Release
buildProperties: cswinrt_nuget_version=$(NugetVersion);cswinrt_exe=$(Build.SourcesDirectory)\release_x86\native\cswinrt.exe;interop_winmd=$(Build.SourcesDirectory)\release_x86\native\WinRT.Interop.winmd;netstandard2_runtime=$(Build.SourcesDirectory)\release_x86\netstandard2.0\WinRT.Runtime.dll;net8_runtime=$(Build.SourcesDirectory)\release_x86\net8.0\WinRT.Runtime.dll;net9_runtime=$(Build.SourcesDirectory)\release_x86\net9.0\WinRT.Runtime.dll;source_generator_roslyn4080=$(Build.SourcesDirectory)\release_x86\netstandard2.0\roslyn4080\WinRT.SourceGenerator.dll;source_generator_roslyn4120=$(Build.SourcesDirectory)\release_x86\netstandard2.0\roslyn4120\WinRT.SourceGenerator.dll;winrt_shim=$(Build.SourcesDirectory)\release_x86\net8.0\WinRT.Host.Shim.dll;winrt_host_x86=$(Build.SourcesDirectory)\release_x86\native\WinRT.Host.dll;winrt_host_x64=$(Build.SourcesDirectory)\release_x64\native\WinRT.Host.dll;winrt_host_arm64=$(Build.SourcesDirectory)\release_arm64\native\WinRT.Host.dll;winrt_host_resource_x86=$(Build.SourcesDirectory)\release_x86\native\WinRT.Host.dll.mui;winrt_host_resource_x64=$(Build.SourcesDirectory)\release_x64\native\WinRT.Host.dll.mui;winrt_host_resource_arm64=$(Build.SourcesDirectory)\release_arm64\native\WinRT.Host.dll.mui;guid_patch=$(Build.SourcesDirectory)\release_x86\net8.0\IIDOptimizer\*.*
buildProperties: cswinrt_nuget_version=$(NugetVersion);cswinrt_exe=$(Build.SourcesDirectory)\release_x86\native\cswinrt.exe;interop_winmd=$(Build.SourcesDirectory)\release_x86\native\WinRT.Interop.winmd;netstandard2_runtime=$(Build.SourcesDirectory)\release_x86\netstandard2.0\WinRT.Runtime.dll;net8_runtime=$(Build.SourcesDirectory)\release_x86\net8.0\WinRT.Runtime.dll;net8_comserver_helpers=$(Build.SourcesDirectory)\release_x86\net8.0\ComServerHelpers.dll;net9_runtime=$(Build.SourcesDirectory)\release_x86\net9.0\WinRT.Runtime.dll;source_generator_roslyn4080=$(Build.SourcesDirectory)\release_x86\netstandard2.0\roslyn4080\WinRT.SourceGenerator.dll;source_generator_roslyn4120=$(Build.SourcesDirectory)\release_x86\netstandard2.0\roslyn4120\WinRT.SourceGenerator.dll;winrt_shim=$(Build.SourcesDirectory)\release_x86\net8.0\WinRT.Host.Shim.dll;winrt_host_x86=$(Build.SourcesDirectory)\release_x86\native\WinRT.Host.dll;winrt_host_x64=$(Build.SourcesDirectory)\release_x64\native\WinRT.Host.dll;winrt_host_arm64=$(Build.SourcesDirectory)\release_arm64\native\WinRT.Host.dll;winrt_host_resource_x86=$(Build.SourcesDirectory)\release_x86\native\WinRT.Host.dll.mui;winrt_host_resource_x64=$(Build.SourcesDirectory)\release_x64\native\WinRT.Host.dll.mui;winrt_host_resource_arm64=$(Build.SourcesDirectory)\release_arm64\native\WinRT.Host.dll.mui;guid_patch=$(Build.SourcesDirectory)\release_x86\net8.0\IIDOptimizer\*.*
packDestination: $(ob_outputDirectory)\packages

- task: NuGetCommand@2
2 changes: 2 additions & 0 deletions build/AzurePipelineTemplates/CsWinRT-Variables.yml
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ variables:
value: 0
- name: WinRT.Runtime.AssemblyVersion
value: '2.3.0.0'
- name: ComServerHelpers.AssemblyVersion
value: '1.0.0.0'
- name: Net5.SDK.Feed
value: 'https://dotnetcli.blob.core.windows.net/dotnet'
- name: Net8.SDK.Version
4 changes: 3 additions & 1 deletion nuget/Microsoft.Windows.CsWinRT.nuspec
Original file line number Diff line number Diff line change
@@ -34,6 +34,8 @@
<file src="readme.txt"/>
<file src="$net8_runtime$" target="lib\net8.0\"/>
<file src="$net9_runtime$" target="lib\net9.0\"/>
<file src="$net8_comserver_helpers$" target="lib\net8.0\"/>
<file src="$net8_comserver_helpers$" target="lib\net9.0\"/>
<file src="$source_generator_roslyn4080$" target="analyzers\dotnet\cs\roslyn4.8"/>
<file src="$source_generator_roslyn4120$" target="analyzers\dotnet\cs\roslyn4.12"/>
<file src="$winrt_host_x64$" target ="hosting\x64\native"/>
@@ -72,7 +74,7 @@
<file src="..\src\WinRT.Runtime\Projections\*netstandard2.0*.cs" target="embedded\netstandard2.0\" />

<file src="..\src\WinRT.Runtime\Configuration\*.cs" exclude="..\src\WinRT.Runtime\Configuration\*.net*.cs" target="embedded\any\" />

<file src="sources\StubExe.c" target="build\sources"/>

<file src="$guid_patch$" target="build\tools\IIDOptimizer"/>
40 changes: 40 additions & 0 deletions src/ComServerHelpers/BaseActivationFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Runtime.Versioning;

namespace ComServerHelpers;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should really think about what actual namespace names to use. And type names.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's your suggestion?


/// <summary>
/// Base for a WinRT Activation Factory for a .NET type.
/// </summary>
[SupportedOSPlatform("windows8.0")]
public abstract class BaseActivationFactory
{
/// <inheritdoc/>
public abstract object ActivateInstance();

/// <summary>
/// Gets the Activatable Class ID.
/// </summary>
public abstract string ActivatableClassId
{
get;
}

/// <summary>
/// Occurs when a new instance is created.
/// </summary>
public event EventHandler<InstanceCreatedEventArgs>? InstanceCreated;

/// <summary>
/// Raises the <see cref="InstanceCreated"/> event.
/// </summary>
/// <param name="instance">The created instance.</param>
/// <event cref="InstanceCreated"/>
internal void OnInstanceCreated(object instance)
{
InstanceCreated?.Invoke(this, new InstanceCreatedEventArgs(instance));
}
}
52 changes: 52 additions & 0 deletions src/ComServerHelpers/BaseClassFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Runtime.Versioning;

namespace ComServerHelpers;

/// <summary>
/// Base for a COM class factory for a .NET type.
/// </summary>
/// <remarks>Does not support aggregation. Will always return <c>CLASS_E_NOAGGREGATION</c> if requested.</remarks>
[SupportedOSPlatform("windows6.0.6000")]
public abstract class BaseClassFactory
{
/// <summary>
/// Creates an instance of the object.
/// </summary>
/// <returns>An instance of the object.</returns>
protected internal abstract object CreateInstance();

/// <summary>
/// Occurs when a new instance is created.
/// </summary>
public event EventHandler<InstanceCreatedEventArgs>? InstanceCreated;

/// <summary>
/// Gets the <c>CLSID</c>.
/// </summary>
protected internal abstract Guid Clsid
{
get;
}

/// <summary>
/// Gets the <c>IID</c>.
/// </summary>
protected internal abstract Guid Iid
{
get;
}

/// <summary>
/// Raises the <see cref="InstanceCreated"/> event.
/// </summary>
/// <param name="instance">The created instance.</param>
/// <event cref="InstanceCreated"/>
internal void OnInstanceCreated(object instance)
{
InstanceCreated?.Invoke(this, new InstanceCreatedEventArgs(instance));
}
}
239 changes: 239 additions & 0 deletions src/ComServerHelpers/ComServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using ComServerHelpers.Internal;
using ComServerHelpers.Internal.Windows;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using static Windows.Win32.PInvoke;

namespace ComServerHelpers;

/// <summary>
/// An Out of Process COM Server.
/// </summary>
/// <remarks>
/// <para>Allows for types to be created using COM activation instead of WinRT activation like <see cref="WinRtServer"/>.</para>
/// <para>Typical usage is to call from a <see langword="using"/> block, using <see cref="WaitForFirstObjectAsync"/> to not close until it is safe to do so.</para>
/// <code language="cs">
/// <![CDATA[
/// using (ComServer server = new ComServer())
/// {
/// server.RegisterClass<RemoteThing, IRemoteThing>();
/// server.Start();
/// await server.WaitForFirstObjectAsync();
/// }
/// ]]>
/// </code>
/// </remarks>
/// <see cref="IDisposable"/>
/// <threadsafety static="true" instance="false"/>
[SupportedOSPlatform("windows6.0.6000")]
public sealed class ComServer : IDisposable
{
/// <summary>
/// Map of class factories and the registration cookie from the CLSID that the factory creates.
/// </summary>
private readonly Dictionary<Guid, (BaseClassFactory factory, uint cookie)> factories = [];

private readonly StrategyBasedComWrappers comWrappers = new();

/// <summary>
/// Tracks the creation of the first instance after server is started.
/// </summary>
private TaskCompletionSource<object>? firstInstanceCreated;

/// <summary>
/// Initializes a new instance of the <see cref="ComServer"/> class.
/// </summary>
public unsafe ComServer()
{
using ComPtr<IGlobalOptions> options = default;
Guid clsid = CLSID_GlobalOptions;
Guid iid = IGlobalOptions.IID_Guid;
if (CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, (void**)options.GetAddressOf()) == HRESULT.S_OK)
{
options.Get()->Set(GLOBALOPT_PROPERTIES.COMGLB_RO_SETTINGS, (nuint)GLOBALOPT_RO_FLAGS.COMGLB_FAST_RUNDOWN);
}
}

/// <summary>
/// Register a class factory with the server.
/// </summary>
/// <param name="factory">The class factory to register.</param>
/// <param name="comWrappers">The implementation of <see cref="ComWrappers"/> to use for wrapping.</param>
/// <returns><see langword="true"/> if <paramref name="factory"/> was registered; otherwise, <see langword="false"/>.</returns>
/// <remarks>Only one factory can be registered for a CLSID.</remarks>
/// <exception cref="ObjectDisposedException">The instance is disposed.</exception>
/// <exception cref="ArgumentNullException"><paramref name="factory"/> or <paramref name="comWrappers"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The server is running.</exception>
/// <seealso cref="UnregisterClassFactory(Guid)"/>
public unsafe bool RegisterClassFactory(BaseClassFactory factory, ComWrappers comWrappers)
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
ArgumentNullException.ThrowIfNull(factory);
ArgumentNullException.ThrowIfNull(comWrappers);
if (IsRunning)
{
throw new InvalidOperationException("Can only add class factories when server is not running.");
}

Guid clsid = factory.Clsid;

if (factories.ContainsKey(clsid))
{
return false;
}

factory.InstanceCreated += Factory_InstanceCreated;

nint wrapper = this.comWrappers.GetOrCreateComInterfaceForObject(new BaseClassFactoryWrapper(factory, comWrappers), CreateComInterfaceFlags.None);

uint cookie;
CoRegisterClassObject(&clsid, (IUnknown*)wrapper, CLSCTX.CLSCTX_LOCAL_SERVER, (REGCLS.REGCLS_MULTIPLEUSE | REGCLS.REGCLS_SUSPENDED), &cookie).ThrowOnFailure();

factories.Add(clsid, (factory, cookie));
return true;
}

/// <summary>
/// Unregister a class factory.
/// </summary>
/// <param name="clsid">The CLSID of the server to remove.</param>
/// <returns><see langword="true"/> if the server was removed; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ObjectDisposedException">The instance is disposed.</exception>
/// <exception cref="InvalidOperationException">The server is running.</exception>
/// <seealso cref="RegisterClassFactory(BaseClassFactory, ComWrappers)"/>
public unsafe bool UnregisterClassFactory(Guid clsid)
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
if (IsRunning)
{
throw new InvalidOperationException("Can only remove class factories when server is not running.");
}

if (!factories.TryGetValue(clsid, out (BaseClassFactory factory, uint cookie) data))
{
return false;
}
factories.Remove(clsid);

data.factory.InstanceCreated -= Factory_InstanceCreated;

CoRevokeClassObject(data.cookie).ThrowOnFailure();
return true;
}

private void Factory_InstanceCreated(object? sender, InstanceCreatedEventArgs e)
{
if (IsDisposed)
{
return;
}

InstanceCreated?.Invoke(this, e);
firstInstanceCreated?.TrySetResult(e.Instance);
}

/// <summary>
/// Gets a value indicating whether the server is running.
/// </summary>
public bool IsRunning { get; private set; }

/// <summary>
/// Starts the server.
/// </summary>
/// <remarks>Calling <see cref="Start"/> is non-blocking.</remarks>
/// <exception cref="ObjectDisposedException">The instance is disposed.</exception>
public void Start()
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
if (IsRunning)
{
return;
}

firstInstanceCreated = new();
CoResumeClassObjects().ThrowOnFailure();
IsRunning = true;
}

/// <summary>
/// Stops the server.
/// </summary>
/// <exception cref="ObjectDisposedException">The instance is disposed.</exception>
public void Stop()
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
if (!IsRunning)
{
return;
}

firstInstanceCreated = null;
IsRunning = false;
CoSuspendClassObjects().ThrowOnFailure();
}

/// <summary>
/// Wait for the server to have created an object since it was started.
/// </summary>
/// <returns>The first object created if the server is running; otherwise <see langword="null"/>.</returns>
/// <exception cref="ObjectDisposedException">The instance is disposed.</exception>
public async Task<object?> WaitForFirstObjectAsync()
{
ObjectDisposedException.ThrowIf(IsDisposed, this);

TaskCompletionSource<object>? local = firstInstanceCreated;
if (local is null)
{
return null;
}
return await local.Task.ConfigureAwait(false);
}

/// <summary>
/// Gets a value indicating whether the instance is disposed.
/// </summary>
public bool IsDisposed
{
get;
private set;
}

/// <summary>
/// Force the server to stop and release all resources.
/// </summary>
public void Dispose()
{
if (!IsDisposed)
{
try
{
_ = CoSuspendClassObjects();

IsRunning = false;

foreach (var clsid in factories.Keys)
{
_ = UnregisterClassFactory(clsid);
}
}
finally
{
IsDisposed = true;
}
}
}

/// <summary>
/// Occurs when the server creates an object.
/// </summary>
public event EventHandler<InstanceCreatedEventArgs>? InstanceCreated;
}
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.