Host API in Depth​

The quickstart covers a host's first day. This page is the rest of jai::engine's host API. The rules that run through all of it:

  • A call fails one way. A call that returns a checked_result reports every failure in it; a call that returns a plain value throws. The engine is usable after either.
  • A value belongs to its engine and its engine's thread. Values cross engines by copy and die with their engine.
  • A host T& parameter writes back into the script lvalue it was given; const T& and by-value parameters only read.
  • Write-through belongs to a registered name, never to a copy stored somewhere else.
  • Declare before loading. Registrations the checker reads (functions, constant arguments, nodiscard) come before the scripts that use them.

Registering C++ functions​

C++
// Typed: the signature drives conversion and overload resolution
eng->add_function("square", [](jai::script_int n) { return n * n; });
std::cout << eng->execute("square(12)").as_int() << "\n";

// Variadic: the engine and every argument of the call
eng->add_variadic_function("sum_all", [](jai::engine&, std::span<const jai::script_value> args) {
    jai::script_int total = 0;
    for (const auto& a : args) total += a.as_int();
    return total;
});
std::cout << eng->execute("sum_all(1, 2, 3, 4)").as_int() << "\n";
output
144
10

Registering a name again with another signature adds an overload, resolved by argument types at the call. A variadic function returns what a typed one returns, a script_value, or a checked_result<script_value> whose error the script receives. A script_value it constructs takes the engine it is given (jai::script_value(total, &eng)) or comes from eng.make_value(v), make_null(), make_array() or make_map(). A callable of the argument vector alone, const std::vector<jai::script_value>&, returning a checked_result<script_value>, registers as a variadic function too, and captures the engine it needs.

The raw-arity and overload registrations, custom and container conversions, class and backend queries, text_of, the app folders and set_output_stream: registration and engine queries.

A direct entry for a hot variadic function​

A typed add_function binding has a fast call path unless a parameter is a non-const reference to a value (a T& that writes back). const std::string&, a bound object's T&, const std::vector<jai::script_value>& and const jai::script_value& parameters receive the script's own values, never copies. An engine-first variadic function has one as well: bound alone under its name, with no requirement, it runs on the arguments where they are for a VM call of 1 to 8 arguments, none of them a reference, with no argument vector and no std::function call. Every other call reaches the same callable through the function.

C++
eng->add_variadic_function("label", [](jai::engine&, std::span<const jai::script_value> args) -> std::string {
    if (args.size() != 1) return "?";
    return args[0].is_int() ? "int" : args[0].is_float() ? "float" : "other";
});
std::cout << eng->execute("label(2.5)").as_string() << "\n";
output
float

A variadic function of the argument vector pays a std::function call and an argument vector per call. add_host_direct(name, arity, invoke, target) gives it, or any function a name binds, an entry over the arguments where they are:

C++
// one body for both: the function and its direct entry
static jai::checked_result<jai::script_value> label_of(const void* target, const jai::script_value* args, size_t count) {
    auto* eng = static_cast<jai::engine*>(const_cast<void*>(target));
    if (count != 1) return jai::script_value(std::string("?"), eng);
    return jai::script_value(std::string(args[0].is_int() ? "int" : args[0].is_float() ? "float" : "other"), eng);
}

eng->add_variadic_function("label", [e = eng.get()](const std::vector<jai::script_value>& args) {
    return label_of(e, args.data(), args.size());
});
eng->add_host_direct("label", 1, &label_of, eng.get());   // after the function: it keys on the function "label" binds now
std::cout << eng->execute("label(2.5)").as_string() << "\n";
output
float

The VM calls invoke(target, args, arity) for a call of exactly arity arguments, none of them a reference; jai::engine::host_direct_any_arity takes any call of 1 to 8 arguments. Every other call (another count, a reference argument, a name re-registered or rebound) calls the function, so invoke must return what the function returns. Sharing one body keeps them equal.

Globals: copies and live bindings​

C++
eng->add_global("PI", jai::script_value(3.14159, eng.get()));   // copied in

int hp = 100;
eng->add_global_ref("hp", hp);                    // a live binding: no copy
eng->execute("hp = hp - 25;");
std::cout << "hp after script damage: " << hp << "\n";
hp = 1;                                           // and the script sees C++ writes
std::cout << "script sees: " << eng->execute("hp").as_int() << "\n";
output
hp after script damage: 75
script sees: 1

add_global_ref on a primitive (int8 to int64, the unsigned widths, float, double, bool, char, std::string) binds the C++ variable itself. For any other type it holds a non-owning pointer. Either way the variable must outlive the engine.

Script doesThe C++ variable
reads the nameis read live, every time
hp = 5, hp += 7, ++hp, hp--, string +=is written, at every width. boundInt += 2.5 truncates and still writes through. A bound char is a one-byte int and reads as its code.
auto& r = hp;, then writes r; passes hp to a script int& parameter; auto& h = player.hp; on a bound objectis written: a reference aliases it
stores it in an array element, a map slot or an object member ([hp], {"k": hp}, push, assignment)is snapshotted: the stored copy is detached
(host) clone() of the bound script_valueis snapshotted. A plain copy of the script_value stays bound.

Namespace members​

A qualified name in any registration call (add_global("game::tick", v), add_global_ref, add_function("game::spawn", f)) registers a member of that namespace.

C++
int64_t tick = 41;
eng->add_global_ref("game::tick", tick);
eng->add_function("game::spawn", [](int64_t n) { return n * 2; });
eng->execute(R"(
    namespace game { int next() { return tick + spawn(1); } }   // the namespace's code reads them bare
    print(`${game::tick} ${game::next()}`);
)");
output
41 43
  • It is read-only from script: game::tick = 1 raises Cannot assign to 'tick': it is read-only. The host changes it through the live binding or by registering the name again.
  • A unit loaded before the registration declares it: extern game::tick; (extern).
  • The host names it qualified: get_variable("game::tick"), has_function("game::spawn").

References into C++: as<T&> and T& parameters​

A non-const as<T&> on a bound value is a live reference into the C++ variable, but only when the bound type is exactly the script-side type: int64_t for as<jai::script_int&>, double for as<jai::script_float&>, bool for as<jai::script_bool&>, std::string for as<std::string&>. Any other width throws jai::runtime_error:

C++
jai::script_int gold = 10;
eng->add_global_ref("gold", gold);
auto v = eng->execute("gold");                    // a bound alias, not a snapshot
v.as<jai::script_int&>() += 5;
std::cout << "gold after as<script_int&> += 5: " << gold << "\n";

int32_t narrow = 5;
eng->add_global_ref("narrow", narrow);
try {
    eng->execute("narrow").as<jai::script_int&>();   // an int32_t is not a script_int
} catch (const jai::runtime_error& e) {
    std::cout << "int32 as<script_int&>: " << e.what() << "\n";
}
eng->execute("narrow += 7;");                     // by name every width works
std::cout << "narrow after script += 7: " << narrow << "\n";
output
gold after as<script_int&> += 5: 15
int32 as<script_int&>: cannot bind a non-const script reference to a C++-bound variable of a different width
narrow after script += 7: 12

A non-const T& parameter of a bound function works on any script lvalue:

C++
eng->add_function("heal", [](int64_t& hp, int64_t amount) { hp = std::min<int64_t>(hp + amount, 100); });
eng->add_function("tag", [](std::string& name) { name += " (healed)"; });
jaiscript (with the host above)
int hp = 40;
heal(hp, 25);             // hp is 65
var party = [10, 20];
heal(party[1], 90);       // party[1] is 100
tag(player.name);         // a field, a map value, a C++ property (through its setter), v.x of a vector
heal(hp + 1, 5);          // a temporary: the callee works on a copy, nothing is stored back
  • The callee gets a copy of the argument's value. When it returns or throws, its final value is stored back with the store a script = makes, so the variable's declared type converts or rejects it. A bound argument of exactly matching width is aliased instead.
  • Every lvalue writes back (a variable, a field, an element, a map value, a static, a reference, a C++ property, a math component), and every value kind: numbers, bool, std::string, std::vector, std::map, jai::script_value, std::shared_ptr<T>, enums, math-adapted types. A registered class taken as T& is the live object and needs no store.
  • A binding with no non-const T& parameter has no write-back step.

Running scripts​

C++
eng->add_include_path("scripts");           // where import/include look after the file's own folder
eng->execute_file("scripts/game.jai");      // parse (or load scripts/.cache/game.jai.jaibite), check, run
  • What a script declares (functions, classes, globals) stays in the engine for later calls.
  • execute_file attributes every node to the file's canonical path: check listings, stack traces and debugger breakpoints name it. Code from execute(source) is <script>.
  • execute_file keeps a parsed copy at .cache/game.jai.jaibite beside the source. Add .cache/ to .gitignore, move it with eng->jaibite_cache_directory(dir, root) (jaibite), or turn it off with eng->jaibite_cache(false).
  • execute(source) keeps the last 64 distinct sources parsed and compiled, so repeating a string skips both. For hot scripts hold a jaibite.

Per-execution variables​

execute(source, vars) and execute_file(path, vars) take a jai::instance_variables (a std::unordered_map<std::string, jai::script_value>). A non-empty map is a scope over the globals for that one execution:

C++
eng->execute("var counter = 0;");                        // no overlay: a global

jai::instance_variables locals{{"increment", eng->make_value(int64_t{5})}};
eng->execute("counter = counter + increment;", locals);  // reads the overlay, writes the global
eng->execute("var temp = 1;", locals);                   // temp is declared in the overlay

eng->execute("counter").as_int();                        // 5
eng->execute("increment");                               // raises: the overlay is gone
eng->execute("temp");                                    // raises: temp died with the overlay
  • A supplied name shadows a global of the same name for that execution; the checker treats it as declared.
  • Top-level declarations land in the overlay and vanish with it. With an empty map they are globals.
  • It is not dynamic scope: a function defined by an earlier execute does not see a later execute's overlay.
  • A script-text receiver on a jai::signal runs this way, the emit's arguments as the overlay (Connecting from script).

Calling script from C++​

A script function is a value: look it up and call it with a vector of arguments.

jaiscript
int damage_for(int level) { return level * 3; }
C++
// examples/embed_quickstart makes these calls
jai::script_value fn = eng->get_variable("damage_for");
jai::checked_result<jai::script_value> r = fn.as_function()({eng->make_value(int64_t{4})});
if (!r) { throw std::runtime_error(eng->format_error(r)); }   // "Division by zero", ...
int64_t dmg = r.value().as_int();                                  // 12
  • The result carries every failure, so a result check is all a frame loop needs. eng->format_error(r) is the text and eng->format_stack_trace() the script stack.
  • A typed wrapper throws instead. jai::convert_script_function<int64_t(int64_t)>(fn) gives a std::function whose failures leave as jai::runtime_error. A null value converts to an empty std::function (test it with if (cb)); any other non-function makes the conversion throw.
  • A call made while no script runs is a host entry and gets a fresh execution budget, like execute() (Safety Rails).
  • A call made while a script runs (from a host function the script called, or a signal it emitted) needs jai::engine::external_call_guard guard(eng.get()); around it, so reference parameters do not bind against the script call in flight. The guard is harmless elsewhere.

Reading script state back​

jaiscript
int score = 7;
array<string> log = ["spawned", "hit"];
map<string, int> kills = {"orc": 3, "troll": 1};
array<var> loot = [12, "rope", 2.5];
C++
// examples/embed_quickstart makes these calls
int64_t score = eng->get_variable("score").as_int();
auto log   = eng->get_variable("log").as<std::vector<std::string>>();              // a typed array copies out
auto kills = eng->get_variable("kills").as<std::map<std::string, int64_t>>();

jai::script_value loot = eng->get_variable("loot");   // hold the value: as_array() views into it
const std::vector<jai::script_value>& items = loot.as_array();       // array<var> only

jai::script_value log_value = eng->get_variable("log");
log_value.unchecked_array_node()->for_each(eng.get(), [](const jai::script_value& line) {
    std::cout << line.as_string() << "\n";                         // any array kind, no copy of the buffer
});
  • Typed arrays are raw buffers and copy out as a std::vector; only array<var> has a script_value vector for as_array() to view. On a typed array as_array() raises an error naming the extraction that works. unchecked_array_node() requires is_array().
  • A bound C++ object comes back with as<std::shared_ptr<Monster>>(), sharing the live object.
  • A host function taking bound_array<int64_t>& mutates a typed array in place (core/bound_array.hpp).
  • Write back with add_global(name, value) or an execute that assigns.

How long a value lives​

ValueValid
a borrow: a script_value C++ receives as an argument, reads out of a container, or copies from a held valuefor the call, like a C++ reference parameter
a jai::held_value: what execute, execute_file, get_variable and eng->hold(v) returnuntil its engine is destroyed. It never keeps the engine alive.
a held value after its engine is gonevalid() is false, get_engine() is null, every read throws jai::runtime_error Value outlived its engine.; copying, assigning and destroying it are safe
  • Teardown. A destroyed engine first releases every held payload while it can still run script, so a held instance's destructor runs then.
  • Threads. Another thread may copy, read, assign or drop a value only while the engine is idle, with the hand-off synchronized as for any C++ object. Debug builds assert when an object dies on another thread while its engine runs.
  • Other engines. A value of another engine passed to add_global, make_value, instance variables or a script function's arguments enters by copy: numbers, strings and math values copy, arrays and maps convert element by element. An object, a function or a reference throws Cannot pass an object from another engine: values cross engines by copy (numbers, strings, math values and arrays and maps of them). (or a function, a reference).

Error surfaces​

Which way a call fails depends on the call, never on what went wrong. The failures are the same for all of them: a parse error, a check error, a run-time error, a script throw nothing caught, and an import whose file is missing or fails its check.

CallA failure
execute(source), execute(source, vars), execute(jaibite&), execute_file(path), execute_file_source, jaibite(source)throws
a std::function from convert_script_function<Sig>(fn), or the std::function parameter of a bound host functionthrows jai::runtime_error
fn.as_function()(args) on a script function, a lambda, a static method or a reflect::bind valuein the checked_result
coroutine_handle::next(engine*, n)in the checked_result
extend_checked(...) / extend(...)in the result / throws
a script receiver of a C++ signalthrows out of the emit; the receivers after it do not run

execute never signals failure through its return value. It returns the value of the script's last top-level expression, and a null value when the script ends in anything else (a declaration, a loop), so result.is_null() after a normal return says only that the script produced no value. A script that does not parse or does not pass the checker throws before any statement runs.

execute<T>(...) is execute(...).as<T>(). It takes what execute takes (a string, a script_source, a string with instance variables) and returns the result as a T: int v = eng->execute<int>("2 + 2");. A result that is not a T throws what as<T>() throws. A script int is 64 bits and int is int32_t, so execute<int> of a value past its range throws Integer value out of range for int; ask for int64_t to take any script int. T is a value (a number, a std::string, a copied object); a reference, a pointer or a std::string_view does not compile, because the result does not outlive the call.

ExceptionRaised forwhat()
jai::parse_errorsyntaxparse failed: and every syntax error with its location
jai::static_check_errorload-time type and name errorsstatic check failed: and every error in the source, compiler style
jai::script_exceptionanything raised while running, an uncaught script throw includedthe error's text, the text a script catch sees

All derive from std::exception, so catch (const std::exception&) is a valid backstop. script_exception derives from jai::runtime_error, which with parse_error derives from jai::exception and carries a location(); static_check_error derives from std::runtime_error, and its listing holds the locations.

C++
try {
    eng->execute("int a = missing_one; int b = missing_two;");
} catch (const jai::static_check_error& e) {
    std::cout << e.what();                        // every error in the source, compiler style
}
output
static check failed:
<script>:1:9: error: use of undeclared identifier 'missing_one'
    int a = missing_one; int b = missing_two;
            ^
<script>:1:30: error: use of undeclared identifier 'missing_two'
    int a = missing_one; int b = missing_two;
                                 ^

A name the host injects later is declared with extern; using it undefined is then a run-time error, raised only if control reaches it: eng->execute("extern function spawn_wave; spawn_wave(3);") throws jai::script_exception 'spawn_wave' is declared extern but never defined.

eng->format_stack_trace() and eng->last_stack_trace() give the script stack of the most recent failure:

C++
try {
    eng->execute("function inner() { throw \"boom\"; }\n"
                 "function outer() { inner(); }\n"
                 "outer();");
} catch (const jai::script_exception& e) {
    std::cout << e.what() << "\n" << eng->format_stack_trace();
}
output
boom
  at inner (<script>:1)
  at outer (<script>:2)
  at <script> (<script>:3)
  • Nested calls. A call made from inside running script (a host function calling back into script, a signal emit) is part of that script's run: its failure travels on to the script, which can catch it.
  • Errors nothing can catch (a destructor failing while the engine tears down its globals) go to the sink eng->set_script_error_handler(...) installs; the default writes to std::cerr.

Constant arguments​

Some arguments must never come from data: SQL text, a shell command. engine::constant_argument(callable, index, refusal) declares that argument index of a host function ("run_sql") or bound method ("Database.query") takes only the program's own constants: a string literal, a const variable initialized with a constant, a ternary whose arms are constants, a + of constants, or an array literal of them. A template string with a splice is not one, nor is anything read, computed or passed in.

C++
jai::engine* raw = eng.get();
eng->add_function("run_sql", [raw](const std::string& sql, const jai::script_value& value) -> std::string {
    if (!raw->constant_argument_admits("run_sql", 0, sql)) {
        throw std::runtime_error("SQL text must be a constant; pass values as parameters (?)");
    }
    return execute_with_parameter(sql, value);      // the text is the program's, the value only binds
});
eng->constant_argument("run_sql", 0, "SQL text must be a constant; pass values as parameters (?)");   // before any script loads
jaiscript (with the host above)
const string FIND = "SELECT * FROM posts WHERE id = ?";
run_sql(FIND, id);                                            // a const
run_sql(newest ? "SELECT * FROM posts ORDER BY id DESC" : "SELECT * FROM posts", 0);
run_sql("SELECT * FROM posts WHERE title = '" + title + "'", 0);   // a load error
  • At load the checker refuses any other argument with the host's refusal: argument 1 of 'run_sql' is not a constant: SQL text must be a constant; pass values as parameters (?). For a method it does so wherever it knows the receiver's class.
  • At run time nothing rests on the checker. As each unit loads (a .jaibite and a [[warn_mode]] region included) the engine gathers the constant texts its calls pass to the argument, and constant_argument_admits(callable, index, text) answers whether a text is one of them. The host function refuses the rest, however they arrived (a var, a function value, reflect::invoke).
  • Declare before any script loads: a unit loaded earlier was never gathered, so its texts are refused.

A result the script must use​

A binding cannot read C++'s [[nodiscard]] off a function, so the host declares it in the registration's trailing option, before any script loads:

C++
eng->add_function("try_connect", [](const std::string& host) -> bool { return open_socket(host); }, jai::nodiscard);
jai::dynamic_binder<Inventory>(*eng, "Inventory")
    .method("take", &Inventory::take, jai::nodiscard);

A script statement that drops the result then fails the load, under the language's [[nodiscard]] rule: the result of 'try_connect' is discarded, but 'try_connect' is [[nodiscard]]: use it, or keep it in the placeholder (auto _ = try_connect(...);). For a method this holds wherever the checker knows the receiver's class. add_variadic_function and .static_method take the option too, and it combines with the others: jai::nodiscard | jai::serial | jai::filesystem. is_nodiscard_function(callable) asks, by "name" or "Class.method". A namespace entry described with a host_entry_signature sets its nodiscard field; in_place_twin adds that the entry copies what the same-named method changes in place, and the error then names that method.

The time source​

Every time:: call a script makes (Time) reads an engine-owned clock the host can replace. It lives on eng->time(), in <jaiscript/core/engine_time.hpp>.

ProviderBehindEmpty means
monotonictime::now(), time::engine_elapsed(), time::execute_elapsed(), every stopwatchsteady_clock
wall (nanoseconds since the Unix epoch, UTC)time::unix_time(), every datesystem_clock
sleepa sleep that blocksthis_thread::sleep_for

A virtual clock is the whole integration for tests, replays and deterministic simulation:

C++
std::chrono::nanoseconds sim_now{0};
const auto epoch = std::chrono::nanoseconds(1700000000000000000LL);   // 2023-11-14T22:13:20Z
eng->time().source(
    [&] { return sim_now; },                                          // monotonic
    [&] { return epoch + sim_now; },                                  // wall
    [&](std::chrono::nanoseconds d) { sim_now += d; });              // sleep advances the simulation
sim_now += std::chrono::milliseconds(1500);
eng->execute("print(time::now(), ' ', time::utc_now().to_string());");
output
1.5 2023-11-14T22:13:21.500Z
  • Reads are countable, so a provider that replays recorded readings sees the same sequence on every run of a program. Each time:: call reads the provider it needs once; constructing a stopwatch or a date reads nothing. Beyond those, the engine reads the monotonic provider once per host entry (an execute, a jaibite run, or a function or coroutine the host calls while no script runs), and only after a loaded program names time::execute_elapsed. Nested executes, includes and imports belong to their entry.
  • The budget ignores the source. The execution budget and the import check window run on the real steady clock, so a virtual clock neither stalls nor trips them.
  • Zero is the host's. time::engine_elapsed() counts from engine construction on the system clock; on a host monotonic provider it is the provider's reading.
  • The host reads the same clock through eng->time().monotonic_now(), wall_now(), sleep_for(duration), eng->engine_elapsed() and execute_elapsed(). eng->time().source({}, {}, {}) restores the system clocks.
  • Providers run on the engine's thread only: a parallel body cannot call time::.

Suspendable runs​

execute() runs a program to its end. A program that waits (a cutscene, an AI routine, a level's waves) runs as a run: eng->start(...) runs it to its first sleep and returns a jai::script_run (<jaiscript/core/script_run.hpp>), which the host resumes when the time has passed.

C++
std::chrono::nanoseconds now{0};
eng->time().source([&] { return now; }, {}, {});
jai::script_run run = eng->start(R"(
    for (int wave = 1; wave <= 3; ++wave) {
        print("wave {} at {}", wave, time::now());
        sleep(2.0);
    }
    "cleared";
)");
while (!run.done()) {
    now = run.wake_at();   // a game moves its clock each frame and resumes; this jumps to the wake time
    run.resume();
}
std::cout << run.result().value().to_string() << "\n";
output
wave 1 at 0.0
wave 2 at 2.0
wave 3 at 4.0
cleared

A run also has state() and cancel(), and its destructor cancels a run still suspended. Every call: script_run calls.

  • Failure. A run from source, a file or a jaibite fails as execute() does: the start or resume() that ends it throws, and the run is then failed with the text in result(). A callable's run reports through result() alone.
  • Runs are independent. Several can wait at once and resume in any order, between the host's own executes and calls; each keeps its frames, budget and debugger step. A host function a run calls may start a run of its own.
  • Each resume is a host entry with a fresh execution budget, memory allowance and step budget; time asleep never counts. time::execute_elapsed() counts from the start of the run.
  • A run suspends only where script alone stands between the host and the sleep. A sleep reached through native code (a host function's callback, a destructor, a signal receiver, an include or import's file, hot_reload_migrate, a builtin's callback) blocks on the sleep provider, and the run reports one warning through set_script_warning_handler. Under execute() and a host's call of a function value, sleep always blocks.
  • Native stacks. Each run has a native stack of its own, reserved as deep as the starting thread's, so deep recursion stops at the same depth. Once warm, start allocates nothing.
  • Teardown. Destroying the engine cancels every suspended run; a script_run that outlives its engine stays done.
  • The debugger. A breakpoint stops a run like any script. A suspended run is not a stop, and a step over a sleep ends at the run's next statement once it resumes.

Resuming on another thread, from inside the run, or through a moved-from script_run throws jai::runtime_error: script_run misuse.

Parallel worker count​

A parallel region (Parallel Loops) runs on at most eng->parallel_thread_count() threads, the calling thread among them. eng->parallel_thread_count(n) sets an exact count and 0 restores the default. Results never depend on it. The default is jai::thread_pool::performance_core_count(), capped at std::thread::hardware_concurrency() - 1 and at least 1: a region waits for its slowest chunk, so it stays off the slow cores.

performance_core_count() counts the physical cores of the fastest class the platform reports, once: the highest efficiency class on Windows, hw.perflevel0.physicalcpu on Apple platforms, the CPUs an Intel hybrid chip lists in /sys/devices/cpu_core/cpus on Linux (every online CPU on other Linux machines), and std::thread::hardware_concurrency() anywhere else or when the query fails.

The engine builds its pool with jai::thread_pool pool{count, jai::thread_pool::fast_threads}: each worker asks the system never to throttle it to save power (EcoQoS on Windows, user-initiated QoS on Apple platforms).

The HTTP transport​

The spider:: client (spider::catch, sling, get, post, spider::Response) is opt-in and sends nothing by itself: the host supplies the HTTP stack. Three calls make a working client:

C++
#include <jaiscript/stdlib/spider.hpp>
#include <jaiscript/net/http_socket_transport.hpp>   // or your own jai::http_transport

jai::stdlib::register_spider(*eng);                  // the functions, spider::Response, the "network" permission
eng->http_transport(std::make_shared<jai::net::http_socket_transport>());
eng->enable("network");                              // registered disabled: the host grants it

Without a transport a call raises spider::catch: no HTTP transport installed: the host provides one with engine::http_transport(); without the permission, Insufficient privilege: network.

A transport implements one function of jai::http_transport (<jaiscript/core/http_transport.hpp>): http_reply exchange(const http_request&).

  • The request carries the method, an absolute http or https URL, the headers, the body and the seconds left for the call.
  • The reply is the status, every header line as it arrived and the decoded body, or an error text. "timed out" is reported as timed out after N s. An exception out of exchange is reported like an error text.
  • A transport follows no redirect. The library applies options, redirect policy and error texts above it, so every transport behaves alike.
  • exchange runs on the script's thread and blocks it until the answer is complete. One transport may serve many engines on many threads when its exchange takes concurrent calls, as the two below do.

Two transports ship as opt-in headers that nothing in the library includes:

<jaiscript/net/http_socket_transport.hpp> provides jai::net::http_socket_transport, plain HTTP/1.1 over sockets unless its constructor is given a jai::net::tls_layer. <jaiscript/net/openssl_tls.hpp> provides jai::net::openssl_tls, that layer on OpenSSL. Versions, trust setup and the plain-HTTP error: HTTP transport headers.

The runner and jai_spider make one transport per process: WinHTTP on Windows (the system's TLS and certificate store), sockets with openssl_tls on Linux and macOS when the build found OpenSSL, and plain sockets otherwise, where an https URL raises the plain-HTTP error. JAISCRIPT_HTTP_TRANSPORT=socket in the environment makes them use the socket transport on Windows too (plain HTTP only there).