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
| 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);: 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 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 for an overloaded C++ name); the call resolves by argument types |
Classes
| ChaiScript | JaiScript |
|---|---|
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 engine | a static jai::registrar<T, void> with a configure lambda, and jai::bind_registrar(eng) per engine |
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 |
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, 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:
#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");#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;
nextis a function namednextthat dot syntax reaches through its first argument. The binder makes them members ofRng, andbuild()registers the class. - The math whitelist goes. ChaiScript's core has no math library, so the ChaiScript adapter binds
sqrt,cos,sin,atan2andflooritself. The JaiScript standard library has them asmath::sqrtand the rest; the script spells the namespace. - The constants are globals.
add_globalhas no const form; a script that must not reassign a value declares itconstitself, or the host runs that declaration.
Loading the game and calling into it change the same way:
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()");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:
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:
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)
.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:
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, 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:
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);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):
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:
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();
}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:
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 emptyThe 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.
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());42 3 / 99 4Interpolation uses backticks
ChaiScript interpolates ${...} inside double quotes. In JaiScript a double-quoted string is literal text, and a backtick string interpolates (Template strings):
int hp = 7;
print("plain: hp is ${hp}");
print(`template: hp is ${hp}`);plain: hp is ${hp}
template: hp is 7Names 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':
int score = 0;
function setup() { score = 10; }
setup();
print(score);10Integers 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):
print(2147483647 + 1);
int big = 9223372036854775807;
try { big = big + 1; } catch (e) { print("caught: ", e); }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.
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");0 is false, "" is false, null is falseClasses 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):
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());1.0 12.0Where 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.