Начало

Введение

Большая часть манипуляций со скриптовой системой (посредством API s4g) сделана на стеке исполнения виртуальной машины. Однако стек в s4g не следует классическому правилу стеков (последний пришел, первый ушел), и может предоставить доступ к любому элементу, положительный индекс означает абсолютную позицию в стеке, отрицательный – относительно вершины стека (-1 вершина стека, -2 на одну позицию ниже вершины и так далее), однако вставка/выталкивание возможно только по классическому принципу.

Начало работы

Для начала работы необходимо скачать дистрибутив с исходным кодом скриптового языка s4g https://s4g.su/download.html.

Проект s4g для vs 2013 предназначен для компиляции в dll.

Для включения dll в программу необходимо подключить файл s4g.h (а также lib файл dll), также необходимо в настройках проекта программы объявить два дефайна:

#define S4G_BUILD_LIB
#define S4G_EXE

подробное их описание находится в файле s4g.h

Далее необходимо объявить о наличии скриптовой системы s4g:

s4g_Main* s4gm = 0;

Затем инициализировать, указав произвольное имя:

s4gm = s4g_MainInit("");

Загрузить код из файла:

s4g_LoadCode(s4gm, "D:/test.script");

Либо загрузить строку с кодом:

char* exestr = "print(\”Hello world!\”);";
s4g_LoadCode(s4gm, exestr, false);

Исполнение кода

Загрузка скрипта не приводит к его исполнению! Во время исполнения тела (кода) скрипта будут создаваться необходимые переменные и данные (таблицы, функции и прочее), и после этого виртуальная машина уже будет знать всю информацию о том, что находится в скрипте, и будет готова выполнить любую известную ей функцию. Поэтому важно помнить, что первоначально необходимо выполнить тело скрипта и только потом вызывать функции.

Исполнить тело скрипта:

s4g_Call(s4gm);

Вызов функции (в данном случае будет рассматриваться код вызова языковой функции print, однако для других типов функций алгоритм одинаков):

s4g_Precall(s4gm);
s4g_StackGet2Top(s4gm, S4G_NM_SYS, "print");
s4g_StackPush_Str(s4gm, "Hello world!");
s4g_Call(s4gm,true);
s4g_StackPop(s4gm,1);

Алгоритм вызова функций прост, для начала необходимо сообщить виртуальной машине о том, что будет вызвана функция, какая не важно. Для этого есть функция s4g_Precall, после сего необходимо положить саму функцию на вершину стека (воспользовавшись функцией s4g_StackGet2Top, которой можно осуществлять поиск данных на любых доступных уровнях). Затем поочередно положить все аргументы функции. После этого совершить вызов, указав вторым аргументом true.

Важно! Вызванная функция обязательно возвращает значение, если скриптом явно не указано что это за значение тогда виртуальная машина сама вставит значение null. Поэтому после отработки функции необходимо вытолкнуть один элемент из стека.

Сборка мусора

Сборщик мусора необходимо вызывать самостоятельно и также самостоятельно выбирать периоды его вызова.

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

Следует помнить о том, что каждый вызов работы виртуальной машины и как возможное следствие – исполнение кода, может создавать новые данные, а старые данные, которые уже недействительны, но все же занимают место в памяти еще не удалены, и не будут тронуты до тех пор, пока их не удалит сборщик мусора, который вызывается хост программой самостоятельно.

Возможно, вызов сборщика мусора каждый раз после исполнения кода скрипта будет не рациональным (опять же смотря какие цели использования, какова критичность ко времени исполнения, и каков объем данных, который будет создаваться). Но вызов через N исполнений кода скрипта может вполне подойти, остается определить это количество N.

Однако API s4g позволяет узнать в текущий момент времени, занимаемый объем памяти в байтах посредством функции:

int s4g_GCgetMemBusy(s4g_main* s4gm);

Таким образом, опираясь на эти данные можно написать примерно следующий код:

#define MAX_MEM_FOR_CLEAR 1024 * 1024 * 10 /*10 мегабайт */

//...
    if(s4g_GCgetMemBusy(s4gm) > MAX_MEM_FOR_CLEAR){
        s4g_GCcall(s4gm);
    }

То есть некий предел памяти при превышении, которого необходимо вызывать сборщик мусора. В данном примере используется предел в 10 мегабайт.

Экспорт функций и переменных

Для более подробного рассмотрения вопроса об экспорте из хост программы на сторону скриптов смотрите файл s4g_lib_std.h который осуществляет экспорт необходимых языковых функций и данных.

Все экспортируемые функции из хост программы (через стандартное API s4g), должны иметь тип который описан в файле s4g.h:

typedef int(*s4g_c_function)(s4g_main* vm);

Важно! Каждая экспортируемая функция, в случае ошибки должна возвращать S4G_ERROR, а в случае успеха S4G_OK.

Пример экспорта из s4g_lib_std.h:

int s4g_LibStd_Print(s4g_Main* s4gm)
{
    int countarg = s4g_Cfunc_CountArg(s4gm);

    S4G_STDLIB_COND_ARG(s4gm, countarg, 1);

    if (s4g_CfuncIs_ArgStr(s4gm, 1))
    {
        const char* str = s4g_CfuncGet_ArgStr(s4gm, 1);
        printf("%s", s4g_CfuncGet_ArgStr(s4gm, 1));
    }
    else if (s4g_CfuncIs_ArgInt(s4gm, 1))
    {
        printf("%d", s4g_CfuncGet_ArgInt(s4gm, 1));
    }
    else if (s4g_CfuncIs_ArgUint(s4gm, 1))
    {
        printf("%u", s4g_CfuncGet_ArgUint(s4gm, 1));
    }
    else if (s4g_CfuncIs_ArgFloat(s4gm, 1))
    {
        printf("%f", s4g_CfuncGet_ArgFloat(s4gm, 1));
    }
    else if (s4g_CfuncIs_ArgBool(s4gm, 1))
    {
        printf("%s", s4g_CfuncGet_ArgBool(s4gm, 1) ? "true" : "false");
    }
    else
    {
        s4g_GenMsg(s4gm, S4G_MSG_LEVEL_ERROR, "[%s]:%d function '%s' expected arg #1 string, but got '%s'", s4g_Dbg_GetCurrFile(s4gm), s4g_Dbg_GetCurrStr(s4gm), s4g_Dbg_GetCurrFunc(s4gm), s4g_CfuncGet_TypeStr(s4gm, 1));
        return S4G_ERROR;
    }

    return S4G_OK;
}

//...

s4g_StackPush_Cfunc(s4gm, s4g_LibStd_Print);
s4g_StackStore(s4gm, S4G_NM_SYS, "print");

Для работы внутри вызываемой C(++) функции со скриптовой системой можно воспользоваться API s4g.

А теперь рассмотрим подробнее.

Строка:

s4g_StackPush_Cfunc(s4gm, s4g_LibStd_Print);

кладет на вершину стека функцию s4g_LibStd_Print, а затем строка:

s4g_StackStore(s4gm, S4G_NM_SYS, "print");

создает переменную (поле в таблице) с именем print в языковом пространстве имен, на это указывает второй аргумент функции S4G_NM_SYS, и присваивает этой переменной значение s4g_LibStd_Print.

Не трудно догадаться, что таким же образом, но с другим индексом (вторым параметром функции s4g_StackStore) можно экспортировать данную функцию из хост программы в любое доступное пространство имен в скриптах.

Так, указывая индекс S4G_NM_GLOBAL, сохраненяем в глобальном пространстве имен.

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

s4g_StackPush_TableEmpty(s4gm, 8);
s4g_StackStore(s4gm, S4G_NM_GLOBAL, "table");
s4g_StackGet2Top(s4gm, S4G_NM_GLOBAL, "table");
int table_pos = s4g_sgettop(s4gm)-1;
s4g_StackPush_Cfunc(s4gm, s4g_LibStd_Print);
s4g_StackStore(s4gm, table_pos, "print");

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

Экспорт классов

Экспорт классов аналогичен экспорту функций за исключением некоторых моментов.

На данный момент для экспорта класса необходимо использовать низкоуровневые API функции, поэтому каждый метод экспортируемого класса необходимо описать s4g_c_function функцией, то есть надо сделать обертки.

Для примера создадим тестовый класс:

class TestClass
{
public:
    TestClass(){}
    ~TestClass(){}
    void SetStr(const char *szStr){ sStr = szStr;}
    const char* GetStr(){ return sStr.c_str();}

    String sStr;
};

В итоге необходимо описать 2 метода класса, а также для каждого экспортируемого класса необходимо наличие конструктора и деструктора!

Конструктор:

int TestClass_Constructor(s4g_Main *s4gm)
{
    if(s4g_CfuncIs_ArgClassObject(s4gm, 1))
    {
        TestClass *pTC = new TestClass();
        s4g_ClassObjectSet_Data(s4g_CfuncGet_Arg(s4gm, 1), pTC);
        return S4G_OK;
    }

    return S4G_ERROR;
}

Первым аргументом в каждом методе и конструкторе/деструкторе идет объект this (на стороне скриптов объект «объект класса» создается до вызова конструктора на стороне C++), которые есть созданный объект класса на стороне скриптовой системы.

Для того чтобы записать пользовательские данные (в данном случае это указатель на объект экспортируемого класса), необходимо объекту класса скриптовой системы передать указатель на данные, посредством s4g_ClassObjectSet_Data(s4g_CfuncGet_Arg(s4gm, 1), pTC); после чего объект класса СС будет иметь объект C++, который может быть доступен из любого оборачиваемого метода C++ объекта.

Деструктор:

int TestClass_Destructor(s4g_Main *s4gm)
{
    if(s4g_CfuncIs_ArgClassObject(s4gm, 1))
    {
        TestClass *pTC = (TestClass*)s4g_ClassObjectGet_Data(s4g_CfuncGet_Arg(s4gm, 1));
        mem_delete(pTC);
        return S4G_OK;
    }

    return S4G_ERROR;
}

В деструкторе, получая this, извлекаем из него пользовательские данные которые содержат указатель на объект C++ экспортируемого класса, и удаляем этот объект.

Метод SetStr:

int TestClass_SetStr(s4g_Main *s4gm)
{
    if(s4g_CfuncIs_ArgClassObject(s4gm, 1) && s4g_CfuncIs_ArgStr(s4gm, 2))
    {
        TestClass *pTC = (TestClass*)s4g_ClassObjectGet_Data(s4g_CfuncGet_Arg(s4gm, 1));
        pTC->SetStr(s4g_CfuncGet_ArgStr(s4gm, 2));
        return S4G_OK;
    }

    return S4G_ERROR;
}

Метод GetStr:

int TestClass_GetStr(s4g_Main *s4gm)
{
    if(s4g_CfuncIs_ArgClassObject(s4gm, 1))
    {
        TestClass *pTC = (TestClass*)s4g_ClassObjectGet_Data(s4g_CfuncGet_Arg(s4gm, 1));
        s4g_StackPush_Str(s4gm, pTC->GetStr());
        return S4G_OK;
    }

    return S4G_ERROR;
}

Во всех методах необходимо получить указатель на пользовательские данные, которые содержат C++ указатель на экспортируемый класс, и вызывать соответствующий метод у этого объекта.

Для экспорта класса необходимо: - создать класс на стеке - положить в этот класс поочередно обернутые методы - сохранить

Пример (на основании C++ класса выше):

s4g_StackPush_Class(s4gm);
s4g_StackPush_ClassMethod(s4gm, "__constructor", TestClass_Constructor);
s4g_StackPush_ClassMethod(s4gm, "__destructor", TestClass_Destructor);
s4g_StackPush_ClassMethod(s4gm, "SetStr", TestClass_SetStr);
s4g_StackPush_ClassMethod(s4gm, "GetStr", TestClass_GetStr);
s4g_StackStore(s4gm, S4G_NM_SYS, "TestClass");

Обработка ошибок

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

При возникновении ошибки на любом этапе, скриптовая система блокирует дальнейшую работу с ней через API, и при обращении к функциям будет выдавать сообщение с ошибкой, однако блокируются не все функции. Функции получения со стека и определения типов буду работать (сделано для возможности детального анализа ошибки), остальные (которые могут влиять на стек) будут недоступны.

Для трассировки стека вызовов можно воспользоваться функцией s4g_StackTrace.

Узнать текущее состояние скриптовой системы можно при помощи функции s4g_MainGetState.

Чтобы получить текст ошибки используйте s4g_MainGetErrorStr.

Для сброса ошибки можно воспользоваться функцией s4g_MainClear(s4gm) которая сбросит все предыдущие загрузки и полностью очистит скриптовую систему.

Очистка (для перезагрузки) и завершение работы

В случае если есть необходимость загрузки нового скрипта (либо перезагрузки), то перед новой загрузкой в уже существующую скриптовую систему, ее необходимо очистить от данных прошлого скрипта:

s4g_MainClear(s4gm);

И после очистки можно работать с данной системой как с только что созданной.

Для удаления скриптововй системы необходимо вызвать:

s4g_MainKill(s4gm);

Заключение

Так как большая часть взаимодействия между хост программой и скриптовой системой построена на основе стека, то программисту хост программы необходимо следить за размером стека. Так как неправильная работа может привести к переполнению стека и прекращению работы, либо к неправильной работе виртуальной машины.

Для того чтобы узнать текущий размер стека можно воспользоваться функцией:

int s4g_StackGetTop(s4g_main* s4gm);

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

int pos = s4g_StackGetTop(s4gm)-1;