Files
akbasic/docs/10-embedding.md

184 lines
7.7 KiB
Markdown
Raw Normal View History

# 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
Execute every documented example as a test docs/ and README.md carry 85 fenced blocks. Every one was checked by hand exactly once, when it was written, which is not a standard that survives a changing interpreter -- and four were already wrong: two transcripts showing a leading space PRINT does not emit, akbasic_TextSink in README.md missing the two members it had grown hours earlier, and FILTER's refusal quoted with wording the code does not use. tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds. BASIC programs and transcripts run and are byte-compared against an `output` block; C snippets compile with -fsyntax-only against the real include path, which CMake writes out because it is transitive through akerror, akstdlib and akgl; shell blocks run in a sandbox. Anything that would reconfigure the build tree, hit the network or re-enter the suite is tagged norun with the reason in MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision. An untagged block is a failure rather than a default, and the pass line reports what it executed by kind. Both exist because the way a harness like this dies is by quietly matching nothing and passing -- which it duly did on the first CTest run, where a generator expression evaluating to nothing still contributed an empty argument that the script read as a filename. The count is what caught it. The excerpt check earns its own mention: a block tagged `c excerpt=include/akbasic/sink.h` must still appear in that header, comments and whitespace ignored. Compiling it would only redefine the type, so a compile check could not have found the stale struct, and did not. Registered as the CTest case docs_examples in both configurations. Fixing the four wrong examples turned up two interpreter defects, fixed in the previous commit and recorded in TODO.md section 8. MAINTENANCE.md is new: the fence-tag reference, what to do when the case fails, and the conventions that until now only existed inside source comments -- the three test lists and how two of them invert "passed", the sorted verb table, that a golden file is never edited to suit this interpreter, and that a fix gets mutation-checked with a file copy rather than git checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:41:36 -04:00
```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:
Execute every documented example as a test docs/ and README.md carry 85 fenced blocks. Every one was checked by hand exactly once, when it was written, which is not a standard that survives a changing interpreter -- and four were already wrong: two transcripts showing a leading space PRINT does not emit, akbasic_TextSink in README.md missing the two members it had grown hours earlier, and FILTER's refusal quoted with wording the code does not use. tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds. BASIC programs and transcripts run and are byte-compared against an `output` block; C snippets compile with -fsyntax-only against the real include path, which CMake writes out because it is transitive through akerror, akstdlib and akgl; shell blocks run in a sandbox. Anything that would reconfigure the build tree, hit the network or re-enter the suite is tagged norun with the reason in MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision. An untagged block is a failure rather than a default, and the pass line reports what it executed by kind. Both exist because the way a harness like this dies is by quietly matching nothing and passing -- which it duly did on the first CTest run, where a generator expression evaluating to nothing still contributed an empty argument that the script read as a filename. The count is what caught it. The excerpt check earns its own mention: a block tagged `c excerpt=include/akbasic/sink.h` must still appear in that header, comments and whitespace ignored. Compiling it would only redefine the type, so a compile check could not have found the stale struct, and did not. Registered as the CTest case docs_examples in both configurations. Fixing the four wrong examples turned up two interpreter defects, fixed in the previous commit and recorded in TODO.md section 8. MAINTENANCE.md is new: the fence-tag reference, what to do when the case fails, and the conventions that until now only existed inside source comments -- the three test lists and how two of them invert "passed", the sorted verb table, that a golden file is never edited to suit this interpreter, and that a fix gets mutation-checked with a file copy rather than git checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:41:36 -04:00
```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.
Split the documentation by who reads it README.md was 577 lines and answered four different questions at once: what the project is, how to build it, every verb and function in the language, and how to maintain the test harness. The verb and function lists had already been written a second time in docs/11 and docs/12, which is how a list of that size goes stale -- there is no way to notice the two have drifted apart. README.md is now 150 lines and holds only what somebody evaluating the project needs: what it is, the quickstart, why it was rewritten in C, the five rules embedding imposes on the design, the two ways to use it, and where everything else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md. The akbasic_TextSink struct moved to docs/10-embedding.md rather than being deleted. It was the corpus's only `c excerpt=` block -- the check that caught the stale struct two commits ago -- so dropping it with the README would have quietly retired a test. docs/10 also stopped claiming README.md carries the full API surface and the pool limits, which the trim made false. CLAUDE.md went from 458 lines to 62, because almost none of it was agent-specific. The project goals, the Go reference and its architecture, the dependency version and ABI rules, the four ways an embedded build collides, the libakerror convention, the error-code range map and the style rules are all things a maintainer needs, and they are now in MAINTENANCE.md with one copy to keep true. CLAUDE.md points there and keeps only the rules no test enforces: tests in the same commit asserting the correct contract, file a missing dependency capability upstream, do not edit generated output or tests/reference/, co-author your commits. Four claims did not survive the move, having gone stale where nothing could notice: - "The repository is currently empty apart from its submodules -- no commits, no source tree, no build files." There are 43 commits. - libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE) at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is gone; only a historical mention in a comment remains. - "akbasic_init() claims 512-767." There is no akbasic_init. It is akbasic_error_register(), called from akbasic_runtime_init(). - Time-relative phrasing ("libakgl hit two of them in the last week"). Five places pointed at CLAUDE.md for the range map or the file-it-upstream rule and now point at MAINTENANCE.md: include/akbasic/error.h, src/runtime_disk.c and three entries in TODO.md. Both source changes are comments. deps/libakgl/TODO.md cites it too and is left alone; it is a submodule, and the rule it quotes is still reachable from CLAUDE.md. ctest is green at 95 of 95, docs_examples included: 36 programs, 9 transcripts, 44 output comparisons, 3 C snippets, 1 excerpt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:59:43 -04:00
## 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);
} 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.
`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
Execute every documented example as a test docs/ and README.md carry 85 fenced blocks. Every one was checked by hand exactly once, when it was written, which is not a standard that survives a changing interpreter -- and four were already wrong: two transcripts showing a leading space PRINT does not emit, akbasic_TextSink in README.md missing the two members it had grown hours earlier, and FILTER's refusal quoted with wording the code does not use. tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds. BASIC programs and transcripts run and are byte-compared against an `output` block; C snippets compile with -fsyntax-only against the real include path, which CMake writes out because it is transitive through akerror, akstdlib and akgl; shell blocks run in a sandbox. Anything that would reconfigure the build tree, hit the network or re-enter the suite is tagged norun with the reason in MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision. An untagged block is a failure rather than a default, and the pass line reports what it executed by kind. Both exist because the way a harness like this dies is by quietly matching nothing and passing -- which it duly did on the first CTest run, where a generator expression evaluating to nothing still contributed an empty argument that the script read as a filename. The count is what caught it. The excerpt check earns its own mention: a block tagged `c excerpt=include/akbasic/sink.h` must still appear in that header, comments and whitespace ignored. Compiling it would only redefine the type, so a compile check could not have found the stale struct, and did not. Registered as the CTest case docs_examples in both configurations. Fixing the four wrong examples turned up two interpreter defects, fixed in the previous commit and recorded in TODO.md section 8. MAINTENANCE.md is new: the fence-tag reference, what to do when the case fails, and the conventions that until now only existed inside source comments -- the three test lists and how two of them invert "passed", the sorted verb table, that a golden file is never edited to suit this interpreter, and that a fix gets mutation-checked with a file copy rather than git checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:41:36 -04:00
```c wrap=hostbody
PASS(e, akbasic_runtime_set_devices(&RUNTIME, &graphics, &audio, &input, &sprites));
```
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:
Execute every documented example as a test docs/ and README.md carry 85 fenced blocks. Every one was checked by hand exactly once, when it was written, which is not a standard that survives a changing interpreter -- and four were already wrong: two transcripts showing a leading space PRINT does not emit, akbasic_TextSink in README.md missing the two members it had grown hours earlier, and FILTER's refusal quoted with wording the code does not use. tests/docs_examples.sh reads a fence-tag vocabulary and runs what it finds. BASIC programs and transcripts run and are byte-compared against an `output` block; C snippets compile with -fsyntax-only against the real include path, which CMake writes out because it is transitive through akerror, akstdlib and akgl; shell blocks run in a sandbox. Anything that would reconfigure the build tree, hit the network or re-enter the suite is tagged norun with the reason in MAINTENANCE.md, and the two cmake blocks stay hand-maintained by decision. An untagged block is a failure rather than a default, and the pass line reports what it executed by kind. Both exist because the way a harness like this dies is by quietly matching nothing and passing -- which it duly did on the first CTest run, where a generator expression evaluating to nothing still contributed an empty argument that the script read as a filename. The count is what caught it. The excerpt check earns its own mention: a block tagged `c excerpt=include/akbasic/sink.h` must still appear in that header, comments and whitespace ignored. Compiling it would only redefine the type, so a compile check could not have found the stale struct, and did not. Registered as the CTest case docs_examples in both configurations. Fixing the four wrong examples turned up two interpreter defects, fixed in the previous commit and recorded in TODO.md section 8. MAINTENANCE.md is new: the fence-tag reference, what to do when the case fails, and the conventions that until now only existed inside source comments -- the three test lists and how two of them invert "passed", the sorted verb table, that a golden file is never edited to suit this interpreter, and that a fix gets mutation-checked with a file copy rather than git checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:41:36 -04:00
```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.
Take libakerror 2.0.1, and guard the exit status it fixes 2.0.0 makes the error pool and the status registry thread safe, and it is an ABI break carrying the soname to libakerror.so.2. The break is a quiet one: __akerr_last_ignored became thread-local and akerr_next_error() now returns a context that already holds a reference, so objects compiled against a 1.x header and linked against 2.x count every reference twice and never give a slot back. Nothing about that fails to link, which is exactly what a guard is for -- include/akbasic/error.h feature-tests AKERR_THREAD_SAFE instead of AKERR_FIRST_CONSUMER_STATUS, which 2.0.0 also still defines and which therefore no longer distinguishes anything. 2.0.1 is the release this band needed most. The default unhandled-error handler ended in exit(errctx->status), and a process exit status is one byte: AKBASIC_ERR_BASE is 512, and 512 truncates to 0, so an unhandled AKBASIC_ERR_SYNTAX reported success to anything watching $?. Every other code in the band came out as some unrelated error's number. akerr_exit() substitutes 125 for anything a byte cannot carry, and a probe raising AKBASIC_ERR_DEVICE through FINISH_NORETURN now exits 125 rather than 7. It was latent here rather than live -- src/main.c handles the context and returns EXIT_FAILURE, and every test with a top-level ATTEMPT carries a HANDLE_DEFAULT -- but "no caller relies on it today" is not a property a header can keep true. tests/version_check.c asserts the mapping and fails if AKBASIC_ERR_BASE ever stops truncating to zero, because that is the day this stops being about our base. Chapter 10 gains a threading section: libakerror is safe from any thread now, and this interpreter is not and has no lock anywhere in it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 08:00:24 -04:00
## 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
Split the documentation by who reads it README.md was 577 lines and answered four different questions at once: what the project is, how to build it, every verb and function in the language, and how to maintain the test harness. The verb and function lists had already been written a second time in docs/11 and docs/12, which is how a list of that size goes stale -- there is no way to notice the two have drifted apart. README.md is now 150 lines and holds only what somebody evaluating the project needs: what it is, the quickstart, why it was rewritten in C, the five rules embedding imposes on the design, the two ways to use it, and where everything else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md. The akbasic_TextSink struct moved to docs/10-embedding.md rather than being deleted. It was the corpus's only `c excerpt=` block -- the check that caught the stale struct two commits ago -- so dropping it with the README would have quietly retired a test. docs/10 also stopped claiming README.md carries the full API surface and the pool limits, which the trim made false. CLAUDE.md went from 458 lines to 62, because almost none of it was agent-specific. The project goals, the Go reference and its architecture, the dependency version and ABI rules, the four ways an embedded build collides, the libakerror convention, the error-code range map and the style rules are all things a maintainer needs, and they are now in MAINTENANCE.md with one copy to keep true. CLAUDE.md points there and keeps only the rules no test enforces: tests in the same commit asserting the correct contract, file a missing dependency capability upstream, do not edit generated output or tests/reference/, co-author your commits. Four claims did not survive the move, having gone stale where nothing could notice: - "The repository is currently empty apart from its submodules -- no commits, no source tree, no build files." There are 43 commits. - libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE) at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is gone; only a historical mention in a comment remains. - "akbasic_init() claims 512-767." There is no akbasic_init. It is akbasic_error_register(), called from akbasic_runtime_init(). - Time-relative phrasing ("libakgl hit two of them in the last week"). Five places pointed at CLAUDE.md for the range map or the file-it-upstream rule and now point at MAINTENANCE.md: include/akbasic/error.h, src/runtime_disk.c and three entries in TODO.md. Both source changes are comments. deps/libakgl/TODO.md cites it too and is left alone; it is a submodule, and the rule it quotes is still reachable from CLAUDE.md. ctest is green at 95 of 95, docs_examples included: 36 programs, 9 transcripts, 44 output comparisons, 3 C snippets, 1 excerpt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 22:59:43 -04:00
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).