From Squirrel Bindings
A Squirrel host talks to its VM through a stack: push the closure, push the arguments, sq_call, read the result off the top, pop what you pushed. Natives read their arguments by stack index, and a class is a table of closures with a type tag and a block of user data. JaiScript has no stack in its API. A host function is a typed C++ callable, a class is one dynamic_binder chain, and a call into script takes a vector of values and returns a result. The script languages are closer than the APIs: braces, classes, null, generators. The differences that change results are copies (an instance, an array or a table assigned in Squirrel is shared), the empty string, and names that must be declared. This page maps the Squirrel API, converts a real host's binding code, and then lists the script habits that get a different answer. The binding API is documented in full in Embedding Quickstart and dynamic_binder.
The concept map
Headers: <jaiscript/jaiscript.hpp> for the engine, values and binders, and <jaiscript/stdlib/stdlib.hpp> for the standard library.
VM and scripts
| Squirrel | JaiScript |
|---|---|
HSQUIRRELVM v = sq_open(1024); ... sq_close(v); | auto eng = jai::engine::make();, a std::shared_ptr<jai::engine> freed with its last owner |
sqstd_register_mathlib(v);, sqstd_register_stringlib(v); | jai::stdlib::register_all(eng); |
sq_compilebuffer, sq_pushroottable, sq_call, sq_pop | eng->execute(code); |
| reading a file into a buffer and compiling it | eng->execute_file(path); |
a host-bound include("file.nut") native (Squirrel has no module system) | import "file.jai"; (runs once and is cached) or include "file.jai";, resolved through eng->add_include_path(dir) |
sq_setprintfunc(v, print, error) | eng->set_output_stream(stream) for print, eng->set_script_error_handler(fn) for errors nothing can catch |
sq_setforeignptr(v, host) to reach the host from a native | the lambda captures what it needs |
SQInteger and SQFloat, 64-bit and double only with _SQ64 and SQUSEDOUBLE | jai::script_int (int64_t) and jai::script_float (double) in every build |
Functions
| Squirrel | JaiScript |
|---|---|
an SQFUNCTION that reads arguments with sq_getinteger(v, 2, &n), pushes its result and returns 1 | eng->add_function("name", [](int64_t n) { ... return result; });: the parameters and the return type do the stack work |
sq_newclosure, sq_newslot into the root table | add_function |
sq_setparamscheck(v, 2, ".s") | the C++ signature; the checker rejects a call no overload can take before the script runs, and a call that reaches the function with the wrong kind raises No overload of 'name' takes (int) |
return sq_throwerror(v, "msg"); | throw std::runtime_error("msg");, which the script catches as msg |
| one native that inspects argument types to serve several signatures | one add_function per signature under one name: an overload set resolved by argument types |
Classes
| Squirrel | JaiScript |
|---|---|
sq_newclass, sq_settypetag, sq_setclassudsize(v, -1, sizeof(T)) | jai::dynamic_binder<T>(*eng, "T") ... .build();; the object lives in a std::shared_ptr<T> |
a constructor closure that placement-news T into the user data | .constructor<int64_t>(), once per overload |
a method closure that fetches this with sq_getinstanceup(v, 1, &up, tag, SQTrue) | .method("name", &T::name); the binder checks the receiver's class before the call |
sq_setreleasehook to run a destructor | nothing: the destructor runs when the last shared_ptr goes |
_get / _set metamethods for a field-like member | .property("hp", &T::hp), or .property("hp", getter, setter) |
_add, _cmp and the other metamethods | .method("+", ...), .method("<=>", ...), and the other operator tokens |
a class that extends another, registered through the base's class object | .base_class<Base>() |
Values, calls and errors
| Squirrel | JaiScript |
|---|---|
sq_pushstring(v, "HOST_SEED", -1); sq_pushinteger(v, seed); sq_newslot(v, -3, SQFalse); | eng->add_global("HOST_SEED", seed); |
sq_pushroottable, sq_pushstring, sq_get, sq_getinteger, sq_pop | eng->get_variable("name").as_int() |
push the closure, push the root table as this, push the arguments, sq_call(v, 1 + n, SQTrue, SQTrue), read the top, pop | eng->get_variable("fn").as_function()({args...}), a jai::checked_result; or jai::convert_script_function<Sig>(...) for a std::function that throws on failure |
sq_getlasterror and sq_tostring | eng->format_error(result), or the exception's what() |
sq_setcompilererrorhandler | jai::parse_error, and jai::static_check_error listing every error in the program before anything runs |
sqstd_seterrorhandlers printing the call stack | eng->format_stack_trace() after a failure |
a generator: yield in a function, resume g, g.getstatus() | a coroutine function, g.next(), g.done() (below, and From Lua Coroutines) |
A host converted
GLOOM, the raycast shooter, runs on one shared C++ host with an adapter per language: the JaiScript original and, among the comparison ports, a Squirrel one written against the raw C API. Both adapters bind the same host API: the seeded random generator every port shares, host services, and the host's flags. These are their binding blocks, trimmed to a few lines of each kind. The Squirrel adapter's class, one method of it, and the helper every registration goes through:
#include <squirrel.h>
#include <sqstdmath.h>
#include <sqstdstring.h>
SQUserPointer rng_tag() {
return reinterpret_cast<SQUserPointer>(&rng_tag);
}
gloom_rng* rng_self(HSQUIRRELVM v) {
SQUserPointer up = nullptr;
sq_getinstanceup(v, 1, &up, rng_tag(), SQTrue);
return static_cast<gloom_rng*>(up);
}
SQInteger rng_construct(HSQUIRRELVM v) {
SQInteger seed = 0;
sq_getinteger(v, 2, &seed);
gloom_rng* self = rng_self(v);
if (!self) { return sq_throwerror(v, "Rng: bad instance"); }
new (self) gloom_rng(seed);
return 0;
}
SQInteger rng_roll(HSQUIRRELVM v) {
SQInteger lo = 0, hi = 0;
sq_getinteger(v, 2, &lo);
sq_getinteger(v, 3, &hi);
sq_pushinteger(v, rng_self(v)->roll(lo, hi));
return 1;
}
SQInteger native_itrunc(HSQUIRRELVM v) {
SQFloat f = 0;
sq_getfloat(v, 2, &f);
sq_pushinteger(v, static_cast<SQInteger>(f));
return 1;
}
void bind_closure(HSQUIRRELVM v, const SQChar* name, SQFUNCTION fn, SQInteger nparams, const SQChar* mask) {
sq_pushstring(v, name, -1);
sq_newclosure(v, fn, 0);
if (mask) { sq_setparamscheck(v, nparams, mask); }
sq_newslot(v, -3, SQFalse);
}
HSQUIRRELVM vm = sq_open(1024);
sq_pushroottable(vm);
sqstd_register_mathlib(vm);
sqstd_register_stringlib(vm);
bind_closure(vm, "host_log", native_host_log, 2, ".s");
bind_closure(vm, "itrunc", native_itrunc, 2, ".n");
set_global_int(vm, "HOST_SEED", opt.seed);
sq_pushstring(vm, "Rng", -1);
sq_newclass(vm, SQFalse);
sq_settypetag(vm, -1, rng_tag());
sq_setclassudsize(vm, -1, sizeof(gloom_rng));
bind_closure(vm, "constructor", rng_construct, 2, "xn");
bind_closure(vm, "roll", rng_roll, 3, "xnn");
sq_newslot(vm, -3, SQFalse);
sq_pop(vm, 1);#include <jaiscript/jaiscript.hpp>
#include <jaiscript/stdlib/stdlib.hpp>
auto eng = jai::engine::make();
jai::stdlib::register_all(eng);
jai::dynamic_binder<gloom_rng>(*eng, "Rng")
.constructor<jai::script_int>()
.method("roll", &gloom_rng::roll)
.build();
eng->add_function("host_log", [log](const std::string& line) { log(line); });
eng->add_function("itrunc", [](jai::script_float v) -> jai::script_int {
return static_cast<jai::script_int>(v);
});
eng->add_global("HOST_SEED", eng->make_value(opt.seed));- The stack work is the signature. Every
sq_get*that reads an argument and everysq_push*that returns one becomes a C++ parameter or return type, and the parameter-check mask becomes the types themselves. - The object is a C++ object. Squirrel reserves
sizeof(gloom_rng)bytes in the instance and the constructor builds the object there, so a type with a destructor also needs a release hook. The binder constructs it withnewunder astd::shared_ptr, and its destructor runs when the last owner lets go. - The type tag goes. The tag check in
rng_selfguards against a method called on another class's instance; the binder makes that check before every call.
Calling into the game. The Squirrel adapter pushes the root table, fetches the closure, pushes the root again as this and then the arguments, calls, reads the string off the top and pops three slots:
sq_pushroottable(vm);
sq_pushstring(vm, "gloom_frame", -1);
if (SQ_FAILED(sq_get(vm, -2))) { sq_pop(vm, 1); throw std::runtime_error("gloom_frame: not defined by scripts"); }
sq_pushroottable(vm);
sq_pushfloat(vm, static_cast<SQFloat>(dt));
sq_pushstring(vm, key.c_str(), static_cast<SQInteger>(key.size()));
sq_pushfloat(vm, static_cast<SQFloat>(fps));
sq_pushfloat(vm, static_cast<SQFloat>(ms_sim));
sq_pushfloat(vm, static_cast<SQFloat>(ms_draw));
if (SQ_FAILED(sq_call(vm, 6, SQTrue, SQTrue))) { fail("gloom_frame"); }
const SQChar* s = nullptr;
SQInteger len = 0;
sq_getstringandsize(vm, -1, &s, &len);
std::string out(s ? s : "", s ? static_cast<size_t>(len) : 0);
sq_pop(vm, 3);jai::script_value frame = eng->get_variable("gloom_frame");
jai::checked_result<jai::script_value> result = frame.as_function()({
eng->make_value(dt), eng->make_value(key), eng->make_value(fps),
eng->make_value(ms_sim), eng->make_value(ms_draw)});
if (!result) { throw std::runtime_error(eng->format_error(result)); }
std::string out = result.value().as_string();There is nothing to balance afterwards, and nothing to unwind with sq_settop when a call fails: the engine is ready for the next call either way. The Squirrel adapter's fail reads sq_getlasterror through sq_tostring; JaiScript's format_error is that text, and eng->format_stack_trace() is the script stack the error handler printed.
Errors at load arrive together. The Squirrel adapter installs a compiler error handler that records one message; eng->execute_file(path) throws jai::parse_error for a syntax error and jai::static_check_error whose what() lists every error the checker found in the program, before any statement runs. All JaiScript exceptions derive from std::exception.
Script habits that get a different answer
Squirrel and JaiScript share braces, class, null, + on strings and numbers, integer division between integers, and a remainder that takes the left operand's sign. Where a Squirrel habit gives another result, the cause is usually JaiScript's C++ semantics; From C++ lists them, and Value Semantics & References is the chapter to read first. The spellings:
| Squirrel | JaiScript |
|---|---|
local x = 5; | auto x = 5; (inferred, then enforced), int x = 5;, or var x = 5; (dynamic) |
score <- 0;, a new slot | int score = 0; at program scope |
::score, the root table | ::score, the program-scope name a member of the same name hides |
foreach (v in arr), foreach (k, v in table) | for (auto v : arr), for (auto [k, v] : m) |
class P { x = 0; constructor(px) { x = px; } } | class P { int x = 0; P(int px) { x = px; } } |
class Q extends P, base.method() | class Q : P, super::method(), and override on a redefinition |
throw "msg";, try { } catch (e) { } | the same |
Assignment copies an instance, an array and a table
In Squirrel, local q = p; makes a second name for one instance, and the same holds for arrays and tables. In JaiScript a class instance, an array and a map are values: assignment copies them. new makes a shared_ptr handle, which shares:
class Point {
int x = 0;
int y = 0;
Point(int px, int py) { x = px; y = py; }
int sum() { return x + y; }
}
Point p = Point(1, 2);
Point q = p; // a copy, where Squirrel's q = p shares the instance
q.x = 10;
auto h = new Point(1, 2); // a shared handle
auto h2 = h;
h2.x = 10;
print(p.x, " ", q.sum(), " / ", h.x);1 12 / 10array<int> a = [1, 2, 3];
array<int> b = a;
b[0] = 99;
map<string, int> t = {"hp": 10};
auto u = t;
u["hp"] = 0;
print(a[0], " ", b[0], " ", t["hp"]);1 99 10The empty string is false
Squirrel treats null, 0, 0.0 and false as false and every string as true. JaiScript agrees on the first four and also treats the empty string as false:
var empty = "";
print(empty ? "\"\" is true" : "\"\" is false");"" is falseNames are declared, and checked before anything runs
A Squirrel function that assigns a name nobody created loads, then fails when it runs (the index 'score' does not exist); <- creates the slot. A JaiScript program-scope declaration is the global, functions read and write it, and an undeclared name stops the load with use of undeclared identifier 'score':
int score = 0;
function setup() { score = 10; }
setup();
print(score);10Integers do not wrap
A 64-bit Squirrel build wraps 9223372036854775807 + 1 to the most negative integer. A JaiScript int is the same 64 bits, and an operation that overflows it raises an error (Checked overflow):
int big = 9223372036854775807;
try { big = big + 1; } catch (e) { print("caught: ", e); }caught: Integer overflow in '+'Generators are coroutines
A Squirrel function that contains yield is a generator: calling it makes a suspended generator, resume g runs it to the next yield, and g.getstatus() reports "dead" when it has returned. JaiScript marks the function coroutine; calling it returns a handle, next() advances it and done() reports completion. The last next() hands back the return value, as the last resume does:
coroutine int gen() {
yield 1;
yield 2;
return 3;
}
auto g = gen();
print(g.next());
print(g.next());
print(g.next());
print(g.done());1
2
3
trueSquirrel's threads (newthread, suspend, wakeup) suspend from any call depth; a JaiScript yield sits in the coroutine's own body. From Lua Coroutines covers the patterns that need the difference.
Where to next
Embedding Quickstart covers the engine, functions, globals, calls into script and every error surface; dynamic_binder covers every binder verb, JAI_PROPERTY classes that are reflected, scripted and saved from one declaration, and inheritance. From sol2 and From ChaiScript convert the same host from the other comparison ports.