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
| sol2 | JaiScript |
|---|---|
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() check | eng->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 thread | one engine per thread at a time, the same rule |
Functions
| sol2 | JaiScript |
|---|---|
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 parameter | eng->add_variadic_function("f", fn), whose fn receives a const std::vector<jai::script_value>& |
a sol::this_state parameter | the lambda captures the engine pointer |
Usertypes
| sol2 | JaiScript |
|---|---|
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
| sol2 | JaiScript |
|---|---|
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_nil | eng->make_null(); null in script |
Calls into script and errors
| sol2 | JaiScript |
|---|---|
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::function | auto f = jai::convert_script_function<int64_t(int64_t)>(eng->get_variable("f"));, which throws jai::runtime_error on failure |
sol::error | jai::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 script | throw "msg";, which reaches the host as jai::script_exception with that text |
| a C++ exception from a bound function, turned into a Lua error | a run-time error the script can catch, carrying the exception's what() |
sol::coroutine, sol::thread | a 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:
#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;#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:
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>();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:
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; }
};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; }));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:
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)))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));ogre hp=0 alive=false max=100 monster ogre
int 3 / string x / truesol2 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:
| Lua | JaiScript |
|---|---|
local x = 5 | auto x = 5; (inferred, then enforced), int x = 5;, or var x = 5; (dynamic) |
nil, ~=, and, or, not | null, !=, &&, ||, ! |
"hp " .. hp | "hp " + hp, or a template string `hp ${hp}` |
#t | t.size() |
t[1], the first element | t[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 order | for (auto [k, v] : m), in key order |
obj:method(x), self | obj.method(x); inside a method, fields and methods are in scope without this |
return a, b | return [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:
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"]);1 99 15 0Indexes 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:
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); }ana cy 3
0 ana
1 bo
2 cy
caught: Array index 3 out of bounds for array of size 3Division 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):
print(7 / 2, " ", 7.0 / 2, " ", -7 / 2, " ", -7 % 2, " ", cast<float>(7) / 2);3 3.5 -3 -1 3.50 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.
var zero = 0;
var empty = "";
print(zero ? "0 is true" : "0 is false", ", ", empty ? "\"\" is true" : "\"\" is false");0 is false, "" is falseNames 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:
int big = 9223372036854775807;
try { big += 1; } catch (e) { print("caught: ", e); }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:
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);1 9Maps 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:
map<string, int> loot = {"zinc": 3, "amber": 1, "moss": 2};
for (auto [name, count] : loot) { print(name, " ", count); }amber 1
moss 2
zinc 3Where 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.