ActiveX Scripting Engines – це просто! Інтерпретація зовнішнього скрипта в С + + (исходники), Різне, Програмування, статті

Вступ


Іноді дуже хочеться додати в програму можливість інтерпретації зовнішнього скрипта. Одна з порівняно простих і потужних можливостей – використовувати ActiveX Scripting Engines і використовувати VBScript або JavaScript.

На перший погляд, для цього потрібні глибокі знання OLE COM технології. Наявні на сайті Microsoft приклади можуть відлякати чимось зовсім незрозумілим, наприклад, оголошенням METHOD_PROLOGUE і подальшим використанням незрозуміло звідки взявся покажчика pThis.

Meжду тим, реалізувати підтримку ActiveScript зовсім нескладно. Глибоко зрозуміти внутрішні приховані механізми важче, але це і не потрібно – мета зовсім інша: впровадити підтримку скрипта, не вдаючись у тонкощі. Для цього використовуємо те, що вже реалізовано, а саме MFC.

Повні файли прикладу знаходяться в прікреплнном архіві. Тут в описі наводяться тільки фрагменти для ілюстрації певних принципів.

Починаємо працювати


Скрипт повинен взаємодіяти з нашою С + + програмою – використовувати і змінювати значення змінних, оголошених в С + + частини програми, або викликати функції. Скрипт зразок такого:






   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) або фінкціі:



Відзначимо важливу річ: в конструкторі класу обов’язково повинен бути присутнім виклик методу 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

“dim x

“dim y

“x = 10

“y = 20

“VALUE = GetMax(x, y)

“PrintValue(VALUE)

“Message “Hello, Script!”
“;
/ / Про цих об’єктах вже стільки сказано:
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();

Висновок


Ось власне і все. Сподіваюся, що описаний вище допоможе підключити підтримку скриптів.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*