Files
akbasic/docs/10-embedding.md
Tachikoma d5a0edd692 Write the GALAGA tutorial chapters and the repeated-host-calls guide
docs/20 builds the engine and the boundary: the startup order, the
starfield, actors and collision, booting a DEF-only script, the issue #8
mode workaround, the custom update hook, first light, screens, and the
headless harness. docs/21 builds the three shared structures and the AI:
the host type tables, the actor binding, the randomness route around
issue #16, the measured case against structure arguments (issue #36),
the three language rules that shape the script, the maneuvers, the
argued formation decision, the script-death policy, and the interop
proof. Every fenced block runs under tests/docs_examples.sh in both
build configurations; five new preludes carry the C fragments.

docs/10 gains the 'Calling a function every frame' section the chapters
lean on: the per-call akbasic_environment_zero() rule, the set_mode(RUN)
workaround, the clear_error() revival, and the case for rebinding over
structure arguments. Index rows and chapter counts updated.

Co-authored-by: andrew <andrew@aklabs.net>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XiGgpHuXUm2mR4Wzndw3dc
2026-08-04 08:47:43 -04:00

240 lines
11 KiB
Markdown

# 10. Embedding
The whole design of this interpreter is shaped by one requirement: a game must be able
to embed it as a scripting engine without giving up control. That produces four rules,
and they explain most of what looks unusual elsewhere in this guide.
1. **Nothing in the library terminates the process.** Errors come back as
`akerr_ErrorContext *` for you to handle.
2. **The interpreter owns no window, no renderer and no event loop.** It draws through
whatever you already created.
3. **It never blocks.** `SLEEP`, `GETKEY`, `PLAY` and `WAIT` hold the program without
holding your frame rate.
4. **You can bound it.** A script with `10 GOTO 10` cannot take your game with it.
## Linking
```cmake
add_subdirectory(deps/akbasic EXCLUDE_FROM_ALL)
target_link_libraries(YOUR_GAME PRIVATE akbasic::akbasic)
```
| Target | What it is | Link it? |
|---|---|---|
| `akbasic` | The interpreter. No SDL, nothing that exits. | Always |
| `akbasic_akgl` | The graphics, sound, input and sprite backends, drawing through *your* renderer | If you want them |
| `akbasic_frontend` | The standalone program's host: creates the window, owns the loop | **No.** You are the host |
## The shortest useful host
```c wrap=hostloop
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
static akbasic_Runtime RUNTIME; /* too big for a stack */
static akbasic_TextSink SINK;
static akbasic_StdioSink SINKSTATE;
akerr_ErrorContext AKERR_NOIGNORE *run_script(const char *source)
{
PREPARE_ERROR(e);
PASS(e, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL));
PASS(e, akbasic_runtime_init(&RUNTIME, &SINK));
PASS(e, akbasic_runtime_load(&RUNTIME, source));
PASS(e, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
while ( RUNTIME.mode != AKBASIC_MODE_QUIT ) {
PASS(e, akbasic_runtime_settime(&RUNTIME, your_clock_ms()));
PASS(e, akbasic_runtime_run(&RUNTIME, 256)); /* 256 steps, then return */
your_draw_a_frame();
}
SUCCEED_RETURN(e);
}
```
`akbasic_runtime_run(rt, n)` runs at most `n` steps and returns. That bound is what
keeps a runaway script from owning your process.
**The source you hand `akbasic_runtime_load()` does not need line numbers.** A game
script is written in a text editor and branches by `LABEL`, so numbering its lines is
work with nothing on the other end of it:
```c wrap=hostloop
static const char *SCRIPT =
"PRINT \"SPAWNING\"\n"
"FOR I# = 1 TO HEALTH#\n"
" PRINT I#\n"
"NEXT I#\n"
"GOTO DONE\n"
"PRINT \"NOT REACHED\"\n"
"LABEL DONE\n"
"PRINT \"READY\"\n";
```
Lines that carry a number are filed under it and lines that do not are given the next
one going, so the two can be mixed and a numbered script still loads exactly as it did.
What a script may *not* do is `GOTO 100` when nothing wrote a line 100 — the interpreter
refuses that before the first line runs rather than branching somewhere plausible and
wrong.
`akbasic_runtime_settime()` is how the interpreter knows what time it is. It reads no
clock of its own, because it owns no loop. If you never call it, every duration expires
immediately — audible, but never a hang.
## Exchanging variables with a script
Use `akbasic_runtime_global()`. It finds or creates the variable in the script's
outermost scope, which is the only place both of you can reliably see:
```c wrap=hostbody
akbasic_Variable *health = NULL;
int64_t subscript[1] = { 0 };
PASS(e, akbasic_runtime_global(&RUNTIME, "HEALTH#", &health));
PASS(e, akbasic_variable_set_integer(health, 100, subscript, 1));
```
Do not reach for `akbasic_environment_get()`. A script suspended part-way through a
bounded run is usually inside a `FOR` or `GOSUB` body, and a variable created there
dies when the body pops — silently, with the script reading it correctly right up until
it stops.
## Calling a function every frame
`akbasic_runtime_call_function()` calls a `DEF` by name with values you already
hold — the entry point a game loop wants. A host that calls it repeatedly signs
up for three rules the one-shot examples never meet:
```c wrap=hostcalls
CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, "THINK", argp, 1, &result));
/* ...consume the result... */
CATCH(errctx, akbasic_environment_zero(SCRIPT.environment));
```
1. **Reset the value scratch after every call, once the result is consumed.**
Each call parks its result in the caller environment's per-line scratch
(`AKBASIC_MAX_VALUES` slots), and a host calling in a loop never crosses the
line boundary that would reset it. Skip the `akbasic_environment_zero()` and
the pool drains — measured at under two frames of forty calls — after which
every call fails with `Maximum values per line reached`. The reset also
invalidates `result`, which is why it comes after the consumption.
2. **Force RUN mode once after the boot run.** A multi-line `DEF` body only
runs while the runtime is in RUN mode, and by the time a host can call, the
program that filed the definitions has ended. One
`akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN)` after
`akbasic_runtime_run()` makes the bodies run, and the mode stays put because
nothing steps the runtime between calls. Issue #8 tracks making this
unnecessary.
3. **Revive after a script error, deliberately.** A BASIC-level error inside a
called body reports through the sink, answers a stale value, and latches:
the runtime leaves RUN mode and every later call does nothing. When your
policy is to absorb the error and keep calling — a game marking one actor
dumb rather than killing the frame — the revival is two calls:
`akbasic_runtime_clear_error()`, then `akbasic_runtime_set_mode(RUN)` again.
The latch is deliberate for *programs* — the first error ends a run, once,
with one line — so nothing clears it for you.
Do not pass structures as per-frame arguments. A structure or pointer parameter
spends a value-pool slot on every call and the pool never reclaims, so the
interface dies after about a thousand calls — issue #36 has the measurements.
Bind the instance once with `akbasic_host_bind()` and point it at each object
with `akbasic_host_rebind()` ([Chapter 16](16-structures.md)), which spends
nothing per call. The GALAGA tutorial ([Chapters 20](20-tutorial-galaga.md)
and [21](21-tutorial-galaga-enemies.md)) is this whole recipe as a working
game, forty calls a frame.
## Where the output goes
`PRINT` writes through an `akbasic_TextSink`, which is a record of function pointers plus
whatever state you hang off `self`:
```c excerpt=include/akbasic/sink.h
typedef struct akbasic_TextSink
{
void *self;
akerr_ErrorContext AKERR_NOIGNORE *(*write)(struct akbasic_TextSink *self, const char *text);
akerr_ErrorContext AKERR_NOIGNORE *(*writeln)(struct akbasic_TextSink *self, const char *text);
akerr_ErrorContext AKERR_NOIGNORE *(*readline)(struct akbasic_TextSink *self, char *dest, size_t len, bool *eof);
akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_TextSink *self);
akerr_ErrorContext AKERR_NOIGNORE *(*moveto)(struct akbasic_TextSink *self, int col, int row);
akerr_ErrorContext AKERR_NOIGNORE *(*window)(struct akbasic_TextSink *self, int left, int top, int right, int bottom);
akerr_ErrorContext AKERR_NOIGNORE *(*grid)(struct akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh);
akerr_ErrorContext AKERR_NOIGNORE *(*graphic)(struct akbasic_TextSink *self, int mode, int split);
} akbasic_TextSink;
```
`akbasic_sink_init_stdio()` ships with the library and is what the driver uses. A game
supplies its own and draws into a text layer. `readline` is expected to set `*eof` rather
than block — that is how `INPUT` behaves sanely inside a frame.
**The last four are optional and may be NULL**, which is how `CHAR`, `WINDOW`, `RWINDOW`
and `GRAPHIC` know to refuse by name rather than pretending. Supply `grid` if your text layer
has a character cell: it is the only way a script can find out how big one is, and
without it anything placing a character and a sprite at the same spot has to hardcode a
number measured against your font.
`akbasic_sink_init_tee()` also ships, and composes two sinks into one: writes go to both,
and `readline` comes from whichever of the two you name as the reader. That is how the SDL
build puts `PRINT` in a window *and* on stdout. It needs no SDL, so you can use it to log a
script's output to a file while you draw.
## Lending devices
```c wrap=hostbody
PASS(e, akbasic_runtime_set_devices(&RUNTIME, &graphics, &audio, &input, &sprites));
PASS(e, akbasic_runtime_set_ui(&RUNTIME, &ui));
```
The fifth one is set on its own rather than as a fifth argument to the first, because
that signature predates it and had twenty-eight call sites that do not care about menus.
Any of them may be `NULL`, and that is how you withhold a capability: a script given no
audio backend gets an error from `SOUND` rather than silence. Each is a record of
function pointers, so you can supply your own and never link the graphics library at
all.
`akbasic_akgl` provides implementations that draw through a renderer *you* created:
```c wrap=akglbody requires=akgl
PASS(e, akbasic_graphics_init_akgl(&graphics, &gstate, my_renderer));
PASS(e, akbasic_sprite_init_akgl(&sprites, &sstate, my_renderer, &gstate));
```
Sprites become real actors in your registry, so your game can see them.
## Where a script's errors go
A BASIC-level error is reported through the sink and stops the script; it does not come
back to you as a failure. What comes back to you is an error in the *interpreter* —
pool exhaustion, a NULL argument — which is yours to handle.
That is the split to hold on to: a script's mistakes are the script's problem, and your
program keeps running.
## Threads
**One runtime belongs to one thread, and there is no lock anywhere in this interpreter.**
An `akbasic_Runtime` is a large struct of fixed pools mutated in place by every step, so
two threads calling `akbasic_runtime_step()` on the same runtime will corrupt it. If your
game is threaded, drive the script from whichever thread owns it and hand results across
yourself.
Two runtimes on two threads are fine — they share no state. What they *do* share is
`libakerror`'s error pool and status registry, and those became thread safe in 2.0.0, so
raising, handling and releasing errors from either thread is safe with no coordination
from you. One error context still belongs to the thread that raised it; passing one to
another thread is your synchronization.
Call `akbasic_error_register()` once, during single-threaded startup, before you spawn
anything. It is idempotent and safe to repeat, but registering a status *name* while
another thread looks one up is the single registry operation no lock can make safe.
## Reading it all
Two complete hosts are checked in and built by every build, so neither can rot:
`examples/embed.c` runs a script a bounded number of steps at a time, and
`examples/hostvars.c` passes integers, floats and strings in both directions. The full API
surface is the headers under `include/akbasic/`, which `doxygen Doxyfile` renders; the pool
limits are the table at the end of [Chapter 13](13-differences.md).