Search code examples
.netcomout-of-processreg-free

Windows COM: Client 32bit C++ native application vs. server .NET 6 out-of-proc registry-free possible?


We have an old large 32bit C++ application (created with Borland C++ Builder 5) on its memory limits. We would like to have it call modules written in C# that reside in another process. With .NET 6, Microsoft propagated .NET COM servers and registry-free (!) access from a client, but the examples show a .NET client with in-process access only. The documentation "COM activation for .NET Core on Windows" is not very clear about whether it is possible, it says:

COM activation in this document is currently limited to in-proc scenarios. Scenarios involving out-of-proc COM activation are deferred.

Microsoft demos:

Is it possible to achieve this? Out-of-process is mandatory, and registry-free is very important to us because of deployment issues we would have otherwise.

I still hope I get things running with a manifest file for the client.

Addendum

Out-of-Process

The out-of-process part seems to come for free anyway: When I register (regsvr32) my .NET dll (built for target AnyCPU), it can be called from my 32bit native app. Because AnyCPU means 64bit for COM on a 64bit Windows, it automatically starts the built-in surrogate process dllhost.exe.

Registry-free

So my remaining task is the registry-free configuration with application manifests. It turns out we already have a manifest for registry-free COM with some 3rd party OCX controls, but unfortunately, adding the entry from Microsoft's working reg-free example (.NET7 server and .NET7 client) does not work. What I did was adding the following reference to the already existing application manifest of the native client:

  <dependency>
    <dependentAssembly>
      <assemblyIdentity type="win32" name="DllServer.X" version="1.0.0.0" />
    </dependentAssembly>
  </dependency>

I put the build output from the COM server to the build output of the client, along with it the client's manifest DllServer.X.manifest which is referenced from the code above:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<assembly manifestVersion="1.0"
    xmlns="urn:schemas-microsoft-com:asm.v1">
    <assemblyIdentity type="win32" name="DllServer.X" version="1.0.0.0" />
    <file name="DllServer.comhost.dll">
        <comClass clsid="{af080472-f173-4d9d-8be7-435776617347}" threadingModel="Apartment" />
    </file>
</assembly>

At runtime, activating the server class (CoCreateInstance) fails with HRESULT 0x80070057 (E_INVALIDARG) which is irritating because the same client code works with a registered COM server, and the client code should not be affected here. If I exchange the dependency tag above in the client application manifest by another tag which originates from DllServer.X.manifest as follows

    <file name="DllServer.comhost.dll">
        <comClass clsid="{af080472-f173-4d9d-8be7-435776617347}" threadingModel="Apartment" />
    </file>

(and all the already working existing COM references in our app manifest actually look like this), the CoCreateInstance call at runtime fails with: 0x80040154 (REGDB_E_CLASSNOTREG).

Same with the Microsoft out-of-proc example as base with the following changes:

  1. Make the DllServer project registry-free by adding <EnableRegFreeCom>True</EnableRegFreeCom> next to the EnableComHosting tag.

  2. Compile the project and copy its output to the NativeClient's output directory.

  3. Create a new file NativeClient.manifest in the NativeClient project directory with the following contents (with or without the assemblyIdentity for NativeClient does not seem to make a difference):

    <?xml version="1.0" encoding="utf-8"?>
    <!-- https://docs.microsoft.com/windows/desktop/sbscs/assembly-manifests -->
    <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
      <!-- <assemblyIdentity
        type="win32" 
        name="NativeClient"
        version="1.0.0.0" /> -->
    
      <dependency>
        <dependentAssembly>
          <!-- RegFree COM matching the registration of the generated managed COM server -->
          <assemblyIdentity
              type="win32"
              name="DllServer.X"
              version="1.0.0.0"/>
        </dependentAssembly>
      </dependency>
    </assembly>
    
  4. Compile the NativeClient project (I changed it to x86 with the Configuration Manager because this is what we have here, and it eases the out-of-proc case if the COM client is 64bit due to the automatic COM surrogate involvation).

  5. Embed manifest in client EXE: Open a Visual Studio Command Prompt, cd to the NativeClient directory and execute:

    mt.exe -manifest NativeClient.manifest -outputresource:..\Debug\NativeClient.exe;1
    
  6. Run the Client. For me, it says CoCreateInstance failure: 0x80040154
    Why? What is the correct application manifest?

In the log produced by sxstrace trace -logfile:sxstrace.etl the NativeClient did not even show up.

By the way: providing the ManagedClient with the manifest file via reference in the project file actually works, but we need it for the native client.


Solution

  • COM does allow 32bit <=> 64 bits cross-process communication between clients and servers. It "just" requires them to properly implement marshaling: proxy / stub for IUnknown-derived interfaces and/or type libraries (.tlb) for IDispatch-derived interfaces.

    The difficulty with reg-free COM is it's relatively recent (more than COM itself) and poorly supported by tooling and documentation.

    The idea is really for COM servers and COM clients to declare:

    • as a COM server, what services (COM objects, COM interfaces) they serve
    • as a COM client, what services they need

    And both sides also need to declare how connection will work, which is how these interfaces will be marshaled across processes, possibly including satellite files or embedded resources.

    To "declare this, both sides use Application manifests, to enable COM to find the same information it would have found looking in the registry, but now without using it.

    I've made a full working sample here on github: RegfreeNetComServer. In this sample, the server application manifest looks like this:

    <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
       <!-- server name can be anything but must not collide with client -->
        <assemblyIdentity version="1.0.0.0" name="RegfreeNetComServer.app"/>
    
        <!-- this means hey, "there's a satellite file I come with" -->
        <file name="server.tlb">
            <!-- this means "and it contains a typelib with this id" -->
            <typelib tlbid="{46F3FEB2-121D-4830-AA22-0CDA9EA90DC3}"
              version="1.0" helpdir="" />
        </file>
    
        <!-- this is for an IDispatch-derived interface named "IServer" -->
        <comInterfaceExternalProxyStub
          iid="{F586D6F4-AF37-441E-80A6-3D33D977882D}"
          name="IServer"
          tlbid="{46F3FEB2-121D-4830-AA22-0CDA9EA90DC3}"
          proxyStubClsid32="{00020424-0000-0000-C000-000000000046}" />
        
        ... other things
    </assembly>
    

    For the client, it's the same, but the identity (the client's app) is obviously different:

    <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
        <!-- client name can be anything but must not collide with server -->
        <assemblyIdentity version="1.0.0.0" name="NativeClient.app" type="win32" />
    
        <!-- the rest is identical since the tlb file is the same in this case -->
        <file name="server.tlb">
            <!-- this means "and it contains a typelib with this id" -->
            <typelib tlbid="{46F3FEB2-121D-4830-AA22-0CDA9EA90DC3}"
              version="1.0" helpdir="" />
        </file>
    
        <comInterfaceExternalProxyStub
          iid="{F586D6F4-AF37-441E-80A6-3D33D977882D}"
          name="IServer"
          tlbid="{46F3FEB2-121D-4830-AA22-0CDA9EA90DC3}"
          proxyStubClsid32="{00020424-0000-0000-C000-000000000046}" />
    </assembly>
    

    In my sample, the tlb is in an external "server.tlb" file, and this file must simply be present aside both client and server .exe, but it can also be embedded as a Win32 resource in each of these. .NET 6+ allows this, Visual Studio C++ allows this for native projects.

    If your reg-free configuration is really invalid, you'll get Windows' MessageBox alerts with errors. The easier to fix is to check the Event Viewer / Application tab which contains quite informative events.