Hosting managed code and garbage collection

2019-01-19 07:39发布

问题:

I have a C++ out-of-process COM server that hosts a lot of C# code to support the API exposed by the C++ COM objects.

For a variety of reasons, I am considering eliminating the C++ portion of my solution. However, because of constraints outside of my control I have to retain the out-of-process COM server. Microsoft does have a canonical example of this here.

Looking at this example there is something I don't understand. Before the message loop starts, a timer is created to call GC.Collect every 5 seconds. The only mention of this that I can find indicates it's to ensure the COM objects are released in a reasonable timeframe. I'm a little confused about this...does my C++ host currently call GC.Collect automatically? I'm certainly not doing it. And yet I am creating managed objects (with COMVisible(true) as COM objects in the C++ code. Does that mean I should be calling GC.Collect every 5 seconds now? If not, why do I need to call it in this new C# out of process server. Is that to make up for the automatic process that cleans up unreferenced COM objects in a normal C++ application? (Which I assume is happening sometime during the message loop.)

Calling GC.Collect every 5 seconds seems like it could be a bad idea. Am I wrong to worry? Is there some other method by which I could achieve the same results?

I am using .NET 4.5 and Visual Studio 2012.

回答1:

IMO, the easiest way to create a COM out-of-proc server in C# is to use a DLL surrogate process.

You're still limited to dual interfaces (ComInterfaceType.InterfaceIsDual), and you'd need to register the generated type library (and do it as part of deployment, too):

C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe ManagedServer.dll /codebase /tlb

This will allow you to utilize the COM type library marshaller, because you don't have a dedicated COM proxy/stuf DLL for your C# COM objects.

Make sure to use the correct RegAsm.exe binary, depending on the target bit-ness of your ManagedServer.dll assembly. The above assumes x86 code.

Here is a complete working template example. It takes care of the surrogate registration:

using Microsoft.Win32;
using System;
using System.Runtime.InteropServices;

namespace ManagedServer
{
    [ComVisible(true), Guid("1891CF89-1282-4CA8-B7C5-F2608AF1E2F1")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IManagedComObject
    {
        string ComMethod(string data);
    }

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(IManagedComObject))]
    [Guid("989162CD-A6A6-4A7D-A7FB-C94086A4E90A")]
    [ProgId("Noseratio.ManagedComObject")]

    public class ManagedComObject : IManagedComObject
    {
        // public constructor
        public ManagedComObject()
        {
        }

        // IManagedComObject
        public string ComMethod(string data)
        {
            return data;
        }

        // registration
        [ComRegisterFunction()]
        public static void Register(Type type)
        {
            var guid = type.GUID.ToString("B");
            using (var appIdKey = Registry.ClassesRoot.CreateSubKey(@"AppID\" + guid))
            {
                appIdKey.SetValue("DllSurrogate", String.Empty);
            }
            using (var appIdKey = Registry.ClassesRoot.CreateSubKey(@"CLSID\" + guid))
            {
                appIdKey.SetValue("AppId", guid);
            }
        }

        [ComUnregisterFunction()]
        public static void Unregister(Type type)
        {
            var guid = type.GUID.ToString("B");
            using (var appIdKey = Registry.ClassesRoot.OpenSubKey(@"AppID\" + guid, writable: true))
            {
                if (appIdKey != null)
                    appIdKey.DeleteValue("DllSurrogate", throwOnMissingValue: false);
            }
            Registry.ClassesRoot.DeleteSubKeyTree(@"CLSID\" + guid, throwOnMissingSubKey: false);
        }
    }
}

By default, the objects will be created in MTA apartment, so the interface methods may possibly be called on any thread, you'd need to implement thread safety.

If you need an STA thread with message pump inside the surrogate process for your objects, you could do that explicitly by implementing a factory singleton and using CoMarshalInterThreadInterfaceInStream/CoGetInterfaceAndReleaseStream to export objects outside the STA thread (this might be related).

Another point, your COM client code should use CLSCTX_LOCAL_SERVER when creating this an instance of ManagedComObject. This is not the case with Activator.CreateInstance(Type.GetTypeFromProgID("Noseratio.ManagedComObject")), which apparently uses CLSCTX_ALL. This can be easily taken care of:

using System;
using System.Runtime.InteropServices;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            // dynamic obj = Activator.CreateInstance(Type.GetTypeFromProgID("Noseratio.ManagedComObject"));

            dynamic obj = ComExt.CreateInstance(
                Type.GetTypeFromProgID("Noseratio.ManagedComObject").GUID, 
                localServer: true);

            Console.WriteLine(obj.ComMethod("hello"));
        }
    }

    // COM interop
    public static class ComExt
    {
        const uint CLSCTX_LOCAL_SERVER = 0x4;
        const uint CLSCTX_INPROC_SERVER = 0x1;

        static readonly Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046");

        [DllImport("ole32.dll", ExactSpelling = true, PreserveSig = false)]
        static extern void CoCreateInstance(
           [MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
           [MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter,
           uint dwClsContext,
           [MarshalAs(UnmanagedType.LPStruct)] Guid riid,
           [MarshalAs(UnmanagedType.Interface)] out object rReturnedComObject);

        public static object CreateInstance(Guid clsid, bool localServer)
        {
            object unk;
            CoCreateInstance(clsid, null, localServer ? CLSCTX_LOCAL_SERVER : CLSCTX_INPROC_SERVER, IID_IUnknown, out unk);
            return unk;
        }
    }
}