From pybind11 and Boost.Python​

pybind11 and its ancestor Boost.Python describe a C++ class to Python with one class_ declaration: constructors, methods, fields, properties, bases, operators, chained off one object. jai::dynamic_binder is the same shape, and most of a binding converts line for line. What differs sits around the declaration. There is no holder to choose (every bound object lives in a std::shared_ptr), no return value policy, no interpreter lock, and no keyword arguments. A script override does not reach a C++ virtual call, so there are no trampoline classes. The host can run many engines, one per thread at a time. This page maps both libraries, converts a class binding, and then lists the Python habits that get a different answer in the script. 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.

Modules and the interpreter​

pybind11 / Boost.PythonJaiScript
PYBIND11_MODULE(game, m), PYBIND11_EMBEDDED_MODULE(game, m), BOOST_PYTHON_MODULE(game)bindings run against an engine: binder chains and add_function calls in a function the host calls per engine, or a static jai::registrar<T, void> per class applied with jai::bind_registrar(eng)
py::scoped_interpreter guard{};, Py_Initialize()auto eng = jai::engine::make();; engines share nothing, so a program runs as many as it needs
the GIL, py::gil_scoped_release / gil_scoped_acquireno lock: an engine serves one thread at a time, and another thread makes its own engine
py::exec(code), py::eval_file(path), bp::execeng->execute(code), eng->execute_file(path)
py::module_::import("enemies"), import enemies in scriptimport "enemies.jai";, resolved through eng->add_include_path(dir)
m.attr("HOST_SEED") = 42;, bp::scope().attr(...)eng->add_global("HOST_SEED", int64_t{42});

Functions​

pybind11 / Boost.PythonJaiScript
m.def("load_level", &load_level);, bp::def(...)eng->add_function("load_level", &load_level);
two m.def of one name with py::overload_cast<int64_t>(&f)two add_function of one name, with static_cast for an overloaded C++ name; the call resolves by argument types
py::arg("speed") = 1.0, keyword argumentsnone: script calls are positional, as in C++. A parameter with a default becomes an overload without it
py::args, py::kwargseng->add_variadic_function("f", fn), whose fn receives a const std::vector<jai::script_value>&
std::function parameters through pybind11/functional.ha std::function parameter takes a script function or lambda as is

Classes​

pybind11 / Boost.PythonJaiScript
py::class_<Monster, Entity, std::shared_ptr<Monster>>(m, "Monster"), bp::class_<Monster, bp::bases<Entity>, std::shared_ptr<Monster>>("Monster")jai::dynamic_binder<Monster>(*eng, "Monster").base_class<Entity>() ... .build();
.def(py::init<std::string, int64_t>()), bp::init<...>().constructor<std::string, int64_t>(); script Monster(...) makes a value and new Monster(...) a shared_ptr
.def("alive", &Monster::alive).method("alive", &Monster::alive)
.def_readwrite("name", &Entity::name).property("name", &Entity::name)
.def_readonly(...), .def_property_readonly(...).property("max_hp", getter, nullptr); a store raises Cannot assign to 'max_hp': it is read-only
.def_property("hp", &Monster::health, &Monster::setHealth), .add_property(...).property("hp", &Monster::health, &Monster::setHealth)
.def(py::self == py::self), .def(bp::self == bp::self).method("==", &Monster::operator==); != follows from ==
.def_static(...), .staticmethod(...).static_method("name", fn), called as Monster::name()
py::enum_<Mood>, bp::enum_an enum crosses as its integer; a script enum with the same values names them
a trampoline class with PYBIND11_OVERRIDE, bp::wrapperno counterpart. A script class can derive the bound class and override a method (extend), and the override runs wherever script calls it; a call C++ makes runs the C++ body. C++ reaches script through a std::function parameter or a signal
py::dynamic_attr()no ad-hoc attributes: a script class that derives the bound class declares its fields

Holders and lifetime​

pybind11 / Boost.PythonJaiScript
a holder type per class (std::unique_ptr by default, std::shared_ptr when named)no choice to make: a bound object lives in a std::shared_ptr
returning or passing a std::shared_ptr<T>the same object on both sides: eng->make_object(ptr) hands one to script, v.as<std::shared_ptr<T>>() takes it back
py::return_value_policy::reference, reference_internal, py::keep_aliveno policies. eng->add_global_ref("player", player) binds an object the host keeps alive, and a bound member of a registered class type refers to the live member
py::none() into a std::shared_ptr<T> parameterscript null arrives as an empty std::shared_ptr or std::weak_ptr

Values, calls and errors​

pybind11 / Boost.PythonJaiScript
py::object, o.cast<T>(), bp::extract<T>(o)jai::script_value, v.as<T>()
py::list, py::dict, the conversions in pybind11/stl.harrays and maps, converted to and from std::vector and std::map without an extra header; eng->make_array(), eng->make_map()
py::module_::import("__main__").attr("damage_for")(4).cast<int64_t>()jai::convert_script_function<int64_t(int64_t)>(eng->get_variable("damage_for"))(4), which throws on failure, or eng->get_variable("damage_for").as_function()({args}), a jai::checked_result
py::error_already_set, bp::error_already_set with PyErr_Print()jai::runtime_error (and jai::script_exception for an uncaught script throw), jai::parse_error, jai::static_check_error; eng->format_stack_trace()
a C++ exception becoming a Python exception (std::runtime_error is RuntimeError), py::register_exceptiona C++ exception becomes a run-time error the script catches; catch (e) receives its what() text

A class converted​

A class with two constructors, an overloaded method, a property with a getter and a setter, a read-only property, 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; }
};

int64_t load_level(const std::string& name) {
    if (name.empty()) { throw std::runtime_error("level name is empty"); }
    return 1;
}
C++ (pybind11)
#include <pybind11/embed.h>
#include <pybind11/operators.h>

namespace py = pybind11;

PYBIND11_EMBEDDED_MODULE(game, m) {
    py::class_<Entity, std::shared_ptr<Entity>>(m, "Entity")
        .def_readwrite("name", &Entity::name)
        .def("describe", &Entity::describe);

    py::class_<Monster, Entity, std::shared_ptr<Monster>>(m, "Monster")
        .def(py::init<>())
        .def(py::init<std::string, int64_t>())
        .def("damage", py::overload_cast<int64_t>(&Monster::damage))
        .def("damage", py::overload_cast<int64_t, double>(&Monster::damage))
        .def("alive", &Monster::alive)
        .def_property("hp", &Monster::health, &Monster::setHealth)
        .def_property_readonly("max_hp", [](const Monster&) { return int64_t{100}; })
        .def(py::self == py::self);

    m.def("describe", [](std::shared_ptr<Entity> e) { return e->describe(); });
    m.def("load_level", &load_level);
    m.attr("HOST_SEED") = 42;
}
C++ (Boost.Python)
#include <boost/python.hpp>

int64_t monster_max_hp(const Monster&) { return 100; }

BOOST_PYTHON_MODULE(game) {
    using namespace boost::python;
    class_<Entity, std::shared_ptr<Entity>>("Entity")
        .def_readwrite("name", &Entity::name)
        .def("describe", &Entity::describe);

    class_<Monster, bases<Entity>, std::shared_ptr<Monster>>("Monster", init<>())
        .def(init<std::string, int64_t>())
        .def("damage", static_cast<void (Monster::*)(int64_t)>(&Monster::damage))
        .def("damage", static_cast<void (Monster::*)(int64_t, double)>(&Monster::damage))
        .def("alive", &Monster::alive)
        .add_property("hp", &Monster::health, &Monster::setHealth)
        .add_property("max_hp", &monster_max_hp)
        .def(self == self);

    def("load_level", &load_level);
}
C++ (JaiScript)
#include <jaiscript/jaiscript.hpp>
#include <jaiscript/stdlib/stdlib.hpp>

void bind_game(jai::engine& eng) {
    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("max_hp", [](const Monster&) { return int64_t{100}; }, nullptr)
        .build();

    eng.add_function("describe", [](std::shared_ptr<Entity> e) { return e->describe(); });
    eng.add_function("load_level", &load_level);
    eng.add_global("HOST_SEED", int64_t{42});
}

The same session in each script:

python
import game
m = game.Monster("ogre", 30)
m.damage(5)
m.damage(10, 1.5)
m.hp = m.hp - 100
print(m.name, "hp=" + str(m.hp), "alive=" + str(m.alive()), "max=" + str(m.max_hp), m.describe(), game.describe(m))
try:
    game.load_level("")
except RuntimeError as e:
    print("script caught:", e)
jaiscript (with the host above)
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.max_hp, " ", m.describe(), " ", describe(m));
try { load_level(""); } catch (e) { print("script caught: ", e); }
output (both)
ogre hp=0 alive=false max=100 monster ogre monster ogre
script caught: level name is empty

(Python prints False where the JaiScript line prints false.) The holder argument and the separate module function are the only lines without a counterpart; the binding itself is a one-for-one rename.

Calling into script​

C++ (pybind11)
py::scoped_interpreter guard{};
py::exec("def damage_for(level):\n    return level * 3\n");
py::object damage_for = py::module_::import("__main__").attr("damage_for");
int64_t dmg = damage_for(4).cast<int64_t>();                   // 12

auto boss = std::make_shared<Monster>("boss", 50);
py::module_::import("__main__").attr("boss") = boss;
py::exec("boss.damage(20)");                                     // boss->hp is 30

try {
    py::exec("raise ValueError('from script')");
} catch (const py::error_already_set& e) {
    std::cerr << e.what();
}
C++ (JaiScript)
auto eng = jai::engine::make();
jai::stdlib::register_all(eng);
bind_game(*eng);
eng->execute("int damage_for(int level) { return level * 3; }");
auto damage_for = jai::convert_script_function<int64_t(int64_t)>(eng->get_variable("damage_for"));
int64_t dmg = damage_for(4);                                     // 12

auto boss = std::make_shared<Monster>("boss", 50);
eng->add_global("boss", eng->make_object(boss));
eng->execute("boss.damage(20);");                                // boss->hp is 30

try {
    eng->execute("throw \"from script\";");
} catch (const jai::script_exception& e) {
    std::cerr << e.what();                                       // from script
}

A frame loop that should not throw calls through as_function() and tests the jai::checked_result it returns; the engine is ready for the next call after a failed one (Error surfaces).

Python habits that get a different answer​

JaiScript's semantics are C++'s. A dynamic-language habit that changes is usually covered in From JavaScript, and Value Semantics & References is the chapter to read first. The spellings:

PythonJaiScript
indentationbraces and semicolons
def f(x):int f(int x) { ... }, or function f(x) { ... } with types optional
True, False, None, and, or, nottrue, false, null, &&, ||, !
len(xs), xs.append(v)xs.size(), xs.push(v)
7 / 2 is 3.5, 7 // 2 is 37 / 2 is 3, 7.0 / 2 is 3.5; // starts a comment
f"hp {hp:.1f}"`hp ${hp:.1f}`, a backtick template string with the same format specs
self.count += 1 in a methodcount += 1: fields and methods are in scope
b = a on a list, a dict or an objecta copy; auto& b = a; or a shared_ptr shares
raise, try / exceptthrow, try / catch (e)
yield in a function makes a generatora coroutine function (From Lua Coroutines)
jaiscript
array<int> scores = [3, 7];
auto copy = scores;                // a copy, where Python's copy = scores shares the list
copy.push(9);
float hp = 72.456;
print(scores.size(), " ", copy.size(), " ", 7 / 2, " ", 7.0 / 2, " ", `hp ${hp:.1f}`);
output
2 3 3 3.5 hp 72.5
jaiscript
class Counter {
    int count = 0;
    void bump() { count += 1; }    // no self: fields and methods are in scope
}
Counter c;
c.bump();
c.bump();
print(c.count, " ", true && !false, " ", null == null);
output
2 true true

Where to next​

dynamic_binder covers every binder verb, and JAI_PROPERTY classes that are reflected, scripted and saved from one declaration; Script-Extensible Objects covers script classes deriving bound C++ classes; Embedding Quickstart covers globals, calls into script and every error surface.