Сообщений 32 Оценка 160 Оценить |
Исходники к статье - Source.zip (12.7K)
В этой статье мы с вами обсудим вопросы взаимодействия с компонентами .NET при помощи COM. Прочитав и осмыслив то, что здесь изложено, вы сможете с легкостью использовать компоненты .NET в ваших приложениях при помощи COM. А значит, сможете из любого вашего старого приложения, использующего WinApi, использовать практически все современные средства .NET.
Для начала нам с вами придётся написать тот самый компонент, который мы хотим в дальнейшем научиться использовать при помощи COM. Это будет простой компонент, который занимается умножением целых чисел и возведением их в квадрат, а также может сообщить информацию о себе. Я выбрал такую простую задачу, чтобы не затуманивать код лишними строками, не относящимися к интересующему нас вопросу. Я не буду детально описывать компонент, так как считаю, что вам самим лучше посмотреть в код. Это будет гораздо понятнее и полезнее.
'File: Some.vb 'Author: Copyright (C) 2001 Dubovcev Aleksey Imports System Imports System.Reflection <Assembly:AssemblyKeyFile("../Orbit.snk")> <Assembly:AssemblyVersion("1.0.0.0")> Namespace TestComponentLib Public Interface ITestComponent Function Mul (x As Integer,y As Integer) As Integer ReadOnly Property About() As String End Interface Public class TestComponent Implements ITestComponent Public Function Mul (x As Integer,y As Integer) As Integer Implements ITestComponent.Mul Mul = x * y End Function Public ReadOnly Property About() As String Implements ITestComponent.About Get Return "This component is written in Visual Basic.NET" End Get End Property End Class End Namespace |
/* File: Some.cs Author: Copyright (C) 2001 Dubovcev Aleksey */ using System; using System.Reflection; //Данный атрибут определяет файл, в котором находится //пара личных криптографических ключей, //однозначно идентифицирующих сборку [assembly:AssemblyKeyFile("../Orbit.snk")] [assembly:AssemblyVersion("1.0.0.0")] namespace TestComponentLib { //Этот интерфейс нужен для взаимодействия //с COM public interface ITestComponent { int Mul(int First, int Second); int Square { get; set; } String About { get; } } //Данный класс реализует интерфейс ITestComponent public class TestComponent : ITestComponent { public TestComponent() { m_iSquare = 0; } public int Mul( int First, int Second) { return First * Second; } private int m_iSquare; public int Square { get { return m_iSquare; } set { m_iSquare = value * value; } } public String About { get { return "This component is written in C#"; } } } } |
/* File: Some.cpp Author: Copyright (C) 2001 Dubovcev Aleksey */ #using <mscorlib.dll> using namespace System; using namespace System::Reflection; [assembly:AssemblyKeyFile("../Orbit.snk")]; [assembly:AssemblyVersion("1.0.0.1")]; namespace TestComponentLib { public __gc __interface ITestComponent { int Mul (int First, int Second); __property System::String* get_About(); __property int get_Square (); __property void set_Square (int Param); }; public __gc class TestComponent : public ITestComponent { private: int m_iSquare; public: int Mul (int First, int Second) { return First * Second; } __property System::String* get_About() { return L"This component is written in Managed Extension for C++"; } __property int get_Square ( ) { return m_iSquare; } __property void set_Square (int Param) { m_iSquare = Param * Param; } }; } |
/* File: Some.il Author: Copyright (C) 2001 Dubovcev Aleksey */ .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. .ver 1:0:2411:0 } .assembly Some { .hash algorithm 0x00008004 .ver 1:0:0:0 } .module Some.dll .imagebase 0x00400000 .subsystem 0x00000003 .file alignment 512 .corflags 0x00000009 .namespace TestComponentLib { //Этот интерфейс нужен для взаимодействия //с COM .class interface public abstract auto ITestComponent { .method public newslot virtual abstract instance int32 Mul(int32 First, int32 Secont) cil managed {} //Мы должны задавать .method public newslot specialname virtual abstract instance int32 get_Square() cil managed {} .method public newslot specialname virtual abstract instance void set_Square(int32 'value') cil managed {} .method public newslot specialname virtual abstract instance string get_About() cil managed {} //Данные объявления свойств нужны нам только для того, чтобы с компонентом //можно было бы общаться через интерфейс автоматизации IDispatch, //так как языки типа VC будут скорее всего вызывать функции напрямую через //таблицу виртуальных функций (интерфейс). //Скажу даже больше: если вы хотите, чтобы данные свойства были недоступны //для языков высокого уровня, просто не объявляйте их при помощи //ключевого слова .property. .property instance int32 Square() { .get instance int32 TestComponentLib.ITestComponent::get_Square() .set instance void TestComponentLib.ITestComponent::set_Square(int32) } .property instance string About() { .get instance string TestComponentLib.ITestComponent::get_About() } } //Данный класс реализует интерфейс ITestComponent .class public beforefieldinit auto TestComponent extends [mscorlib]System.Object implements TestComponentLib.ITestComponent { .field private int32 m_iSquare .method public specialname instance void .ctor() cil managed { ldarg.0 //Загружаем This call instance void [mscorlib]System.Object::.ctor() ldarg.0 //Загружаем This ldc.i4.0 stfld int32 TestComponentLib.TestComponent::m_iSquare ret } .method public newslot final virtual instance int32 Mul(int32 First, int32 Second) cil managed { .maxstack 2 ldarg.1 //Первый параметр ldarg.2 //Второй параметр mul ret } .method public newslot specialname final virtual instance int32 get_Square() cil managed { .maxstack 1 ldarg.0 //Загружаем This ldfld int32 TestComponentLib.TestComponent::m_iSquare ret } .method public hidebysig newslot specialname final virtual instance string get_About() cil managed { .maxstack 1 ldstr "This component is writen in IL" ret } .method public newslot specialname final virtual instance void set_Square(int32 'value') cil managed { .maxstack 8 ldarg.0 //Загружаем This ldarg.1 //Первый параметр ldarg.1 //Первый параметр mul //Перемножаем stfld int32 TestComponentLib.TestComponent::m_iSquare ret } .property instance int32 Square() { .get instance int32 TestComponentLib.TestComponent::get_Square() .set instance void TestComponentLib.TestComponent::set_Square(int32) } .property instance string About() { .get instance string TestComponentLib.TestComponent::get_About() } } } |
Взглянув на код, вы можете увидеть, что мы используем файл Orbit.snk. (Для IL этот файл задаётся параметром командной строки /KEY). Это пара криптографических ключей, которые будут идентифицировать нашу сборку в GAC (Global Assebly Cache - Глобальный кэш сборок). В него нам впоследствии придется поместить нашу сборку, чтобы ее увидел COM. А все сборки, помещаемые в GAC, должны, как известно, идентифицироваться открытым криптографическим ключом. Эту пару ключей вы можете создать для себя, используя утилиту sn следующим образом:
sn.exe -k Имя_вашего_файла_с_ключом.sn |
Теперь сделайте саму сборку. У вас должна получиться DLL, в которой будет находиться наша с вами сборка. Теперь, когда у нас с вами есть эта сборка, давайте заглянем ей "под капот". Смело запускайте утилиту ildasm:
ildasm имя_вашей_сборки.dll /adv |
Вот что получилось у меня:
ildasm
Я предлагаю вам немного поизучать эту сборку при помощи ildasm, которою я описывал в статье "Немного о сборках".
Итак, у нас есть сборка, которая несет в себе полную информацию о типах, в чем мы уже убедились при помощи ildasm. Но эти типы понятны только для среды исполнения .NET и не несут никакой полезной информации для COM. Чтобы COM смогла использовать типы, описанные в сборке, мы с вами создадим и зарегистрируем библиотеку типов COM (tlb). Исполнить этот замысел нам поможет утилита regasm, которая как раз для этого и предназначена. Создавать библиотеку будем так:
regasm /tlb Имя_вашей_борки.dll |
После выполнения данной команды утилита regasm создаст библиотеку типов COM из вашей сборки и автоматически зарегистрирует её в системном реестре. Если у вас нет необходимости регистрировать библиотеку типов в реестре, вы можете воспользоваться для её создания утилитой tlbexp.exe. Данная утилита предназначена для создания библиотек типов COM из сборок. Если вам нужно создать библиотеку типов программным путём, для этого вам следует использовать класс TypeLibConverter, расположенный в пространстве имен System.Runtime.Interop. Для регистрации же сборок предназначена утилита regasm, которую я упоминал немного выше.
После регистрации в реестре вырисовывается следующая картина:
Реестр
Для начала регистрируется ProgID нашего компонента, в данном случае это TestComponentLib.TestComponent, который находится в подключе HKCR\CLSID. По этому ключу COM будет искать CLSID, если его запросят создать компонент при помощи ProgID. К примеру, из какого-нибудь скриптового языка, который не поддерживает CLSID непосредственно. Далее регистрируется CLSID приложения в подключе HKCR\CLSID\{XXX...XXX}. Кстати, это значение всегда будет постоянно и не будет меняться от билда к билду. Далее нас может смутить значение праметра (Default) в ключе ...InprocServer32, оно явно не похоже на нашу библиотеку. И правильно, это среда исполнения .NET. Как COM с ней взаимодействует, я расскажу немного позднее, а сейчас давайте вернемся к реестру. Далее мы с вами видим параметр Assembly, задающий нашу сборку. Вы можете удивиться почему к ней не указано имя: оно в данном случае не нужно, так как сборка храниться в GAC, в котором она будет найдена при помощи параметров Some, Version=1.0.0.0, Culture=neutral, PublicKeyToken=023eff9be46a57df. Здесь будет уместно раскрыть смысл параметров данной строки:
Далее идет название класса и версия среды исполнения, в которой была создана сборка. Затем указана потоковая модель, которую поддерживает компонент. По умолчанию компонент поддерживает обе потоковые модели (STA и MTA).
Для начала взгляните, как использовать созданный нами .NET-компонент при помощи COM. Я умышленно привожу пример только на языке VC++, так как языки более высокого уровня, такие как Visual Basic, практически полностью скрывают от нас процесс создания COM-компонента. Они весьма "любезно" исполняют за нас всю черную работу, на которую я хочу обратить ваше внимание.
#include <windows.h> #include <stdio.h> #include <comdef.h> #import "../Some.tlb" raw_interfaces_only //Название данного пространства имен должно быть //эквивалентно имени сборки компонента. using namespace Some; void main() { //Инициализируем СOM как STA, хотя значения это, в общем-то, не имеет, //так как наш компонент автоматически поддерживает обе потоковые модели. if (FAILED(CoInitialize(NULL))) return; _bstr_t bstrCLSID; CLSID clsidOrbit; ITestComponent* pIOrbit; HRESULT hr; //Это ProgID нашего компонента. //Как видите, он складывается из области видимости и имени //самого компонента, записанных через точку. bstrCLSID = "TestComponentLib.TestComponent"; //Получаем CLSID (Class ID) через ProgID if (FAILED(CLSIDFromProgID (bstrCLSID,&clsidOrbit))) return; //Создаём объект //и сразу же получаем интересующий нас интерфейс ITestComponent if (FAILED(CoCreateInstance(clsidOrbit,0,CLSCTX_ALL,__uuidof(ITestComponent),(void**)&pIOrbit))) return; BSTR strAbout; //Запрашиваем информацию о компоненте pIOrbit->get_About(&strAbout); //Для вывода используем UNICODE-версию MessageBox MessageBoxW(0,strAbout,0,0); //Ну а теперь демонстрируем возможности нашего компонента long lResult; pIOrbit->Mul(5,4,&lResult); printf("Multiply 5 * 4 = %d\n",lResult); pIOrbit->put_Square(5); pIOrbit->get_Square(&lResult); printf("Printf square of 5 = %d\n",lResult); //Освобождаем интерфейс pIOrbit->Release(); CoUninitialize(); } |
Я надеюсь, что вам понятна суть происходящего. Я не хочу останавливаться на подробностях COM, поскольку это выходит за рамки данной статьи. Наиболее интересным в этом участке кода является вызов функции CoCreateInstance. Чтобы уяснить ситуацию, взгляните на рисунок.
Таким образом, мы видим, что CoCreateInstance создает в памяти не сам компонент, а библиотеку mscoree.dll, которая является средой исполнения .NET. Эта библиотека предоставляет интерфейс фабрики классов IClassFactory для создания компонентов. При попытке создания компонента через этот интерфейс среда исполнения автоматически создаёт нужный компонент .NET и предоставляет его COM-совместимый интерфейс. При обращениях из среды COM, .NET берет на себя заботу о маршалинге данных из среды в среду.
В дополнение я покажу вам, как можно использовать классы поддержки COM для упрощения работы с компонентами .NET.
#include <windows.h> #include <stdio.h> #import "../Some.tlb" raw_interfaces_only using namespace Some; void main() { if (FAILED(CoInitialize(NULL))) return; //Это интеллектуальный указатель, который объявляется в файле Some.tlh //при помощи макроса _COM_SMARTPTR_TYPEDEF. //Данный указатель базируется на классе _com_ptr, //который поставляется вместе с компилятором VC. //Мы получаем CLSDID при помощи оператора __uuidof, //который извлекает CLSDID, определённый в файле Some.tlh. //Вся идея в том, что мы не создаем компонент самостоятельно. //За нас это делает интеллектуальный указатель, он-то и вызывает //CoCreateInstance. ITestComponentPtr pIOrbit(__uuidof(TestComponent)); BSTR strAbout; pIOrbit->get_About(&strAbout); MessageBoxW(0,strAbout,0,0); long lResult; pIOrbit->Mul(5,4,&lResult); printf("Multiply 5 * 4 = %d\n",lResult); pIOrbit->put_Square(5); pIOrbit->get_Square(&lResult); printf("Printf square of 5 = %d\n",lResult); //Внимание: освобождать интерфейс путём вызова Release() //нет необходимости, так как за нас это сделает //интеллектуальный указатель. CoUninitialize(); } |
Далее я приведу примеры использования нашего компонента на разных языках. Сразу извиняюсь за отсутствие примера на Visual Basic. Его не будет, поскольку у меня на компьютере он попросту не установлен. (Прошу не путать Visual Basic и VBScript).
var Orbit; var WshShell; //Данный объект нам понадобиться для отображения //всплывающего окна (MessageBox по русски) WshShell = new ActiveXObject("WScript.Shell") //Создаём наш компонент по ProgID //Как видите нам не нужна библиотека типов, //так мы используем автоматизацию через интерфейс IDispatch Orbit = new ActiveXObject("TestComponentLib.TestComponent"); //Выводим информацию о компоненте WshShell.Popup(Orbit.About) //Показываем возможности нашего компонента WshShell.Popup(Orbit.Mul(5,4)); Orbit.Square = 5; WshShell.Popup(Orbit.Square); |
Dim Orbit 'Создаём компонент Set Orbit = CreateObject("TestComponentLib.TestComponent") 'Выводим информацию о компоненте MsgBox(Orbit.About) 'Далее демонстрируем возможности нашего компонента Orbit.Square = 5 MsgBox(Orbit.Square) MsgBox(Orbit.Mul(5,4)) |
<HTML> <SCRIPT language="VBScript"> Sub OnVB Dim Orbit 'Создаём компонент Set Orbit = CreateObject("TestComponentLib.TestComponent") 'Выводим информацию о компоненте MsgBox(Orbit.About) 'Далее демонстрируем возможности нашего компонента Orbit.Square = 5 MsgBox(Orbit.Square) MsgBox(Orbit.Mul(5,4)) End Sub </SCRIPT> <SCRIPT language="JScript"> function OnJs { var Orbit; var WshShell; //Данный объект нам понадобиться для отображения //всплывающего окна (MessageBox по русски) WshShell = new ActiveXObject("WScript.Shell") //Создаём наш компонент по ProgID //Как видите нам не нужна библиотека типов, //так мы используем автоматизацию через интерфейс IDispatch Orbit = new ActiveXObject("TestComponentLib.TestComponent"); //Выводим информацию о компоненте WshShell.Popup(Orbit.About) //Показываем возможности нашего компонента WshShell.Popup(Orbit.Mul(5,4)); Orbit.Square = 5; WshShell.Popup(Orbit.Square); } </SCRIPT> <BODY> <CENTER> <INPUT TYPE="BUTTON" value="VBScript Test" OnClick="OnVB"> <INPUT TYPE="BUTTON" value="Jscript Test" OnClick="OnJS"> </CENTER> <BR><HR> </BODY> </HTML> |
Ну как, впечатляет? Главное, посмотрите, сколько вы затратили усилий на создание компонента. Разве много? Вот и я о том же. Теперь вы можете создавать ваши собственные .NET компоненты и с легкостью, присущей матёрому профессионалу, использовать их как COM-компоненты.
Сообщений 32 Оценка 160 Оценить |