From ChaiScript Bindings
ChaiScript and JaiScript embed the same way: a header-driven library, an engine object, C++ functions and classes registered by pointer, script that calls them by name. A ChaiScript host moves over mostly line for line.
What changes: a class registration is one fluent dynamic_binder chain, the standard library is opt-in, a call into script can report failure without throwing, and the script language is typed where you want it and statically checked before it runs. The binding API is documented in Embedding Quickstart and dynamic_binder.
The concept map
Headers: <jaiscript/jaiscript.hpp> for the engine, values and binders, <jaiscript/stdlib/stdlib.hpp> for the standard library, and <jaiscript/core/registrar.hpp> for registrars.
Engine and scripts
| ChaiScript | JaiScript |
|---|---|
chaiscript::ChaiScript chai;, standard library included | auto eng = jai::engine::make(); (a std::shared_ptr<jai::engine>) and jai::stdlib::register_all(eng); |
chai.eval(code), chai.eval<T>(code) | eng->execute(code), eng->execute(code).as<T>() |
chai.eval_file(path) | eng->execute_file(path) |
use("file.chai") in script | import "file.jai"; (runs once and is cached; include runs the file each time) |
the search paths passed to the ChaiScript constructor | eng->add_include_path(dir) |
one engine shared by threads (unless CHAISCRIPT_NO_THREADS) | one engine per thread at a time; engines share nothing, so a second thread makes its own |
Functions
| ChaiScript | JaiScript |
|---|---|
chai.add(chaiscript::fun(&f), "f"); | eng->add_function("f", &f); |
chai.add(chaiscript::fun([](double v) { ... }), "f"); | eng->add_function("f", [](double v) { ... }); |
an overloaded C++ function: fun(static_cast<...>(&f)) once per overload, under one name | add_function once per overload under one name, with the same casts; the call resolves by argument types |
Classes
| ChaiScript | JaiScript |
|---|---|
chai.add(user_type<T>(), "T"); | jai::dynamic_binder<T>(*eng, "T") ...; |
chai.add(constructor<T(int64_t)>(), "T"); | .constructor<int64_t>(), once per overload; script calls T(...) for a value and new T(...) for a shared_ptr |
chai.add(fun(&T::method), "method");, a function any matching first argument can call with dot syntax | .method("method", &T::method), a method of T |
overloaded methods: fun(static_cast<void (T::*)(int64_t)>(&T::damage)) per overload | .method("damage", static_cast<void (T::*)(int64_t)>(&T::damage)) per overload |
a data member: fun(&T::field) | .property("field", &T::field) |
| a getter and a setter registered as two functions | .property("hp", &T::health, &T::setHealth); nullptr as the setter makes it read-only |
chai.add(base_class<Base, Derived>()); | .base_class<Base>() on the derived binder |
chai.add(fun(&T::operator==), "=="); | .method("==", &T::operator==); != follows from == |
utility::add_class<T>(*module, "T", {constructors}, {{fun(...), "name"}, ...}) | the same binder chain |
chaiscript::ModulePtr filled once and chai.add(module) into each engine | a static jai::registrar<T, void> with a configure lambda, and jai::bind_registrar(eng) per engine (below) |
Globals and values
| ChaiScript | JaiScript |
|---|---|
chai.add_global(var(x), "x");, a copy | eng->add_global("x", x); |
chai.add_global(var(std::ref(x)), "x");, shared with C++ | eng->add_global_ref("x", x);: script reads and writes the C++ variable |
chai.add_global_const(const_var(v), "N"); | eng->add_global("N", v); is assignable from script; a constant is a const declaration the host runs: eng->execute("const int MAX_LEVEL = 20;"); |
var(std::shared_ptr<T>) for a live object | eng->add_global("boss", eng->make_object(boss));; script writes land in the C++ object |
chaiscript::Boxed_Value, boxed_cast<T>(bv) | jai::script_value, v.as<T>() (and as_int(), as_float(), as_string(), as_bool()) |
Boxed_Number(bv).get_as<int64_t>() | v.as_int(): a script integer is always an int64_t (jai::script_int), a float a double (jai::script_float) |
Calls into script and errors
| ChaiScript | JaiScript |
|---|---|
chai.eval<std::function<int (int)>>("damage_for") | jai::convert_script_function<int64_t(int64_t)>(eng->get_variable("damage_for")); a failure inside it throws jai::runtime_error |
| (no counterpart) | eng->get_variable("damage_for").as_function()({args}) returns a jai::checked_result<jai::script_value> that carries the failure instead of throwing it; eng->format_error(result) is its text |
chaiscript::exception::eval_error for parse and run-time errors, with pretty_print() | jai::parse_error, jai::static_check_error (the whole check listing, before anything runs) and jai::runtime_error; eng->format_stack_trace() is the script stack |
a script throw(value), caught as a thrown Boxed_Value | jai::script_exception (a jai::runtime_error) whose what() is the thrown text |
| a C++ exception thrown by a registered function | a run-time error the script can catch, carrying the exception's what(); uncaught, it leaves execute as a jai::runtime_error |
A host converted
GLOOM, the raycast shooter, runs on one shared C++ host with an adapter per language: the JaiScript original and comparison ports, one of them in ChaiScript. Each adapter binds the same host API: a seeded random generator class (Rng), host services such as host_log, and flags such as HOST_SEED. Three things change beyond the spellings in the tables:
- The class is one statement. ChaiScript registers the type, its constructor and each method as separate engine entries; a method is a free function that dot syntax reaches through its first argument. The binder makes them members of
Rng, andbuild()registers the class (below). - The math whitelist goes. ChaiScript's core has no math library, so its adapter binds
sqrt,cos,sin,atan2andflooritself. The JaiScript standard library has them asmath::sqrtand the rest. - The constants are globals.
add_globalhas no const form; a script that must not reassign a value declares itconst, or the host runs that declaration.
Loading and calling follow the tables: eval_file becomes execute_file (or an import), chai.eval<std::function<...>>("gloom_frame") becomes jai::convert_script_function<...>(eng->get_variable("gloom_frame")), and a frame loop that should not throw calls through as_function() and checks the result (From sol2 shows the call).
Classes in full
A class with two constructors, an overloaded method, a getter and setter pair, 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; }
};chai.add(chaiscript::user_type<Entity>(), "Entity");
chai.add(chaiscript::fun(&Entity::name), "name");
chai.add(chaiscript::fun(&Entity::describe), "describe");
chai.add(chaiscript::user_type<Monster>(), "Monster");
chai.add(chaiscript::base_class<Entity, Monster>());
chai.add(chaiscript::constructor<Monster()>(), "Monster");
chai.add(chaiscript::constructor<Monster(std::string, int64_t)>(), "Monster");
chai.add(chaiscript::fun(static_cast<void (Monster::*)(int64_t)>(&Monster::damage)), "damage");
chai.add(chaiscript::fun(static_cast<void (Monster::*)(int64_t, double)>(&Monster::damage)), "damage");
chai.add(chaiscript::fun(&Monster::alive), "alive");
chai.add(chaiscript::fun(&Monster::health), "health");
chai.add(chaiscript::fun(&Monster::setHealth), "set_health");
chai.add(chaiscript::fun(&Monster::operator==), "==");jai::dynamic_binder<Entity>(*eng, "Entity")
.property("name", &Entity::name)
.method("describe", &Entity::describe);
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);
eng->add_function("describe", [](std::shared_ptr<Entity> e) { return e->describe(); });Script reads the getter and setter as one property, and a value made in script passes to a function that takes the base:
var m = Monster("ogre", 30);
m.damage(5);
m.damage(10, 1.5);
m.hp -= 100; // through setHealth, which clamps at 0
print(m.name, " hp=", m.hp, " alive=", m.alive(), " max=", m.maxHp, " ", m.describe(), " ", describe(m));
try { m.maxHp = 5; } catch (e) { print("caught: ", e); }ogre hp=0 alive=false max=100 monster ogre monster ogre
caught: Cannot assign to 'maxHp': it is read-onlyA C++ class that points at its own type binds its self-typed members like any other:
jai::dynamic_binder<CppTreeNode>(*eng, "CppTreeNode")
.constructor<int>()
.property("value", &CppTreeNode::value)
.property("left", &CppTreeNode::left)
.property("right", &CppTreeNode::right);A ChaiScript host that registers an = for std::shared_ptr<CppTreeNode> so script can reassign a node handle drops it: JaiScript assigns shared_ptr values without help.
Modules and registrars
A ModulePtr collects registrations once so several engines can take them. A jai::registrar does the same at program start, from the translation unit that defines the class: static jai::registrar<gloom_rng, void> rngRegistrar("Rng", [](jai::dynamic_binder<gloom_rng>& binder) { binder.constructor<int64_t>().method("next", &gloom_rng::next); });, and jai::bind_registrar(eng) applies every registrar to an engine. The registrar's name is also the class's serialization identity (From Cereal).
Errors
The ChaiScript adapter wraps every call in a guard that turns ChaiScript's two failure shapes into one:
try {
fn();
} catch (const chaiscript::exception::eval_error& e) {
last_trace = e.pretty_print();
throw std::runtime_error(e.what());
} catch (const chaiscript::Boxed_Value& bv) {
std::string msg = "script threw a value";
try { msg = chaiscript::boxed_cast<std::string>(bv); } catch (...) {}
throw std::runtime_error(msg);
}Every JaiScript exception derives from std::exception, so the guard is ordinary catch clauses, and the most specific ones are optional:
try {
eng->execute_file(scripts_dir + "/main.jai");
} catch (const jai::static_check_error& e) { // nothing ran: e.what() lists every error in the program
std::cerr << e.what();
} catch (const jai::exception& e) { // parse_error, runtime_error, script_exception
std::cerr << e.what() << "\n" << eng->format_stack_trace();
}The checker reads a whole program before its first statement runs, so mistakes ChaiScript reports when control reaches them (a misspelled name, a wrong argument count to a typed function) arrive as one jai::static_check_error at load. Names the host registers before the script loads are visible to it; a name the host adds later is declared in script with extern (Static Checking).
Script habits that get a different answer
The languages look alike: braces, var, for (x : xs). Where a ChaiScript habit gives another result, the cause is usually JaiScript's C++ semantics: see From C++ and Value Semantics & References.
Arguments copy unless the parameter is a reference
The languages agree on a declaration: var b = a; of a Vector makes a copy in both, and ChaiScript's reference assignment var b := a is a reference declaration, auto& b = a;. They differ at a call. A ChaiScript function receives the caller's object, so a push_back inside it changes the caller's vector. A JaiScript parameter is a copy unless it is declared a reference, and the checker rejects a write to a by-value parameter that is never read again. push_back is push.
int grown(array<int> xs) { xs.push(4); return xs.size(); } // xs is a copy
function grow(array<int>& xs) { xs.push(4); } // xs is the caller's array
array<int> a = [1, 2, 3];
print(grown(a), " ", a.size());
grow(a);
auto& c = a; // an alias, ChaiScript's :=
c[0] = 42;
print(a[0], " ", a.size());4 3
42 4Interpolation uses backticks
ChaiScript interpolates ${...} inside double quotes. In JaiScript "hp is ${hp}" is literal text, and the backtick string `hp is ${hp}` interpolates (Template strings).
Names are declared before they are used
ChaiScript finds names when a statement runs: a function that assigns an undeclared score loads, then fails when called, and global score = 10; inside a function creates a global when it runs. In JaiScript a program-scope declaration (int score = 0;) is the global, functions read and write it, and an undeclared name stops the load with use of undeclared identifier 'score'.
Integers are 64-bit, and overflow is an error
A ChaiScript integer literal is a C++ int, so 2147483647 + 1 overflows 32 bits. A JaiScript int is 64-bit (that sum is 2147483648), and an operation that overflows it raises an error a catch sees, Integer overflow in '+' (Checked overflow).
Conditions test values
ChaiScript requires a bool condition and rejects if (0). JaiScript tests a value the C++ way: 0, the empty string and null are false.
Classes declare their fields
A ChaiScript class declares var x; and a constructor def Point(px, py) { this.x = px; }, and methods reach fields through this. A JaiScript class types its fields, names the constructor like C++, and reads fields without this: class Point { float x; Point(float px) { x = px; } }. An instance is a value, and assignment copies it (Classes). A script class can also derive a bound C++ class, which ChaiScript has no counterpart for (Script-Extensible Objects).