![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Продолжаем серию постингов "C++ - говно". На этот раз у нас в гостях C#, а тема - написание COM-компонентов.
В интернете, да и не только в интернете, я много раз слышал, что COM - крайне неудобная в использовании технология, громоздкая и переусложненная. А теперь еще к этому добавляются реплики о том, что и устаревшая.
На самом деле это не так. COM в своей основе крайне прост, и является одной из самых удобных технологий для интероперабельности между различными платформами, рантаймами языков программирования и программами. Для случая RPC же, COM вообще является чуть ли не самой простой и удобной из всех таких технологий, и одной из наименее затратных по памяти и производительности(особенно в сравнении со всякими XML-RPC).
COM это в первую очередь бинарный стандарт простой объектной модели; настолько простой, что в ней нет даже наследования реализации, а есть только наследование интерфейсов. Во вторую очередь, это некоторые средства для создания компонентов программ и операционной системы Windows(на самом деле, COM вполне себе кроссплатформенная технология, но по некоторым причинам прижился он только на винде - на всяких там линуксах как всегда предпочитают велосипедить свое(типа D-Bus), совершенно не оглядываясь на существующие технологии, естественно).
Объектная модель COM обладает строгим ABI, полиморфизмом, двумя видами связывания(раннее - vtbl и позднее - IDispatch), кое-какой интроспекцией(а в случае с IDispatch даже очень ничего такой) и некоторыми другими фишками. Конечно, не CLOS, и даже не Smalltalk, но для ОО-системы, которую можно использовать из практически любого языка программирования, от Си до Visual Basic - очень даже неплохо.
Причина, по которой многие так не любят COM, на мой взгляд, заключается в том, что долгое время основными языками программирования для работы с ним, были C++ и Visual Basic(VB, VBA). Оба, мягко говоря, не ахти.
Но, если из VB с COM-компонентами работать всегда было более-менее удобно, то в C++ использование COM, а особенно написание своих компонентов, превращалось в настоящий ад; особенно в случае использования позднего связывания(а его, во времена расцвета VB, надо было использовать часто - любой уважающий себя компонент должен был реализовывать IDispatch, чтобы с ним мог работать Visual Basic и другие подобные скриптовые языки).
Чтобы как-то облегчить нелегкую жизнь писателя компонентов на C++, Microsoft нагородила в свое время кучи костылей, оборачивающих COM в C++ные классы и шаблоны, и даже добавила специальные ключевые слова в свой компилятор. Не сказать, правда, чтобы это очень уж помогло, скорее наоборот - ко всем недостаткам C++, со которыми программисту на C++ приходилось бороться, ко всей его сложности, добавились сложности и недостатки всех этих ATL и MFC, которые, кстати, долгое время были полны багов и глюков, что тоже добавляло "радостей" программисту.
Каков итог всего это, мы сегодня прекрасно знаем - несмотря на то, что писателей на плюсах все эти недостатки и сложности не останавливали, сама MS в итоге в этой куче говна захлебнулась, начала сворачивать поддержку упомянутых фреймворков, да и самого C++, и выкатила на всеобщее обозрение свой .NET
Из .NET с COM работать действительно намного проще и приятнее, чем из C++, при этом, .NET ограничениями Visual Basic не страдает, но давайте уже рассмотрим пример, который я тут подготовил.
А пример такой: создаем out-of-procss COM-сервер, у которого есть интерфейс, реализующий две функции для операции над векторами из 4х float.
Почему out-of-process? Потому, что это интереснее, и потому, что многие лиспы не умеют собираться в .dll :)
Вообще, что такое out-of-process сервер? Ну, во-первых, это такой COM-компонент, реализующий некоторый, кому-нибудь нужный, функционал. Грубо говоря, это такая штука, в которой живут COM-объекты. Отличается он от in-process сервера тем, что реализуется он не как .dll, а как отдельное приложение, .exe, отдельный процесс в операционной системе(возможно даже и на другом компьютере, но в примере - на локальном), с которым клиенты общаются через RPC.
Преимущества перед inproc-сервером заключаются в большей надежности и безопасности(адресное пространство то разное!), и, в теории, в меньшем расходе памяти, а вот главный недостаток это производительность(вызовы методов и их аргументы надо маршалить и передавать между процессами).
Несмотря на такие вот кардинальные различия, внешне работа с COM-объектам out-of-proc сервера практически ничем не отличается от работы с таковыми из inproc dll'ки. Достигается это за счет вышеупомянутого маршалинга и промежуточных прокси-объектов, представляющих объекты, живущие в процессе-сервере.
Содержание IDL-файла для примера следующее:
Если на него натравить midl-компилятор от Microsoft, мы получим файл с расширением .tlb.
Этот файл, а также класс и интерфейс, реализуемый нашим компонентом, надо перед использованием зарегистрировать в реестре Windows.
Что вообще такое tlb, и зачем нужна регистрация в реестре? .tlb расшифровывается как Type Library, "Библиотека Типов" - файл, содержащий описания типов, интерфейсов и классов, которые реализует наш компонент.
Регистрировать в реестре все это дело надо вот для чего:
Теперь описание реализации на Лиспе.
Первым делом нужно определить пакет, в котором будет находиться весь код, и структуру данных, с которой мы будем работать:
Пакет импортирует символы из пяти других. `CL' это стандартная библиотека Common Lisp. Virgil - моя библиотека-FFI для маршалинга лисповых данных в неуправляемую память(кстати, я таки сподобился написать для нее документацию, скоро опубликую), а остальные три - из моей библиотеки Doors, которая представляет собой интерфейс к Windows API, в том числе к COM и OLE; она пока что в стадии разработки, многого еще нет, например не допилен OLE Automation(тот самый IDispatch).
В пакете `doors' определены какие-то базовые вещи, вроде базовых типов, с которыми работает Windows(числовые типы, строки, и так далее), там же находятся разнообразные базовые системные функции(работа с процессами, консолью, и подобное). `doors.com' реализует интерфейс к COM, а в `doors.ui' находятся, например, биндинги к Windows USER (в примере это потребуется для организации цикла сообщений STA).
Макрос define-struct это надстройка над лисповым defstruct, и очень похож на последний. Он добавляет к структуре некоторую метаинформацию о типе, необходимую для маршалинга.
В данном случае у нас вектор из 4х float, а так как в опциях присутствует
Теперь надо описать интерфейс, который мы будем реализовывать:
Получилось очень похоже на IDL. Вообще, я стремился сделать интерфейс doors настолько декларативным, насколько возможно. Как уже видно, и как будет видно дальше, никакой ручной работы по, например, маршалингу нам делаться не придется, а придется только описывать и определять.
Дальше нужно определить класс, реализующий IClassFactory. Вообще, что это такое? Объект, реализующий IClassFactory в COM - промежуточный объект, предназначенный для создания объектов самого COM-класса.
В лиспе у нас классы сами являются объектами, так что наиболее органичное решение для лиспа заключается в том, чтобы этот интерфейс реализовывался некоторым метаклассом.
В данном случае я ввожу дополнительный метакласс
Теперь собственно класс, реализующий наш компонент и его методы. Думаю, тут, в принципе, все наглядно и очевидно.
Каждый основной метод, как видно, возвращает все свои аргументы. Зачем это нужно? Затем, что virgil, и значит, методы интерфейсов, способны маршалить в принципе произвольные типы данных, произвольным образом; а значит, отличить на этапе компиляции, какие параметры в коллбэке-трамплине маршалятся in, какие out, а какие inout, в контексте этого - невозможно. Кстати, упомянутый выше ссылочный тип(&) не является чем-то особенным - средствами Virgil он мог бы быть введен и пользователем библиотеки.
В C#, как будет видно дальше, тип HRESULT, у возвращаемого значения, подразумевается по умолчанию; .NET опускает retval параметр в сигнатуре метода и использует в качестве него возвращаемое значение.
В doors это совсем не так - сигнатура методов интерфейсов всегда идентична настоящей. Почему? Потому, что хотя описанное изменение сигнатуры вполне нормально в контексте OLE Automation, его нельзя экстраполировать на все возможные случаи - например, у интерфейсов DirectX присутствуют методы, возвращающие совсем не HRESULT(там же, кстати, присутствуют интерфейсы, которые не наследуются от IUnknown - doors позволяет с такими интерфейсами работать, а вот стандартный interop из .NET - нет, потому что подразумевает что все интерфейсы должны от упомянутого наследоваться)
Наш сервер будет работать в STA, поэтому ему нужен цикл сообщений. Он реализуется у нас в функции main, там же у нас проходит регистрация сервера и отмена регистрации после завершения его работы:
Вот вобщем-то и все. Как видно, все достаточно просто и декларативно.
Полный код в самом низу постинга, там еще определена фунцкция
Я сначала думал, что на C# все будет не сложнее чем на лиспе, а то и проще.
Вот декларация интерфейса и класса:
Но, оказалось, что в стандартном COM interop заложена большая, но поначалу незаметная, жопа.
Дело в том, что стандартный COM Callable Wrapper управляется исключительно сборщиком мусора(ну помимо внутреннего подсчета ссылок), а всю механику подсчета ссылок от пользователя прячет, не давая совершенно никаких способов прицепиться к нему или как-либо мониторить. Аналогично он прячет и механику IClassFactory.
Для in-process серверов это не фатально, хотя и неприятно в некоторых случаях, но вот для out-of-process - вполне. Никаких IDisposable.Dispose после достижения счетчиком ссылок нуля CCW не вызывает, поэтому когда нашему серверу завершаться - мы сказать не можем, разве что повесив код завершения в финализатор. Но, это чревато тем, что наш сервер может часами провисеть в подвешенном полумертвом состоянии, а то и не выключиться никогда(GC то не детерминирован).
Я тут посмотрел в интернете, какие костыли люди городят для обхода этой проблемы, и выбрал наиболее простой - патчинг таблицы виртуальных функций IUnknown для CCW, и перехват вызовов AddRef и Release. Костыли, конечно, те еще, и здорово портят код, но а что делать:
Полный код приведен в архиве по ссылке внизу постинга.
Вариант на C++ я сначала попытался реализовать вручную. Получаться начало какое-то дерьмо, которое даже стыдно выложить на публику, поэтому я не стал дальше заморачиваться и взял ATL.
Кстати, надо сказать, найти ATL в свободном доступе сейчас стало довольно сложно. В Windows SDK Microsoft его больше не включает и на сайте у них нигде кнопки "скачать ATL" не висит - видимо, стало стыдно, и теперь заметают следы. С MFC, кстати, такая же история.
Ну, в итоге я нашел последнюю версию ATL в Windows Driver Kit(что, конечно, само по себе смешно).
Взять последний можно тут: src
Несмотря на все старания MS, код на C++, работающий с COM, по-прежнему выглядит как куча говна из малопонятных макросов и шаблонов, за которыми практически не видно сути. Чего стоит одно только определение класса:
И это еще не учитывая возню с ресурсами и дублированием скрипта регистрации в них.
Кстати, как насчет отображения HRESULT в исключения, как это сделано в .NET и у меня в doors? А никак; и правильно.
Опробовать любой из трех серверов можно, например, из лиспа. Вот таким кодом:
Какие выводы? А выводы такие, что COM, в сочетании с нормальными языками - хорошая и полезная технология. Не нужно закапывать COM, нужно закапывать C++
Полный код примеров: http://rghost.ru/5093043
В интернете, да и не только в интернете, я много раз слышал, что COM - крайне неудобная в использовании технология, громоздкая и переусложненная. А теперь еще к этому добавляются реплики о том, что и устаревшая.
На самом деле это не так. COM в своей основе крайне прост, и является одной из самых удобных технологий для интероперабельности между различными платформами, рантаймами языков программирования и программами. Для случая RPC же, COM вообще является чуть ли не самой простой и удобной из всех таких технологий, и одной из наименее затратных по памяти и производительности(особенно в сравнении со всякими XML-RPC).
COM это в первую очередь бинарный стандарт простой объектной модели; настолько простой, что в ней нет даже наследования реализации, а есть только наследование интерфейсов. Во вторую очередь, это некоторые средства для создания компонентов программ и операционной системы Windows(на самом деле, COM вполне себе кроссплатформенная технология, но по некоторым причинам прижился он только на винде - на всяких там линуксах как всегда предпочитают велосипедить свое(типа D-Bus), совершенно не оглядываясь на существующие технологии, естественно).
Объектная модель COM обладает строгим ABI, полиморфизмом, двумя видами связывания(раннее - vtbl и позднее - IDispatch), кое-какой интроспекцией(а в случае с IDispatch даже очень ничего такой) и некоторыми другими фишками. Конечно, не CLOS, и даже не Smalltalk, но для ОО-системы, которую можно использовать из практически любого языка программирования, от Си до Visual Basic - очень даже неплохо.
Причина, по которой многие так не любят COM, на мой взгляд, заключается в том, что долгое время основными языками программирования для работы с ним, были C++ и Visual Basic(VB, VBA). Оба, мягко говоря, не ахти.
Но, если из VB с COM-компонентами работать всегда было более-менее удобно, то в C++ использование COM, а особенно написание своих компонентов, превращалось в настоящий ад; особенно в случае использования позднего связывания(а его, во времена расцвета VB, надо было использовать часто - любой уважающий себя компонент должен был реализовывать IDispatch, чтобы с ним мог работать Visual Basic и другие подобные скриптовые языки).
Чтобы как-то облегчить нелегкую жизнь писателя компонентов на C++, Microsoft нагородила в свое время кучи костылей, оборачивающих COM в C++ные классы и шаблоны, и даже добавила специальные ключевые слова в свой компилятор. Не сказать, правда, чтобы это очень уж помогло, скорее наоборот - ко всем недостаткам C++, со которыми программисту на C++ приходилось бороться, ко всей его сложности, добавились сложности и недостатки всех этих ATL и MFC, которые, кстати, долгое время были полны багов и глюков, что тоже добавляло "радостей" программисту.
Каков итог всего это, мы сегодня прекрасно знаем - несмотря на то, что писателей на плюсах все эти недостатки и сложности не останавливали, сама MS в итоге в этой куче говна захлебнулась, начала сворачивать поддержку упомянутых фреймворков, да и самого C++, и выкатила на всеобщее обозрение свой .NET
Из .NET с COM работать действительно намного проще и приятнее, чем из C++, при этом, .NET ограничениями Visual Basic не страдает, но давайте уже рассмотрим пример, который я тут подготовил.
А пример такой: создаем out-of-procss COM-сервер, у которого есть интерфейс, реализующий две функции для операции над векторами из 4х float.
Почему out-of-process? Потому, что это интереснее, и потому, что многие лиспы не умеют собираться в .dll :)
Вообще, что такое out-of-process сервер? Ну, во-первых, это такой COM-компонент, реализующий некоторый, кому-нибудь нужный, функционал. Грубо говоря, это такая штука, в которой живут COM-объекты. Отличается он от in-process сервера тем, что реализуется он не как .dll, а как отдельное приложение, .exe, отдельный процесс в операционной системе(возможно даже и на другом компьютере, но в примере - на локальном), с которым клиенты общаются через RPC.
Преимущества перед inproc-сервером заключаются в большей надежности и безопасности(адресное пространство то разное!), и, в теории, в меньшем расходе памяти, а вот главный недостаток это производительность(вызовы методов и их аргументы надо маршалить и передавать между процессами).
Несмотря на такие вот кардинальные различия, внешне работа с COM-объектам out-of-proc сервера практически ничем не отличается от работы с таковыми из inproc dll'ки. Достигается это за счет вышеупомянутого маршалинга и промежуточных прокси-объектов, представляющих объекты, живущие в процессе-сервере.
Содержание IDL-файла для примера следующее:
[ uuid(AD814CEC-0C0E-492E-9C24-CE6494233A76), helpstring("VectorMath Type Library"), version(1.0) ] library VectorMath { importlib("stdole32.tlb"); [ uuid(D5BC0063-BFD3-4272-834B-196F9F803BF2), oleautomation ] interface IVectorMath : IUnknown { HRESULT VectorSum([out] float result[4], [in] float v1[4], [in] float v2[4]); HRESULT VectorDot([in] float v1[4], [in] float v2[4], [out, retval] float* dotProduct); } [ uuid(6047851B-D193-4FD1-A6AD-9C862C5F98FD) ] coclass VectorMath { interface IVectorMath; }; }
Если на него натравить midl-компилятор от Microsoft, мы получим файл с расширением .tlb.
Этот файл, а также класс и интерфейс, реализуемый нашим компонентом, надо перед использованием зарегистрировать в реестре Windows.
Что вообще такое tlb, и зачем нужна регистрация в реестре? .tlb расшифровывается как Type Library, "Библиотека Типов" - файл, содержащий описания типов, интерфейсов и классов, которые реализует наш компонент.
Регистрировать в реестре все это дело надо вот для чего:
- Во-первых, для того, чтобы рантайм COM смог найти наш компонент в системе. Когда мы создаем объект класса, мы не указываем его точное местоположение, мы просто передаем в функции, занимающиеся созданием объектов, 16-байтовый идентификатор класса, и вместе с ним 16-байтовый идентификатор интерфейса, через который мы с созданным объектом будем общаться.
- Во-вторых, через библиотеку типов в COM реализуется интроспекция - различные утилиты и программы, используя ее, могут посмотреть, что собственно наш компонент из себя представляет.
- И в-третьих, и для этого вот примера это очень важно - если мы поставляем с нашим компонентом библиотеку типов, мы можем использовать стандартный способ маршалинга, предоставляемый рантаймом COM. Естественно, не зная, что наши интерфейсы и классы из себя представляют, рантайм COM маршалить их не сможет, и нам придется городить свои прокси-dll'ки, в которых мы ручками должны будем со всем этим работать.
Теперь описание реализации на Лиспе.
Первым делом нужно определить пакет, в котором будет находиться весь код, и структуру данных, с которой мы будем работать:
(defpackage #:vector-math (:use #:cl #:virgil #:doors #:doors.com #:doors.ui) (:export #:main #:build-executable)) (in-package #:vector-math) (define-struct (float4 (:constructor make-float4) (:constructor float4 (x y z w)) (:type (vector single-float))) (x float) (y float) (z float) (w float))
Пакет импортирует символы из пяти других. `CL' это стандартная библиотека Common Lisp. Virgil - моя библиотека-FFI для маршалинга лисповых данных в неуправляемую память(кстати, я таки сподобился написать для нее документацию, скоро опубликую), а остальные три - из моей библиотеки Doors, которая представляет собой интерфейс к Windows API, в том числе к COM и OLE; она пока что в стадии разработки, многого еще нет, например не допилен OLE Automation(тот самый IDispatch).
В пакете `doors' определены какие-то базовые вещи, вроде базовых типов, с которыми работает Windows(числовые типы, строки, и так далее), там же находятся разнообразные базовые системные функции(работа с процессами, консолью, и подобное). `doors.com' реализует интерфейс к COM, а в `doors.ui' находятся, например, биндинги к Windows USER (в примере это потребуется для организации цикла сообщений STA).
Макрос define-struct это надстройка над лисповым defstruct, и очень похож на последний. Он добавляет к структуре некоторую метаинформацию о типе, необходимую для маршалинга.
В данном случае у нас вектор из 4х float, а так как в опциях присутствует
:type
, то вместо структуры определяется алиас на (simple-array single-float (4))
Теперь надо описать интерфейс, который мы будем реализовывать:
(define-interface vector-math ("{D5BC0063-BFD3-4272-834B-196F9F803BF2}" unknown) ;; (IID Parent) (vector-sum (hresult rv result) (result (& float4 :out) :optional) (v1 (& float4)) (v2 (& float4))) (vector-dot (hresult rv dot-product) (v1 (& float4)) (v2 (& float4)) (dot-product (& float :out) :aux)))
Получилось очень похоже на IDL. Вообще, я стремился сделать интерфейс doors настолько декларативным, насколько возможно. Как уже видно, и как будет видно дальше, никакой ручной работы по, например, маршалингу нам делаться не придется, а придется только описывать и определять.
define-interface
раскрывается в определение CLOS-класса и методов обобщенных функций, специализированных на этом классе. Первым параметром в этом макросе идет имя интерфейса, вторым - список из IID и имени родителя, а потом идут описания методов, в каждом из которых первым идет имя метода, потом - список из типа возвращаемого значения, имени переменной на которую оно биндится, и некоторой формы, которая перед возвратом из метода выполняется, а потом идет список аргументов метода. Типы аргументов - типы, которые использует Virgil, например (& float4) это ссылка на структуру float4, которая маршалится как :in-параметр.Дальше нужно определить класс, реализующий IClassFactory. Вообще, что это такое? Объект, реализующий IClassFactory в COM - промежуточный объект, предназначенный для создания объектов самого COM-класса.
В лиспе у нас классы сами являются объектами, так что наиболее органичное решение для лиспа заключается в том, чтобы этот интерфейс реализовывался некоторым метаклассом.
В данном случае я ввожу дополнительный метакласс
factory-class
, но вообще, я планирую позже добавить в Doors какой-нибудь standard-com-class, реализующий этот, и некоторые другие интерфейсы стандартным образом.(defclass factory-class (com-class) ((lock-count :initform 0 :accessor class-lock-count)) (:metaclass com-class) (:interfaces class-factory)) (defmethod lock-server ((class factory-class) lock) (if lock (incf (class-lock-count class)) (unless (> (decf (class-lock-count class)) 0) (post-quit-message))) (values nil lock)) (defmethod create-instance ((class factory-class) iid &optional outer) (when outer (error 'com-error :code error-not-implemented)) (values nil outer iid (acquire-interface (make-instance class) iid)))
Теперь собственно класс, реализующий наш компонент и его методы. Думаю, тут, в принципе, все наглядно и очевидно.
(defclass vector-math-object (com-object) () (:metaclass factory-class) (:clsid . "{6047851B-D193-4FD1-A6AD-9C862C5F98FD}") (:interfaces vector-math)) (defmethod vector-sum ((object vector-math-object) v1 v2 &optional (result (make-float4))) (map-into result #'+ v1 v2) (values nil result v1 v2)) (defmethod vector-dot ((object vector-math-object) v1 v2) (values nil v1 v2 (reduce #'+ (map 'float4 #'* v1 v2)))) (defmethod add-ref :after ((object vector-math-object)) (incf (class-lock-count (class-of object)))) (defmethod release :after ((object vector-math-object)) (unless (> (decf (class-lock-count (class-of object))) 0) (post-quit-message)))
Каждый основной метод, как видно, возвращает все свои аргументы. Зачем это нужно? Затем, что virgil, и значит, методы интерфейсов, способны маршалить в принципе произвольные типы данных, произвольным образом; а значит, отличить на этапе компиляции, какие параметры в коллбэке-трамплине маршалятся in, какие out, а какие inout, в контексте этого - невозможно. Кстати, упомянутый выше ссылочный тип(&) не является чем-то особенным - средствами Virgil он мог бы быть введен и пользователем библиотеки.
В C#, как будет видно дальше, тип HRESULT, у возвращаемого значения, подразумевается по умолчанию; .NET опускает retval параметр в сигнатуре метода и использует в качестве него возвращаемое значение.
В doors это совсем не так - сигнатура методов интерфейсов всегда идентична настоящей. Почему? Потому, что хотя описанное изменение сигнатуры вполне нормально в контексте OLE Automation, его нельзя экстраполировать на все возможные случаи - например, у интерфейсов DirectX присутствуют методы, возвращающие совсем не HRESULT(там же, кстати, присутствуют интерфейсы, которые не наследуются от IUnknown - doors позволяет с такими интерфейсами работать, а вот стандартный interop из .NET - нет, потому что подразумевает что все интерфейсы должны от упомянутого наследоваться)
Наш сервер будет работать в STA, поэтому ему нужен цикл сообщений. Он реализуется у нас в функции main, там же у нас проходит регистрация сервера и отмена регистрации после завершения его работы:
(defun main () (initialize-com) (let ((token (register-class-object 'vector-math-object :local-server :multiple-use))) (loop :for msg = (get-message) :while msg :do (dispatch-message msg)) (revoke-class-object token)) (uninitialize-com))
Вот вобщем-то и все. Как видно, все достаточно просто и декларативно.
Полный код в самом низу постинга, там еще определена фунцкция
build-executable
, она дампит лисп-систему в исполняемый файл.Я сначала думал, что на C# все будет не сложнее чем на лиспе, а то и проще.
Вот декларация интерфейса и класса:
[ Guid("D5BC0063-BFD3-4272-834B-196F9F803BF2"), ComVisible(true), InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ] public interface IVectorMath { void VectorSum([MarshalAs(UnmanagedType.LPArray, SizeConst = 4), Out] float[] result, [MarshalAs(UnmanagedType.LPArray, SizeConst = 4)] float[] v1, [MarshalAs(UnmanagedType.LPArray, SizeConst = 4)] float[] v2); float VectorDot([MarshalAs(UnmanagedType.LPArray, SizeConst = 4)] float[] v1, [MarshalAs(UnmanagedType.LPArray, SizeConst = 4)] float[] v2); } [ Guid("6047851B-D193-4FD1-A6AD-9C862C5F98FD"), ComVisible(true), ClassInterface(ClassInterfaceType.None) ] public class VectorMath : IVectorMath { public void VectorSum(float[] result, float[] v1, float[] v2) { result[0] = v1[0] + v2[0]; result[1] = v1[1] + v2[1]; result[2] = v1[2] + v2[2]; result[3] = v1[3] + v2[3]; } public float VectorDot(float[] v1, float[] v2) { return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2] + v1[3] * v2[3]; } }
Но, оказалось, что в стандартном COM interop заложена большая, но поначалу незаметная, жопа.
Дело в том, что стандартный COM Callable Wrapper управляется исключительно сборщиком мусора(ну помимо внутреннего подсчета ссылок), а всю механику подсчета ссылок от пользователя прячет, не давая совершенно никаких способов прицепиться к нему или как-либо мониторить. Аналогично он прячет и механику IClassFactory.
Для in-process серверов это не фатально, хотя и неприятно в некоторых случаях, но вот для out-of-process - вполне. Никаких IDisposable.Dispose после достижения счетчиком ссылок нуля CCW не вызывает, поэтому когда нашему серверу завершаться - мы сказать не можем, разве что повесив код завершения в финализатор. Но, это чревато тем, что наш сервер может часами провисеть в подвешенном полумертвом состоянии, а то и не выключиться никогда(GC то не детерминирован).
Я тут посмотрел в интернете, какие костыли люди городят для обхода этой проблемы, и выбрал наиболее простой - патчинг таблицы виртуальных функций IUnknown для CCW, и перехват вызовов AddRef и Release. Костыли, конечно, те еще, и здорово портят код, но а что делать:
static void InstallVtableHooks(Object server) { IntPtr serverVtbl = Marshal.ReadIntPtr(Marshal.GetIUnknownForObject(server)); trueAddRef = (AddRef)Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(serverVtbl, 1 * IntPtr.Size), typeof(AddRef)); Marshal.WriteIntPtr(serverVtbl, 1 * IntPtr.Size, Marshal.GetFunctionPointerForDelegate((AddRef)AddRefObject)); trueRelease = (Release)Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(serverVtbl, 2 * IntPtr.Size), typeof(Release)); Marshal.WriteIntPtr(serverVtbl, 2 * IntPtr.Size, Marshal.GetFunctionPointerForDelegate((Release)ReleaseObject)); }
Полный код приведен в архиве по ссылке внизу постинга.
Вариант на C++ я сначала попытался реализовать вручную. Получаться начало какое-то дерьмо, которое даже стыдно выложить на публику, поэтому я не стал дальше заморачиваться и взял ATL.
Кстати, надо сказать, найти ATL в свободном доступе сейчас стало довольно сложно. В Windows SDK Microsoft его больше не включает и на сайте у них нигде кнопки "скачать ATL" не висит - видимо, стало стыдно, и теперь заметают следы. С MFC, кстати, такая же история.
Ну, в итоге я нашел последнюю версию ATL в Windows Driver Kit(что, конечно, само по себе смешно).
Взять последний можно тут: src
Несмотря на все старания MS, код на C++, работающий с COM, по-прежнему выглядит как куча говна из малопонятных макросов и шаблонов, за которыми практически не видно сути. Чего стоит одно только определение класса:
class ATL_NO_VTABLE CVectorMath : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CVectorMath, &CLSID_VectorMath>, public IVectorMath { public: CVectorMath() { } DECLARE_REGISTRY_RESOURCEID(IDR_VECTORMATH) DECLARE_CLASSFACTORY() DECLARE_NOT_AGGREGATABLE(CVectorMath) DECLARE_PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(CVectorMath) COM_INTERFACE_ENTRY(IVectorMath) END_COM_MAP() public: STDMETHOD(VectorSum)(float result[4], float v1[4], float v2[4]) { result[0] = v1[0] + v2[0]; result[1] = v1[1] + v2[1]; result[2] = v1[2] + v2[2]; result[3] = v1[3] + v2[3]; return S_OK; } STDMETHOD(VectorDot)(float v1[4], float v2[4], float* dotProduct) { *dotProduct = v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2] + v1[3]*v2[3]; return S_OK; } };
И это еще не учитывая возню с ресурсами и дублированием скрипта регистрации в них.
Кстати, как насчет отображения HRESULT в исключения, как это сделано в .NET и у меня в doors? А никак; и правильно.
Опробовать любой из трех серверов можно, например, из лиспа. Вот таким кодом:
(defparameter *interface* (create-com-instance (clsid-from-progid "VectorMath") 'vector-math)) (vector-sum *interface* (float4 1.0 2.0 3.0 4.0) (float4 5.0 6.0 7.0 8.0)) ;; => #(6.0 8.0 10.0 12.0) (vector-dot *interface* (float4 1.0 2.0 3.0 4.0) (float4 5.0 6.0 7.0 8.0)) ;; => 70.0
Какие выводы? А выводы такие, что COM, в сочетании с нормальными языками - хорошая и полезная технология. Не нужно закапывать COM, нужно закапывать C++
Полный код примеров: http://rghost.ru/5093043