`RGR(1)` and `RGR(2)` gave the window in pixels and nothing gave columns, rows or the cell size -- so anything placing a character *and* a sprite at the same spot had to hardcode a number measured by hand against whatever font the host loaded. The Breakout in examples/ does exactly that, `CW# = 16`, and it is the one thing in that listing that breaks on a different font or window. **`RWINDOW` is BASIC 7.0's own answer and had never been implemented here.** `RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns. `RWINDOW(2)` reports a C128's 40 or 80 column screen mode, and this interpreter has neither -- refused by name, because answering 0 would be a plausible lie, which is worse than a refusal that says why. The cell size in pixels is `RGR(3)` and `RGR(4)`, beside the surface's own dimensions rather than on `RWINDOW`. Two reasons: a cell size is a fact about the surface, and `RWINDOW` reports the *window*, so dividing `RGR(1)` by a column count stops being right the moment a program calls `WINDOW`. Both read a new optional `grid` entry point on `akbasic_TextSink` -- columns, rows, cell width, cell height -- implemented by the akgl sink and forwarded by the tee, in the shape `moveto` and `window` already had. NULL everywhere else, so both verbs refuse by name against a sink with no grid. `akbasic_sink_init_ stdio()` clears it for the same reason it now clears the other two. Measured on the standalone build: `RGR(3)` answers 16 and `RWINDOW` answers 50 columns by 37 rows -- the three numbers the Breakout listing had written out as constants -- and `RWINDOW` follows a `WINDOW` call while `RGR(3)` does not. tests/console_verbs.c drives the answers through a stand-in sink with a grid, since the harness sink is stdio and has none; tests/graphics_verbs.c covers the new `RGR` fields, their refusal, and the moved range bound. The `c excerpt=` block in docs/10-embedding.md moves with the header, which is `docs_examples` doing its job. TODO.md section 6 item 31's second half, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.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.
- 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.
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:
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:
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);
akerr_ErrorContext AKERR_NOIGNORE *(*grid)(struct akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh);
} 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 three are optional and may be NULL, which is how CHAR, WINDOW and
RWINDOW 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
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.