Автор: Роман Панышев (irrona)
Внешние локальные сервера
По сути Outproc-сервер ничем не отличается от Inproc-сервера кроме расширения файла. Поскольку он должен выполняться в отдельном от вызывающей программы адресном пространстве, то компилируется, как файл с расширением *.exe. В результате получается отдельный исполняемый файл со своим циклом сообщений и собственным потоком. А это значит, что вызовы от клиента к серверу должны каким-то образом передаваться через границы потоков. Эту функциональность призван обеспечить маршалинг вызовов.
рис.1 Понятие маршалинга |
Честно говоря, я не хочу сейчас заострять Ваше внимание на маршалинге, потому что в большинстве случаев COM-объект сам заботится о выполнении маршалинга и синхронизации вызовов. Вместо этого приступим к написанию простого Outproc-сервера. Для этого опять воспользуемся Visual Basic. Выполняйте все шаги по созданию проекта так же, как это описано в моей прошлой статье, но только в качестве проекта выберите ActiveX Exe. Дайте имена проекту, классу и методу на свой вкус и скомпилируйте проект в файл с расширением *.exe. После этого возьмите пример клиента из прошлой статьи, замените в нем lpProgID и FuncName на выбранные Вами для Вашего сервера. После компиляции Ваш клиент готов к работе с внешним COM-сервером.
Довольно интересно запускать клиента с помощью дебагера, например OllyDbg. После выполнения функции CoCreateInstance, Вы можете запустить Task Manager и полюбоваться на Ваш объект, висящий в списке запущенных процессов.
Теперь, когда Вы закончили медитировать, продолжим. Итак Вы видите, что при использовании интерфейса IDispatch, для нас нет никакой разницы с каким сервером мы работаем: внутренним или внешним. В этом сила COM.
Внешние удаленные сервера
Но все меняется, когда приходят они
(рекламное)
Представьте себе, что Вам позарез понадобилось получить доступ из программы-клиента к COM-серверу, расположенному на другом компьютере Вашей локальной сети. Просто вызвать объект так, как мы это делали ранее у Вас не получится, по той простой причине, что клиент знать не знает ни о каком сервере, до тех пор, пока тот не будет зарегистрирован в реестре. Ведь именно из реестра функция CoCreateInstance черпает информацию об объекте. А если мы зарегистрируем компонент в локальном реестре, он перестанет быть удаленным. Вот тут-то и приходит на помощь расширенная функция CoCreateInstanceEx, принимающая в качестве параметров не только GUID IDispatch, но и название компьютера, на котором COM-сервер зарегистрирован. Но... давайте по порядку.
Для начала напишем COM-сервер с помощью VB. Как и для первого примера этой статьи, выбираем проект ActiveX Exe, меняем название проекта на remote_com, название класса на remote_class, а методу дадим имя remote_method.
рис.2 Создание COM-сервера для удаленного использования |
В методе класса я определил один строковый параметр strIn, получающий значение по значению. Кроме того, сам метод возвращает вызывающему клиенту строку. Этим я собираюсь убить трех зайцев: показать как работать с COM-объектом на удаленной машине, передавать объекту параметры и получать назад результат выполнения метода (функции) COM-объекта. В меню Project выберите пункт remote_com Properties.... Появится диалоговое окно:
рис.2 Свойства проекта |
Перейдите на вкладку Component и в разделе Version Compatibility нажмите радио-кнопку Binary Compatibility. Если Вы желаете в дальнейшем распространять вместе с COM-объектом библиотеку типов (файл с расширением *.tlb), то в разделе Remote Server отметьте чекбокс Remote Server Files. Тогда Visual Basic сгенерирует все необходимые файлы для регистрации COM-объекта на машине клиента. Нам сейчас это, в принципе, не нужно. Так как мы может сделать обычный COM-объект, а в DCOM он превратится, если будет расположен на другой машине. Но знать об этом не лишне. Вообще-то COM от DCOM отличается тем, что первый мы вызываем посредством использования COM библиотеки локальной машины, а при обращении к удаленному COM-объекту, мы задействуем сервер Автоматизации, который посредством RPC (Remote Procedure Call), осуществляет вызовы по сети... Очень упрощенно. :-))
Не забудьте, что при компиляции, Visual Basic автоматически регистрирует сервер. Поэтому, если вы собираетесь проверить его работоспособность на другой машине, то после компиляции разрегестрируйте компонент. Для этого я обычно пользуюсь программой Com Explorer. Затем перенесите файл remote_com.exe на другую машину (лучше в системную папку, хотя не обязательно) и запустите на выполнение. COM-объект сам зарегистрирует себя в реестре. Если Вы не являетесь администратором сети, то для получения доступа к удаленному COM-объекту, на компьютере, где Вы установили его, запустите утилиту dcomcnfg.exe, входящую в поставку Windows. В ветке DCOM Config найдите remote_com.remote_class и, кликнув по его названию правой кнопкой мыши, выберите пункт Properties. В появившемся диалоговом окне перейдите на вкладку Security, на которой в разделе Access Permissions выберите Customize и с помощью кнопки Edit вызовите диалоговое окно настройки доступа. Добавьте в качестве пользователей COM-объекта Domain Admins и Domain Users. Это позволит обращаться к объекту администраторам и пользователям домена Вашей сети.
Ну вот, вроде с установкой и настройкой COM-объекта (возможно у Вас он уже стал DCOM-объектом) мы уже закончили. Осталось написать программу-клиент. Как и в прошлой статье, я приведу полный текст программы, а затем займусь объяснением требующих внимания моментов.
.586 .model flat, stdcall option casemap :none include windows.inc include kernel32.inc include user32.inc include ole32.inc include oleaut32.inc include oaidl.inc include L.inc includelib kernel32.lib includelib user32.lib includelib ole32.lib includelib oleaut32.lib COSERVERINFO struct dwReserved1 DWORD ? pwszName DWORD ? pAuthInfo DWORD ? dwReserved2 DWORD ? COSERVERINFO ends MULTI_QI struct piid DWORD ? pItf HANDLE ? hr DWORD ? MULTI_QI ends main proto dowork proto .data AppName wchar L(<Remote COM-object Call\0>) dispid dd 0 rclsid dd 0 dclsid dd 0 pIDispath dd 0 lcid dd 0 fn dd offset FuncName FuncName dw 'r','e','m','o','t','e','_','m','e','t','h','o','d',0,0,0 lpClassGuid wchar L(<{cb211c11-84b7-4b57-896b-4e3f3a5d855a}\0>) // подставьте GUID объекта lpSrvName wchar L(<ip-локального-или-удаленного-компьютера\0>) IID_NULL GUID <0,0,0,<0,0,0,0,0,0,0,0>> IID_IDispatch db "{00020400-0000-0000-C000-000000000046}",0 param_in wchar L(<some string\0>) .data? wDispatch db 100 dup(?) dsppar DISPPARAMS <?> ServerInfo COSERVERINFO <?> mqi MULTI_QI <?> .code start: main proc invoke dowork invoke ExitProcess,0 ret main endp dowork proc LOCAL bstr_in:BSTR LOCAL bstr_out:BSTR LOCAL varg_in:VARIANTARG LOCAL varg_out:VARIANTARG invoke GlobalAlloc,GMEM_FIXED,16 mov mqi.piid,eax invoke MultiByteToWideChar,CP_ACP,0,addr IID_IDispatch,sizeof IID_IDispatch,\ addr wDispatch,sizeof wDispatch invoke IIDFromString,addr wDispatch,mqi.piid invoke CLSIDFromString,addr lpClassGuid,addr rclsid lea eax,offset lpSrvName mov ServerInfo.pwszName,eax invoke OleInitialize,0 invoke CoCreateInstanceEx,addr rclsid,0,CLSCTX_REMOTE_SERVER,addr ServerInfo,1,addr mqi .if eax == S_OK invoke GetUserDefaultLCID mov lcid,eax coinvoke mqi.pItf,IDispatch,GetIDsOfNames,addr IID_NULL,addr fn,1,lcid,addr dispid invoke VariantInit,addr varg_in mov varg_in.vt, VT_BSTR invoke SysAllocString,addr param_in mov bstr_in,eax mov varg_in.bstrVal,eax lea eax,varg_in mov dsppar.rgvarg,eax mov dsppar.rgdispidNamedArgs,NULL mov dsppar.cArgs,1 mov dsppar.cNamedArgs,0 invoke VariantInit,addr varg_out mov varg_out.vt,VT_BSTR mov eax,bstr_out mov varg_out.bstrVal,eax coinvoke mqi.pItf,IDispatch,Invoke,dispid,addr IID_NULL,lcid,DISPATCH_METHOD,\ addr dsppar,addr varg_out,NULL,NULL invoke SysFreeString,bstr_out invoke SysFreeString,bstr_in invoke MessageBoxW,0,varg_out.bstrVal,addr AppName,0 .endif invoke OleUninitialize invoke GlobalFree,mqi.piid xor eax,eax ret dowork endp end start
Как вы заметили в секции data произошли некоторые изменения. Во первых имя приложения я инициализировал, как unicode-строку. Зачем? Узнаете позже. Вместо имени COM-объекта, я использую его GUID. Это потому, что я допускаю, что библиотека типов COM-объекта не зарегистрирована на компьютере клиента и, поэтому в реестре нет никаких записей о нем. Следовательно мы вынуждены передать серверу Автоматизации уникальный идентификатор объекта, чтобы он смог сам найти, на каком компьютере сети задействовать объект. Далее идет IP-сервера (можно использовать имя компьютера). Если вы инициализируете клиента и COM-сервер на одном компьютере, то укажите IP локального компьютера в виде 127.0.0.1. И в конце секции присутствует unicode-переменная param_in. Ее я использую для передачи строкового параметра методу COM-объекта.
В секции data? я инициализирую структуры DISPPARAMS (для передаче параметров методу COM-объекта), COSERVERINFO и MULTI_QI. Структура MULTI_QI в MSDN выглядит так:
typedef struct _MULTI_QI { const IID* pIID; // Указатель на идентификатор интерфейса IUnknown * pItf; // Указатель на интерфейс,запрашиваемый в pIID.Должен быть // установлен в NULL HRESULT hr; // Значение,возвращаемое вызовом QueryInterface,необходимое // для запроса интерфейса, определенного в pIID.Обычно возвращает S_OK или // E_NOINTERFACE. Перед использованием должно быть установлено в 0. } MULTI_QI;
Проще говоря, структура MULTI_QI необходима для передачи клиенту не одного указателя на запрашиваемый интерфейс, а массива указателей. Это сделано для того, чтобы уменьшить количество обращений к COM-объекту по сети и, соответственно, снизить траффик. COM-объект просто передает клиенту массив указателей на интерфейсы, из которых Вы можете выбрать желаемый для использования.
invoke GlobalAlloc,GMEM_FIXED,16 mov mqi.piid,eax
Этими строками кода мы резервируем память под структуру MULTI_QI, и затем указатель на блок памяти передаем первому параметру структуры MULTI_QI.
invoke MultiByteToWideChar,CP_ACP,0,addr IID_IDispatch,sizeof IID_IDispatch,\ addr wDispatch,sizeof wDispatch invoke IIDFromString,addr wDispatch,mqi.piid
Переводим байтовую строку GUID IDispatch в unicode. Затем вызываем функцию IIDFromString для конвертации этой строки в идентификатор интерфейса:
WINOLEAPI IIDFromString( LPOLESTR lpsz, //Указатель на строковое представление IID LPIID lpiid //Указатель на запрашиваемый интерфейс );
Далее мы вызываем функцию CLSIDFromString. Как я уже рассказывал ранее, она предназначена для получения CLSID СOM-объекта из строкового представления в unicode.
lea eax,offset lpSrvName mov ServerInfo.pwszName,eax
Структура COSERVERINFO в MSDN выглядит так:
typedef struct _COSERVERINFO { DWORD dwReserved1; // зарезервировано, должно быть 0 LPWSTR pwszName; // Указатель на имя компьютера COAUTHINFO *pAuthInfo; // Указатель на структуру COAUTHINFO.Использовать не будем. DWORD dwReserved2; // зарезервировано, должно быть 0 } COSERVERINFO;
Таким образом, нас интересует только второй элемент структуры, в который мы должны передать имя компьютера, на котором зарегестрирован COM-сервер. Заметьте, что имя компьютера у нас также в виде unicode-строки.
invoke OleInitialize,0 ... invoke OleUninitialize
Стандартная инициализация и деинициализация библиотеки COM.
invoke CoCreateInstanceEx,addr rclsid,0,CLSCTX_REMOTE_SERVER,addr ServerInfo,1,addr mqi
Эта функция позволяет нам обратиться к удаленному COM-объекту и запросить у него массив, необходимых для работы указателей на интерфейсы.
HRESULT CoCreateInstanceEx( REFCLSID rclsid, //CLSID создаваемого объекта IUnknown * punkOuter, //IUnknown (не используем) DWORD dwClsCtx, //флаги энумератора CLSCTX COSERVERINFO * pServerInfo, //Компьютер,на котором установлен объект ULONG cmq, //Количество структур MULTI_QI,указанных в pResults MULTI_QI* pResults //Массив структур MULTI_QI );
Надеюсь до сих пор все понятно. Мы подготовили структуры MULTI_QI и COSERVERINFO, а затем передали их в качестве параметров функции CoCreateInstanceEx. Эта функция за нас осуществляет обращение к внутренней функции QueryInterface и возвращает нам назад заполненную указателями на интерфейсы объекта структуру MULTI_QI.
invoke GetUserDefaultLCID mov lcid,eax coinvoke mqi.pItf,IDispatch,GetIDsOfNames,addr IID_NULL,addr fn,1,lcid,addr dispid
Даже не хочу останавливаться на этом моменте. Все объяснено в первой статье.
invoke VariantInit,addr varg_in mov varg_in.vt, VT_BSTR invoke SysAllocString,addr param_in mov bstr_in,eax mov varg_in.bstrVal,eax
Мы не можем просто передать аргументы вызываемому методу COM-объекта в виде строки. Перед этим необходимо немного поработать головой. Структура DISPPARAMS принимает аргументы не в виде строки, а в виде массива другой структуры, которая называется VARIANTARG (она же VARIANT). В файле oaidl.inc она опеределена так:
VARIANT STRUCT vt WORD VT_EMPTY wReserved1 WORD 0 wReserved2 WORD 0 wReserved3 WORD 0 Union lVal SDWORD ? ; VT_I4 bVal WORD ? ; VT_UI1 iVal SWORD ? ; VT_I2 fltVal REAL4 ? ; VT_R4 dblVal REAL8 ? ; VT_R8 boolVal VARIANT_BOOL ? ; VT_BOOL scode SCODE ? ; VT_ERROR cyVal QWORD ? ; VT_CY date QWORD ? ; VT_DATE bstrVal BSTR ? ; VT_BSTR punkVal PVOID ? ; VT_UNKNOWN pdispVal PVOID ? ; VT_DISPATCH parray PVOID ? ; VT_ARRAY pbVal PVOID ? ; VT_BYREF|VT_UI1 piVal PVOID ? ; VT_BYREF|VT_I2 plVal PVOID ? ; VT_BYREF|VT_I4 pfltVal PVOID ? ; VT_BYREF|VT_R4 pdblVal PVOID ? ; VT_BYREF|VT_R8 pboolVal PVOID ? ; VT_BYREF|VT_BOOL pscode PVOID ? ; VT_BYREF|VT_ERROR pcyVal PVOID ? ; VT_BYREF|VT_CY pdate PVOID ? ; VT_BYREF|VT_DATE pbstrVal PVOID ? ; VT_BYREF|VT_BSTR ppunkVal PVOID ? ; VT_BYREF|VT_UNKNOWN ppdispVal PVOID ? ; VT_BYREF|VT_DISPATCH pparray PVOID ? ; VT_BYREF|VT_ARRAY pvarVal PVOID ? ; VT_BYREF|VT_VARIANT byref PVOID ? ; Generic ByRef cVal SBYTE ? ; VT_I1 uiVal WORD ? ; VT_UI2 ulVal DWORD ? ; VT_UI4 intVal SWORD ? ; VT_int uintVal WORD ? ; VT_uint pdecVal PVOID ? ; VT_BYREF|VT_DECIMAL pcVal PVOID ? ; VT_BYREF|VT_I1 puiVal PVOID ? ; VT_BYREF|VT_UI2 pulVal PVOID ? ; VT_BYREF|VT_UI4 pintVal PVOID ? ; VT_BYREF|VT_int puintVal PVOID ? ; VT_BYREF|VT_uint ENDS VARIANT ENDS
Как Вы видите, VARIANT это объединение различных типов данных, с которыми может оперировать COM-объект. Для нас же важным является то, что disp-интерфейсы и дуальные интерфейсы могут передавать только те типы данных, которые можно выразить при помощи типов, определенных в структуре VARIANT. Для инициализации структуры VARIANT предназначена функция VariantInit. Она передает в первый элемент структуры VT_EMPRY. Реально в структуре VARIANT используются только первый и пятый элементы, т.к. остальные три зарезервированы для использования в будущем. В первый элемент vt структуры мы передаем флаг типа данных, которые мы собираемся передавать в качестве аргумента нашему методу. Поскольку Visual Basic воспринимает строки в виде т.н. BSTR-типа, то мы передаем флаг VT_BSTR. Затем unicode-строку аргумента нам необходимо привести к данному типу данных, для чего мы используем функцию SysAllocString, используемую в паре с функцией SysFreeString.
BSTR SysAllocString( const OLECHAR * sz);
Эта функция резервирует память под строку BSTR и затем копирует в этот блок памяти unicode-строку, передаваемую ей в качестве параметра. Возвращаемым значением является указатель на блок памяти с BSTR-строкой. Кстати, передать этой функции можно и пустую строку, что мы и делаем для возвращаемого методом параметра. После получения BSTR-строки, мы передаем ее второму элементу структуры VARIANT, как значение bstrVal.
lea eax,varg_in mov dsppar.rgvarg,eax mov dsppar.rgdispidNamedArgs,NULL mov dsppar.cArgs,1 mov dsppar.cNamedArgs,0
После этого, мы спокойно можем инициализировать структуру DISPPARAMS, передав в rgvarg адрес структуры VARIANTARG, и указав в cArgs, что у нас имеется один передаваемый методу COM-объекта аргумент. Так как мы не используем именованные аргументы, то в элементы rgdispidNamedArgs и cNamedArgs мы передаем NULL и 0 соответственно.
invoke VariantInit,addr varg_out mov varg_out.vt,VT_BSTR mov eax,bstr_out mov varg_out.bstrVal,eax
Знакомая Вам уже инициализация структуры VARIANTARG, но только для возвращаемого методом COM-объекта значения. Как Вы помните метод нам должен вернуть строку (соответственно BSTR).
coinvoke mqi.pItf,IDispatch,Invoke,dispid,addr IID_NULL,lcid,DISPATCH_METHOD,\ addr dsppar,addr varg_out,NULL,NULL invoke SysFreeString,bstr_out invoke SysFreeString,bstr_in invoke MessageBoxW,0,varg_out.bstrVal,addr AppName,0
Вызываем на выполнение метод, передавая функции Invoke интерфейса шестым параметром
адрес выходной структуры VARIANTARG. После отработки Invoke, идет стандартная
очистка обоих блоков памяти, зарезервированных ранее под BSTR-строки. После этого вызываем
unicode-функцию MessageBoxW для отображения возвращаемой методом строки.
Всё...
В заключение хочу сказать. Не бойтесь использовать COM-объекты. Этим Вы не уроните своего достоинства. Технология COM в чём-то подчас не понятна, подчас трудна в использовании. Но знать как ею пользоваться необходимо. Не бойтесь экспериментировать. Спасибо всем, кто заинтересовался и дочитал эти статьи до конца.