Reflection: reflect::
The reflect:: namespace answers questions about the program at runtime: what classes exist, what fields and methods they carry, and how to read, write, call, and construct by name. Three properties define it.
It is engine truth. The namespace is reserved (a script cannot declare into it; contrast math::, which scripts legitimately extend), and each engine reflects only what it registered. A sandbox engine knows nothing about your gameplay classes.
It is a live query. Every call reads the current class shape, so after a hot reload the same question returns the new answer. There is no snapshot to invalidate; you just ask again.
Its results are plain values: ordinary arrays and maps minted at call time, safe to hold, iterate, and serialize.
Ask: what shape is this?
reflect::type_name(v) is the precise sibling of type_of: both name a class instance by the class it actually holds, and type_name is the sharper of the two elsewhere: a typed container answers its canonical tag ("array<int>") where type_of answers "array". References answer what they refer to, and every other value answers its kind ("int", "string", "null", …).
reflect::fields(v) (on an instance or a class name) returns one entry per field as a map with name, type, kind, access, static, and from (the declaring class). Order is a contract: base class fields first, then own, each in declaration order, never hash order, identical across engines and backends. Non-class values return an empty array, so a property grid can call it on anything.
class Creature { int hp = 10; private: string name = "grim"; }
class Goblin : Creature { float speed = 1.5; }
for (auto f : reflect::fields(Goblin())) {
print("[{}] {} {} (from {})", f["access"], f["type"], f["name"], f["from"]);
}[public] int hp (from Creature)
[private] string name (from Creature)
[public] float speed (from Goblin)Static fields ride the same list with "static": true, after the instance fields of the class that declares them. For C++-bound classes the entries come from the host's property schema, and kind distinguishes plain fields from "signal" and "observable" members.
reflect::methods(v) enumerates callable members the same way: one entry per overload with name, arity, static, access, from, and params (an array of {name, type, default} maps), sorted by name then arity. Property accessors and constructors are not in the list. They are the fields() and construct() surfaces respectively.
class Soldier {
int hp = 10;
int wound(int amount) { hp = hp - amount; return hp; }
private: int covert() { return hp; }
}
for (auto m : reflect::methods("Soldier")) {
print("{} arity {} ({})", m["name"], m["arity"], m["access"]);
}covert arity 0 (private)
wound arity 1 (public)For quick existence checks there are reflect::has_field(v, "name") and reflect::has_method(v, "name"). Both walk the inheritance chain. And to drive a call dynamically, reflect::method_arguments(v, "name") and reflect::function_arguments("name") return the parameter lists: one param-array per overload, because overloads make any single-signature answer ambiguous:
function greet(string who, int times = 1) -> string { return who; }
for (auto p : reflect::function_arguments("greet")[0]) {
print("{}: {} (default? {})", p["name"], p["type"], p["default"]);
}who: string (default? false)
times: int (default? true)Host-registered functions are type-erased on the C++ side, so they answer an empty overload list rather than a fabricated signature.
Act: read, write, call, construct
reflect::get, reflect::set and reflect::invoke are s.hp, s.hp = 42 and s.wound(5) with the name as a string:
class Soldier {
int hp = 10;
int wound(int amount) { hp = hp - amount; return hp; }
}
var s = Soldier();
print("hp = {}", reflect::get(s, "hp")); // reads like s.hp
reflect::set(s, "hp", 42); // writes like s.hp = 42
print("wound(5) -> {}", reflect::invoke(s, "wound", 5));hp = 10
wound(5) -> 37The one rule that governs all of these: reflection ignores script access control, by design. Reflecting a class is an intentional request for its whole shape, so reflect::get, reflect::set, reflect::invoke, reflect::bind and reflect::construct reach private members and private constructors alike; at that level private is a convention, not a guarantee. Everything else still holds: typed fields convert-or-error with the same text as direct syntax, and direct syntax keeps enforcing private from outside the class. The security boundary is the host and the engine, and reflection never crosses it: a C++-bound class shows only what its binding registered (an unbound member stays unreachable, a read-only property stays read-only), builtin values expose no internal state, and engine capabilities such as file access are never reachable. A load follows the same idea: from_json and from_binary reconstitute the saved state, private fields and classes with private constructors included.
class Vault { private: int gold = 100; }
var v = Vault();
try { v.gold = 0; } catch (e) { print("direct syntax: {}", e); }
reflect::set(v, "gold", 7);
print("through reflection: {}", reflect::get(v, "gold"));direct syntax: Cannot access private member 'gold' of class 'Vault'
through reflection: 7reflect::bind(obj, "name") hands back the method as a callable bound to obj: it holds the object and keeps it alive, so calling it later is obj.name(…) on the object it was taken from, even after the variable that held it is reassigned. It is the one spelling of a method taken as a value (obj.name without a call is an error, see Classes). A string, array or map is a value, so reflect::bind refuses one and points to a lambda: [&]() { s.to_upper(); }.
class Counter { int n = 0; void bump() { n += 1; } }
Counter c;
var bump = reflect::bind(c, "bump");
bump();
bump();
print("{}", c.n);2reflect::call("name", args…) is the free-function twin: it resolves a global or namespace-qualified name and calls it. reflect::construct("Class", args…) and reflect::construct_shared("Class", args…) are spawn-by-name: the first is Class(args) (a value: copies are independent), the second is new Class(args) (a shared handle: copies alias one object; see Value Semantics).
function raise(auto x) -> auto { return x + 1; }
namespace tools { function twice(auto x) -> auto { return x * 2; } }
print("{} {}", reflect::call("raise", 41), reflect::call("tools::twice", 21));
class P { int x = 0; P(int v) { x = v; } }
var a = reflect::construct("P", 1); // == P(1)
var b = a; b.x = 9; // copies: a untouched
var sp = reflect::construct_shared("P", 1); // == new P(1)
var tp = sp; tp.x = 9; // shares: sp sees the write
print("value a.x = {}, shared sp.x = {}", a.x, sp.x);42 42
value a.x = 1, shared sp.x = 9The canonical use is a data-driven spawn table: spawned.push(reflect::construct_shared(row["type"], row["hp"]));.
Navigate: the wider world
Beyond one class: reflect::classes() lists every class this engine registered (script-declared and host-bound), sorted. reflect::functions() lists the callable registration surface, namespace-qualified and sorted. Constructors are excluded (they are classes()). reflect::globals() lists global variable names, sorted. reflect::bases(v) gives the base classes in declaration order, empty at a root. reflect::is_cpp_bound(v) tells host-backed apart from script-declared: for a class name it answers how the class was registered, for a value whether a C++ payload backs it. reflect::is_enabled(requirements...) asks whether the host granted privileges (reflect::is_enabled("filesystem"), jai::filesystem, a custom permission name): true only when all are enabled, false for a name the engine does not know (see Where files are).
reflect::instances("Class") returns the live instances of a class in this engine in creation order, the walk a debugger or an editor's scene panel wants, without keeping its own registry:
class Tracked { int n = 0; }
var a = new Tracked(); var b = new Tracked(); a.n = 7;
var total = 0;
for (auto t : reflect::instances("Tracked")) { total = total + t.n; }
print("{} live, n-sum {}", reflect::instances("Tracked").size(), total);2 live, n-sum 7Hot reload: ask again
Because every query is live, recapture after a reload is just re-asking. reflect::generation("Class") is the cheap poll for tools: it bumps whenever the class is redefined, so a property grid re-reads fields() only when the number moved.
class Evolver { int a = 1; }
auto g0 = reflect::generation("Evolver");
print("has b? {}", reflect::has_field("Evolver", "b"));
class Evolver { int a = 1; int b = 2; } // hot reload: same engine, new shape
print("generation moved: {}", reflect::generation("Evolver") > g0);
print("has b? {}", reflect::has_field("Evolver", "b"));has b? false
generation moved: true
has b? trueThe namespace is reserved
Reflection answers are engine truth, so the spelling cannot be impersonated: declaring into reflect:: is an error at declaration time on both backends.
namespace reflect { function evil() { return 1; } }== error: Namespace 'reflect' is reserved for the engine (reflect:: is C++-registered only)This is deliberately the opposite of math::, which scripts extend freely: script-contributed math is legitimate math; a script reflect::fields would be a lie about the engine.
Where to next
Three chapters remain: Types That Fail Early gathers everything the checker does for you in one place: the load/run-time split you have met throughout, the diagnostic format, cast<T>, extern, and the [[warn_mode]] / [[strict_mode]] attributes; Time covers clocks, stopwatches and dates; Math covers the math:: library and the vector types.