Table of Contents
- Overview
- Create Script VM
- Print Hello world
- Export native code into squirrel script
- Importing squirrel script into native code
- Conclusions
Scripts are gave you always fast loopback between your are changing code and getting results.
This quite useful for rapid development and you application customization. All sources for this article you will find at https://github.com/baldale/squirrel-binding
Overview
There are many scripting languages already exits and some of them are widely used in GameDev, like:
- Lua (https://www.lua.org/)
- Python (https://www.python.org)
- Squirrel Script (http://www.squirrel-lang.org/)
- Angel Script (https://www.angelcode.com/angelscript/)
They all have their own advantages/disadvantages, but personally I’m choose squirrel language. It is compact, easy to integrate and it pretty fast. And it using in GameDev by Valve for example (wikipedia)
Source code of this scripting language is hosted on GitHub: https://github.com/albertodemichelis/squirrel
There are some binding libraries already exists to simplify integration of C++ code with squirrel scripting VM:
- sqall (github)
- sqrat (github)
- sqplus(sourceforge)
But I personally prefer to generate bindings with on my own, since it give me much more control over object lifecycle. Because some of binding objects are controlled by my C++ code and some with squirrel script.
Squirrel is stack based object oriented language. Internally it using vm similar to Lua, but has more readable syntax.
Create Script VM
Squirrel is tack based script vm. So to store all data to call some methods, function you need a stack which is part of vm instance. It shouldn’t be big, since it is required only to store call and call parameters. At initial stage 100 will be ok:
// This is stack size
int stack_size = 100;
// Here we are creating instance and storing it into vm variable
HSQUIRRELVM vm = sq_open(stack_size);
Now we need to define 3 functions to setup it as callbacks for printing, compilation error notifications and error handler:
void print_function(HSQUIRRELVM vm, const SQChar* message, ...)
{
char buffer[1024];
va_list marker;
va_start(marker, message);
scvsprintf(buffer, 1024, message, marker);
va_end(marker);
printf("<info> %s\n", buffer);
}
void error_function(HSQUIRRELVM vm, const SQChar* message, ...)
{
char buffer[1024];
va_list marker;
va_start(marker, message);
scvsprintf(buffer, 1024, message, marker);
va_end(marker);
printf("<error> %s\n", buffer);
}
void compilier_error_function(HSQUIRRELVM vm, const SQChar *error_message,
const SQChar *source, SQInteger line_number,
SQInteger column_number)
{
printf("<compilation_error> Error %s at %s line %lld column %lld\n",
error_message, source, line_number, column_number);
}
SQInteger on_error_function(HSQUIRRELVM vm)
{
if (sq_gettop(vm) >= 1)
{
const SQChar *error_message = 0;
if (SQ_SUCCEEDED(sq_getstring(vm, 2, &error_message)))
{
printf("on_error: %s\n", error_message);
}
}
return SQ_OK;
}
And now let’s register them in the script vm:
// Push root table
sq_pushroottable(vm);
// Setup callbacks
sq_setprintfunc(vm, &print_function, &error_function);
sq_setcompilererrorhandler(vm, &compilier_error_function);
sq_newclosure(vm, &on_error_function, 0);
sq_seterrorhandler(vm);
// Pop root table
sq_pop(vm, 1);
And now we can register standard libraries:
// Register libraries
sqstd_register_iolib(vm);
sqstd_register_mathlib(vm);
sqstd_register_stringlib(vm);
sqstd_register_systemlib(vm);
And now we have empty stack and ready to use script vm
Print Hello world
Now we can do default first example – print Hello, World!!
To do this we need execute compile code:
void execute_script(HSQUIRRELVM vm, const char* source)
{
// Save vm stack size
int __top__ = sq_gettop(vm);
printf("Compiling script...\n");
// First let's compile source code
if (SQ_SUCCEEDED(sq_compilebuffer(vm, source, strlen(source), "inline",
SQTrue /* Raise compilation error if any*/)))
{
// Now on the top of stack we have squirrel closure
printf("Script compiled...\n");
// Extract compile closure
HSQOBJECT compiled_clsoure;
sq_getstackobj(vm, -1, &compiled_clsoure);
sq_addref(vm, &compiled_clsoure);
sq_pop(vm, -1);
// Push root table
sq_pushroottable(vm);
// Call this closure just compiled without returning value into stack
if (SQ_SUCCEEDED(sq_call(vm, 1, SQFalse /* Return value */,
SQTrue /* Raise Error */)))
{
printf("Script executed...\n");
}
else
{
printf("Script failed...\n");
}
sq_release(vm, &compiled_clsoure);
}
else
{
printf("Unable to compile script: %s\n", source);
}
sq_settop(vm, __top__);
}
Now to execute script we need to call:
execute_script(vm, "print(\"Hello, World!\")");
Export native code into squirrel script
First use case of any scripting engine usage is calling native code from script. Such binding called “exports”. It allows to use fast native code rather than to use slow scripting methods. Or you need to interact with underlying game engine. All such stuffs required calls of C++ code. And squirrel scripting engine supports exports of native closure into script.
Export native code to script function
For example we need function that formats date according to the given format. In script we are expecting something like this:
function format_date(date, "yyyy.mm.dd");
First we need to defined this function in our code to format date. All functions which calling from scripts should have signature like in example below. If function put return valu into stack it should return 1, otherwise 0:
SQInteger vm_call_format_date(HSQUIRRELVM vm)
{
// Get number of arguments. __top__ will be number of arguments + 1, since
// first element at the stack will be function object
int __top__ = sq_gettop(vm);
// Check that function got 2 elements on the stack
if (__top__ == 3)
{
// If first argument int and second string
if ( sq_gettype(vm, 2) == OT_INTEGER && sq_gettype(vm, 3) == OT_STRING)
{
// Get first argument stored at index 2
SQInteger date;
sq_getinteger(vm, 2, &date);
// Get second argument stored at index 3
const SQChar* format;
sq_getstring(vm, 3, &format);
// Do some formatting
char msg[60];
tm _time;
_time.tm_year = (date / 10000) % 10000 - 1900;
_time.tm_mon = (date / 100) % 100 - 1;
_time.tm_mday = date % 100;
strftime(msg, sizeof(msg), format, &_time);
// And result at msg buffer. -1 to force squirrel to get string length
sq_pushstring(vm, msg, -1);
// Should return 1, since we just pushed return value into stack
return 1;
}
}
// Should return 0, since we not return any values to stack
return 0;
}
Now we need to register this function under name in script vm. This should be registration in root table:
// Store stack size
int __top__ = sq_gettop(vm);
// Push root table
sq_pushroottable(vm);
// Function name
sq_pushstring(vm, _SC("format_date"), -1);
// Creating new closure in root table at stack => creating function
sq_newclosure(vm, &vm_call_format_date, 0);
// Disabling parameters checks, since will check in function
sq_setparamscheck(vm, 0, NULL);
// Since it is native function, we should define it's name
sq_setnativeclosurename(vm, -1, _SC("format_date"));
// Here we are creating new slot in root table and this finally assigning name to function
if (SQ_FAILED(sq_newslot(vm, -3, SQTrue)))
{
printf("Unable to register format_date\n");
}
// Restore stack size
sq_settop(vm, __top__);
Now we may execute format date in the script:
execute_script(vm, "print(format_date(20220115,\"%m/%d/%Y\"))");
This script will print
01/15/2022
Export native class into script class
Squirrel allows to bind c++ classes into scripts. It supports property/field, constructors and method binding. So if you will export some class from native into scripts you will be able to call native code of class from you squirrel code
First of all let’s create class to export into vm:
/**
* Class that represents storage in YYYYMMDD andwhich makes from 20221001
* integer 1 November 2022
*/
class Date
{
public:
/* Date storage integer */
int date;
Date() :
date(0)
{
}
Date(int date) :
date(date)
{
}
~Date()
{
}
int get_date()
{
return this->date;
}
void set_date(int date)
{
this->date = date;
}
std::string to_string()
{
// Here we are calling predefined function from previous section
char msg[60];
format_date("YYYY/MM/DD", sizeof (msg), msg, this->date);
return msg;
}
int get_year() const
{
return (this->date / 10000) % 10000;
}
int get_month() const
{
return (this->date / 100) % 100;
}
int get_day() const
{
return (this->date) % 100;
}
};
As you can see there some methods which we need to implements before register class:
- constructor
- destructor
- get_date
- set_date
- to_string
- get_year
- get_month
- get_day
It should has default method signature for any function which calling form vm:
SQInteger vm_destructor(SQUserPointer pointer, SQInteger size)
{
Date* date = static_cast<Date*> (pointer);
if (date) delete date;
return SQ_OK;
}
SQInteger vm_constructor(HSQUIRRELVM vm)
{
// Get number of arguments. __top__ will be number of arguments + 1, since
// first element at the stack will be function object
int __top__ = sq_gettop(vm);
// Default constructor
if (__top__ == 1)
{
Date* date = new Date();
if (SQ_FAILED(sq_setinstanceup(vm, 1, date)))
{
delete date;
return sq_throwerror(vm, "Cannot create class instance 'date'");
}
// Add release hookthat this method will be called when by script vm
// To destroy instance
sq_setreleasehook(vm, 1, &vm_destructor);
}
else if (__top__ == 2)
{
// Constructor from int
SQInteger date_int;
sq_getinteger(vm, 2, &date_int);
Date* date = new Date(date_int);
if (SQ_FAILED(sq_setinstanceup(vm, 1, date)))
{
delete date;
return sq_throwerror(vm, "Cannot create class instance 'date'");
}
// Add release hookthat this method will be called when by script vm
// To destroy instance
sq_setreleasehook(vm, 1, &vm_destructor);
}
return 0;
}
SQInteger vm_get_date(HSQUIRRELVM vm)
{
int __top__ = sq_gettop(vm);
if (__top__ == 1)
{
SQUserPointer obj_ptr;
if (SQ_FAILED(sq_getinstanceup(vm, 1, &obj_ptr, NULL)))
{
return sq_throwerror(vm, "Unable to get instamce");
}
else
{
Date* obj = static_cast<Date*>(obj_ptr);
int result = obj->get_date();
sq_pushinteger(vm, result);
return 1; // Return value
}
}
return 0; // No return value
}
SQInteger vm_set_date(HSQUIRRELVM vm)
{
int __top__ = sq_gettop(vm);
if (__top__ == 2)
{
SQUserPointer obj_ptr;
if (SQ_FAILED(sq_getinstanceup(vm, 1, &obj_ptr, NULL)))
{
return sq_throwerror(vm, "Unable to get instamce");
}
else
{
Date* obj = static_cast<Date*>(obj_ptr);
SQInteger value;
sq_getinteger(vm, 2, &value);
obj->set_date(value);
return 0; // No return value
}
}
return 0; // No return value
}
SQInteger vm_get_year(HSQUIRRELVM vm)
{
int __top__ = sq_gettop(vm);
if (__top__ == 1)
{
SQUserPointer obj_ptr;
if (SQ_FAILED(sq_getinstanceup(vm, 1, &obj_ptr, NULL)))
{
return sq_throwerror(vm, "Unable to get instamce");
}
else
{
Date* obj = static_cast<Date*>(obj_ptr);
int result = obj->get_year();
sq_pushinteger(vm, result);
return 1; // Return value
}
}
return 0; // No return value
}
SQInteger vm_get_month(HSQUIRRELVM vm)
{
int __top__ = sq_gettop(vm);
if (__top__ == 1)
{
SQUserPointer obj_ptr;
if (SQ_FAILED(sq_getinstanceup(vm, 1, &obj_ptr, NULL)))
{
return sq_throwerror(vm, "Unable to get instamce");
}
else
{
Date* obj = static_cast<Date*>(obj_ptr);
int result = obj->get_month();
sq_pushinteger(vm, result);
return 1; // Return value
}
}
return 0; // No return value
}
SQInteger vm_get_day(HSQUIRRELVM vm)
{
int __top__ = sq_gettop(vm);
if (__top__ == 1)
{
SQUserPointer obj_ptr;
if (SQ_FAILED(sq_getinstanceup(vm, 1, &obj_ptr, NULL)))
{
return sq_throwerror(vm, "Unable to get instamce");
}
else
{
Date* obj = static_cast<Date*>(obj_ptr);
int result = obj->get_day();
sq_pushinteger(vm, result);
return 1; // Return value
}
}
return 0; // No return value
}
SQInteger vm_to_string(HSQUIRRELVM vm)
{
int __top__ = sq_gettop(vm);
if (__top__ == 1)
{
SQUserPointer obj_ptr;
if (SQ_FAILED(sq_getinstanceup(vm, 1, &obj_ptr, NULL)))
{
return sq_throwerror(vm, "Unable to get instamce");
}
else
{
Date* obj = static_cast<Date*>(obj_ptr);
std::string result = obj->to_string();
sq_pushstring(vm, result.c_str(), result.length());
return 1; // Return value
}
}
return 0; // No return value
}
Now we can register class within script vm:
// Store stack size
int __top__ = sq_gettop(vm);
// Push root table
sq_pushroottable(vm);
// Push class name
sq_pushstring(vm, _SC("date"), -1);
// Define class
if (SQ_FAILED(sq_newclass(vm, SQFalse /* Has no base class*/)))
{
printf("Failed to create class: date\n");
sq_settop(vm, __top__);
return;
}
HSQOBJECT cls;
sq_resetobject(&cls);
sq_getstackobj(vm, -1, &cls);
sq_addref(vm, &cls);
// Constructor
sq_pushstring(vm, _SC("constructor"), -1);
sq_newclosure(vm, &vm_constructor, 0);
sq_setparamscheck(vm, 0, NULL);
sq_setnativeclosurename(vm, -1, _SC("constructor"));
if (SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
{
printf("Failed to create contructor for date\n");
sq_settop(vm, __top__);
return;
}
// get_date
sq_pushstring(vm, _SC("get_date"), -1);
sq_newclosure(vm, &vm_get_date, 0);
sq_setparamscheck(vm, 0, NULL);
sq_setnativeclosurename(vm, -1, _SC("get_date"));
if (SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
{
printf("Failed to create 'get_date' for date\n");
sq_settop(vm, __top__);
return;
}
// set_date
sq_pushstring(vm, _SC("set_date"), -1);
sq_newclosure(vm, &vm_get_date, 0);
sq_setparamscheck(vm, 0, NULL);
sq_setnativeclosurename(vm, -1, _SC("set_date"));
if (SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
{
printf("Failed to create 'set_date' for date\n");
sq_settop(vm, __top__);
return;
}
// get_year
sq_pushstring(vm, _SC("get_year"), -1);
sq_newclosure(vm, &vm_get_year, 0);
sq_setparamscheck(vm, 0, NULL);
sq_setnativeclosurename(vm, -1, _SC("get_year"));
if (SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
{
printf("Failed to create 'get_year' for date\n");
sq_settop(vm, __top__);
return;
}
// get_month
sq_pushstring(vm, _SC("get_month"), -1);
sq_newclosure(vm, &vm_get_month, 0);
sq_setparamscheck(vm, 0, NULL);
sq_setnativeclosurename(vm, -1, _SC("get_month"));
if (SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
{
printf("Failed to create 'get_month' for date\n");
sq_settop(vm, __top__);
return;
}
// get_day
sq_pushstring(vm, _SC("get_day"), -1);
sq_newclosure(vm, &vm_get_day, 0);
sq_setparamscheck(vm, 0, NULL);
sq_setnativeclosurename(vm, -1, _SC("get_day"));
if (SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
{
printf("Failed to create 'get_day' for date\n");
sq_settop(vm, __top__);
return;
}
// to_string
sq_pushstring(vm, _SC("_tostring"), -1);
sq_newclosure(vm, &vm_to_string, 0);
sq_setparamscheck(vm, 0, NULL);
sq_setnativeclosurename(vm, -1, _SC("_tostring"));
if (SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
{
printf("Failed to create '_str' for date\n");
sq_settop(vm, __top__);
return;
}
// Now we are placing new class into slot of root table
if (SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
{
sq_settop(vm, __top__);
return;
}
sq_settop(vm, __top__);
Wow we can use class in the squirrel script:
execute_script(vm, "d <- date(20220723)\n"
"print(d.get_year() + \"/\" + d.get_month() + \"/\" + d.get_day() + \" str=\" + d.tostring())");
This code will print
2022/7/23 str=2022/07/23
Importing squirrel script into native code
The second use major use case is to use scripted code from C++. This is usual story in behaviour customization of event processing. Such binding called “imports”.
Import squirrel function into native code
For example we need to write specialized code in the script and use it on every startup of application. We can declare function in the script and call it from our application. So we are going to call function
function init(path)
{
print("Loading from path:" + path)
}
Now we need to declare C++ function that calls squirrel function init:
void execute_init(HSQUIRRELVM vm, const char* path)
{
int __top__ = sq_gettop(vm);
// Push root table
sq_pushroottable(vm);
sq_pushstring(vm, _SC("init"), -1);
if (SQ_FAILED(sq_get(vm, -2)))
{
printf("<error> Unable to find function init()\n");
sq_settop(vm, __top__);
return;
}
// Remove root table
sq_remove(vm, -2);
// Push root table
sq_pushroottable(vm);
// Push string argument
sq_pushstring(vm, path, strlen(path));
// Call method
if (SQ_FAILED(sq_call(vm, 2, SQFalse /* Return value */ , SQTrue /* Enable error reporting */)))
{
printf("<error> Unable to call init()\n");
return;
}
sq_settop(vm, __top__);
}
First let’s declare squirrel function by executing squirrel script. It might be script defined somewhere near your application in the file for example:
execute_script(vm, "function init(path) { print(\"Loading from path: \" + path)}");
And we can call it from C++ code with previously declared function execute_init():
execute_init(vm, "/my_test_path");
As a result you will get:
Loading from path: /my_test_path
Import squirrel class into you native code
Sometimes we have another use case. W have some native object and we need to implement methods in the squirrel script. For example we have some generic unit in the game and we need specialize some specific unit behaviour:
class Unit {
name = null
constructor(name) {
this.name = name
}
function attack() {
print("attack")
}
function run() {
print("run")
}
}
We are expects that in script will be some classes extending class Unit and should be already declared in the script vm. So let’s declare it from C++ code with calling execute script function:
execute_script(vm, "class Unit {\n"
" name = null\n"
" constructor(name) {\n"
" this.name = name\n"
" }\n"
" function attack() {\n"
" print(\"attack\")\n"
" }\n"
" function run() {\n"
" print(\"run\")\n"
" }\n"
"}");
Now let’s declare C++ functions to interact with derived squirrel classes:
/**
* Method creates instance of derived class, result will be placed into result parameter
*/
void create_unit_instance(HSQUIRRELVM vm, const char* class_name, const char* name, HSQOBJECT& result)
{
// Reset result => make it null
sq_resetobject(&object);
nint __top__ = sq_gettop(vm);
sq_pushroottable(vm);
sq_pushstring(vm, class_name, -1);
// Get derived class
if (SQ_FAILED(sq_get(vm, -2)))
{
printf("<error> Unable to get class '%s'\n", class_name);
sq_settop(vm, __top__);
return;
}
sq_remove(vm, -2); // Remove the root table
sq_pushroottable(vm); // Push root table on top
// Push name to contructor
sq_pushstring(vm, name, strlen(name));
// Create class instance
if (SQ_FAILED(sq_call(vm, 2, SQTrue /* Constructor return value */ , SQTrue /* Enable error reporting */)))
{
printf("<error> Unable to call contructor '%s'\n", class_name);
sq_settop(vm, __top__);
return;
}
sq_remove(vm, -2); //removes the class
// Get resulting object from stack
if (SQ_FAILED(sq_getstackobj(vm, -1, &result)))
{
printf("<error> Unable to get create instance of '%s'\n", class_name);
sq_settop(vm, __top__);
return;
}
// Add reference to object to keep it from garbage collection
sq_addref(vm, &result);
sq_settop(vm, __top__);
}
void call_unit_run(HSQUIRRELVM vm, HSQOBJECT& object)
{
int __top__ = sq_gettop(vm);
sq_pushroottable(vm);
sq_pushobject(vm, object);
// Get method run
sq_pushstring(vm, _SC("run"), -1);
if (SQ_FAILED(sq_get(vm, -2)))
{
printf("<error> Unable to get method run\n");
sq_settop(vm, __top__);
return;
}
// Push object to call
sq_pushobject(vm, object);
// Call method
if (SQ_FAILED(sq_call(vm, 1 /* Since don't have any parameters*/ , SQFalse /* No return value */ , SQTrue /* Enable error reporting */)))
{
printf("<error> Unable to get method run\n");
sq_settop(vm, __top__);
return;
}
sq_settop(vm, __top__);
}
So now we can create some class that was loaded from resources or explicitly defined in the C++ code. For example UnitA derived class that overrides prints for run() and attack() methods:
execute_script(vm, "class UnitA extends Unit{\n"
" constructor(name) {\n"
" base.constructor(name)\n"
" }\n"
" function attack() {\n"
" print(\"UnitA: \" + name + \" attack\")\n"
" }\n"
" function run() {\n"
" print(\"UnitA: \" + name + \" run\")\n"
" }\n"
"}");
To keep reference to vm object we should declare HSQOBJECT variable and create UnitA class:
HSQOBJECT super_soldier;
create_unit_instance(vm, "UnitA", "soldier", "super soldier", super_soldier);
Now we can call run method:
call_unit_run(vm, super_soldier);
As a result we will get on consle:
UnitA: super soldier run
Conclusions
No doubt that generates all this bindings are much more complex thing, but always may use one of the bindings, that I mentioned at the begging. It makes you possible to simplify you binding code, but you should always aware how it works under the hood.