Тема: Re: Програмування С++ Вт Квіт 01, 2008 9:04 pm
ActiveX Scripting Engines: Интерпретация внешнего скрипта в С++
Иногда очень хочется добавить в программу возможность интерпретации внешнего скрипта. Одна из сравнительно простых и мощных возможностей – использовать ActiveX Scripting Engines и использовать VBScript или JavaScript. На первый взгляд, для этого требуются глубокие знания OLE COM технологии. Имеющиеся на сайте Microsoft примеры могут отпугнуть чем-нибудь совсем непонятным, например, объявлением METHOD_PROLOGUE и последующим использованием непонятно откуда взявшегося указателя pThis. Meжду тем, реализовать поддержку ActiveScript совсем несложно. Глубоко понять внутренние скрытые механизмы труднее, но это и не нужно – цель совершенно другая: внедрить поддержку скрипта, не вдаваясь в тонкости. Для этого используем то, что уже реализовано, а именно MFC.
Полные файлы примера находятся в прикреплнном архиве. Здесь в описании приводятся только фрагменты для иллюстрации определенных принципов. Начинаем работать.
Скрипт должен взаимодействовать с нашей С++ программой – использовать и изменять значения переменных, объявленных в С++ части программы, или вызывать функции. Скрипт наподобии такого: PHP код: dim k k = 1 никому не нужен – его исполнение ничего не дает. Нужно, чтобы переменная «k» была обьявлена не в пространстве имен самого скрипта как «dim k», а где-нибудь в C++ модуле как, например, long k, и после выполнения VBScript строки «k = 1» её значение стало равным 1.
Для реализации такой возможности используется класс, порожденный от базового MFC класса CCmdTarget. Этот класс обеспечивает механизм позднего связывания (late binding). Если не вдаваться в детали, всё довольно просто: есть таблица указателей на переменные и функции, а также строковые имена. Доступ к переменной или вызов метода осуществляется поиском соответствующего строкового идентификатора. Если в скрипте есть строка «k = 1», и существует некая long val, то в таблице есть что-то навроде:
PHP код: for (UINT nIndex = 0; nIndex < nEntryCount; nIndex++) if ( table[nIndex].name == "k" ) { *table[nIndex].pval = 1; break; } Разумеется, приведенная выше модель очень грубая и только иллюстрирует общий принцип. На практике задача гораздо сложнее: вызов функций, передача параметров, возвращаемые значения, контроль типов и так далее.
Итак, создадим класс, порожденный от CCmdTarget.
PHP код: #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);
DECLARE_DISPATCH_MAP() }; Самое главное – это объявленные для диспетчеризации: PHP код: long m_nValue; long GetMax(long, long); void PrintValue(long); void Message(LPCSTR);
Именно они используются из скрипта. Для каждого из них зарезервирован числовой идентификатор в перечислении (enum):
Макрос 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().
Гість Гість
Тема: Re: Програмування С++ Вт Квіт 01, 2008 9:05 pm
оздаем механизмы ActiveX Scripting
Забежим немного вперед. Предположим, у нас уже есть инстанциированный объект Microsoft ActiveX Scripting. Для простоты, условно обьявим его так:
PHP код: СActiveXScriptEngine engine; Теперь мы можем вызывать его методы, например:
PHP код: engine.InitNew(); Но сам обьект «engine» не может взаимодействовать с нашей программой – он ничего о ней не знает. Реализовать механизм обратной связи можно было по-разному. Например, передать объекту «engine» указатели на функции (callback function).
Механизм обратной связи ActiveX Scripting построен на основе специального интерфейса (класса) IActiveScriptSite. Грубо говоря, существует объявленный базовый интерфейс (класс) IActiveScriptSite, содержащий набор заранее определенных виртуальных функций. Необходимо создать класс, унаследованный от IActiveScriptSite, и перегрузить его виртуальные функции:
PHP код: class CScriptHost : public IActiveScriptSite {
virtual HRESULT _stdcall OnEnterScript(); virtual HRESULT _stdcall OnLeaveScript(); //. . . }; Теперь нужно создать экземпляр нашего класса CScriptHost и передать обьекту «engine» его адрес:
PHP код: CScriptHost host; engine.SetScriptSite(&host); Совершенно очевидно, что метод SetScriptSite примерно такой:
PHP код: HRESULT SetScriptSite(IActiveScriptSite *psite) { m_pActiveScriptSite = psite; psite->AddRef(); } Теперь внутри реализации самого ActiveX Scripting возможны вызовы методов через указатель m_pActiveScriptSite на основе механизмов виртуальности и преобразования типов:
PHP код: m_pActiveScriptSite->OnLeaveScript(); //Реально был вызван CScriptHost:: OnLeaveScript(); Отметим, что все методы, обьявленные в IActiveScriptSite, являются чисто виртуальными:
PHP код: virtual HRESULT STDMETHODCALLTYPE OnEnterScript( void) = 0; Поэтому придется переопределять их все, иначе нельзя инстанциировать объект, содержащий чисто виртуальные функции. Вдобавок придется позаботится о методах класса IUnknown, от которого унаследован сам IActiveScriptSite (они тоже чисто виртуальные). В результате появляются функции, особо ничего не делающие, например:
Тема: Re: Програмування С++ Вт Квіт 01, 2008 9:05 pm
Передача объекта
Вспомним описанный ранее объект, порожденный от CCmdTarget и служащий, напомню, для позднего связывания: PHP код: class CCodeObject : public CCmdTarget { . . . Пришло его время.
Один из методов интерфейса IActiveScriptSite имеет следующий прототип:
PHP код: HRESULT _stdcall CScriptHost::GetItemInfo(LPCOLESTR pstrName, DWORD dwReturnMask, IUnknown** ppunkItem, ITypeInfo** ppTypeInfo); Во время исполнения скрипта метод GetItemInfo будет вызван с определенными параметрами, говорящими о том, что в ответ нужно вернуть указатель на интерфейс IUnknown*. Это как раз и есть тот момент, когда для дальнейшего исполнения скрипта понадобился экземпляр объекта CCodeObject – например, чтобы «поискать» там какую-нибудь переменную, имя которой использовано в скрипте. К этому моменту в каком-нибудь модуле трансляции уже существует экземпляр класса CCodeObjecе. Например, обьявленный как глобальный – сейчас стиль программирования не особо важен, главное – проиллюстрировать суть происходящего. Итак, где-то объявлен и находится в зоне видимости:
PHP код: CCodeObject codeobj; Теперь в реализации CScriptHost::GetItemInfo() происходит следующее:
*ppunkItem = codeobj.GetIDispatch(TRUE); // . . . } Заметим важный ньюанс – следующая строка НЕПРАВИЛЬНАЯ: PHP код: *ppunkItem = (IUnknown*)&codeobj; // так нельзя!!! Компилятор проглотит, но, хотя наш обьект и унаследован от CCmdTarget, сам класс CCmdTarget не унаследован от IUnknown. Ранее мы создавали CScriptHost, унаследованный от IActiveScriptSite, а сам IActiveScriptSite был унаследован от IUnknown. Это действительно допускает преобразование CScriptHost к IUnknown. Но в случае с классом CCodeObject, порожденным от CCmdTarget, преобразование к типу IUnknown невозможно. Класс CCmdTarget может вернуть указатель на интерфейс IUnknown (или интефейс IDispatch, действительно порожденный от IUnknown). Но делается это путем вызова
PHP код: IUnknown* punk = CCmdTarget::GetInterface(&IID_IUnknown); или
PHP код: IUnknown* punk = CCmdTarget::GetIDispatch(TRUE); В основу положен другой механизм. В очень грубом приближении, в классе CCmdTarget объявлен член класса, имеющий тип IDispatch, а метод GetIDispatch возвращает его адрес:
IUnknown* GetIDispatch(BOOL bAddRef) { if ( bAddRef ) m_xxIDispatch.AddRef(); return &m_xxIDispatch; } }; На самом деле всё несколько сложнее – применена некоторая арифметика указателей и смещений. Проиллюстрируем это на примере:
int ClassB::m_offs = (size_t)&(((ClassB *)0)->m_xxIClassA);
int main(int argc, char* argv[]) {
ClassB b; IClassA* pA = b.GetIClassA();
return 0; } Не будем углубляться дальше. Продолжим работу над главной задачей – запуском скрипта. Осталось совсем немного.
Гість Гість
Тема: Re: Програмування С++ Вт Квіт 01, 2008 9:06 pm
Собираем всё вместе
С учетом всего описанного ранее, получим последний фрагмент кода – собственно запуск скрипта. В приведенном ниже фрагменте для экономии места была убрана всяческая проверка возвращаемых значений на предмет ошибки.
PHP код: // Не забываем инициализировать 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, но я не пробовал.
// Передаем наш хост-объект – как уже говорилось, engine будет вызывать // его методы. pASEngine->SetScriptSite(&host);
// Здесь важен флаг видимости имен SCRIPTITEM_GLOBALMEMBERS. // Если этот флаг не указывать, нужно потом вызывать ParseScriptText с // обязательным pstrObjectName в качестве второго параметра: // pISParser->ParseScriptText( pParseText, // pstrObjectName, // NULL, NULL, 0, 0, 0, NULL, &ei);