From sol2 (Lua) Bindings
sol2 binds C++ to Lua with 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 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 in the script. 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 (Lua habits). 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.
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") ...; |
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, and a C++ object handed over with eng->make_object(ptr) stays one object on both sides |
"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
The GLOOM host that From ChaiScript converts also has a Lua adapter on sol2. Its bindings follow the tables above (the usertype below shows every kind); a script int is an int64_t (jai::script_int), so the host's 64-bit random 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 with jai::convert_script_function, like sol2's conversion to a std::function.
A usertype in full
The Entity and Monster classes of From ChaiScript (two constructors, an overloaded method, a getter and setter pair, a read-only property, an operator, a base class), bound with 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; }));The JaiScript binder for the two classes is the one on that page, entry for entry, and label is two add_function calls under one name. 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 / truesol::constructors makes a value userdata, which a C++ function taking std::shared_ptr<T> cannot share, so a sol2 host binds such a type 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.
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: see From JavaScript and Value Semantics & References. 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 | a template string, `hp ${hp}`, or "hp " + to_string(hp); + joins two strings, and a number beside a string is a load error |
#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 on every run |
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(); (Destructuring), or a class instance |
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 (Array index 3 out of bounds for array of size 3), where Lua returns nil.
Division follows the operand types
Lua's / always produces a float (7 / 2 is 3.5) and // floors. In JaiScript, / between two integers truncates toward zero, as in C++ (7 / 2 is 3, -7 / 2 is -3), and a float operand makes it float division (7.0 / 2 is 3.5). % takes the sign of the left operand (-7 % 2 is -1), where Lua's takes the sign of the right (1).
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 a zero count.
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 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 a catchable error, Integer overflow in '+' (Checked overflow); math::wrap_add and its siblings wrap on purpose.
Coroutines
Coroutines covers the script side of what Lua does with coroutine.wrap, and From Lua Coroutines the conversion.