Автор: Роман Панышев (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 в чём-то подчас не понятна, подчас трудна в использовании. Но знать как ею пользоваться необходимо. Не бойтесь экспериментировать. Спасибо всем, кто заинтересовался и дочитал эти статьи до конца.