/** * @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 #include #include #include #include #include /* * 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; }