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>
5.9 KiB
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.
- Nothing in the library terminates the process. Errors come back as
akerr_ErrorContext *for you to handle. - The interpreter owns no window, no renderer and no event loop. It draws through whatever you already created.
- It never blocks.
SLEEP,GETKEY,PLAYandWAIThold the program without holding your frame rate. - You can bound it. A script with
10 GOTO 10cannot take your game with it.
Linking
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
#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.
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:
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.
Where the output goes
PRINT writes through an akbasic_TextSink, which is a record of function pointers plus
whatever state you hang off self:
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
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:
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.
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.