Как вы, наверное, помните из моей предыдущей статьи, взаимодействие unmanaged и managed кода представляет определенную проблему, даже для опытных разработчиков. Причина этого — необходимость понимать, какие процессы происходят при пересечении данными границы CLR.
К сожалению, нередко проблема наладить взаимодействие встает у тех разработчиков, которые слабо знакомы с подноготной технологии COM и возможностями .NET для обеспечения взаимодействия. Это нормально — нельзя знать все на свете. Потому я не буду здесь объяснять всю суть проблемы маршаллинга данных из unmanaged в managed и обратно, а просто дам несколько рабочих рецептов, которые помогут вам тогда, когда нужно срочно и завтра, и вы с тоской смотрите на английское издание книги Inside OLE и понимаете, что разобраться в этом за день нет никакой возможности.
Однако, для тех, кто неплохо в этом разбирается, в конце статьи есть небольшой бонус — способ организации out-process COM на .NET. Честно говоря, я добросовестно считал, что сделать out-process COM с помощью .NET невозможно, однако буквально вчера выяснилось, что все-таки нет, можно. В связи с этим, рассказывать про архитектуру .NET Pipe RPC я скорее всего не буду — она достаточно сложна, однако все предоставляемые ей возможности с легкостью заменяет out-process COM.
Вообще, существует два механизма, предлагаемых Microsoft — это dll export и COM export. Да, именно так — несмотря на то, что о возможности экспорта функций managed-кода мало кто знает, она все-таки есть. Однако, о ней я рассказывать не буду. Дело в том, что экспорт функций не поддерживается .NET на высоком уровне, то есть невозможно просто описать функцию, повесить на нее какой-нибудь атрибут типа [DllExport] и радоваться жизни — придется либо лезть в дебри IL, либо использовать сторонние решения. Оба решения удовлетворительными назвать нельзя, а потому я не буду на этом подробно останавливаться — практической пользы этот механизм не имеет вовсе.
Механизм COM export хорош тем, что его работа достаточно прозрачна для человека, который разбирается в COM, но для .NET разработчика это темный лес. То есть, для успешной работы с этим механизмом, необходимо понимать, что представляет собой технология COM, как осуществляются вызовы, как следует передавать параметры и прочее. однако, знания есть не у всех, а потому — общие рецепты. Давате попробуем создать inproc-COM сервер на C# и обратиться к нему, например, из VBA. Для этого воспользуемся Excel и его встроенным языком скриптов.
Почему я выбрал Excel? Как показывает опыт работы, VBA является достаточно капризным по отношению к утечкам памяти клиентом — в случае малейшой утечки Excel упадет либо сразу по завершению процедуры, либо по выгрузке dll (то есть где-то через минуту после завершения процедуры). Таким образом, падучесть Excel поможет нам понять, что мы делаем что-то не так.
Начнем, пожалуй. Давайте создадим новую сборку, назовем ее, как это принято, TestInprocCOM и сделаем там один CoClass, который будет реализовывать интерфейсы, через которые мы будем его дергать из VBA.
Но для начала, определимся с тем, что же нам нужно от взаимодействия. Как показывает опыт работы, чаще всего требуется передавать
Венцом нашей сегодняшней работы будете передача массива структур, в которых лежат строки. Этого хватит для 99% реальных приложений. Для оставшегося 1% информация будет слишком спецефична для широкой аудитории Хабра, а потому я не буду заострять на этом внимания.
Прошу прощения за слишком длинное вступление. Также вынужден извиниться за слишком подробное объяснение многих, на первый взгляд тривиальных вещей — я не знал, каков уровень тех людей, которые будут читать эту статью и хотел сделать ее понятной абсолютно всем.
Итак, приступим.
Для начала, определим интерфейс, который будет отвечать за вызовы. Комментарии относительно того, для чего нужна та или иная строка я буду делать прямо в коде, чтобы не увеличивать и без того немаленькие размеры статьи.
Как видите, у каждого параметра появилая атрибут MarshalAs. Именно он говорит, как надо маршаллить параметры при переходе границы CLR. Его не обязательно задавать, если маршаллинг по умолчанию совпадает с тем, какое поведение вам нужно. Однако, строки по умолчанию маршаллятся как LPTStr, а потому для строк маршаллинг в виде BSTR нужно задавать всегда.
Объект создан. Компилируем его, после чего регистрируем в системе с помощью команды
Все, у нас есть зарегистрированный объекь, есть библиотека типов для него (tlb-файл) и мы можем приступать к клиенту. Для этого откроем Excel, перейдем к редактору Visual Basic, подключим через Tools->References нашу библиотеку типов и напишем следующее:
Нам должно вывестись два message box — первый «Hello, mike» и второй «OK». Поздравляю, мы все правильно сделали.
Однако, одними строками сыт не будешь. Попробуем передать структуру. Для этого определим ее.
Теперь, расширим интерфейс. Допишем туда еще одну функцию.
И реализуем ее в нашем CoClass-е.
Как видите, мы помещаем в self параметр this — то есть после вызова этой функции в VBA у нас в этом поле будет лежать ссылка на наш объект, с которым мы сможем работать уже методами позднего связывания (то есть чере IDispatch). Проверим работоспособность объекта — перекомпилируем, перерегистрируем, опять откроем Excel и напишем следующий код:
Если все прошло хорошо, то у нас появляются следующие message box-ы
Обратите внимание на
Кстати, косвенным свидетельством того, что все работает правильно, являются подсказки среды VBA.
И наконец, коронный номер. Массив структур со строками. Определим структуру.
Определим в интерфейсе функцию
Просто? А никто и не говорил, что будет сложно. Единственное, что требуется при задании маршаллинга — представлять, как с твоими параметрами будет работать клиент.
Теперь, реализуем функцию в CoClass.
Компиляция, регистрация, и — тестовый пример, который говорит нам о том, что все прошло гладко.
И если все прошло нормально — то мы должны получить следующие сообщения:
Ура нам!
Однако, архитектура inproc COM нам не всегда подходит — например, в случае, если мы хотим создать одиночку (singleton). Так как dll проецируется в поле памяти процесса, различные процессы могут поднимать один и тот же экземпляр класса, который для каждого процесса будет уникальным и ничего не будет знать о других своих экземплярах. Это не всегда то, что нам надо.
Согласно спецификациям COM, реализация outproc сервера выглядит следующим образом. Когда клиент вызывает
Итак, наша задача — сделать это. Для этого, создадим новый проект (console EXE application), перенесем туда код наших интерфейсов, структур и классов. Теперь, определим требуемый код регистрации в функции Main.
Согласно спецификациям COM, outproc серверы должны поддерживать саморегистрацию с ключом /register. Делается это следующим образом:
И наконец, функция Main.
А теперь у нас проблема — вызвать
Решение пришло внезапно с форумов RSDN. Оказывается, в .NET существует класс с аналогом этой функции, который служит специально для этой цели, и зовется он
Компилируем, регистрируем, испытываем, не забыв предварительно удалить регистрацию inproc COM dll. Ура нам!
К сожалению, нередко проблема наладить взаимодействие встает у тех разработчиков, которые слабо знакомы с подноготной технологии COM и возможностями .NET для обеспечения взаимодействия. Это нормально — нельзя знать все на свете. Потому я не буду здесь объяснять всю суть проблемы маршаллинга данных из unmanaged в managed и обратно, а просто дам несколько рабочих рецептов, которые помогут вам тогда, когда нужно срочно и завтра, и вы с тоской смотрите на английское издание книги Inside OLE и понимаете, что разобраться в этом за день нет никакой возможности.
Однако, для тех, кто неплохо в этом разбирается, в конце статьи есть небольшой бонус — способ организации out-process COM на .NET. Честно говоря, я добросовестно считал, что сделать out-process COM с помощью .NET невозможно, однако буквально вчера выяснилось, что все-таки нет, можно. В связи с этим, рассказывать про архитектуру .NET Pipe RPC я скорее всего не буду — она достаточно сложна, однако все предоставляемые ей возможности с легкостью заменяет out-process COM.
Вообще, существует два механизма, предлагаемых Microsoft — это dll export и COM export. Да, именно так — несмотря на то, что о возможности экспорта функций managed-кода мало кто знает, она все-таки есть. Однако, о ней я рассказывать не буду. Дело в том, что экспорт функций не поддерживается .NET на высоком уровне, то есть невозможно просто описать функцию, повесить на нее какой-нибудь атрибут типа [DllExport] и радоваться жизни — придется либо лезть в дебри IL, либо использовать сторонние решения. Оба решения удовлетворительными назвать нельзя, а потому я не буду на этом подробно останавливаться — практической пользы этот механизм не имеет вовсе.
Механизм COM export хорош тем, что его работа достаточно прозрачна для человека, который разбирается в COM, но для .NET разработчика это темный лес. То есть, для успешной работы с этим механизмом, необходимо понимать, что представляет собой технология COM, как осуществляются вызовы, как следует передавать параметры и прочее. однако, знания есть не у всех, а потому — общие рецепты. Давате попробуем создать inproc-COM сервер на C# и обратиться к нему, например, из VBA. Для этого воспользуемся Excel и его встроенным языком скриптов.
Почему я выбрал Excel? Как показывает опыт работы, VBA является достаточно капризным по отношению к утечкам памяти клиентом — в случае малейшой утечки Excel упадет либо сразу по завершению процедуры, либо по выгрузке dll (то есть где-то через минуту после завершения процедуры). Таким образом, падучесть Excel поможет нам понять, что мы делаем что-то не так.
Начнем, пожалуй. Давайте создадим новую сборку, назовем ее, как это принято, TestInprocCOM и сделаем там один CoClass, который будет реализовывать интерфейсы, через которые мы будем его дергать из VBA.
Но для начала, определимся с тем, что же нам нужно от взаимодействия. Как показывает опыт работы, чаще всего требуется передавать
- строки
- массивы
- структуры
- другие объекты
Венцом нашей сегодняшней работы будете передача массива структур, в которых лежат строки. Этого хватит для 99% реальных приложений. Для оставшегося 1% информация будет слишком спецефична для широкой аудитории Хабра, а потому я не буду заострять на этом внимания.
Прошу прощения за слишком длинное вступление. Также вынужден извиниться за слишком подробное объяснение многих, на первый взгляд тривиальных вещей — я не знал, каков уровень тех людей, которые будут читать эту статью и хотел сделать ее понятной абсолютно всем.
Итак, приступим.
Реализация inproc COM.
Для начала, определим интерфейс, который будет отвечать за вызовы. Комментарии относительно того, для чего нужна та или иная строка я буду делать прямо в коде, чтобы не увеличивать и без того немаленькие размеры статьи.
// Показываем, что интерфейс экспортируется для COM
[ComVisible(true)]
// Определяем интерфейс как дуальный. Таким образом, мы сможем с легкостью работать с ним как с помощью
// механизмов раннего связывания в VBA, так и с помощью позднего связывания. Кроме того, это упрощает работу
// с объектом из C++
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
// Определяем GUID интерфейса. Если этого не сделать - он будет меняться на каждой компиляции, что плохо.
[Guid("5AC0488E-9CAC-4032-B59A-37B8B277C4EF")]
public interface ITestInterface
{
// Для параметров функций COM-интерфейсов я всегда задаю, как именно будет маршаллиться параметр.
// Это позволяет избежать ошибок, связанных с неверными предположениями относительно работы .NET
// Здесь мы маршаллим строку по ссылке как BSTR.
void Hello([MarshalAs(UnmanagedType.BStr)] ref string name);
// Здесь аналогично, только маршаллинг задается для возвращаемого значения.
[return: MarshalAs(UnmanagedType.BStr)]
string Say();
}
* This source code was highlighted with Source Code Highlighter.
Как видите, у каждого параметра появилая атрибут MarshalAs. Именно он говорит, как надо маршаллить параметры при переходе границы CLR. Его не обязательно задавать, если маршаллинг по умолчанию совпадает с тем, какое поведение вам нужно. Однако, строки по умолчанию маршаллятся как LPTStr, а потому для строк маршаллинг в виде BSTR нужно задавать всегда.
// Теперь определим CoClass
[ComVisible(true)]
[Guid("B06CC21C-BCBB-4dde-8ED3-BFBD9A31AD6E")]
// ProgId необходим для того, чтобы можно было вызвать объект по имени, а не через GUID
[ProgId("TestInprocCOM.TestCoClass")]
// Помечаем, что функции CoClass-а не нужно преобразовывать в собственный интерфейс и экспортировать.
// Указав этот атрибут, мы полностью контролируем все интерфейсы, которые экспортируются для COM
// Если указать в качестве параметра другое значение - функции, которые не являются реализациями
// интерфейсов будут выделены в отдельный интерфейс и экспортрованы, что не есть хорошо ИМХО.
[ClassInterface(ClassInterfaceType.None)]
// Указываем default interface. Делать этого не обязательно для VBA, но в С++ немного облегчает жизнь.
[ComDefaultInterface(typeof(ITestInterface))]
public class TestCoClass : ITestInterface
{
public void Hello(ref string name)
{
name = "Hello, " + name;
}
public string Say()
{
return "OK";
}
}
* This source code was highlighted with Source Code Highlighter.
Объект создан. Компилируем его, после чего регистрируем в системе с помощью команды
regasm /tlb:testinproccom.tlb testinproccom.dll /codebase.
Все, у нас есть зарегистрированный объекь, есть библиотека типов для него (tlb-файл) и мы можем приступать к клиенту. Для этого откроем Excel, перейдем к редактору Visual Basic, подключим через Tools->References нашу библиотеку типов и напишем следующее:
Dim a As TestCoClass
Dim s As String
Sub main()
Set a = New TestCoClass
s = "mike"
a.Hello s
MsgBox s
msgbox a.Say()
End Sub
* This source code was highlighted with Source Code Highlighter.
Нам должно вывестись два message box — первый «Hello, mike» и второй «OK». Поздравляю, мы все правильно сделали.
Однако, одними строками сыт не будешь. Попробуем передать структуру. Для этого определим ее.
[ComVisible(true)]
// Определяем, что все объекты структуры в памяти располагаются последовательно в том порядке, в котором
// они должны маршалиться в unmanaged.
[StructLayout(LayoutKind.Sequential)]
[Guid("E1AB60D5-F8F5-41fe-BD0D-AE2AC94237DD")]
public struct MyStruct
{
// Строка
[MarshalAs(UnmanagedType.BStr)]
public string wow;
// Массив. Массив следует маршаллить в виде SafeArray, потому как так с ним можно работать из скриптовых
// языков, подобных VBA или JavaScript. Конечно, в случае C++ работа с SafeArray не самая приятная
// процедура, но это уже издержки производства. Второй параметр задает тип объектов в массиве - в нашем
// случае это четырехбайтное знаковое целое.
[MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4)]
public int[] gig;
// Задам маршаллинг объекта в виде указателя на интерфейс IDispatch. В этом случае .NET требует
// чтобы параметр был определен как object.
[MarshalAs(UnmanagedType.IDispatch)]
public object self;
}
* This source code was highlighted with Source Code Highlighter.
Теперь, расширим интерфейс. Допишем туда еще одну функцию.
// Структуры всегда следует передавать из функции в out или ref параметрах - в случае передачи структуры
// в качестве возвращаемого значения возможно некорректное поведение при обработке строк.
void GetStruct([MarshalAs(UnmanagedType.Struct)] ref MyStruct ms);
* This source code was highlighted with Source Code Highlighter.
И реализуем ее в нашем CoClass-е.
public void GetStruct(ref MyStruct ms)
{
ms.gig = new int[] { 1, 2, 3, 4, 5 };
ms.self = this;
ms.wow = "wow";
}
* This source code was highlighted with Source Code Highlighter.
Как видите, мы помещаем в self параметр this — то есть после вызова этой функции в VBA у нас в этом поле будет лежать ссылка на наш объект, с которым мы сможем работать уже методами позднего связывания (то есть чере IDispatch). Проверим работоспособность объекта — перекомпилируем, перерегистрируем, опять откроем Excel и напишем следующий код:
Dim a As TestCoClass
Dim s As String
Dim b As MyStruct
Sub main()
Set a = New TestCoClass
s = "mike"
a.Hello s
MsgBox s
b.wow = "baaa"
a.GetStruct b
MsgBox b.gig(3)
MsgBox b.self.Say()
MsgBox b.wow
Set a = Nothing
End Sub
* This source code was highlighted with Source Code Highlighter.
Если все прошло хорошо, то у нас появляются следующие message box-ы
- Hello, mike
- 4
- OK
- wow
Обратите внимание на
b.self.Say()
. Мы вызываем функцию Say нашего объекта используя позднее связывание. Этот механизм обеспечивается благодаря тому, что мы задали интерфейс как дуальный (то есть способный работать через vtable и IDispatch одновременно). А потому лучше всегда задавать интерфейсы дуальными — это не требует никакой работы, но дает множество преимуществ.Кстати, косвенным свидетельством того, что все работает правильно, являются подсказки среды VBA.
И наконец, коронный номер. Массив структур со строками. Определим структуру.
[ComVisible(true)]
[StructLayout(LayoutKind.Sequential)]
[Guid("1D40391F-C9CF-42ed-9C9E-4991B3F22907")]
public struct MyStruct2
{
[MarshalAs(UnmanagedType.BStr)]
public string param;
public MyStruct2(string par)
:this()
{
param = par;
}
}
* This source code was highlighted with Source Code Highlighter.
Определим в интерфейсе функцию
// Задаем маршаллинг в виде safearray с custom-типом параметра - нашей структурой.
void SetMyStruct2([MarshalAs(UnmanagedType.SafeArray, SafeArrayUserDefinedSubType = typeof(MyStruct2))] out MyStruct2[] structs);
* This source code was highlighted with Source Code Highlighter.
Просто? А никто и не говорил, что будет сложно. Единственное, что требуется при задании маршаллинга — представлять, как с твоими параметрами будет работать клиент.
Теперь, реализуем функцию в CoClass.
public void SetMyStruct2(out MyStruct2[] structs)
{
structs = new MyStruct2[] { new MyStruct2("bla1"), new MyStruct2("bla2"), new MyStruct2("bla3") };
}
* This source code was highlighted with Source Code Highlighter.
Компиляция, регистрация, и — тестовый пример, который говорит нам о том, что все прошло гладко.
Dim a As TestCoClass
Dim sa() As MyStruct2
Sub main()
Set a = New TestCoClass
a.SetMyStruct2 sa
For i = 0 To 2
MsgBox sa(i).param
Next i
Set a = Nothing
End Sub
* This source code was highlighted with Source Code Highlighter.
И если все прошло нормально — то мы должны получить следующие сообщения:
- bla1
- bla2
- bla3
Ура нам!
Outproc COM.
Однако, архитектура inproc COM нам не всегда подходит — например, в случае, если мы хотим создать одиночку (singleton). Так как dll проецируется в поле памяти процесса, различные процессы могут поднимать один и тот же экземпляр класса, который для каждого процесса будет уникальным и ничего не будет знать о других своих экземплярах. Это не всегда то, что нам надо.
Согласно спецификациям COM, реализация outproc сервера выглядит следующим образом. Когда клиент вызывает
CoGetClassObject
с соответствующими параметрами, COM сначала просматриваем свою внутреннюю таблицу фабрик класса, ища заданный клиентом CLSID. Если фабрика класса в таблице отсутствует, то COM обращается к реестру и запускает соответствующий модуль EXE. Задача последнего — как можно быстрее зарегистрировать свои фабрики класса, чтобы их могла найти COM. Для регистрации фабрики класса EXE используеь функцию COM CoRegisterClassObject.
Итак, наша задача — сделать это. Для этого, создадим новый проект (console EXE application), перенесем туда код наших интерфейсов, структур и классов. Теперь, определим требуемый код регистрации в функции Main.
Согласно спецификациям COM, outproc серверы должны поддерживать саморегистрацию с ключом /register. Делается это следующим образом:
static private void RegisterManagedType(Type type)
{
String strClsId = "{" + Marshal.GenerateGuidForType(type).ToString().ToUpper(CultureInfo.InvariantCulture) + "}";
// Create the HKEY_CLASS_ROOT\CLSID key.
using (RegistryKey ClsIdRootKey = Registry.ClassesRoot.CreateSubKey("CLSID"))
{
// Create the HKEY_CLASS_ROOT\CLSID\<CLSID> key.
using (RegistryKey ClsIdKey = ClsIdRootKey.CreateSubKey(strClsId))
{
ClsIdKey.SetValue("", type.FullName);
// Create the HKEY_CLASS_ROOT\CLSID\<CLSID>\LocalServer32 key.
using (RegistryKey LocalServerKey = ClsIdKey.CreateSubKey("LocalServer32"))
{
LocalServerKey.SetValue("", (new Uri(Assembly.GetExecutingAssembly().CodeBase)).LocalPath);
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
И наконец, функция Main.
[MTAThread]
static void Main(string[] args)
{
if ((args.Length == 1) && (args[0] == "register"))
{
RegisterManagedType(typeof(TestCoClass));
Console.WriteLine("Server registered, press any key to exit");
Console.ReadKey();
return;
}
// Но как зарегистрировать фабрики класса?
}
* This source code was highlighted with Source Code Highlighter.
А теперь у нас проблема — вызвать
CoRegisterClassObject
для регистрации фабрик класса. Казалось бы, простое решение — воспользоваться platform invoke для этой функции и радоваться жизни. Однако, ничего не получится — Microfost явно запрещает p/invoke для функции регистрации фабрик класса.Решение пришло внезапно с форумов RSDN. Оказывается, в .NET существует класс с аналогом этой функции, который служит специально для этой цели, и зовется он
RegistrationServices
. У него есть метод RegisterTypeForComClients
который делает ровненько то же самое, что требуется нам от метода CoRegisterClassObject.
RegistrationServices rs = new RegistrationServices();
rs.RegisterTypeForComClients(
typeof(TestCoClass),
RegistrationClassContext.RemoteServer | RegistrationClassContext.LocalServer | RegistrationClassContext.InProcessServer,
RegistrationConnectionType.MultipleUse);
Console.WriteLine("Server started, press any key to exit");
Console.ReadKey();
* This source code was highlighted with Source Code Highlighter.
Компилируем, регистрируем, испытываем, не забыв предварительно удалить регистрацию inproc COM dll. Ура нам!