From sol2 (Lua) Bindings​

sol2 makes binding C++ to Lua a matter of a few calls on a sol::state: new_usertype for a class, set_function or a table assignment for a function or a value, a protected_function to call back. JaiScript's binding API covers the same ground with a fluent dynamic_binder per class and plain engine calls, and a sol2 host converts mostly line for line. The larger change is on the script side. JaiScript is not a table language: arrays and maps are values that copy on assignment, indexes start at 0, / between integers truncates, and 0 is false. This page maps the sol2 API, converts a real host's binding code, and then lists the Lua 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.

State and scripts​

sol2JaiScript
sol::state lua;auto eng = jai::engine::make();, a std::shared_ptr<jai::engine>
lua.open_libraries(sol::lib::base, sol::lib::math, ...)jai::stdlib::register_all(eng);: opt-in in both, all of it in one call (math is math::sqrt and its namespace, not math.sqrt)
lua.script(code)eng->execute(code); both throw on failure
lua.safe_script(code, sol::script_pass_on_error) and a valid() checkeng->execute(code) in a try: loading a program reports by exception
lua.script_file(path), lua.safe_script_file(path, ...)eng->execute_file(path)
lua["package"]["path"] = dir + "/?.lua"; and require("enemies")eng->add_include_path(dir); and import "enemies.jai";
one sol::state per threadone engine per thread at a time, the same rule

Functions​

sol2JaiScript
lua.set_function("f", &f);, lua["f"] = [](double v) { ... };eng->add_function("f", &f);, eng->add_function("f", [](double v) { ... });
lua.set_function("label", sol::overload(f1, f2));eng->add_function("label", f1); and eng->add_function("label", f2);: one name, an overload set resolved by argument types
a sol::variadic_args parametereng->add_variadic_function("f", fn), whose fn receives a const std::vector<jai::script_value>&
a sol::this_state parameterthe lambda captures the engine pointer

Usertypes​

sol2JaiScript
lua.new_usertype<T>("T", ...);jai::dynamic_binder<T>(*eng, "T") ... .build();
sol::constructors<T(), T(std::string, int64_t)>(), called as T.new(...).constructor<>() and .constructor<std::string, int64_t>(), called as T(...) for a value or new T(...) for a shared_ptr
sol::factories([](int v) { return std::make_shared<T>(v); }).constructor<int>(): new T(v) in script already makes the shared_ptr
"method", &T::method, called as obj:method().method("method", &T::method), called as obj.method()
"field", &T::field.property("field", &T::field)
"hp", sol::property(&T::health, &T::setHealth).property("hp", &T::health, &T::setHealth)
sol::readonly(&T::field), sol::readonly_property(getter).property("name", getter, nullptr); a store raises Cannot assign to 'name': it is read-only
"damage", sol::overload(...).method("damage", ...) once per overload
sol::base_classes, sol::bases<Base>().base_class<Base>()
sol::meta_function::addition, equal_to, less_than, index.method("+", ...), .method("==", ...), .method("<", ...), .method("[]", ...)
a static function in the usertype table, T.axes().static_method("axes", fn), called as T::axes()

Values​

sol2JaiScript
lua["HOST_SEED"] = opt.seed;eng->add_global("HOST_SEED", opt.seed);
int64_t gold = lua["gold"];int64_t gold = eng->get_variable("gold").as_int();
lua["boss"] = boss; with a std::shared_ptr<T>eng->add_global("boss", eng->make_object(boss));
lua["player"] = std::ref(player);eng->add_global_ref("player", player);, which for a number or a string binds the C++ variable itself: script writes land in it
sol::object, obj.as<T>(), obj.is<T>()jai::script_value, v.as<T>(), v.is_int(), v.is_string() and the other tests
sol::table, lua.create_table()an array or a map value: eng->make_array(), eng->make_map(); a typed one copies out with v.as<std::vector<std::string>>() or v.as<std::map<std::string, int64_t>>()
sol::lua_nileng->make_null(); null in script

Calls into script and errors​

sol2JaiScript
sol::protected_function f = lua["f"]; auto r = f(args...); if (!r.valid()) { sol::error e = r; }auto r = eng->get_variable("f").as_function()({args...}); if (!r) { eng->format_error(r); }: a jai::checked_result
std::function<int64_t(int64_t)> f = lua["f"];, sol::functionauto f = jai::convert_script_function<int64_t(int64_t)>(eng->get_variable("f"));, which throws jai::runtime_error on failure
sol::errorjai::parse_error, jai::static_check_error (the whole check listing, before anything runs), jai::runtime_error; all derive from std::exception
protected_function::set_default_handler(lua["debug"]["traceback"])eng->format_stack_trace() after a failure
error("msg") in scriptthrow "msg";, which reaches the host as jai::script_exception with that text
a C++ exception from a bound function, turned into a Lua errora run-time error the script can catch, carrying the exception's what()
sol::coroutine, sol::threada script coroutine's handle, advanced from C++ with next(engine*) or next(engine*, n) (From Lua Coroutines, Coroutine Hosting)

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 Lua one on sol2. 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. sol2:

C++ (sol2)
#include <sol/sol.hpp>

sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string, sol::lib::table, sol::lib::coroutine);

lua.new_usertype<gloom_rng>("Rng",
    sol::constructors<gloom_rng(int64_t)>(),
    "next", &gloom_rng::next,
    "roll", &gloom_rng::roll,
    "chance", &gloom_rng::chance,
    "nextf", &gloom_rng::nextf,
    "state", &gloom_rng::state);

lua["host_log"] = [log](const std::string& line) { log(line); };
lua["itrunc"] = [](double v) -> int64_t { return static_cast<int64_t>(v); };
lua["ESC"] = std::string("\x1b");
lua["HOST_SEED"] = opt.seed;
lua["HOST_GOD"] = opt.god;
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("next", &gloom_rng::next)
    .method("roll", &gloom_rng::roll)
    .method("chance", &gloom_rng::chance)
    .method("nextf", &gloom_rng::nextf)
    .method("state", &gloom_rng::state)
    .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("ESC", eng->make_value(std::string("\x1b")));
eng->add_global("HOST_SEED", eng->make_value(opt.seed));
eng->add_global("HOST_GOD", eng->make_value(opt.god));

The usertype's name-value list becomes a chain of named verbs, and assignments into the state become add_function and add_global. Integers need no care at the boundary: a script int is an int64_t (jai::script_int), so the generator's 64-bit state crosses whole, as it does through a Lua 5.4 integer.

Loading the game and calling into it:

C++ (sol2)
lua["package"]["path"] = dir + "/?.lua";
sol::protected_function_result result = lua.safe_script_file(dir + "/main.lua", sol::script_pass_on_error);
if (!result.valid()) { sol::error err = result; throw std::runtime_error(err.what()); }

sol::protected_function frame = lua["gloom_frame"];
sol::protected_function_result out = frame(dt, key, fps, ms_sim, ms_draw);
if (!out.valid()) { sol::error err = out; throw std::runtime_error(err.what()); }
std::string text = out.get<std::string>();
C++ (JaiScript)
eng->add_include_path(dir);
eng->execute("import \"main.jai\";");

jai::script_value frame = eng->get_variable("gloom_frame");
jai::checked_result<jai::script_value> out = 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 (!out) { throw std::runtime_error(eng->format_error(out)); }
std::string text = out.value().as_string();

The engine is ready for the next call after a failed one, as a Lua state is after a protected call. A host that would rather throw converts the function once: jai::convert_script_function<std::string(double, const std::string&, double, double, double)>(frame) is a std::function, like sol2's conversion to one.

A usertype in full​

A class with two constructors, an overloaded method, a property with a getter and a setter, a read-only property, an operator and a base class:

C++
struct Entity {
    virtual ~Entity() = default;
    std::string name = "entity";
    virtual std::string describe() const { return "entity " + name; }
};

struct Monster : Entity {
    int64_t hp = 10;
    Monster() = default;
    Monster(std::string a_name, int64_t a_hp) : hp(a_hp) { name = std::move(a_name); }
    void damage(int64_t amount) { hp -= amount; }
    void damage(int64_t amount, double multiplier) { hp -= static_cast<int64_t>(amount * multiplier); }
    int64_t health() const { return hp; }
    void setHealth(int64_t a_hp) { hp = a_hp < 0 ? 0 : a_hp; }
    bool alive() const { return hp > 0; }
    std::string describe() const override { return "monster " + name; }
    bool operator==(const Monster& o) const { return name == o.name && hp == o.hp; }
};
C++ (sol2)
lua.new_usertype<Entity>("Entity",
    "name", &Entity::name,
    "describe", &Entity::describe);

lua.new_usertype<Monster>("Monster",
    sol::constructors<Monster(), Monster(std::string, int64_t)>(),
    sol::base_classes, sol::bases<Entity>(),
    "damage", sol::overload(
        static_cast<void (Monster::*)(int64_t)>(&Monster::damage),
        static_cast<void (Monster::*)(int64_t, double)>(&Monster::damage)),
    "alive", &Monster::alive,
    "hp", sol::property(&Monster::health, &Monster::setHealth),
    "maxHp", sol::readonly_property([](const Monster&) { return int64_t{100}; }),
    sol::meta_function::equal_to, &Monster::operator==);

lua.set_function("label", sol::overload(
    [](int64_t n) { return std::string("int ") + std::to_string(n); },
    [](const std::string& s) { return std::string("string ") + s; }));
C++ (JaiScript)
jai::dynamic_binder<Entity>(*eng, "Entity")
    .property("name", &Entity::name)
    .method("describe", &Entity::describe)
    .build();

jai::dynamic_binder<Monster>(*eng, "Monster")
    .base_class<Entity>()
    .constructor<>()
    .constructor<std::string, int64_t>()
    .method("damage", static_cast<void (Monster::*)(int64_t)>(&Monster::damage))
    .method("damage", static_cast<void (Monster::*)(int64_t, double)>(&Monster::damage))
    .method("alive", &Monster::alive)
    .method("==", &Monster::operator==)
    .property("hp", &Monster::health, &Monster::setHealth)
    .property("maxHp", [](const Monster&) { return int64_t{100}; }, nullptr)
    .build();

eng->add_function("label", [](int64_t n) { return std::string("int ") + std::to_string(n); });
eng->add_function("label", [](const std::string& s) { return std::string("string ") + s; });

The same session in each language:

lua
local m = Monster.new("ogre", 30)
m:damage(5)
m:damage(10, 1.5)
m.hp = m.hp - 100
print(m.name .. " hp=" .. m.hp .. " alive=" .. tostring(m:alive()) .. " max=" .. m.maxHp .. " " .. m:describe())
print(label(3) .. " / " .. label("x") .. " / " .. tostring(Monster.new("a", 1) == Monster.new("a", 1)))
jaiscript (with the host above)
var m = Monster("ogre", 30);
m.damage(5);
m.damage(10, 1.5);
m.hp -= 100;
print(m.name, " hp=", m.hp, " alive=", m.alive(), " max=", m.maxHp, " ", m.describe());
print(label(3), " / ", label("x"), " / ", Monster("a", 1) == Monster("a", 1));
output (both)
ogre hp=0 alive=false max=100 monster ogre
int 3 / string x / true

sol2 notes that sol::constructors makes a value userdata, which a C++ function taking std::shared_ptr<T> cannot share, so the comparison benchmarks bind their tree node with sol::factories returning std::make_shared. In JaiScript the constructor list serves both: Monster(...) is a value, new Monster(...) is a shared_ptr handle, and a C++ object handed over with make_object stays one object on both sides.

Lua habits that get a different answer​

JaiScript's semantics are C++'s, so most Lua habits that change are the ones a JavaScript programmer meets too; From JavaScript covers the dynamic-language side in more depth, and Value Semantics & References is the chapter to read first. The spellings:

LuaJaiScript
local x = 5auto x = 5; (inferred, then enforced), int x = 5;, or var x = 5; (dynamic)
nil, ~=, and, or, notnull, !=, &&, ||, !
"hp " .. hp"hp " + hp, or a template string `hp ${hp}`
#tt.size()
t[1], the first elementt[0]
for i = 1, #t do ... end, for _, v in ipairs(t)for (int i = 0; i < t.size(); ++i) { ... }, for (auto v : t)
for k, v in pairs(m), in no set orderfor (auto [k, v] : m), in key order
obj:method(x), selfobj.method(x); inside a method, fields and methods are in scope without this
return a, breturn [a, b]; and auto [a, b] = f();
error("msg"), pcall(f)throw "msg";, try { f(); } catch (e) { ... }
-- comment// comment, which is why 7 // 2 is 7 followed by a comment

Arrays and maps are values​

In Lua, b = a makes a second name for one table. In JaiScript, assignment copies an array or a map, as passing one by value does; a reference declaration (auto&), a reference parameter or a shared_ptr shares one:

jaiscript
array<int> a = [1, 2, 3];
array<int> b = a;                  // a copy, where Lua's b = a shares the table
b[0] = 99;
map<string, int> stats = {"hp": 10};
auto copy = stats;
copy["hp"] = 0;
auto& alias = stats;               // a reference names the same map
alias["hp"] += 5;
print(a[0], " ", b[0], " ", stats["hp"], " ", copy["hp"]);
output
1 99 15 0

Indexes start at 0​

The first element is [0] and the last is [size() - 1]. Reading past the end is an error, where Lua returns nil:

jaiscript
array<string> names = ["ana", "bo", "cy"];
print(names[0], " ", names[names.size() - 1], " ", names.size());
for (int i = 0; i < names.size(); ++i) { print(i, " ", names[i]); }
try { print(names[3]); } catch (e) { print("caught: ", e); }
output
ana cy 3
0 ana
1 bo
2 cy
caught: Array index 3 out of bounds for array of size 3

Division follows the operand types​

Lua's / always produces a float (7 / 2 is 3.5) and // floors. In JaiScript, / between two integers is integer division truncating toward zero, as in C++, and a float operand makes it float division. % takes the sign of the left operand, where Lua's takes the sign of the right (-7 % 2 is 1 in Lua):

jaiscript
print(7 / 2, " ", 7.0 / 2, " ", -7 / 2, " ", -7 % 2, " ", cast<float>(7) / 2);
output
3 3.5 -3 -1 3.5

0 and the empty string are false​

Lua treats every value but nil and false as true. JaiScript tests a value the C++ way: 0, the empty string and null are false, so if (count) skips an empty count.

jaiscript
var zero = 0;
var empty = "";
print(zero ? "0 is true" : "0 is false", ", ", empty ? "\"\" is true" : "\"\" is false");
output
0 is false, "" is false

Names are declared, and checked before anything runs​

A Lua assignment to an unknown name creates a global, and a misspelled read is nil. JaiScript declares every name; a program-scope declaration is the global, and the strict checker rejects an undeclared name, a wrong argument count or a mismatched typed value before the first statement runs (Strict checking). Names the host registers before the script loads are visible to the checker.

Integers do not wrap​

Lua 5.4 integers wrap around (math.maxinteger + 1 == math.mininteger). A JaiScript int is the same 64 bits, but an operation that overflows it raises an error (Checked overflow); math::wrap_add and its siblings wrap on purpose:

jaiscript
int big = 9223372036854775807;
try { big += 1; } catch (e) { print("caught: ", e); }
output
caught: Integer overflow in '+='

Several results come back as one value​

A function returns one value. Return an array and destructure it (Destructuring), or return a class instance:

jaiscript
function minmax(array<int> xs) {
    int lo = xs[0];
    int hi = xs[0];
    for (auto x : xs) {
        if (x < lo) { lo = x; }
        if (x > hi) { hi = x; }
    }
    return [lo, hi];
}
auto [lo, hi] = minmax([4, 9, 1]);
print(lo, " ", hi);
output
1 9

Maps iterate in key order​

pairs visits a table in no promised order. A JaiScript map iterates in key order, every run, so a loop over one is deterministic:

jaiscript
map<string, int> loot = {"zinc": 3, "amber": 1, "moss": 2};
for (auto [name, count] : loot) { print(name, " ", count); }
output
amber 1
moss 2
zinc 3

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. Coroutines covers the script side of what Lua does with coroutine.wrap, and Coroutine Hosting the host side.