From ChaiScript Bindings​

ChaiScript and JaiScript embed in a C++ program 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 is the shape of a class registration (one fluent dynamic_binder chain in place of separate add calls), the standard library (opt-in), the way a call into script reports failure, and the script language itself: typed where you want it, statically checked before it runs, and copying containers on assignment. This page maps the ChaiScript API, converts a real host's binding code, and then lists the script 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, <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);: the standard library is opt-in
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 for an overloaded C++ name); the call resolves by argument types

Classes​

ChaiScriptJaiScript
chai.add(user_type<T>(), "T");jai::dynamic_binder<T>(*eng, "T") ... .build();
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

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
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, among the comparison ports, a ChaiScript one. 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. ChaiScript:

C++ (ChaiScript)
#include <chaiscript/chaiscript.hpp>

chaiscript::ChaiScript chai;
using namespace chaiscript;

chai.add(user_type<gloom_rng>(), "Rng");
chai.add(constructor<gloom_rng(int64_t)>(), "Rng");
chai.add(fun(&gloom_rng::next), "next");
chai.add(fun(&gloom_rng::roll), "roll");
chai.add(fun(&gloom_rng::chance), "chance");
chai.add(fun(&gloom_rng::nextf), "nextf");
chai.add(fun(&gloom_rng::state), "state");

chai.add(fun([log](const std::string& line) { log(line); }), "host_log");
chai.add(fun([](double v) -> int64_t { return static_cast<int64_t>(v); }), "itrunc");
chai.add(fun([](double v) -> double { return std::sqrt(v); }), "sqrt");

chai.add_global_const(const_var(std::string("\x1b")), "ESC");
chai.add_global_const(const_var(opt.seed), "HOST_SEED");
chai.add_global_const(const_var(opt.god), "HOST_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 class is one statement. ChaiScript registers the type, its constructor and each method as separate entries in the engine; next is a function named next that dot syntax reaches through its first argument. The binder makes them members of Rng, and build() registers the class.
  • The math whitelist goes. ChaiScript's core has no math library, so the ChaiScript adapter binds sqrt, cos, sin, atan2 and floor itself. The JaiScript standard library has them as math::sqrt and the rest; the script spells the namespace.
  • The constants are globals. add_global has no const form; a script that must not reassign a value declares it const itself, or the host runs that declaration.

Loading the game and calling into it change the same way:

C++ (ChaiScript)
chai.eval_file(scripts_dir + "/main.chai");

auto frame = chai.eval<std::function<std::string(double, const std::string&, double, double, double)>>("gloom_frame");
std::string out = frame(dt, key, fps, ms_sim, ms_draw);

chaiscript::Boxed_Value bv = chai.eval("gloom_state_hash()");
int64_t hash = chaiscript::Boxed_Number(bv).get_as<int64_t>();
bool quit = chai.eval<bool>("gloom_wants_quit()");
C++ (JaiScript)
eng->add_include_path(scripts_dir);
eng->execute("import \"main.jai\";");

auto frame = jai::convert_script_function<std::string(double, const std::string&, double, double, double)>(
    eng->get_variable("gloom_frame"));
std::string out = frame(dt, key, fps, ms_sim, ms_draw);

int64_t hash = eng->execute("gloom_state_hash()").as_int();
bool quit = eng->execute("gloom_wants_quit()").as<bool>();

A frame loop that should not throw calls through as_function() instead, and checks the result:

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

Classes in full​

A class with two constructors, an overloaded method, a getter and setter pair, an operator and a base class. The C++ side is ordinary:

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)
    .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("describe", [](std::shared_ptr<Entity> e) { return e->describe(); });

Script reads the pair 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, like the tree node of the comparison benchmarks, binds its self-typed members with jai::skip_type_check, because the type is not registered yet while its own binder runs; ChaiScript's add_class needs no such tag:

C++ (ChaiScript)
chaiscript::ModulePtr m = std::make_shared<chaiscript::Module>();
chaiscript::utility::add_class<CppTreeNode>(*m,
    "CppTreeNode",
    { chaiscript::constructor<CppTreeNode(int)>() },
    { {chaiscript::fun(&CppTreeNode::value), "value"},
      {chaiscript::fun(&CppTreeNode::left), "left"},
      {chaiscript::fun(&CppTreeNode::right), "right"} }
);
m->add(chaiscript::fun(&cpp_insertNode), "cpp_insertNode");
chai.add(m);
C++ (JaiScript)
jai::dynamic_binder<CppTreeNode>(*eng, "CppTreeNode")
    .constructor<int>()
    .property("value", &CppTreeNode::value)
    .property("left", &CppTreeNode::left, jai::skip_type_check)
    .property("right", &CppTreeNode::right, jai::skip_type_check)
    .build();
eng->add_function("cpp_insertNode", &cpp_insertNode);

The ChaiScript benchmark also registers an = for std::shared_ptr<CppTreeNode> so script can reassign a node handle; 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, and jai::bind_registrar applies every registrar to an engine. The registrar's name is also the class's serialization identity (From Cereal):

C++
static jai::registrar<gloom_rng, void> rngRegistrar("Rng", [](jai::dynamic_binder<gloom_rng>& binder) {
    binder.constructor<int64_t>()
        .method("next", &gloom_rng::next)
        .method("roll", &gloom_rng::roll);
});

auto eng = jai::engine::make();
jai::bind_registrar(eng);

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();
}

A host function that throws is part of the script's run. The script can catch it, and an uncaught one reaches the host as a jai::runtime_error with the same text:

C++
eng->add_function("load_level", [](const std::string& name) -> int64_t {
    if (name.empty()) { throw std::runtime_error("level name is empty"); }
    return 1;
});
eng->execute(R"(try { load_level(""); } catch (e) { print("script caught: ", e); })");   // script caught: level name is empty

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 two languages look alike: braces, var, for (x : xs), + on strings. Where a ChaiScript habit gives another result, the cause is usually JaiScript's C++ semantics; From C++ lists them all, and Value Semantics & References is the chapter to read first.

Assignment copies a container​

In ChaiScript, var b = a; of a Vector leaves both names on one vector, and writing through b changes a. A JaiScript array or map is a value: assignment copies it. ChaiScript's reference assignment := is a reference declaration, auto&. push_back is push.

jaiscript
array<int> a = [1, 2, 3];
array<int> b = a;          // a copy
b[0] = 99;
b.push(4);
auto& c = a;               // an alias, ChaiScript's :=
c[0] = 42;
print(a[0], " ", a.size(), " / ", b[0], " ", b.size());
output
42 3 / 99 4

Interpolation uses backticks​

ChaiScript interpolates ${...} inside double quotes. In JaiScript a double-quoted string is literal text, and a backtick string interpolates (Template strings):

jaiscript
int hp = 7;
print("plain: hp is ${hp}");
print(`template: hp is ${hp}`);
output
plain: hp is ${hp}
template: hp is 7

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 (Can not find object: score), and global score = 10; inside a function creates a global when it runs. In JaiScript a program-scope declaration is the global, functions read and write it, and an undeclared name stops the load with use of undeclared identifier 'score':

jaiscript
int score = 0;
function setup() { score = 10; }
setup();
print(score);
output
10

Integers are 64-bit, and overflow is an error​

A ChaiScript integer literal is a C++ int, so 2147483647 + 1 wraps to -2147483648. A JaiScript int is 64-bit, and an operation that overflows it raises an error a catch sees (Checked overflow):

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

Conditions test values​

ChaiScript requires a bool condition and stops with Condition not boolean on if (0). JaiScript tests a value the C++ way: 0, the empty string and null are false.

jaiscript
var zero = 0;
var empty = "";
var nothing = null;
print(zero ? "0 is true" : "0 is false", ", ", empty ? "\"\" is true" : "\"\" is false", ", ", nothing ? "null is true" : "null is false");
output
0 is false, "" is false, null is 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. An instance is a value, and assignment copies it (Classes):

jaiscript
class Point {
    float x;
    float y;
    Point(float px, float py) { x = px; y = py; }
    float sum() { return x + y; }
}
Point p = Point(1.0, 2.0);
Point q = p;
q.x = 10.0;
print(p.x, " ", q.sum());
output
1.0 12.0

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. Script-Extensible Objects covers a script class deriving a bound C++ class, which ChaiScript has no counterpart for.