Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m23s
akbasic CI Build / sanitizers (push) Failing after 4m36s
akbasic CI Build / coverage (push) Failing after 3m41s
akbasic CI Build / akgl_build (push) Failing after 4m45s
akbasic CI Build / mutation_test (push) Failing after 3m31s
Group K, and the first verbs to reach the akgl_ui subsystem 0.9.0 brought in: MENU and GETMENU and RMENU, DIALOG, HUD and UISTYLE. A program that wanted a title screen had to draw one out of CHAR and GETKEY, which is what both breakout tutorials make a reader do. The interesting part is the impedance mismatch. libakgl's UI is immediate mode -- widgets are re-declared inside a frame bracket every frame and clay borrows their text until the bracket closes -- and a BASIC program says MENU 1, "START" on line 100 and expects it up on line 900, several hundred frames later. So src/ui_akgl.c is retained on this side and immediate on that one: the record's entry points are setters that copy into akbasic_AkglUi, and akbasic_ui_akgl_render() replays the whole set once a frame from the host's pump. No BASIC string, which lives in the per-line value pool, is ever what clay is handed. The shapes are borrowed rather than invented. MENU retires the way SOLID does -- no entries retires one, no arguments retire them all. GETMENU holds the step loop the way GETKEY does, so parking is not blocking: the step still returns, the host keeps its frame rate, and the sprite, audio and collision services keep running underneath because they run before the blocking checks. RMENU(n,1) reads and clears the way BUMP() does. Withdrawing the device or retiring the menu releases a holding GETMENU with 0 rather than wedging the script, which is akbasic_input_service()'s rule for a withdrawn keyboard. One thing a program has to know, and docs/19-user-interface.md says it twice: a menu that is up owns the cursor keys and Return. It has to, and retiring it gives them back -- forget the MENU n before an INPUT and the INPUT never sees the Return that ends it. akbasic_runtime_set_ui() is its own function rather than a fifth argument to akbasic_runtime_set_devices(), whose signature has twenty-eight call sites in tests and documentation that are about something else. deps/libakgl is not touched. akgl_UiAnchor has the four corners and dead centre, so HUD offers exactly those five; TODO.md records what a top-centre and bottom-centre would cost upstream, along with the three other things this deliberately leaves out. No new error code either -- DEVICE, BOUNDS, SYNTAX and TYPE cover the group, and 520 stays free. tools/screenshot.c had to learn that "needs a font" and "draws the text grid" are two questions. They were one, and a UI figure came out black: the text layer owns every pixel of the rows it covers and painted over the widgets. The new ui=1 fence attribute asks for the first without the second; MAINTENANCE.md documents it. 112/112 in both configurations, 112/112 under ASan and UBSan, coverage 94.1% against the 90% gate with src/runtime_ui.c at 99% of lines and 100% of functions, doxygen clean, and the four new figures byte-identical on a re-render. TODO.md section 8's gate table was stale on several counts besides these and is refreshed with measured numbers. Co-Authored-By: Tachikoma (Claude Code Opus 5 1M) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
195 lines
8.4 KiB
Markdown
195 lines
8.4 KiB
Markdown
# 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
|
|
|
|
```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:
|
|
|
|
```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.
|
|
|
|
## 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);
|
|
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
|
|
|
|
```c wrap=hostbody
|
|
PASS(e, akbasic_runtime_set_devices(&RUNTIME, &graphics, &audio, &input, &sprites));
|
|
PASS(e, akbasic_runtime_set_ui(&RUNTIME, &ui));
|
|
```
|
|
|
|
The fifth one is set on its own rather than as a fifth argument to the first, because
|
|
that signature predates it and had twenty-eight call sites that do not care about menus.
|
|
|
|
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:
|
|
|
|
```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.
|
|
|
|
## 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](13-differences.md).
|