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. 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.

This page compares against Squirrel's raw C API, which the host converted below uses. Binding libraries such as Sqrat wrap that API in class and function binders; a host that uses one maps its binder calls to dynamic_binder the way a sol2 host does, and the rest of this page applies unchanged.

The script languages are closer than the APIs: braces, classes, null, generators. The differences that change results are copies, the empty string, and names that must be declared. The binding API is documented 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​

SquirrelJaiScript
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_popeng->execute(code);
sqstd_dofile(v, path, retval, printerror), or sqstd_loadfile and a calleng->execute_file(path);
dofile("file.nut") and loadfile in script, with sqstd_register_iolib; each call runs or compiles the file againimport "file.jai"; (runs once and is cached) or include "file.jai"; (runs each time), 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 nativethe lambda captures what it needs
SQInteger and SQFloat, 64-bit and double only with _SQ64 and SQUSEDOUBLEjai::script_int (int64_t) and jai::script_float (double) in every build

Functions​

SquirrelJaiScript
an SQFUNCTION that reads arguments with sq_getinteger(v, 2, &n), pushes its result and returns 1eng->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 tableadd_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 signaturesone add_function per signature under one name: an overload set resolved by argument types

Classes​

SquirrelJaiScript
sq_newclass, sq_settypetag, sq_setclassudsize(v, -1, sizeof(T))jai::dynamic_binder<T>(*eng, "T") ...;; 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 destructornothing: 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​

SquirrelJaiScript
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_popeng->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, popeng->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_tostringeng->format_error(result), or the exception's what()
sq_setcompilererrorhandlerjai::parse_error, and jai::static_check_error listing every error in the program before anything runs; both derive from std::exception
sqstd_seterrorhandlers printing the call stackeng->format_stack_trace() after a failure
a generator: yield in a function, resume g, g.getstatus()a coroutine function, g.next(), g.done() (below)

A host converted​

The GLOOM host that From ChaiScript converts also has a Squirrel adapter, written against the raw C API. Its class, one method, and the helper every registration goes through:

C++ (Squirrel)
#include <squirrel.h>
#include <sqstdmath.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;
}

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);
bind_closure(vm, "host_log", native_host_log, 2, ".s");

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);
C++ (JaiScript)
#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);

eng->add_function("host_log", [log](const std::string& line) { log(line); });
  • The stack work is the signature. Every sq_get* that reads an argument and every sq_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 under a std::shared_ptr, and its destructor runs when the last owner lets go.
  • The type tag goes. The tag check in rng_self guards against a method called on another class's instance; the binder makes that check before every call.

Calling into the game:

C++ (Squirrel)
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);
C++ (JaiScript)
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. Where the Squirrel adapter's compiler error handler records one message, a JaiScript load reports every error the checker found, before any statement runs.

Script habits that get a different answer​

Squirrel and JaiScript share braces, class, null, throw with try/catch (e), 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: see From C++ and Value Semantics & References. The spellings:

SquirrelJaiScript
local x = 5;auto x = 5; (inferred, then enforced), int x = 5;, or var x = 5; (dynamic)
score <- 0;, a new slotint score = 0; at program scope
::score, the root table::score, the program-scope name a member of the same name hides
"hp " + hp, which converts the numbera template string, `hp ${hp}`, or "hp " + to_string(hp); + joins two strings, and a number beside a string is a load error
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

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:

jaiscript
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;
array<int> a = [1, 2, 3];
array<int> b = a;                  // a copy too
b[0] = 99;
print(p.x, " ", q.sum(), " / ", h.x, " / ", a[0], " ", b[0]);
output
1 12 / 10 / 1 99

The 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.

Names 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'.

Integers 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 a catchable error, Integer overflow in '+' (Checked overflow).

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.

Squirrel'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.