Глава 4

Реализация объектов COM и элементов управления ими в Lua

4.1 Введение

С LuaCOM возможно реализация полноценных COM объектов и элементов управления (контролов) OLE, использующих Lua. Здесь мы понимаем COM объект как состоящий из следующих частей: Данные реестра привязывают ProgID к CLSID, который, в свою очередь, сопоставляется с сервером. Сведения о типе описывают компонент - то есть, какие интерфейсы он предоставляет и каков его интерфейс по умолчанию.

LuaCOM упрощает эти задачи, предоставляя некоторые вспомогательные функции для работ по регистрации и инстанцированию COM серверов. LuaCOM поддерживает как локальные, внепроцессные (EXE), так и внутрипроцессные (DLL) серверы.

LuaCOM также предоставляет вспомогательные функции для регистрации и инстанцирования (создания экземпляра) элементов управления OLE (со своим пользовательским интерфейсом, встраиваемым в основную программу - хостинг-приложение). Этому типу объекта требуется внутрипроцессный сервер и поддерживаемый набор инструментов Lua GUI (на сегодняшний день это IUP).

4.2 Полезно ли это в действительности?

Кто-то может доказывать, что лучше бы было реализовать COM-объект на языках вроде C++ или Visual Basic®. Во многих ситуациях это правильно, но неверно в некоторых других. Во-первых, работа с COM не так проста, а LuaCOM скрывает большинство его сложностей; кроме того, имеется еще одна убедительная причина для использования LuaCOM по крайней мере в некоторых ситуациях: семантика таблиц Lua и способ реализации LuaCOM позволяет делать некоторые вещи очень аккуратно: Конечно, вся эта гибкость имеет свою цену, в первую очередь сказывается на производительности. Впрочем, зависит от приложения, нехватка производительности может быть ничтожно мала.

LuaCOM не решает всех проблем: остается потребность в библиотеке типов, которая должна быть создана с использованием сторонних инструментов.

4.3 Терминология

Во избежание недоразумений, тут представлены значения, которые мы даем некоторым терминам, используемым в данной главе. Мы не даем здесь официальных определений: мы просто хотим облегчить понимание некоторых концепций. Чтобы лучше представлять эти понятия смотрите COM документацию.

Component (компонент, деталь, составная часть) - часть программного обеспечения с некоторой функциональностью, которая может быть использована другими компонентами. Он состоит из набора объектов, что реализуют данную функциональность.

Component Object (составной объект) - объект, через который можно получать все функциональные возможности компонента, включая другие его объекты. Данный объект может иметь много интерфейсов.

Application Object (объект-приложение) Составной объект с интерфейсом, который содержит все высокоуровневые функциональные возможности компонента; клиенту не нужно использовать другие интерфейсы составного объекта. Такое представление упрощает понимание компонента, поскольку размещает все свои функциональные возможности в иерархическом порядке (объект-приложение вместе со своими вложенными объектами, которые могут быть доступны только через методы и свойства объекта-приложения).

COM server (COM сервер) Некоторая часть кода, что реализует (имплементирует) один или более составных объектов. COM сервер должен сообщить другим приложениям и компонентам какие составные объекты он делает доступными. Он делает это, выставляя (показывая) их.

OLE control (элемент управления OLE) Объект, имеющий пользовательский интерфейс, который может быть встроен внутрь других приложений, имеющих OLE контейнеры (обычно приложения на C++ или Visual Basic).

CoClass (от component object class - класс составного объекта) Библиотека типов описывающая компонент, обязанный иметь соответствующую запись CoClass, определяющую некоторые сведения о компоненте: Lua Application Object (объект-приложение Lua) Это Lua таблица, используемая для реализации (имплементации) объекта-приложения.

4.4 Построение COM сервера в LuaCOM

Для построения COM сервера с помощью LuaCOM имеется несколько шагов:
  1. определение компонента;
  2. определяем, что собираемся экспортировать: объект-приложение Lua и его вложенные объекты;
  3. создаем библиотеку типов для компонента;
  4. определяем сведения для регистрации компонента;
  5. регистрируем объект компонент;
  6. реализуем (имплементируем) и выставляем COM-объекты;
  7. добавляем инициализацию COM и код завершения.
4.4.1 Определение компонента
Это первый шаг: устанавливаем какие функциональные возможности компонента будут выставлены. Эта функциональность представляется иерархическим порядком объектов, начинающимся с объекта-приложения. Каждый из этих объектов должен реализовывать интерфейс.

Пример:   Предположим, что у нас есть библиотека Lua которая реализует доступ к базам данных, содержащих в конкретной СУБД. Эта библиотека имеет три типа объектов: базы данных, запросы и отчеты. В мире COM, это можно представить объектом-приложением, который открывает базы данных и возвращает объект Database. Объект Database имеет, среди прочего, метод Query (запрос). Этот метод принимает SQL предложение и возвращает объект Query. Объект Query является коллекцией (сборником), которая может быть итерирована (повторно обработана) с использованием параметризованного свойства Records, что возвращает объект типа Record.
4.4.2 Объекты для экспорта
Экспортируемые объекты - это те, что входят в состав иерархического порядка базирующегося на объекте-приложении (Application object). В мире языка Lua объекты обычно представлены в виде таблиц или пользовательских данных (userdata). (Здесь таблицы и пользовательские данные - это типы данных, принятых в Lua. - примечание переводчика) Таким образом, это необходимо для идентификации (или реализации) таблиц Lua, используемых для реализации экспортируемых объектов.
4.4.3 Создание библиотеки типов
Библиотека типов должна содержать записи для всех интерфейсов экспортируемых объектов и запись для CoClass, определяющего интерфейс объекта-приложения и интерфейса, используемого для отправки событий.

Наиболее распространенным способом создания библиотеки типов является написание IDL описание библиотеки типов и затем использовать IDL компилятор, такой как MIDL от Microsoft®. Обратите внимание что, все интерфейсы должны быть диспетчерскими интерфейсами — то есть, должны наследовать от IDispatch и должны иметь флаг oleautomation.
4.4.4 Регистрационные сведения
Здесь следует указать сведения, используемые COM для обнаружения компонента. Смотрите документацию по RegisterObject.
4.4.5 Регистрация объекта Component
Перед тем как стать доступным для других приложений, объект компонент должен быть зарегистрирован в реестре системы. Это можно сделать с помощью функции API RegisterObject. Данная функция получает таблицу с регистрационными сведениями объекта. Посмотрите поля этой таблицы в Полнофункциональном примере.
4.4.6 Реализация и выставление компонента
Имеются две различные ситуации, требующие разных действий:

Реализация (имплементация) объекта-приложения Тут нужно использовать LuaCOM метод NewObject для создания объекта COM с привязкой его к таблице объекта-приложения Lua (Lua Application Object). Затем этот объект нужно сделать доступным (выставить его) для других приложений с помощью ExposeObject.

Реализация (имплементация) других объектов Другие объекты компонента получаются от объекта-приложения Lua в качестве возвращаемых значений функций или как значения, хранящиеся в полях объекта-приложения Lua (то есть, через доступ к свойству). Эти объекты реализуются с помощью ImplInterface. Они могут быть реализованы при инициализации (и затем где-то храниться) или могут реализовываться по потребности (то есть, каждый раз при возвращении COM-объекта делается вызов ImplInterface).

Обратите внимание, что поля Lua-таблицы, используемые для реализации компонента COM, будут доступны, только если они представлены в библиотеке типов. Если это не так, то они будут невидимы для COM.
4.4.7 Инициализация и завершение
Инициализация
Если был реализован свой собственный сервер, вместо использования встроенной поддержки, то он должен вызывать функции инициализации COM (OleInitialize или CoInitialize) перед запуском LuaCOM. Другой задачей инициализации является реализация и представление COM объектов. Эта задача может быть значительно упрощена использованием функции C/C++ API LuaCOM luacom_detectAutomation.

Если же нужно использовать встроенную поддержку, инициализация необходима только для вызова функции API DetectAutomation в конце скрипта, который реализует объекты, передавая таблицу с методами для регистрации и выставления объектов.
Завершение
COM сервер должен вызывать (в Lua) функцию RevokeObject для каждого представленного объекта. Затем он должен вызвать функции завершения COM, ПОСЛЕ вызова lua_close; в противном случае могут возникать фатальные ошибки.

4.5 Запуск COM сервера

COM сервер выстроенный по представленным рекомендациям может использоваться как любой другой COM-объект — то есть, используя CoCreateInstance, CreateObject или подобные им.

4.6 Создание событий

Метод NewObject возвращает точку подключения объекта-контейнера. Этот объект позволяет компоненту отправлять события своим клиентам простым вызовом методов на этом объекте, передавая ожидаемые параметры. Возвращение значений пока не разрешено.

4.7 Полнофункциональный пример

Здесь дан пример полнофункционального Lua COM сервера. В примере предполагается что этот скрипт называется testobj.lua:
-- This is the implementation of the COM object
-- Реализация объекта COM
path_to_obj = "\\Path\\To\\Script\\"
TestObj = {}
function TestObj:showWindow() print("Show!") events:OnShow() end
function TestObj:hideWindow() print("Hide!") events:OnHide() end
COM = {}
function COM:StartAutomation() -- creates the object using its default interface -- создаем объект, используя его интерфейс по умолчанию COMAppObject, events, e = luacom.NewObject(TestObj, "TEST.Test") -- This error will be caught by detectAutomation -- Эта ошибка будет выловлена detectAutomation if COMAppObject == nil then error("NewObject failed: "..e) end -- Exposes the object -- Предоставляем объект cookie = luacom.ExposeObject(COMAppObject) if cookie == nil then error("ExposeObject failed!") end end
function COM:Register() -- fills table with registration information -- заполняем таблицу с регистрационными сведениями local reginfo = {} reginfo.VersionIndependentProgID = "TEST.Test" reginfo.ProgID = reginfo.VersionIndependentProgID..".1" reginfo.TypeLib = "test.tlb" reginfo.CoClass = "Test" reginfo.ComponentName = "Test Component" reginfo.Arguments = "/Automation" reginfo.ScriptFile = path_to_script .. "testobj.lua" -- stores component information in the registry -- сохраняем сведения о компонентах в реестре local res = luacom.RegisterObject(reginfo) if res == nil then error("RegisterObject failed!") end end
function COM:UnRegister() -- fills table with registration information -- заполняем таблицу с регистрационными сведениями local reginfo = {} reginfo.VersionIndependentProgID = "TEST.Test" reginfo.ProgID = reginfo.VersionIndependentProgID..".1" reginfo.TypeLib = "test.tlb" reginfo.CoClass = "Test" -- removes component information from the registry -- удаляем сведения о компонентах из реестра local res = luacom.UnRegisterObject(reginfo) if res == nil then error("UnRegisterObject failed!") end end
-- Starts automation server -- Запуск сервера автоматизации return luacom.DetectAutomation(COM)

4.8 Построение элементов управления OLE Lua

Большинство из того, что требуется для построения элемента управления (контрола) OLE уже было описано в предыдущей главе. Элементы управления подобны обычным объектам LuaCOM, но создаются при помощи функции API NewControl вместо NewObject. Таблица регистрационных сведений также должна иметь поле Control, установленное как true.

Таблица реализующая элемент управления (контрол) также должна реализовывать несколько дополнительных методов, часть протокола контрола. Это следующие:

InitialSize    Контрол может использовать этот метод для возвращения своего первоначального размера, в пикселах.

CreateWindow    Вызывается когда контрол создает свое окно. Параметрами этой функции являются дескриптор (handle - хэндл) родительского окна (в виде пользовательских данных - userdata), начальное положение и первоначальный размер окна. Контрол должен возвратить пользовательские данные со своим дескриптором окна.

SetExtent    Вызывается всякий раз, когда основное приложение (хост) хочет изменить размеры элемента управления (контрола). Параметрами являются новые размеры. Должен возвращать значение true, если контрол принимает изменение размера, в противном случае возвращается значение false.

GetClass    должен возвращать идентификатор класса (class id) элемента управления (контрола).

DestroyWindow    Вызывается когда основная программа завершила работу с контролом и он (контрол) должен уничтожить свое окно и высвободить все занимаемые ресурсы.

На домашней странице LuaCOM, в директории demo/control имеется пример элемента управления (контрола).


LuaCOM был разработан Renato Cerqueira, Vinicius Almendra и Fabio Mascarenhas.
Проект финансируется компанией TeCGraf
(Technology Group on Computer Graphics).