JaiScript · scripting for C++ engines
Edit. Save.
It's already running.
An embedded scripting language for C++ engines. It reads like the C++ you already write, copies the way C++ copies, stops typed mistakes before the program runs, reloads classes without losing the world, and splits a loop it can prove safe across worker threads with the serial loop's exact result. Underneath, two independent backends (a tree-walking interpreter and a bytecode VM) must print the same output and the same error text, byte for byte.
every standalone script sample on this site runs on both backends, and the output shown is what they print
- C++ value semantics
- hot reload keeps state
- strict checker at load
- coroutines
- parallel_for
- guides from JS / C++ / C#
// raise spd: a faster gator class Gator : Mob { int spd = 1; int slices = 0; void tick() { step_toward(cake, spd); if (beside(cake)) { slices++; say("Nom."); } } } Gator gator; // kept by every reload void hot_reload_migrate(map<string, var> previous) { gator.spd = Gator().spd; // live gator takes the edit }
Get JaiScript
Clone the repository, build the runner with one command, and play Gator Quest on it. The build needs a C++20 compiler, CMake 3.25+ and Ninja; on Windows, run it from a Visual Studio developer prompt.
git clone https://github.com/Devacor/jaiscript.git
cd jaiscript
cmake --workflow --preset runner
build/bin/jaiscript examples/gatorquest/scripts/gatorquest.jaiTo embed it, add the folder as a CMake subproject, fetch it with FetchContent, install it as a
package, or drop the sources into a Visual Studio or Xcode project: Building
JaiScript into a Host has each route in full.
Two lines to hello
auto e = jai::engine::make();
e->execute("print(\"hello\");");Yes, really two lines: a bare engine ships a basic print. One more,
jai::stdlib::register_all(e);, adds the formatted {} print and the
standard library (math, containers, JSON). It is a static lib plus headers, with no global interpreter state.
The embedding chapters take it from there: bind a C++ class in a
fluent chain (one declaration makes a field reflected,
script-visible and serialized), connect host signals to script, give a script a budget and a memory cap,
and hot-reload it. Every sample on this site assumes the stdlib is registered. The standalone runner does
that for you, and the two-line host needs the register_all line first. Here is what the
language itself looks like:
var anything = 5; // dynamic — the explicit, visible opt-out
anything = "now a string"; // fine; you asked for this
class Hero {
int hp = 10;
void heal(int amount) { hp += amount; }
}
function bump(int& x) { x += 1; } // mutation is visible at both ends
auto h = Hero();
h.heal(5);
bump(h.hp); // fields and elements bind by reference too
print("hp = {}", h.hp);hp = 16No host yet? The repository ships a standalone runner. Save the script above as hello.jai and
run jaiscript hello.jai. Strict checking and the VM backend are the defaults, jaiscript
alone is a REPL, and jaiscript --watch hello.jai re-runs on every save.
Building the runner is one CMake target;
its flags are on the reference page.
A web application is a host away too: jai_spider app.jai spins the app over HTTP, with SQLite, cookie sessions,
static files and the spider:: client, and applies each saved edit to the next request (jai_spider).
Coming from another language
The Language Bridges start from what you already write. Each sets JavaScript, C++ or C# beside JaiScript idiom by idiom, then shows, running, the habits that get a different answer. C++ code moving from Cereal, Boost.Serialization, Boost.Signals2, ChaiScript, sol2, Squirrel, pybind11, nlohmann::json, Qt properties, RTTR or coroutine schedulers starts in the Code Migrations, which map each library's calls to JaiScript's and convert real code.
The seven pillars
01The type ladder
int x is enforced. auto x is inferred, then enforced. var x is
dynamic, a choice you can see in a diff, not a default you fall into. The static checker (always on,
always strict) maps onto it exactly: typed code gets load-time errors before a statement runs, var
opts out (like TypeScript's any, but designed-in), and [[warn_mode]] lets a region's
errors through as warnings.
02Value semantics, references by declaration
Assignment, parameters, and returns copy. Nothing aliases at a distance; a function cannot mutate your
data unless its signature says &. Deterministic saves, no spooky mutation, and
to_json of any value means what you think it means.
03Two implementations, one executable spec
A tree-walking interpreter and a bytecode VM, required to agree byte-for-byte (including error text), enforced by more than 4,700 tests run against both, plus a differential fuzzer that files a bug whenever they disagree. Semantics cannot rot silently. The one documented exception is recursion depth: the backends cap it differently, and the VM reuses a bare self tail call's frame.
04Zero global state
No statics, no singletons; every value carries its engine. Two engines on two threads need no locks,
tests cannot contaminate each other, and in-language parallelism (parallel_for,
parallel_transform, deterministic at any worker count) falls out of this property instead of
fighting it.
05Hot reload that never bricks
Redefine a class mid-session and live instances migrate: fields keep values, changed types convert permissively, new fields take their initializers. Migration never fails. A reload never bricks an instance; a source that does not parse or check simply does not load, and the old class keeps running. Code already running when the reload lands, a suspended coroutine included, finishes on the version it started with. The dungeon crawler reloads its own scripts mid-game on a keypress.
06Safe at the boundary, by construction
Beyond the core builtins, a script can only touch what the host registered. The
binding surface is the allowlist.
Checked integer overflow, catchable recursion limits and an execution budget are on by default; a memory
cap is one call, and so is switching off the core's file loading (include,
import), which is on by default. A hostile script cannot catch-and-swallow its way past the
budget or the cap. Untrusted scripts are a configuration, not a rewrite, though it is not an OS sandbox.
07Parse once, run anywhere
engine->jaibite(source) parses once, executes many, and save("level.jaibite")
ships precompiled: symbols relocate on load, so one file serves any engine. Even naive
execute(str) in a loop pays the parse once via the source cache.
The receipts
script_value size, static-assert-gated (four per cache line)[[warn_mode]] lets one file, block or statement through as warningsThe honest tradeoffs
- Copies cost. Value semantics means passing a big structure by value deep-copies it.
The idiom is
¶meters for anything heavy, cheap by design. If you write Python-shaped code expecting reference semantics, your first hour will surprise you; read Value Semantics first and it won't. - Lua is faster. On microbenchmarks the VM wins or ties Lua 5.4 on statements, containers, range-for and method calls; Lua runs a hot arithmetic loop about 3× faster and call-dense recursion about 2.8× faster. On whole programs Lua 5.4 is ahead by 1.2 to 1.6× on the games (Gator Quest, GLOOM) and by about 2.4× on the object-heavy benchmark apps (Richards, DeltaBlue), the VM is ahead on the raymarched OSSUARY scene and the bytecode-interpreter app, and LuaJIT is far ahead of both. JaiScript's VM does reuse the frame for a bare self tail call, so tail recursion runs at loop speed to any depth. The head-to-head numbers, one machine, one protocol: Performance.
- Two backends cost maintenance. That's the price of the executable spec, and shared semantic kernels keep the duplicated surface small.
Who it's for
Anyone embedding scripting in a C++ engine who wants iteration speed (hot reload, script classes), safety
rails a host can tighten in a few calls, types that scale from prototype (var) to production (int,
checked strictly) within the same file, and a language whose behavior is enforced by two
implementations arguing with each other. It was built alongside a real 2D game engine, hardened by
adversarial review and differential fuzzing, and dogfooded by building
real, playable things in it. The source
is MIT-licensed, and Stability lists what is settled before 1.0 and what may still change.
If you ship a game or a tool with JaiScript inside, a small line in its credits, such as “Scripted with JaiScript”,
would be lovely to see. It is a request, not a condition: the MIT license is the only term that applies.