Как создать класс ComVisible в свой собственный домен приложения в однопотоковом клиенте?
Эта проблема
При создании двух независимых классов.NET COM-видимых в одном однопоточном COM-клиенте.NET загружает их оба в один и тот же домен приложений.
Я предполагаю, что это потому, что они загружаются в один поток / процесс.
Пример такого поведения показан в этом репозитории GitHub.
По сути, демонстрация выглядит следующим образом:
- Создание одного класса COM
- Установите атрибут для первого COM-объекта, который во внутренних вызовах
SetData
наCurrentDomain
, - Создайте второй, независимый COM-класс (другое имя интерфейса, GUID и т. Д.)
- Прочитайте
AppDomain
атрибут - Продемонстрировать, что это выглядит так же
- Кроме того, получите хэш-код от обоих
AppDomain
с, отмечая, что это также то же самое
Почему это проблема?
Когда оба класса имеют AppDomain.CurrentDomain.AssemblyResolve
реализованное событие (или любое другое событие AppDomain, в этом отношении), события могут мешать друг другу. Это как минимум одно осложнение; Я предполагаю, что могут быть и другие.
Идея
Я подумал, что лучший способ справиться с этим - создать новый домен приложения для каждого COM-объекта. Поскольку я не мог найти (или Google) способ сделать это управляемым способом, я подумал, что может быть необходимо сделать это в неуправляемом коде.
Я сделал небольшую детективную работу. В OleView атрибут InprocServer32 для COM-видимого класса.NET имеет вид mscoree.dll
, Итак, я создал "Shim" DLL, которая пересылала все свои EXPORTS
в mscoree.dll. В процессе устранения (исключая экспорт до тех пор, пока COM больше не будет загружаться), я обнаружил, что DllGetClassObject
вmscoree
был ответственным за запуск среды выполнения.NET и возврат созданного экземпляра COM-объекта.
Итак, что я могу сделать, это реализовать свой собственныйDllGetClassObject
, вот так:
- Разместите среду выполнения.NET в неуправляемой сборке с помощью CLRCreateInstance.
- Создать объект в новом
AppDomain
и вернуть его
(Я предполагаю, что это не так просто, как кажется)
Вопрос
Прежде чем приступить к этому потенциально сложному и длительному процессу, я хотел бы знать:
- Есть ли управляемый способ заставить класс.NET COM-видимого работать в своем собственном домене приложений?
- Если нет, это "правильный" способ сделать это, или я упускаю очевидное решение?
2 ответа
Если код не должен запускаться в одном и том же процессе, проще всего будет использовать внепроцессный сервер. Проходить CLSCTX_LOCAL_SERVER
в CoCreateInstance
и каждый класс будет создан в dllhost
процесс хостинга.
Например на клиенте:
public static object CreateLocalServer(Guid clsid)
{
return CoCreateInstance(clsid, null, CLSCTX.LOCAL_SERVER, IID_IUnknown);
}
public static object CreateLocalServer(string progid)
{
Contract.Requires(!string.IsNullOrEmpty(progid));
Guid clsid;
CLSIDFromProgID(progid, out clsid);
return CreateLocalServer(clsid);
}
enum CLSCTX : uint
{
INPROC_SERVER = 0x1,
INPROC_HANDLER = 0x2,
LOCAL_SERVER = 0x4,
INPROC_SERVER16 = 0x8,
REMOTE_SERVER = 0x10,
INPROC_HANDLER16 = 0x20,
RESERVED1 = 0x40,
RESERVED2 = 0x80,
RESERVED3 = 0x100,
RESERVED4 = 0x200,
NO_CODE_DOWNLOAD = 0x400,
RESERVED5 = 0x800,
NO_CUSTOM_MARSHAL = 0x1000,
ENABLE_CODE_DOWNLOAD = 0x2000,
NO_FAILURE_LOG = 0x4000,
DISABLE_AAA = 0x8000,
ENABLE_AAA = 0x10000,
FROM_DEFAULT_CONTEXT = 0x20000,
ACTIVATE_32_BIT_SERVER = 0x40000,
ACTIVATE_64_BIT_SERVER = 0x80000
}
[DllImport(Ole32, ExactSpelling = true, PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface)]
public static extern object CoCreateInstance(
[In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
[MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter,
CLSCTX dwClsContext,
[In, MarshalAs(UnmanagedType.LPStruct)] Guid riid);
[DllImport(Ole32, CharSet = CharSet.Unicode, PreserveSig = false)]
public static extern void CLSIDFromProgID(string progId, out Guid rclsid);
Вы также можете зарегистрировать собственный хост и поменять местами стандартный InProcServer32
за LocalServer32
, Для примера сервера
// StandardOleMarshalObject keeps us single-threaded on the UI thread
// https://msdn.microsoft.com/en-us/library/74169f59(v=vs.110).aspx
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ProgId(IpcConstants.CoordinatorProgID)]
public sealed class Coordinator : StandardOleMarshalObject, ICoordinator
{
public Coordinator()
{
// required for regasm
}
#region Registration
[ComRegisterFunction]
internal static void RegasmRegisterLocalServer(string path)
{
// path is HKEY_CLASSES_ROOT\\CLSID\\{clsid}", we only want CLSID...
path = path.Substring("HKEY_CLASSES_ROOT\\".Length);
using (RegistryKey keyCLSID = Registry.ClassesRoot.OpenSubKey(path, writable: true))
{
// Remove the auto-generated InprocServer32 key after registration
// (REGASM puts it there but we are going out-of-proc).
keyCLSID.DeleteSubKeyTree("InprocServer32");
// Create "LocalServer32" under the CLSID key
using (RegistryKey subkey = keyCLSID.CreateSubKey("LocalServer32"))
{
subkey.SetValue("", Assembly.GetExecutingAssembly().Location, RegistryValueKind.String);
}
}
}
[ComUnregisterFunction]
internal static void RegasmUnregisterLocalServer(string path)
{
// path is HKEY_CLASSES_ROOT\\CLSID\\{clsid}", we only want CLSID...
path = path.Substring("HKEY_CLASSES_ROOT\\".Length);
Registry.ClassesRoot.DeleteSubKeyTree(path, throwOnMissingSubKey: false);
}
#endregion
}
Хорошо... вот управляемое доказательство концепции, использующее RGiesecke.DllExport, которое работает; хорошее решение еще предстоит выяснить... так: используйте на свой страх и риск. Я все еще ищу лучшие ответы.
Одна вещь, которую можно улучшить, это то, что нам не нужно новое AppDomain
для каждого экземпляра; только для каждого объекта. Я уверен, что есть и другие тонкости, которые мне не хватает.
Мы компилируем и регистрируем DLL, затем, используя OleView (или реестр), меняем значение ProcServer32 по умолчанию, чтобы оно указывало на саму управляемую DLL. Это можно автоматизировать, предоставив метод в DLL, который украшен [ComRegisterFunction()]
,
using System;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;
using System.IO;
namespace Com_1
{
[Guid("F35D5D5D-4A3C-4042-AC35-CE0C57AF8383")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface IComClass1
{
void SetAppDomainData(string data);
string GetAppDomainData();
int GetAppDomainHash();
}
//https://gist.github.com/jjeffery/1568627
[Guid("00000001-0000-0000-c000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
internal interface IClassFactory
{
void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject);
void LockServer(bool fLock);
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("3CA12D49-CFE5-45A3-B114-22DF2D7A0CAB")]
[Description("Sample COM Class 1")]
[ProgId("Com1.ComClass1")]
public class ComClass1 : MarshalByRefObject, IComClass1, IClassFactory
{
public void SetAppDomainData(string data)
{
AppDomain.CurrentDomain.SetData("CurrentDomainCustomData", data);
}
public string GetAppDomainData()
{
return (string)AppDomain.CurrentDomain.GetData("CurrentDomainCustomData");
}
public int GetAppDomainHash()
{
return AppDomain.CurrentDomain.GetHashCode();
}
[DllExport]
public static uint DllGetClassObject(Guid rclsid, Guid riid, out IntPtr ppv)
{
ppv = IntPtr.Zero;
try
{
if (riid.CompareTo(Guid.Parse("00000001-0000-0000-c000-000000000046")) == 0)
{
//Call to DllClassObject is requesting IClassFactory.
var instance = new ComClass1();
IntPtr iUnk = Marshal.GetIUnknownForObject(instance);
//return instance;
Marshal.QueryInterface(iUnk, ref riid, out ppv);
return 0;
}
else
return 0x80040111; //CLASS_E_CLASSNOTAVAILABLE
}
catch
{
return 0x80040111; //CLASS_E_CLASSNOTAVAILABLE
}
}
public void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject)
{
IntPtr ppv = IntPtr.Zero;
//http://stackru.com/a/13355702/864414
AppDomainSetup domaininfo = new AppDomainSetup();
domaininfo.ApplicationBase = Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName;
var curDomEvidence = AppDomain.CurrentDomain.Evidence;
AppDomain newDomain = AppDomain.CreateDomain("MyDomain", curDomEvidence, domaininfo);
Type type = typeof(ComClass1);
var instance = newDomain.CreateInstanceAndUnwrap(
type.Assembly.FullName,
type.FullName);
ppvObject = instance;
}
public void LockServer(bool fLock)
{
//Do nothing
}
}
}