Иногда наш COM?компонент должен отправить клиенту уведомление или вызвать функцию обратного вызова. Схема простая: компонент публикует интерфейс, клиент создаёт унаследованный от интерфейса объект и передаёт его компоненту, компонент в свою очередь вызывает функции интерфейса, тем самым вызывая функции на стороне клиента.


В случае Visual Basic или Visual Basic for Applicatons мы можем написать класс, унаследованный от любого интерфейса, однако для файлов сценария VBScript такой возможности нет.


Здесь нам спешит на помощь интерфейс IDispatch. С помощью этого интерфейса наш могучий компонент смиренно примет на себя скромную роль клиента, а маленький сценарий превратится в настоящий сервер автоматизации.


Разрабатывать компонент будем на языке программирования FreeBASIC.


Классы в файле сценария


В файлах сценариев можно объявлять и использовать классы. Такие классы неявно наследуются от интерфейса IDispatch и являются настоящими COM?классами.


Объявим класс, экземпляр которого впоследствии мы передадим нашему компоненту:


Class CallBack

    Function CallBack(Param)
        ' Показываем переданный параметр
        WScript.Echo Param
        CallBack = 0
    End Function

End Class

Наш компонент получит экземпляр класса CallBack, вызовет функцию CallBack и передаст ей в параметре строку с текстом.


' Компонент
Dim Component
Set Component = CreateObject("BatchedFiles.TestCOMServer")

' Класс в файле сценария, выступающий в роли сервера автоматизации
Dim objCallBack
Set objCallBack = New CallBack

' Отправляем компоненту указатель на наш сервер
Component.SetCallBack objCallBack, "Пользователь"

' Заставляет компонент вызвать функцию сервера автоматизации
result = Component.InvokeCallBack()
WScript.Echo result

Set objCallBack = Nothing
Set Component = Nothing

IDispatch


Этот интерфейс — камень преткновения автоматизации. Обычно реализация IDispatch основывается на библиотеке типов через ITypeInfo->Invoke или функцию CreateStdDispatch, но в данном случае сервер автоматизации расположен в скрипте и библиотеки типов не имеет, а наш компонент выступает в роли клиента. Упрощая, IDipatch работает так: принимает имя функции и передаёт ей управление.


Определение интерфейса лежит заголовочнике «oaidl.bi» (отступы и переносы строк добавлены для читабельности):


Type IDispatch As IDispatch_

Type LPDISPATCH As IDispatch Ptr

Type IDispatchVtbl

    ' Наследование от IUnknown
    Dim InheritedTable As IUnknownVtbl

    GetTypeInfoCount As Function( _
        ByVal this As IDispatch Ptr, _
        ByVal pctinfo As UINT Ptr _
    )As HRESULT

    GetTypeInfo As Function( _
        ByVal this As IDispatch Ptr, _
        ByVal iTInfo As UINT, _
        ByVal lcid As LCID, _
        ByVal ppTInfo As ITypeInfo Ptr Ptr _
    )As HRESULT

    GetIDsOfNames As Function( _
        ByVal this As IDispatch Ptr, _
        ByVal riid As Const IID Const Ptr, _
        ByVal rgszNames As LPOLESTR Ptr, _
        ByVal cNames As UINT, _
        ByVal lcid As LCID, _
        ByVal rgDispId As DISPID Ptr _
    )As HRESULT

    Invoke As Function( _
        ByVal this As IDispatch Ptr, _
        ByVal dispIdMember As DISPID, _
        ByVal riid As Const IID Const Ptr, _
        ByVal lcid As LCID, _
        ByVal wFlags As WORD, _
        ByVal pDispParams As DISPPARAMS Ptr, _
        ByVal pVarResult As VARIANT Ptr, _
        ByVal pExcepInfo As EXCEPINFO Ptr, _
        ByVal puArgErr As UINT Ptr _
    )As HRESULT

End Type

Type IDispatch_
    lpVtbl As IDispatchVtbl Ptr
End Type

Наиболее интересны в этом интерфейсе функции GetIDsOfNames и Invoke.


GetIDsOfNames


Принимает имя функции и возвращает её диспетчерский идентификатор DISPID. DISPID — это псевдоним для типа LONG.


С точки зрения клиента DISPID — просто средство оптимизации, позволяющее избежать передачи строк. Для сервера же DISPID — идентификатор функции, которую хочет вызвать клиент.


Параметр Описание
riid Зарезервировано. Следует передавать указатель на IID_NULL.
rgszNames Массив имён функций, для которых необходимо вернуть диспетчерские идентификаторы.
cNames Размер массива.
lcid Информация локализации.
rgDispId Массив, куда функция запишет DISPID для каждого имени функции или DISPID_UNKNOWN если не найдёт функцию с таким именем.

Invoke


По диспетчерскому идентификатору выполняет соответствующую функцию.


Параметр Описание
dispIdMember Диспетчерский идентификатор вызываемой функции.
riid Зарезервировано. Следует передавать указатель на IID_NULL.
lcid Информация локализации.
wFlags Флаги типа функции. Для простых функций следует устанавливать в DISPATCH_METHOD, для получения значения свойства — DISPATCH_PROPERTYGET, для установки значения свойства — DISPATCH_PROPERTYPUT, по ссылке — DISPATCH_PROPERTYPUTREF.
pDispParams Специальная структура с параметрами вызова функции.
pVarResult Указатель на тип VARIANT, куда функция занесёт результат работы.
pExcepInfo Указатель на структуру, куда функция запишет выброшенное исключение. Можно устанавливать в NULL.
puArgErr Индексы аргументов, вызвавших ошибку. Можно устанавливать в NULL.

DISPPARAMS


В этой структуре содержатся параметры вызываемой функции. Все параметры упаковываются в VARIANT.


Type tagDISPPARAMS

    ' Указатель на массив безымянных параметров
    rgvarg As VARIANTARG Ptr

    ' Указатель на массив именованных параметров
    rgdispidNamedArgs As DISPID Ptr

    ' Количество безымянных параметров
    cArgs As UINT

    ' Количество именованных параметров
    cNamedArgs As UINT

End Type

Type DISPPARAMS As tagDISPPARAMS

Для упрощения кода мы не будем использовать именованные аргументы, вместо них будем ставить NULL.


Компонент


Для использования в скриптах компонет также следует прямо или косвенно наследовать от IDipatch.


Интерфейс ITestCOMServer


Построим интерфейс ITestCOMServer с двумя функциями SetCallBack и InvokeCallBack. Первая будет сохранять объект сервера автоматизации, вторая — вызывать функцию объекта.


Type ITestCOMServer As ITestCOMServer_

Type LPITESTCOMSERVER As ITestCOMServer Ptr

Type ITestCOMServerVirtualTable

    ' Наследование от IDispatch
    Dim InheritedTable As IDispatchVtbl

    Dim SetCallBack As Function( _
        ByVal this As ITestCOMServer Ptr, _
        ByVal CallBack As IDispatch Ptr, _
        ByVal UserName As BSTR _
    )As HRESULT

    Dim InvokeCallBack As Function( _
        ByVal this As ITestCOMServer Ptr _
    )As HRESULT

End Type

Type ITestCOMServer_
    Dim pVirtualTable As ITestCOMServerVirtualTable Ptr
End Type

Класс TestCOMServer


Теперь можно оъявлять COM?класс:


Type TestCOMServer

    ' Указатель на таблицу виртуальных функций
    Dim pVirtualTable As ITestCOMServerVirtualTable Ptr

    ' Счётчик ссылок
    Dim ReferenceCounter As ULONG

    ' Объект сервера автоматизации
    Dim CallBackObject As IDispatch Ptr

    ' Сохранённый параметр
    Dim UserName As BSTR

End Type

Функция SetCallBack


Реализация функции SetCallBack проста: сохраняем переданный клиентом объект сервера автоматизации и параметр вызова функции.


Function TestCOMServerSetCallBack( _
        ByVal pTestCOMServer As TestCOMServer Ptr, _
        ByVal CallBack As IDispatch Ptr, _
        ByVal UserName As BSTR _
    )As HRESULT

    ' Если у нас уже есть оъект, то сперва его надо освободить
    If pTestCOMServer->CallBackObject <> NULL Then
        IDispatch_Release(pTestCOMServer->CallBack)
    End If

    pTestCOMServer->CallBackObject = CallBack

    ' Увеличиваем счётчик ссылок
    If pTestCOMServer->CallBackObject <> NULL Then
        IDispatch_AddRef(pTestCOMServer->CallBack)
    End If

    ' Удаляем предыдущие данные
    SysFreeString(pTestCOMServer->UserName)

    ' Сохраняем параметр функции обратного вызова
    pTestCOMServer->UserName = SysAllocStringLen(UserName, SysStringLen(UserName))

    Return S_OK

End Function

Функция InvokeCallBack


А вот с функцией InvokeCallBack придётся потрудиться. Для начала необходимо получить диспетчерский идентификатор функции CallBack сервера автоматизации.


Function TestCOMServerInvokeCallBack( _
        ByVal pTestCOMServer As TestCOMServer Ptr _
    )As HRESULT

    If pTestCOMServer->CallBack = NULL Then
        Return E_POINTER
    End If

    ' Количество сопоставляемых имён
    Const cNames As UINT = 1
    ' Массив с именами функций
    Dim rgszNames(cNames - 1) As WString Ptr = {@"CallBack"}
    ' Массив с DISPID
    Dim rgDispId(cNames - 1) As DISPID = Any

    Dim hr As HRESULT = IDispatch_GetIDsOfNames( _
        pTestCOMServer->CallBackObject, _
        @IID_NULL, _
        @rgszNames(0), _
        cNames, _
        GetUserDefaultLCID(), _
        @rgDispId(0) _
    )
    If FAILED(hr) Then
        MessageBoxW(NULL, "Не получил DISPID", NULL, MB_OK)
        Return E_FAIL
    End If

После того, как DISPID функции получен, её можно вызывать:


    ' Отправим клиенту строку вида «Привет, %UserName%»
    Dim Greetings As BSTR = SysAllocString("Привет, ")
    Dim GreetingsUserName As BSTR = Any
    VarBstrCat(Greetings, pTestCOMServer->UserName, @GreetingsUserName)

    Const ParamsCount As Integer = 1

    ' Инициализация массива параметров
    Dim varParam(ParamsCount - 1) As VARIANT = Any
    For i As Integer = 0 To ParamsCount - 1
        VariantInit(@varParam(i))
    Next

    ' Тип параметра — строка
    varParam(0).vt = VT_BSTR
    varParam(0).bstrVal = GreetingsUserName

    Dim Params(0) As DISPPARAMS = Any
    Params(0).rgvarg = @varParam(0)
    Params(0).cArgs = ParamsCount
    Params(0).rgdispidNamedArgs = NULL
    Params(0).cNamedArgs = 0

    ' Результат вызова функции из скрипта
    Dim VarResult As VARIANT = Any
    Dim ExcepInfo As EXCEPINFO = Any
    Dim uArgErr As UINT = Any

    ' Вызываем функцию из скрипта
    hr = IDispatch_Invoke( _
        pTestCOMServer->CallBackObject, _
        rgDispId(0), _
        @IID_NULL, _
        GetUserDefaultLCID(), _
        DISPATCH_METHOD, _
        @Params(0), _
        @VarResult, _
        NULL, _
        NULL _
    )

    ' Очистка массива параметров
    For i As Integer = 0 To ParamsCount - 1
        VariantClear(@varParam(i))
    Next

    SysFreeString(Greetings)

    Return S_OK

End Function

Вывод


Как видно, даже с файлом сценария компонент может получить обратную связь. Это полезно для уведомления клиента о завершившихся операциях со стороны компонента.


Классы в сценариях можно регистрировать в реестре, в таком случае они будут доступны для всей системы по ProgID, но это уже совсем другая история.


Ссылки


Код проекта на сайте гитхаба: https://github.com/zamabuvaraeu/TestCOMServer


P.S. Куда?то исчезла подсветка для BASIC?синтаксиса, вместо него использовал VBScript, а с ним не подсвечиваются некоторые операторы.

Комментарии (0)