Skip to content

Latest commit

 

History

History
240 lines (185 loc) · 19.1 KB

COM-activation.md

File metadata and controls

240 lines (185 loc) · 19.1 KB

COM activation for .NET Core on Windows

Purpose

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.

Requirements

  • Discover all installed versions of .NET Core.
  • Load the appropriate version of .NET Core for the class if a .NET Core instance is not running, or validate the currently existing .NET Core instance can satisfy the class requirement.
  • Return an IClassFactory implementation that will construct an instance of the .NET class.
  • Support the discrimination of concurrently loaded CLR versions.

Environment matrix

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.

Design

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.

.NET Framework class COM activation

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.

.NET Core class COM activation

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);

HRESULT DllCanUnloadNow();

HRESULT DllRegisterServer();

HRESULT DllUnregisterServer();

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().

  1. Determine additional registration information needed for activation.
    • The shim will check for an embedded manifest. If the shim does not contain an embedded manifest, the shim will check if a file with the <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.
    • The manifest will contain a mapping from 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.
    • The manifest will define an exhaustive list of .NET classes the shim is permitted to provide.
    • If a .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.
  2. The DllGetClassObject() function verifies the CLSID mapping has a mapping for the CLSID.
    • If the CLSID is unknown in the mapping the traditional CLASS_E_CLASSNOTAVAILABLE is returned.
  3. The shim attempts to load the latest version of the hostfxr library and retrieves the hostfxr_initialize_for_runtime_config() and hostfxr_get_runtime_delegate() exports.
  4. The target assembly name is computed by stripping off the .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.
  5. The hostfxr_initialize_for_runtime_config() export is called.
  6. Based on the .runtimeconfig.json the framework to use can be determined and the appropriate hostpolicy library path is computed.
  7. The hostpolicy library is loaded and various exports are retrieved.
    • If a hostpolicy instance is already loaded, the one presently loaded is re-used.
    • If a CLR is active within the process, the requested CLR version will be validated against that CLR. If version satisfiability fails, activation will fail.
  8. The corehost_load() export is called to initialize hostpolicy.
    • Prior to .NET Core 3.0, during application activation the 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.
  9. The hostfxr_get_runtime_delegate() export is called
  10. The hostfxr_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.
    • If a CLR is active within the process, the requested CLR version will be validated against that CLR. If version satisfiability fails, activation will fail.
  11. A request to the CLR is made to create a managed delegate to a static "activation" method. The delegate is returned to the shim to attempt activation of the requested class.
    • The details of the activation API are implementation defined, but presently reside in 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);
          ...
      }
      Note this API is not exposed outside of System.Private.CoreLib and is subject to change at any time.
    • The loading of the assembly will take place in a new 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.
    • The created AssemblyLoadContext will use an AssemblyDependencyResolver that was supplied with the path to the assembly to load assemblies.
  12. The 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().

CLSID map format

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>"
    }
}

.NET Core COM server creation

  1. A new .NET Core class library project is created using dotnet.exe.
  2. A class is defined that has the GuidAttribute("<GUID>") and the ComVisibleAttribute(true).
    • In .NET Core, unlike .NET Framework, there is no generated class interface generation (i.e. IClassX). This means it is advantageous for users to have the class implement a marshalable interface.
    • A ProgID for the class can be defined using the 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.
  3. The EnableComHosting property is added to the project file.
    • i.e. <EnableComHosting>true</EnableComHosting>
  4. During class project build, the following actions occur if the EnableComHosting property is true:
    1. A .runtimeconfig.json file is created for the assembly.
    2. The resulting assembly is interrogated for classes with the attributes defined above and a CLSID map is created on disk (.clsidmap).
    3. The target Framework's shim binary (i.e. comhost.dll) is copied to the local output directory.
    4. The comhost.dll binary is renamed to <assembly>.comhost.dll.
    5. The generated CLSID map (.clsidmap) is embedded as a resource in the renamed <assembly>.comhost.dll binary.

.NET Core COM server registration

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.

Registry

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.

Registration-Free

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:

  1. The native application will still define an application manifest, but instead of specifying the managed assembly as a dependency the application will define the shim as a dependent assembly.
    <?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>
  2. The tool chain can optionally generate a SxS manifest for the shim. Both the SxS manifest and the shim library will need to be app-local for the scenario to work. Note that the application developer is responsible for adding to or merging the generated shim's manifest with one the user may have defined for other scenarios. An example shim manifest is defined below and with it the SxS logic will naturally know to query the shim for the desired class. Note that multiple 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>
  3. When the native application starts up, its SxS manifest will be read and dependency assemblies discovered. Exported COM classes will also be registered in the process.
  4. COM activation then proceeds as defined above starting with a call to the shim's DllGetClassObject() export.

Compatibility concerns

  • Side-by-side concerns with the registration of classes that are defined in both .NET Framework and .NET Core.
    • i.e. Both classes have identical Guid values.
  • RegFree COM will not work the same between .NET Framework and .NET Core.
    • See details above.
  • Servicing of the .NET Framework shim (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.
  • There is no support for different versions of .NET Core running concurrently. This is not the case in .NET Framework where a 2.0 and 4.0 runtime can run in parallel. A potential future solution would be support for out-of-proc COM servers.

References

Calling COM Components from .NET Clients

Calling a .NET Component from a COM Component

Using COM Types in Managed Code

Exposing .NET Framework Components to COM