Files
akbasic/docs/10-embedding.md
Andrew Kesterson 342e4c07da
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 3m2s
akbasic CI Build / sanitizers (push) Successful in 3m52s
akbasic CI Build / coverage (push) Failing after 3m24s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Has been cancelled
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

4.2 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.

  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

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.

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

README.md in the repository root carries the full API surface, the pool limits, and the exact list of what each target pulls in.