Files
akbasic/docs/10-embedding.md
Andrew Kesterson 8077806598 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

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

  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.

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.

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.