ActiveX Scripting Engines: Интерпретация внешнего скрипта в С++
ОГЛАВЛЕНИЕ
Иногда очень хочется добавить в программу возможность интерпретации внешнего скрипта. Одна из сравнительно простых и мощных возможностей – использовать ActiveX Scripting Engines и использовать VBScript или JavaScript. На первый взгляд, для этого требуются глубокие знания OLE COM технологии. Имеющиеся на сайте Microsoft примеры могут отпугнуть чем-нибудь совсем непонятным, например, объявлением METHOD_PROLOGUE и последующим использованием непонятно откуда взявшегося указателя pThis. Meжду тем, реализовать поддержку ActiveScript совсем несложно. Глубоко понять внутренние скрытые механизмы труднее, но это и не нужно – цель совершенно другая: внедрить поддержку скрипта, не вдаваясь в тонкости. Для этого используем то, что уже реализовано, а именно MFC.
Полные файлы примера находятся в прикреплнном архиве. Здесь в описании приводятся только фрагменты для иллюстрации определенных принципов.
никому не нужен – его исполнение ничего не дает.
Нужно, чтобы переменная «k» была обьявлена не в пространстве имен самого скрипта как «dim k», а где-нибудь в C++ модуле как, например, long k, и после выполнения VBScript строки «k = 1» её значение стало равным 1.
Для реализации такой возможности используется класс, порожденный от базового MFC класса CCmdTarget. Этот класс обеспечивает механизм позднего связывания (late binding). Если не вдаваться в детали, всё довольно просто: есть таблица указателей на переменные и функции, а также строковые имена. Доступ к переменной или вызов метода осуществляется поиском соответствующего строкового идентификатора. Если в скрипте есть строка «k = 1», и существует некая long val, то в таблице есть что-то навроде:
Теперь строка скрипта «k = 1» исполняется так:
Разумеется, приведенная выше модель очень грубая и только иллюстрирует общий принцип. На практике задача гораздо сложнее: вызов функций, передача параметров, возвращаемые значения, контроль типов и так далее.
Итак, создадим класс, порожденный от CCmdTarget.
Самое главное – это объявленные для диспетчеризации:
Именно они используются из скрипта. Для каждого из них зарезервирован числовой идентификатор
в перечислении (enum):
Затем в .cpp модуле объявлена таблица:
Макрос DISP_PROPERTY_ID добавляет переменную m_nValue с типом данных VT_I4 в таблицу. Её строковый идентификатор "VALUE", числовой id_Value.
Макрос DISP_FUNCTION_ID добавляет функцию GetMax с возвращаемым типом VT_I4 и двумя параметрами VTS_I4 и VTS_I4, перечисленными через пробел.
Теперь понятно, как добавить новую переменную (свойство, property) или финкцию:
- объявить соответствующий член класса;
- добавить в enum новый id;
- добавить макрос где-нибудь между BEGIN_DISPATCH_MAP и END_DISPATCH_MAP.
Отметим важную вещь: в конструкторе класса обязательно должен присутствовать вызов метода EnableAutomation().
Теперь мы можем вызывать его методы, например:
Но сам обьект «engine» не может взаимодействовать с нашей программой – он ничего о ней не знает. Реализовать механизм обратной связи можно было по-разному. Например, передать объекту «engine» указатели на функции (callback function).
Механизм обратной связи ActiveX Scripting построен на основе специального интерфейса (класса) IActiveScriptSite. Грубо говоря, существует объявленный базовый интерфейс (класс) IActiveScriptSite, содержащий набор заранее определенных виртуальных функций. Необходимо создать класс, унаследованный от IActiveScriptSite, и перегрузить его виртуальные функции:
Теперь нужно создать экземпляр нашего класса CScriptHost и передать обьекту «engine» его адрес:
Совершенно очевидно, что метод SetScriptSite примерно такой:
Теперь внутри реализации самого ActiveX Scripting возможны вызовы методов через указатель m_pActiveScriptSite на основе механизмов виртуальности и преобразования типов:
Отметим, что все методы, обьявленные в IActiveScriptSite, являются чисто виртуальными:
Поэтому придется переопределять их все, иначе нельзя инстанциировать объект, содержащий чисто виртуальные функции. Вдобавок придется позаботится о методах класса IUnknown, от которого унаследован сам IActiveScriptSite (они тоже чисто виртуальные). В результате появляются функции, особо ничего не делающие, например:
Пришло его время.
Один из методов интерфейса IActiveScriptSite имеет следующий прототип:
Во время исполнения скрипта метод GetItemInfo будет вызван с определенными параметрами, говорящими о том, что в ответ нужно вернуть указатель на интерфейс IUnknown*. Это как раз и есть тот момент, когда для дальнейшего исполнения скрипта понадобился экземпляр объекта CCodeObject – например, чтобы «поискать» там какую-нибудь переменную, имя которой использовано в скрипте.
К этому моменту в каком-нибудь модуле трансляции уже существует экземпляр класса CCodeObjecе. Например, обьявленный как глобальный – сейчас стиль программирования не особо важен, главное – проиллюстрировать суть происходящего. Итак, где-то объявлен и находится в зоне видимости:
Теперь в реализации CScriptHost::GetItemInfo() происходит следующее:
Заметим важный ньюанс – следующая строка НЕПРАВИЛЬНАЯ:
Компилятор проглотит, но, хотя наш обьект и унаследован от CCmdTarget, сам класс CCmdTarget не унаследован от IUnknown.
Ранее мы создавали CScriptHost, унаследованный от IActiveScriptSite, а сам IActiveScriptSite был унаследован от IUnknown. Это действительно допускает преобразование CScriptHost к IUnknown.
Но в случае с классом CCodeObject, порожденным от CCmdTarget, преобразование к типу IUnknown невозможно.
Класс CCmdTarget может вернуть указатель на интерфейс IUnknown (или интефейс IDispatch, действительно порожденный от IUnknown). Но делается это путем вызова
или
В основу положен другой механизм. В очень грубом приближении, в классе CCmdTarget объявлен член класса, имеющий тип IDispatch, а метод GetIDispatch возвращает его адрес:
На самом деле всё несколько сложнее – применена некоторая арифметика указателей и смещений. Проиллюстрируем это на примере:
Не будем углубляться дальше. Продолжим работу над главной задачей – запуском скрипта. Осталось совсем немного.
Полные файлы примера находятся в прикреплнном архиве. Здесь в описании приводятся только фрагменты для иллюстрации определенных принципов.
Начинаем работать.
Скрипт должен взаимодействовать с нашей С++ программой – использовать и изменять значения переменных, объявленных в С++ части программы, или вызывать функции. Скрипт наподобии такого: dim k k = 1 |
Нужно, чтобы переменная «k» была обьявлена не в пространстве имен самого скрипта как «dim k», а где-нибудь в C++ модуле как, например, long k, и после выполнения VBScript строки «k = 1» её значение стало равным 1.
Для реализации такой возможности используется класс, порожденный от базового MFC класса CCmdTarget. Этот класс обеспечивает механизм позднего связывания (late binding). Если не вдаваться в детали, всё довольно просто: есть таблица указателей на переменные и функции, а также строковые имена. Доступ к переменной или вызов метода осуществляется поиском соответствующего строкового идентификатора. Если в скрипте есть строка «k = 1», и существует некая long val, то в таблице есть что-то навроде:
struct ENTRY { CString name; long* pval; }; long val; ENTRY table[] = { { "k", &val } }; UINT nEntryCount = 1; |
Теперь строка скрипта «k = 1» исполняется так:
for (UINT nIndex = 0; nIndex < nEntryCount; nIndex++) if ( table[nIndex].name == "k" ) { *table[nIndex].pval = 1; break; } |
Разумеется, приведенная выше модель очень грубая и только иллюстрирует общий принцип. На практике задача гораздо сложнее: вызов функций, передача параметров, возвращаемые значения, контроль типов и так далее.
Итак, создадим класс, порожденный от CCmdTarget.
#include <afx.h> #include <afxdisp.h> class CCodeObject : public CCmdTarget { public: CCodeObject(); virtual ~CCodeObject(); private: long m_nValue; long GetMax(long, long); void PrintValue(long); void Message(LPCSTR); enum { id_Value = 1, id_PrintValue, id_GetMax, id_Message }; DECLARE_DISPATCH_MAP() }; |
Самое главное – это объявленные для диспетчеризации:
long m_nValue; long GetMax(long, long); void PrintValue(long); void Message(LPCSTR); |
Именно они используются из скрипта. Для каждого из них зарезервирован числовой идентификатор
в перечислении (enum):
enum { id_Value = 1, id_PrintValue, id_GetMax, id_Message }; |
Затем в .cpp модуле объявлена таблица:
BEGIN_DISPATCH_MAP(CCodeObject, CCmdTarget) DISP_PROPERTY_ID(CCodeObject, "VALUE", id_Value, m_nValue, VT_I4) DISP_FUNCTION_ID(CCodeObject, "GetMax", id_GetMax, GetMax, VT_I4, VTS_I4 VTS_I4) DISP_FUNCTION_ID(CCodeObject, "PrintValue", id_PrintValue, PrintValue, VT_EMPTY, VTS_I4) DISP_FUNCTION_ID(CCodeObject, "Message", id_Message, Message, VT_EMPTY, VTS_BSTR) END_DISPATCH_MAP() |
Макрос DISP_PROPERTY_ID добавляет переменную m_nValue с типом данных VT_I4 в таблицу. Её строковый идентификатор "VALUE", числовой id_Value.
Макрос DISP_FUNCTION_ID добавляет функцию GetMax с возвращаемым типом VT_I4 и двумя параметрами VTS_I4 и VTS_I4, перечисленными через пробел.
Теперь понятно, как добавить новую переменную (свойство, property) или финкцию:
- объявить соответствующий член класса;
- добавить в enum новый id;
- добавить макрос где-нибудь между BEGIN_DISPATCH_MAP и END_DISPATCH_MAP.
Отметим важную вещь: в конструкторе класса обязательно должен присутствовать вызов метода EnableAutomation().
Создаем механизмы ActiveX Scripting
Забежим немного вперед. Предположим, у нас уже есть инстанциированный объект Microsoft ActiveX Scripting. Для простоты, условно обьявим его так: СActiveXScriptEngine engine; |
Теперь мы можем вызывать его методы, например:
engine.InitNew(); |
Но сам обьект «engine» не может взаимодействовать с нашей программой – он ничего о ней не знает. Реализовать механизм обратной связи можно было по-разному. Например, передать объекту «engine» указатели на функции (callback function).
Механизм обратной связи ActiveX Scripting построен на основе специального интерфейса (класса) IActiveScriptSite. Грубо говоря, существует объявленный базовый интерфейс (класс) IActiveScriptSite, содержащий набор заранее определенных виртуальных функций. Необходимо создать класс, унаследованный от IActiveScriptSite, и перегрузить его виртуальные функции:
class CScriptHost : public IActiveScriptSite { virtual HRESULT _stdcall OnEnterScript(); virtual HRESULT _stdcall OnLeaveScript(); //. . . }; |
Теперь нужно создать экземпляр нашего класса CScriptHost и передать обьекту «engine» его адрес:
CScriptHost host; engine.SetScriptSite(&host); |
Совершенно очевидно, что метод SetScriptSite примерно такой:
HRESULT SetScriptSite(IActiveScriptSite *psite) { m_pActiveScriptSite = psite; psite->AddRef(); } |
Теперь внутри реализации самого ActiveX Scripting возможны вызовы методов через указатель m_pActiveScriptSite на основе механизмов виртуальности и преобразования типов:
m_pActiveScriptSite->OnLeaveScript(); //Реально был вызван CScriptHost:: OnLeaveScript(); |
Отметим, что все методы, обьявленные в IActiveScriptSite, являются чисто виртуальными:
virtual HRESULT STDMETHODCALLTYPE OnEnterScript( void) = 0; |
Поэтому придется переопределять их все, иначе нельзя инстанциировать объект, содержащий чисто виртуальные функции. Вдобавок придется позаботится о методах класса IUnknown, от которого унаследован сам IActiveScriptSite (они тоже чисто виртуальные). В результате появляются функции, особо ничего не делающие, например:
HRESULT _stdcall CScriptHost::GetDocVersionString(BSTR* pbstrVersionString) { return E_NOTIMPL ; } HRESULT _stdcall CScriptHost::OnScriptTerminate(const VARIANT* pvarResult, const EXCEPINFO* pexcepinfo) { return S_OK; // successful } |
Передача объекта
Вспомним описанный ранее объект, порожденный от CCmdTarget и служащий, напомню, для позднего связывания: class CCodeObject : public CCmdTarget { \\. . . |
Пришло его время.
Один из методов интерфейса IActiveScriptSite имеет следующий прототип:
HRESULT _stdcall CScriptHost::GetItemInfo(LPCOLESTR pstrName, DWORD dwReturnMask, IUnknown** ppunkItem, ITypeInfo** ppTypeInfo); |
Во время исполнения скрипта метод GetItemInfo будет вызван с определенными параметрами, говорящими о том, что в ответ нужно вернуть указатель на интерфейс IUnknown*. Это как раз и есть тот момент, когда для дальнейшего исполнения скрипта понадобился экземпляр объекта CCodeObject – например, чтобы «поискать» там какую-нибудь переменную, имя которой использовано в скрипте.
К этому моменту в каком-нибудь модуле трансляции уже существует экземпляр класса CCodeObjecе. Например, обьявленный как глобальный – сейчас стиль программирования не особо важен, главное – проиллюстрировать суть происходящего. Итак, где-то объявлен и находится в зоне видимости:
CCodeObject codeobj; |
Теперь в реализации CScriptHost::GetItemInfo() происходит следующее:
HRESULT _stdcall CScriptHost::GetItemInfo(LPCOLESTR pstrName, DWORD dwReturnMask, IUnknown** ppunkItem, ITypeInfo** ppTypeInfo) { // . . . *ppunkItem = codeobj.GetIDispatch(TRUE); // . . . } |
Заметим важный ньюанс – следующая строка НЕПРАВИЛЬНАЯ:
*ppunkItem = (IUnknown*)&codeobj; // так нельзя!!! |
Компилятор проглотит, но, хотя наш обьект и унаследован от CCmdTarget, сам класс CCmdTarget не унаследован от IUnknown.
Ранее мы создавали CScriptHost, унаследованный от IActiveScriptSite, а сам IActiveScriptSite был унаследован от IUnknown. Это действительно допускает преобразование CScriptHost к IUnknown.
Но в случае с классом CCodeObject, порожденным от CCmdTarget, преобразование к типу IUnknown невозможно.
Класс CCmdTarget может вернуть указатель на интерфейс IUnknown (или интефейс IDispatch, действительно порожденный от IUnknown). Но делается это путем вызова
IUnknown* punk = CCmdTarget::GetInterface(&IID_IUnknown); |
или
IUnknown* punk = CCmdTarget::GetIDispatch(TRUE); |
В основу положен другой механизм. В очень грубом приближении, в классе CCmdTarget объявлен член класса, имеющий тип IDispatch, а метод GetIDispatch возвращает его адрес:
class CCmdTarget: { // . . . IDispatch m_xxIDispatch; IUnknown* GetIDispatch(BOOL bAddRef) { if ( bAddRef ) m_xxIDispatch.AddRef(); return &m_xxIDispatch; } }; |
На самом деле всё несколько сложнее – применена некоторая арифметика указателей и смещений. Проиллюстрируем это на примере:
class IClassA { }; class ClassB { int dummy1, dummy2, dummy3; IClassA m_xxIClassA; static int m_offs; public: IClassA* GetIClassA() { IClassA* pia = (IClassA*)((BYTE*)this + m_offs); return pia; } }; int ClassB::m_offs = (size_t)&(((ClassB *)0)->m_xxIClassA); int main(int argc, char* argv[]) { ClassB b; IClassA* pA = b.GetIClassA(); return 0; } |
Не будем углубляться дальше. Продолжим работу над главной задачей – запуском скрипта. Осталось совсем немного.
Собираем всё вместе
С учетом всего описанного ранее, получим последний фрагмент кода – собственно запуск скрипта. В приведенном ниже фрагменте для экономии места я убрал всяческую проверку возвращаемых значений на предмет ошибки. // Не забываем инициализировать COM библиотеку (сколько раз я наступал на эти грабли!!!) CoInitializeEx(NULL, COINIT_MULTITHREADED); // А это наш скрипт. CString script = "k = 10 \r\n" "dim x \r\n" "dim y \r\n" "x = 10 \r\n" "y = 20 \r\n" "VALUE = GetMax(x, y)\r\n" "PrintValue(VALUE)\r\n" "Message \"Hello, Script!\" \r\n"; // Об этих объектах уже столько сказано: CCodeObject codeobj; CScriptHost host; // Глобальное имя объекта скрипта. Это и есть та строка, которая // (помните?) передается потом как параметр при вызове // CScriptHost::GetItemInfo(LPCOLESTR pstrName, ... ) LPCOLESTR pstrObjectName = L"CodeObject"; // Передадим наш codeobj, вернее, его IDispatch интерфейс. // обьект host его запомнит, а потом, когда будет нужно, передаст в // при вызове CScriptHost::GetItemInfo(), как говорилось ранее. host.AssociateNamedObject( codeobj.GetIDispatch(TRUE), pstrObjectName ); // Создаем (инстанциируем) обьекты ActiveX Scripting Engine для // VBScript. При желании, можно заменить L"VBScript" на L"JScript" для // JavaScript. В документации Microsoft указано, что возможно // использовать также Perl и Lisp, но я не пробовал. IActiveScript *pASEngine = NULL; IActiveScriptParse *pISParser = NULL; CLSID EngineClsid; CLSIDFromProgID(L"VBScript", &EngineClsid); CoCreateInstance(EngineClsid, NULL, CLSCTX_INPROC_SERVER, IID_IActiveScript, (void **)&pASEngine); pASEngine->QueryInterface(IID_IActiveScriptParse, (void**)&pISParser); // Передаем наш хост-объект – как уже говорилось, engine будет вызывать // его методы. pASEngine->SetScriptSite(&host); // Здесь важен флаг видимости имен SCRIPTITEM_GLOBALMEMBERS. // Если этот флаг не указывать, нужно потом вызывать ParseScriptText с // обязательным pstrObjectName в качестве второго параметра: // pISParser->ParseScriptText( pParseText, // pstrObjectName, // NULL, NULL, 0, 0, 0, NULL, &ei); pASEngine->AddNamedItem( pstrObjectName, SCRIPTITEM_ISVISIBLE | SCRIPTITEM_ISSOURCE | SCRIPTITEM_GLOBALMEMBERS); pISParser->InitNew(); // Ну что ж, передаем скрипт для парсинга. EXCEPINFO ei = {0}; BSTR pParseText = script.AllocSysString(); pISParser->ParseScriptText(pParseText, NULL, NULL, NULL, 0, 0, 0, NULL, &ei); SysFreeString(pParseText); // Всё, пора. Запускаем скрипт на выполнение!!! pASEngine->SetScriptState(SCRIPTSTATE_CONNECTED); // Ну вот и всё... pASEngine->Close(); pISParser->Release(); pASEngine->Release(); |