Портабельное программирование с COM
Работа с COM в C# вообще и Visual Studio в частности организована достаточно удобно: нужно просто добавить ссылку на нужный для разработки компонент с помощью IDE, и всё. Среда и компилятор сами смогут извлечь из окружения нужную метаинформацию и сгенерировать обёртки, так что программисту останется только написать корректный код, который использует эти средства. Да, есть определённые тонкости и проблемы, связанные с некорректно описанными метаданными (в частности, сам я сталкивался с некоторыми проблемами при интеграции с MS Project), но в большинстве случаев никаких проблем не возникает.
Однако, у такого способа работы с COM есть и некоторые заметные ограничения. В частности, для каждой компиляции требуется присутствие на машине установленных компонентов. Естественно, как же без этого компилятор сможет извлечь из них метаинформацию и подготовить сборку! Однако же, в реальности это требование может стать проблематичным.
Как правило, современный код с использованием COM уже не пишут, а используется он в основном для интеграции с различными устаревшими подсистемами, в изобилии встречающимися на производстве (и хорошо ещё, если там использован всего лишь COM, а не какие-нибудь эзотерические компоненты на Прологе или COBOL). Моя практика показывает, что в разработку такого рода интеграционных средств, как правило, вовлекается один или два разработчика. Всем остальным причастным к разработке лицам (в том числе и билд-серверу) устанавливать эти устаревшие компоненты совершенно не хочется, и было бы очень неприятно, если бы им обязательно пришлось это делать для компиляции программы.
Конечно, можно подойти к этому вопросу по-разному, в зависимости от архитектуры
системы. Можно изолировать такого рода интеграционные компоненты в отдельные
сборки, можно добавлять в проект специальные конфигурации и делать ссылки на
COM-компоненты доступными только в этих конфигурациях. В данной статье я
предлагаю ещё один возможный вариант взаимодействия с использованием добавленной
в .NET 4 возможности работы с COM с использованием ключевого слова dynamic
.
Использование COM-библиотеки
Итак, предположим, что у нас есть компонент, реализующий интерфейс
IComService
, который мы хотим использовать. В первую очередь устанавливаем
компонент у себя на машине и пишем код, который этот класс использует. Выглядеть
это будет примерно вот так:
IComService instance = new IComService();
instance.HelloWorld();
При разработке я и в дальнейшем рекомендую использовать именно такой код (то есть код, написанный с использованием явной ссылки на COM-библиотеку). Это позволит иметь автодополнение, подсказки от IDE и компилятора, и прочие плюшки, которые мы привыкли ожидать от строго и статически типизированного языка.
Динамическое связывание
Однако понятно, что этот код не скомпилируется на машинах коллег, которые не
установили соответствующую COM-библиотеку: ссылка на эту библиотеку в их
окружении будет считаться некорректной, и тип IComService
, разумеется, там не
существует.
Эту проблему можно решить динамическим связыванием с COM-библиотекой. C# начиная с версии 4 предоставляет очень удобные возможности для этого. Для начала посмотрим GUID интересующего нас COM-типа (есть разные способы его узнать, но я просто нажимаю F12 в Visual Studio и смотрю значение атрибута [Guid]
в открывшемся редакторе с метаданными типа).
Затем мы можем во время выполнения приложения получить этот тип, создать его
экземпляр через System.Activator
, а затем работать с ним динамически
(COM-обёртка будет диспетчеризовать все наши вызовы, и в основном она с этим
справляется корректно):
const string TypeGuid = "03653ea3-b63b-447b-9d26-fa86e679087b";
Type type = Type.GetTypeFromCLSID(Guid.Parse(TypeGuid));
dynamic instance = Activator.CreateInstance(type);
instance.HelloWorld();
Теперь мы получили код, который замечательно компилируется и у нас на машине, и у наших коллег (и при этом работает на целевой машине — если на ней нужные компоненты установлены, разумеется), однако в нём не работает автодополнение. Чтобы решить эту проблему, я предлагаю использовать гибридное решение.
Если присмотреться к старому и новому коду, можно обнаружить, что разница — лишь
в способах инициализации объекта, а код, который этот объект использует (в
примере это только строка instance.HelloWorld();
), не отличается для случаев
статического и динамического связывания. Поэтому я у себя на проектах ввёл
практику условной компиляции такого кода через #define
.
Разработчик, у которого на машине этот компонент установлен (и который активно с
ним работает), заводит для себя конфигурацию, в которой определён макрос вида
COM_LIBRARY_INSTALLED
. Код инициализации компонента мы переписываем таким
образом:
#if COM_LIBRARY_INSTALLED
IComService instance = new IComService();
#else
const string TypeGuid = "03653ea3-b63b-447b-9d26-fa86e679087b";
Type type = Type.GetTypeFromCLSID(Guid.Parse(TypeGuid));
dynamic instance = Activator.CreateInstance(type);
#endif
instance.HelloWorld();
Разработчики, у которых компонент на машине не установлен, собирают проект без
этого макроса, и получают вариант с dynamic
. А основной разработчик
интеграционного кода при этом собирает проект с активным макросом, и поэтому
получает все преимущества того, что компонент у него на машине присутствует.
Сборочный сервер также собирает проект без макроса, и клиенту (и тестировщикам)
мы поставляем версию с dynamic
.
Обработчики событий
Есть некоторые случаи, когда код в варианте с dynamic
отличается от основного
варианта кода, или даже вообще непонятно, как его писать. Одна из причин таких
проблем — это события, генерируемые в COM-библиотеке. Такие события обычно
представляются обычными CLI-событиями, сопоставленными с типами-делегатами,
отдельно сгенерированными внутри COM-обёртки для каждого события. Представим,
что у нас в варианте со строгой типизацией скомпилировалась такая конструкция
[1]:
instance.DataReceived += () => Console.WriteLine("Data received");
Эта конструкция возможна благодаря синтаксическому сахару, появившемуся, по-моему, в C# 2. Компилятор представляет эту конструкцию в следующем виде [2]:
instance.DataReceived += new DataReceivedEventHandler(() => Console.WriteLine("Data received"));
Здесь DataReceivedEventHandler
— это тип-делегат, который сгенерирован внутри
COM-обёртки. Само собой, при динамическом связывании у нас этого типа нет (более
того, у него даже не указан GUID, так что сослаться на него динамически мы тоже
не можем). Эта сущность существует только на стороне обёртки, и нужна только для
этого конкретного обработчика. Соответственно, этот код [1] у нас в
dynamic
-сборке даже не скомпилируется, т.к. компилятор не сможет понять, чего
мы от него хотим.
В обычной ситуации проблема может быть решена явным прописыванием имени типа (то есть написанием кода, аналогичного варианту [2]), однако мы же не знаем правильного имени типа, и его не существует! Что же делать?
Честно признаться, какого-то внятного ответа в документации я не нашёл, но эксперимент показал, что можно использовать любой подходящий по типам делегат. В частности, код из примера следует переписать так:
instance.DataReceived += new Action(() => Console.WriteLine("Data received"));
Если требуется отписка от события (что может быть критично в некоторых ситуациях), не забывайте помещать этот делегат в отдельную переменную, иначе отписка не будет работать! Например:
Action handler = new Action(() => Console.WriteLine("Data received"));
instance.DataReceived += handler;
// По окончании работы:
instance.DataReceived -= handler;
instance = null;
На сегодня всё. Будьте осторожны при обращении с COM и внимательно тестируйте весь код: ошибки здесь подстерегают на каждом углу!