Embedding Quickstart​

Six steps take a C++ program to a scripted, hot-reloading game loop. JaiScript compiles into your program as plain C++ with no dependencies. An engine is an ordinary object with no global state behind it: two engines are fully isolated, so two threads can each run their own without locks. Every sample here is compiled and run against the library, and the output under it is what it prints.

Two pages back this one up: Building JaiScript into a host has every build route in full, and Host API in depth has the complete set of calls a host makes.

1. Add JaiScript to your build​

A host needs JaiScript's sources compiled in, its include folder on the include path, and C++20. With CMake, add the folder (or fetch it with FetchContent) and link the target, which carries the include path, the standard and the flags:

CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(my_game CXX)
add_subdirectory(JaiScript)
add_executable(my_game main.cpp)
target_link_libraries(my_game PRIVATE JaiScript::jaiscript)

Without CMake, add the one file JaiScript/source/jaiscript.cpp (the whole library) to the project, put JaiScript/include on the include path and select C++20. Visual Studio also needs /bigobj; Xcode needs -ffp-contract=off, which keeps floating-point results identical across platforms. Build integration lists each setting, and Visual Studio, Step by Step walks through it from a fresh install.

2. Run a script and read the result​

jai::engine::make() returns a std::shared_ptr<jai::engine>. execute() runs source text and returns the value of its last expression, which as_int(), as_float(), as_string(), as_bool() or as<T>() converts:

C++ — main.cpp
#include <jaiscript/core/engine.hpp>
#include <jaiscript/core/dynamic_binder.hpp>   // section 3
#include <jaiscript/stdlib/stdlib.hpp>
#include <filesystem>                          // section 5
#include <fstream>
#include <iostream>

int main() {
    auto eng = jai::engine::make();                    // an engine is an object: make as many as you like
    jai::stdlib::register_all(eng);                    // formatted print, to_string, JSON, random, time

    eng->execute(R"(print("hello from {}", "JaiScript");)");

    jai::held_value r = eng->execute("6 * 7");         // the value of the last expression
    std::cout << "6 * 7 = " << r.as_int() << "\n";
    std::cout << eng->execute(R"("jai" + "script")").as<std::string>() << "\n";
    std::cout << std::boolalpha << eng->execute("2.5 * 2 > 4").as<bool>() << "\n";
    return 0;
}
output
hello from JaiScript
6 * 7 = 42
jaiscript
true
  • The standard library is opt-in. A bare engine runs the whole language with a concatenating print, template strings, the container and string methods, math::, regex and binary::. jai::stdlib::register_all adds formatted printing, to_string, type_of, JSON, random, time::, crypto:: and reflect::. The reference lists both sets.
  • Output goes to std::cout unless set_output_stream names another stream.
  • Keep a value in a jai::held_value. execute, execute_file, get_variable and hold(v) return one. It stays readable as long as the engine lives and never keeps the engine alive; after the engine is destroyed, reading it throws Value outlived its engine. A plain jai::script_value, such as an argument your C++ function receives, is a borrow for the duration of the call: store it with eng->hold(v). Values pass between engines by copy (Host API).

3. Bind a class and a function​

A C++ class needs no macros. jai::dynamic_binder names the constructors, members and methods scripts may use, and add_function registers any C++ callable. From here on the samples run inside main, after the engine is made:

C++
struct Player {
    std::string name;
    int64_t hp = 100;
    explicit Player(std::string a_name) : name(std::move(a_name)) {}
    void hit(int64_t damage) { hp -= damage; }
};

// in main, after the engine is made:
jai::dynamic_binder<Player>(*eng, "Player")
    .constructor<std::string>()
    .property("name", &Player::name)
    .property("hp", &Player::hp)
    .method("hit", &Player::hit);
eng->add_function("roll", [](int64_t sides) { return sides / 2 + 1; });   // any C++ callable

auto hero = std::make_shared<Player>("Ada");     // a live C++ object the script shares
eng->add_global("hero", eng->make_object(hero));
eng->execute(R"(
    hero.hit(roll(20));
    Player rat = Player("rat");                   // scripts construct the class too
    rat.hit(30);
    print(hero.name, " ", hero.hp, ", ", rat.name, " ", rat.hp);
)");
std::cout << "C++ sees hp " << hero->hp << "\n";
output
Ada 89, rat 70
C++ sees hp 89

make_object hands the script a handle to your live object, so its writes land in C++; an object the script constructs is its own. dynamic_binder covers getters and setters, overloads, operators and inheritance.

4. Call a script function​

A script function is a value. Look it up with get_variable and call it with a vector of arguments; the call returns a checked_result to test before reading value(). convert_script_function wraps the same function in a typed std::function:

C++
eng->execute("int damage_for(int level) { return level * 3; }");

jai::held_value fn = eng->get_variable("damage_for");   // held: safe to keep and call later
jai::checked_result<jai::script_value> r = fn.as_function()({eng->make_value(int64_t{4})});
if (r) { std::cout << "damage_for(4) = " << r.value().as_int() << "\n"; }

auto damage = jai::convert_script_function<int64_t(int64_t)>(fn);   // or a typed std::function
std::cout << "damage(10) = " << damage(10) << "\n";
output
damage_for(4) = 12
damage(10) = 30

5. A hot-reload frame loop​

Put the rules in a module, and call a script function every frame that reaches the import. When the module's file changes, the next call re-runs it and the new functions apply from then on. The sample writes its two files, then saves an edit to rules.jai between frames 2 and 3, as an editor would:

C++
namespace fs = std::filesystem;
const fs::path dir = fs::temp_directory_path() / "jai_quickstart";
fs::create_directories(dir);
auto save = [&](const char* name, const char* text) { std::ofstream(dir / name) << text; };
save("rules.jai", "int damage_for(int frame) { return frame * 2; }");
save("game.jai", "import \"rules.jai\";\n"
                 "int tick(int frame) { import(\"rules.jai\"); return damage_for(frame); }");

eng->import_recheck_interval(0);                  // see a save at once (the default looks every 0.2 s)
eng->execute_file((dir / "game.jai").string());
jai::held_value tick = eng->get_variable("tick");   // kept across frames
for (int64_t frame = 1; frame <= 4; ++frame) {
    if (frame == 3) { save("rules.jai", "int damage_for(int frame) { return frame * 100; }"); }   // an edit lands
    auto r = tick.as_function()({eng->make_value(frame)});
    if (!r) { std::cout << "frame " << frame << ": " << eng->format_error(r) << "\n"; continue; }
    std::cout << "frame " << frame << ": " << r.value().as_int() << "\n";
}
output
frame 1: 2
frame 2: 4
frame 3: 300
frame 4: 400

A changed module is parsed and checked before any of it runs. If the edit has a mistake, that frame's call reports the listing in its result, the program keeps the code it had, and the next save tries again. Hot Reload covers which state survives a reload and how live objects migrate when a class changes.

6. Handle errors​

Two rules cover every case. execute() throws, and everything it throws derives from std::exception. A call through as_function() never throws for what the script did: the failure comes back in its result. Either way the engine is ready for the next call:

C++
try {
    eng->execute("undefined_thing + 1");           // rejected before anything runs
} catch (const std::exception& e) {
    std::cout << e.what() << "\n";
}
try {
    eng->execute(R"(throw "out of mana";)");       // a throw nothing caught
} catch (const std::exception& e) {
    std::cout << "script threw: " << e.what() << "\n";
}
eng->execute("int risky(int x) { return 10 / x; }");
auto r = eng->get_variable("risky").as_function()({eng->make_value(int64_t{0})});
if (!r) { std::cout << "risky(0) failed: " << eng->format_error(r) << "\n"; }   // a call reports, never throws
std::cout << "the engine carries on: " << eng->execute("40 + 2").as_int() << "\n";
output
static check failed:
<script>:1:1: error: use of undeclared identifier 'undefined_thing'
    undefined_thing + 1
    ^

script threw: out of mana
risky(0) failed: Division by zero
the engine carries on: 42

Checking is strict: a mistake the checker can see is reported before a statement runs, with every error in the file listed. Error surfaces lists the exception types and which calls throw; Static Checking covers names the host injects at run time.

Where to go next​

  • Host API in depth: functions, live bindings to C++ variables, reading script state back, per-execution variables and the time source.
  • dynamic_binder: binding classes in full.
  • Safety Rails: budgets, memory caps and what a script can reach.
  • Hot Reload and Serialization: keeping state across edits and saves.
  • examples/embed_quickstart/ in the repository: a complete host with a bound class, a signal, saves, hot reload, rails and the debugger.