Carries the reference README across, adjusted where C changes the answer: cmake instead of make, the ak* libraries instead of the go-sdl2 bindings, and the limits table now includes the three ceilings the Go version did not need because it called make(). The new "Embedding the interpreter" section is the point of the rewrite, so it states the four rules the library holds to -- nothing terminates the process, nothing calls malloc, no file-scope mutable state, the host owns the loop -- and each was checked against the tree rather than asserted. Adds akbasic_runtime_load(). Writing the section turned up a real gap: a host usually already holds its script as a string and wants the sink reserved for output, and the only path that existed was AKBASIC_MODE_RUNSTREAM reading the program through the sink's readline, which forces a game to point its output device at its source text. The alternative was reaching into the header's "internal API" block for store_line. Neither is something to put in a README. Adds examples/embed.c, which is the code the README quotes -- a custom sink, a bounded per-frame run, and the PASS-not-CATCH rule for a loop inside an ATTEMPT. It is built by every build and registered as a CTest case, so a signature change breaks the build instead of rotting the document. The README's own snippet is compiled separately as a check; both were run before committing. The "What Isn't Implemented / Isn't Working" section leads with the eleven inherited defects rather than burying them, because five of them were found by this port and a reader deserves to know that 1 - 2 - 3 computes 1 - 2 before they hit it. Corrected two claims while verifying: the runtime is 10.1MB rather than the ~8MB first written, and its largest single cost is the environment pool at 4.1MB, not the source table. ctest 60/60; ASan+UBSan 60/60; no warnings under -Wall -Wextra. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
186 lines
5.7 KiB
C
186 lines
5.7 KiB
C
/**
|
|
* @file embed.c
|
|
* @brief Embedding akbasic in a host program: the whole surface, in one file.
|
|
*
|
|
* This is the code in README.md's "Embedding the interpreter" section, kept here
|
|
* so it is compiled by every build rather than rotting in a document. Build it
|
|
* with -DAKBASIC_BUILD_EXAMPLES=ON and run `./build/examples/embed`.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
#include <akbasic/sink.h>
|
|
|
|
/*
|
|
* The runtime carries every pool the interpreter owns -- a few megabytes -- so it
|
|
* goes in static storage or inside the host's own state, never on the stack.
|
|
*/
|
|
static akbasic_Runtime SCRIPT;
|
|
|
|
/* ----------------------------------------------------------- a custom sink -- */
|
|
|
|
/*
|
|
* A sink is a record of function pointers plus whatever state the host needs.
|
|
* This one collects output into a fixed buffer, which is what a game would do
|
|
* before blitting it to a text layer. A real one would draw through the akgl
|
|
* renderer the host already initialized.
|
|
*/
|
|
typedef struct
|
|
{
|
|
char buffer[4096];
|
|
size_t used;
|
|
} ConsoleState;
|
|
|
|
static ConsoleState CONSOLE;
|
|
static akbasic_TextSink CONSOLE_SINK;
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *console_write(akbasic_TextSink *self, const char *text)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
ConsoleState *state = NULL;
|
|
size_t length = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in console write");
|
|
state = (ConsoleState *)self->self;
|
|
length = strlen(text);
|
|
FAIL_ZERO_RETURN(errctx, (length < sizeof(state->buffer) - state->used),
|
|
AKBASIC_ERR_BOUNDS, "console buffer is full");
|
|
memcpy(state->buffer + state->used, text, length);
|
|
state->used += length;
|
|
state->buffer[state->used] = '\0';
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *console_writeln(akbasic_TextSink *self, const char *text)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, console_write(self, text));
|
|
PASS(errctx, console_write(self, "\n"));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *console_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in console readline");
|
|
/* No keyboard wired up: INPUT sees end-of-stream rather than blocking. */
|
|
(void)len;
|
|
dest[0] = '\0';
|
|
*eof = true;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *console_clear(akbasic_TextSink *self)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
ConsoleState *state = NULL;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in console clear");
|
|
state = (ConsoleState *)self->self;
|
|
state->used = 0;
|
|
state->buffer[0] = '\0';
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *console_sink_init(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
CONSOLE.used = 0;
|
|
CONSOLE.buffer[0] = '\0';
|
|
CONSOLE_SINK.self = &CONSOLE;
|
|
CONSOLE_SINK.write = console_write;
|
|
CONSOLE_SINK.writeln = console_writeln;
|
|
CONSOLE_SINK.readline = console_readline;
|
|
CONSOLE_SINK.clear = console_clear;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------- the driver -- */
|
|
|
|
static const char *PROGRAM =
|
|
"10 PRINT \"COUNTING:\"\n"
|
|
"20 FOR I# = 1 TO 5\n"
|
|
"30 PRINT I# * I#\n"
|
|
"40 NEXT I#\n"
|
|
"50 PRINT \"DONE\"\n";
|
|
|
|
/*
|
|
* One frame's worth of script. The interpreter never surrenders control: it runs
|
|
* at most `budget` source lines and returns, so a host game keeps its frame rate
|
|
* no matter what the script does -- including an infinite loop.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *step_script(int budget)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
if ( SCRIPT.mode == AKBASIC_MODE_QUIT ) {
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
PASS(errctx, akbasic_runtime_run(&SCRIPT, budget));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* The host's game loop. A loop that calls an error-returning function has to be
|
|
* its own function using PASS: CATCH expands to a C `break`, so inside a loop
|
|
* within an ATTEMPT it would leave the loop rather than the ATTEMPT and let the
|
|
* rest of the block run with an error pending.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *game_loop(int frames, int budget, int *ran)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
for ( *ran = 0; *ran < frames && SCRIPT.mode != AKBASIC_MODE_QUIT; (*ran)++ ) {
|
|
PASS(errctx, step_script(budget));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int frames = 0;
|
|
int rc = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, console_sink_init());
|
|
CATCH(errctx, akbasic_runtime_init(&SCRIPT, &CONSOLE_SINK));
|
|
CATCH(errctx, akbasic_runtime_load(&SCRIPT, PROGRAM));
|
|
CATCH(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
|
|
|
|
/*
|
|
* Ten frames at two source lines each. The budget is artificially mean to
|
|
* make the point visible: the script gets partway through and stops, and
|
|
* the host stayed in control the whole time.
|
|
*/
|
|
CATCH(errctx, game_loop(10, 2, &frames));
|
|
printf("--- after %d frames of 2 lines each ---\n%s\n", frames, CONSOLE.buffer);
|
|
|
|
/*
|
|
* Then let it finish. A budget of 0 is unbounded -- fine for a tool, not
|
|
* for a game. Note the interpreter only reaches MODE_QUIT after walking to
|
|
* the end of the line table, which is AKBASIC_MAX_SOURCE_LINES steps and
|
|
* is what the reference does too.
|
|
*/
|
|
CATCH(errctx, game_loop(AKBASIC_MAX_SOURCE_LINES, 8, &frames));
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} HANDLE_DEFAULT(errctx) {
|
|
LOG_ERROR_WITH_MESSAGE(errctx, "the embedded script failed");
|
|
rc = 1;
|
|
} FINISH_NORETURN(errctx);
|
|
|
|
printf("--- run to completion ---\n%s", CONSOLE.buffer);
|
|
return rc;
|
|
}
|