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​

ChaiScriptJaiScript
chaiscript::ChaiScript chai;, standard library includedauto 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 scriptimport "file.jai"; (runs once and is cached; include runs the file each time)
the search paths passed to the ChaiScript constructoreng->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​

ChaiScriptJaiScript
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 nameadd_function once per overload under one name, with the same casts; the call resolves by argument types

Classes​

ChaiScriptJaiScript
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 enginea static jai::registrar<T, void> with a configure lambda, and jai::bind_registrar(eng) per engine (below)

Globals and values​

ChaiScriptJaiScript
chai.add_global(var(x), "x");, a copyeng->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 objecteng->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​

ChaiScriptJaiScript
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_Valuejai::script_exception (a jai::runtime_error) whose what() is the thrown text
a C++ exception thrown by a registered functiona 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, and build() registers the class (below).
  • The math whitelist goes. ChaiScript's core has no math library, so its adapter binds sqrt, cos, sin, atan2 and floor itself. The JaiScript standard library has them as math::sqrt and the rest.
  • The constants are globals. add_global has no const form; a script that must not reassign a value declares it const, 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:

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++ (ChaiScript)
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==), "==");
C++ (JaiScript)
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:

jaiscript (with the host above)
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); }
output
ogre hp=0 alive=false max=100 monster ogre monster ogre
caught: Cannot assign to 'maxHp': it is read-only

A C++ class that points at its own type binds its self-typed members like any other:

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

C++ (ChaiScript)
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:

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

jaiscript
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());
output
4 3
42 4

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