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) или финкцию:
- объявить соответствующий член класса;
- добавить в enum новый id;
- добавить макрос где-нибудь между BEGIN_DISPATCH_MAP и END_DISPATCH_MAP.

Отметим важную вещь: в конструкторе класса обязательно должен присутствовать вызов метода EnableAutomation().