docs/design/features/COM-activation.md
In order to more fully support the vast number of existing .NET Framework users in their transition to .NET Core, support of the COM activation scenario in .NET Core is required. Without this support it is not possible for many .NET Framework consumers to even consider transitioning to .NET Core. The intent of this document is to describe aspects of COM activation for a .NET class written for .NET Core. This support includes but is not limited to activation scenarios such as the CoCreateInstance() API in C/C++ or from within a Windows Script Host instance.
COM activation in this document is currently limited to in-proc scenarios. Scenarios involving out-of-proc COM activation are deferred.
IClassFactory implementation that will construct an instance of the .NET class.The following list represents an exhaustive activation matrix.
| Server | Client | Current Support |
|---|---|---|
| COM* | .NET Core | Yes |
| .NET Core | COM* | Yes |
| .NET Core | .NET Core | Yes |
| .NET Framework | .NET Core | No |
| .NET Core | .NET Framework | No |
* 'COM' is used to indicate any COM environment (e.g. C/C++) other than .NET.
One of the basic issues with the activation of a .NET class within a COM environment is the loading or discovery of an appropriate CLR instance. The .NET Framework addressed this issue through a system wide shim library (described below). The .NET Core scenario has different requirements and limitations on system impact and as such an identical solution may not be optimal or tenable.
The .NET Framework uses a shim library (mscoree.dll) to facilitate the loading of the CLR into a process performing activation - one of the many uses of mscoree.dll. When .NET Framework 4.0 was released, mscoreei.dll was introduced to provide a level of indirection between the system installed shim (mscoree.dll) and a specific framework shim as well as to enable side-by-side CLR scenarios. An important consideration of the system wide shim is that of servicing. Servicing mscoree.dll is difficult since any process with a loaded .NET Framework instance will have the shim loaded, thus requiring a system reboot in order to service the shim.
During .NET class registration, the shim is identified as the in-proc server for the class. Additional metadata is inserted into the registry to indicate what .NET assembly to load and what type to activate. For example, in addition to the typical in-proc server registry values the following values are added to the registry for the TypeLoadException class.
"Assembly"="mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
"Class"="System.TypeLoadException"
"RuntimeVersion"="v1.1.4322"
The above registration is typically done with the RegAsm.exe tool. Alternatively, registry scripts can be generated by RegAsm.exe.
In .NET Core, our intent will be to avoid a system wide shim library. This decision may add additional cost for deployment scenarios, but will reduce servicing and engineering costs by making deployment more explicit and less magic.
The current .NET Core hosting solutions are described in detail at Documentation/design-docs/host-components.md. Along with the existing hosts an additional customizable COM activation host library (comhost.dll) will be added. This library (henceforth identified as 'shim') will export the required functions for COM class activation and registration and act in a way similar to .NET Framework's mscoree.dll.
HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv);
When DllGetClassObject() is called in a COM activation scenario, the following steps will occur. The calling of DllGetClassObject() is usually accomplished through an implicit or explicit call to CoCreateInstance().
<shim_name>.clsidmap naming format exists adjacent to it. Build tooling handles shim customization, including renaming the shim to be based on the managed assembly's name (e.g. NetComServer.dll will have a custom shim called NetComServer.comhost.dll). If the shim is signed the shim will not attempt to discover the manifest on disk.CLSID to managed assembly name and the Fully-Qualified Name for the type. The format of this manifest is defined below. The shim's embedded mapping always takes precedence and in the case an embedded mapping is found, a .clsidmap file on disk will never be used..runtimeconfig.json file exists adjacent to the target managed assembly (<assembly>.runtimeconfig.json), that file is used to describe the target framework and CLR configuration. The documentation for the .runtimeconfig.json format defines under what circumstances this file may be optional.DllGetClassObject() function verifies the CLSID mapping has a mapping for the CLSID.
CLSID is unknown in the mapping the traditional CLASS_E_CLASSNOTAVAILABLE is returned.hostfxr library and retrieves the hostfxr_initialize_for_runtime_config() and hostfxr_get_runtime_delegate() exports..comhost.dll prefix and replacing it with .dll. Using the name of the target assembly, the path to the .runtimeconfig.json file is then computed.hostfxr_initialize_for_runtime_config() export is called..runtimeconfig.json the framework to use can be determined and the appropriate hostpolicy library path is computed.hostpolicy library is loaded and various exports are retrieved.
hostpolicy instance is already loaded, the one presently loaded is re-used.corehost_load() export is called to initialize hostpolicy.
corehost_load() export would always initialize hostpolicy regardless if initialization had already been performed. For .NET Core 3.0, calling the function again will not re-initialize hostpolicy, but simply return.hostfxr_get_runtime_delegate() export is calledhostfxr_get_runtime_delegate() export calls into hostpolicy and determines if the associated coreclr library has been loaded and if so, uses the existing activated CLR instance. If a CLR instance is not available, hostpolicy will load coreclr and activate a new CLR instance.
System.Private.CoreLib on the Internal.Runtime.InteropServices.ComActivator class:
[StructLayout(LayoutKind.Sequential)]
[CLSCompliant(false)]
public unsafe struct ComActivationContextInternal
{
public Guid ClassId;
public Guid InterfaceId;
public char* AssemblyPathBuffer;
public char* AssemblyNameBuffer;
public char* TypeNameBuffer;
public IntPtr ClassFactoryDest;
}
public static class ComActivator
{
...
[CLSCompliant(false)]
public static int GetClassFactoryForTypeInternal(ref ComActivationContextInternal cxtInt);
...
}
System.Private.CoreLib and is subject to change at any time.AssemblyLoadContext for dependency isolation. Each assembly path will get a separate AssemblyLoadContext. This means that if an assembly provides multiple COM servers all of the servers from that assembly will reside in the same AssemblyLoadContext.AssemblyLoadContext will use an AssemblyDependencyResolver that was supplied with the path to the assembly to load assemblies.IClassFactory instance is returned to the caller of DllGetClassObject() to attempt class activation.The DllCanUnloadNow() function will always return S_FALSE indicating the shim is never able to be unloaded. This matches .NET Framework semantics but may be adjusted in the future if needed.
The DllRegisterServer() and DllUnregisterServer() functions adhere to the COM registration contract and enable registration and unregistration of the classes defined in the CLSID mapping manifest. Discovery of the mapping manifest is identical to that which occurs during a call to DllGetClassObject().
The CLSID mapping manifest is a JSON format (.clsidmap extension when on disk) that defines a mapping from CLSID to an assembly name and type name tuple as well as an optional ProgID. Each CLSID mapping is a key in the outer JSON object.
{
"<clsid>": {
"assembly": "<assembly_name>",
"type": "<type_name>",
"progid": "<prog_id>"
}
}
dotnet.exe.GuidAttribute("<GUID>") and the ComVisibleAttribute(true).
IClassX). This means it is advantageous for users to have the class implement a marshalable interface.ProgIdAttribute. If a ProgID is not explicitly specified, the namespace and class name will be used as the ProgID. This follows the same semantics as .NET Framework COM servers.EnableComHosting property is added to the project file.
<EnableComHosting>true</EnableComHosting>EnableComHosting property is true:
.runtimeconfig.json file is created for the assembly.CLSID map is created on disk (.clsidmap).comhost.dll) is copied to the local output directory.comhost.dll binary is renamed to <assembly>.comhost.dll.CLSID map (.clsidmap) is embedded as a resource in the renamed <assembly>.comhost.dll binary.Two options exist for registration and are a function of the intent of the class's author. The .NET Core platform will impose the deployment of a shim instance with a .clsidmap manifest. In order to address potential security concerns, the .NET Core tool chain by default will embed the .clsidmap in the customized shim. When the .clsidmap is embedded the customized shim allows for the implicit signing of the .clsidmap manifest. Once the shim is signed, the option for loading a non-embedded .clsidmap is disabled.
Class registration in the registry for .NET Core classes is greatly simplified and is now identical to that of a non-managed COM class. This is possible due to the presence of the aforementioned .clsidmap manifest. The application developer will be able to use the traditional regsvr32.exe tool for class registration.
RegFree COM for .NET is another style of registration, but does not require registry access. This approach is complicated by the use of application manifests, but does have benefits for limiting environment impact and simplifying deployment. A severe limitation of this approach is that in order to use RegFree COM with a .NET class, the Window OS assumes the use of mscoree.dll for the in-proc server. Without a change in the Windows OS, this assumption in the RegFree .NET scenario makes the existing manifest approach a broken scenario for .NET Core.
An example of a RegFree manifest for a .NET Framework class is below - note the absence of specifying a hosting server library (i.e. mscoree.dll is implied for the clrClass element).
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="NetComServer"
version="1.0.0.0" />
<clrClass
clsid="{3C58BBC9-3966-4B58-8EE2-398CBBC9FDC4}"
name="NetComServer.Server"
threadingModel="Both"
runtimeVersion="v4.0.30319">
</clrClass>
</assembly>
Due to the above issues with traditional RegFree manifests and .NET classes, an alternative system must be employed to enable a low-impact style of class registration for .NET Core.
The .NET Core steps for RegFree are as follows:
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="COMClientPrimitives"
version="1.0.0.0" />
<dependency>
<dependentAssembly>
<!-- RegFree COM - CoreCLR Shim -->
<assemblyIdentity
type="win32"
name="NetComServer.comhost.X"
version="1.0.0.0" />
</dependentAssembly>
</dependency>
</assembly>
comClass tags can be added.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="NetComServer.comhost.X"
version="1.0.0.0" />
<file name="NetComServer.comhost.dll">
<!-- NetComServer.Server -->
<comClass
clsid="{3C58BBC9-3966-4B58-8EE2-398CBBC9FDC4}"
threadingModel="Both" />
</file>
</assembly>
DllGetClassObject() export.Guid values.mscoree.dll) was done at the system level. In the .NET Core scenario the onus is on the application developer to have a servicing process in place for the shim.Calling COM Components from .NET Clients
Calling a .NET Component from a COM Component
<!-- Common links -->