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;
|
||
|
|
}
|