Coroutine Hosting​

A script coroutine is how a host runs behavior that spans frames: per-entity AI, cutscenes, spawners. This page is the host's side; the language side is the guide's Coroutines chapter. The model:

  • Calling a coroutine function returns a handle and runs nothing. next() runs to the next yield and returns its value, next(n) advances n yields and returns the last one's value, and done() asks without advancing.
  • The next() that finishes the body returns the return value (null when the body runs off its end). One more next() raises Cannot resume a completed coroutine, so test done() first. A range-for over a handle sees only the yielded values.
  • A handle is an ordinary value. Stored in a global it persists across execute() calls, and C++ can hold and drive it directly.
  • Both backends behave identically; nothing a host writes depends on which one runs.

A coroutine yields values to whoever drives it. A whole program that waits on sleep and is resumed by the host's clock is a different tool: eng->start(...) and jai::script_run, in Host API: suspendable runs.

Script surface​

jaiscript
coroutine int countdown(int from) {
    for (int i = from; i > 0; --i) { yield i; }
    return 0;
}

auto c = countdown(3);
while (!c.done()) { print(to_string(c.next())); }     // the last next() delivers the return value

var seen = [];
for (auto v : countdown(4)) { seen.push(v); }          // range-for drives coroutines
print(to_json(seen));                                  // yielded values only
output
3
2
1
0
[4,3,2,1]
  • Spelling. coroutine prefixes any function spelling, methods and static methods included (coroutine int f(), coroutine function f() -> int, coroutine f()); the guide lists every form. A coroutine cannot return a reference (a parse error).
  • Methods. A method coroutine's handle keeps its receiver alive while suspended, this stays bound, and its writes to the object are visible while it is parked.
  • Parameters bind by value, reference-shaped arguments included. Captures behave as a lambda's do.
  • Nothing can be sent in. next() takes only its count and done() takes no arguments.
  • Types. The checker types a coroutine call as a handle whose next() returns an unknown type; a member other than next or done is a load error. Declared yield and return types are enforced at run time, at the next() that runs them: coroutine float hands out 2.0 for yield 2, and a value the type rejects raises Cannot yield string from 'k' (declared -> int) (catchable around the yield) or Cannot return string from 'k' (declared -> int). A bare yield; hands out nothing.
  • Budget. The execution budget applies to each execute() or next() call. An overrun inside a coroutine is terminal: a catch around the yield does not catch it.
  • Hot reload. A suspended coroutine keeps the body it started with until it finishes; new calls get the new definition (Hot Reload).

Hosts moving from Lua coroutines or a C++20 scheduler have a mapping in From Lua Coroutines and C++20 Schedulers.

Driving from C++ through executes​

The frame-loop pattern: create the behavior once in a global, then tick it each frame with a small next() script. Each execute continues where the last left off. Running the declaring line again redeclares c with a fresh handle, and the same pattern works through a jaibite.

C++
eng->execute(R"( coroutine int spawner(int n) {
                     for (int i = 1; i <= n; ++i) { yield i * 10; }
                     return -1;
                 }
                 var c = spawner(3); )");

for (int i = 0; i < 4; ++i) {
    auto v = eng->execute("c.next();");                   // 10, 20, 30, -1
    bool finished = eng->execute("c.done()").as<bool>();  // false ×3, then true
}

Driving from C++ with the native handle​

The script value holds a jai::coroutine_handle. Take the shared_ptr out once and drive it with no script in between:

C++
#include <jaiscript/core/coroutine.hpp>

eng->execute("var c2 = spawner(2);");
auto holder = eng->execute("c2").get_object_holder();
auto handle = std::static_pointer_cast<jai::coroutine_handle>(holder->host_object());

while (!handle->done()) {
    jai::checked_result<jai::script_value> step = handle->next(eng.get());
    if (!step) break;                     // error channel (checked_result)
    std::cout << "C++ next -> " << step.value().as_int() << "\n";   // 10, 20, then -1 (the return value)
}
output
C++ next -> 10
C++ next -> 20
C++ next -> -1
  • next(engine*) returns checked_result<script_value>: test it before reading the value. next(engine*, n) is the script's next(n).
  • get_status() is one of created, running, suspended, completed, failed; done() is true for the last two.
  • The shared_ptr keeps the coroutine alive, and with it a method coroutine's receiver. This is how C++ owns a behavior.