/** * @file hoststruct.c * @brief Sharing a game's own C structures with a script. * * This is the code in docs/15-structures.md's "Sharing a structure with a host" * section, kept here so it is compiled and run by every build rather than * rotting in a document. * * **The thing worth noticing is that there is no marshalling step.** The host * describes its struct once and binds an instance; the script then reads and * writes the game's real memory. `FOE@.HP# = FOE@.HP# - 10` decrements * `GOBLIN.hp` in place. * * The second thing worth noticing is that the language's own distinction -- * assignment copies, `POINT` shares -- turns out to be exactly the distinction * a host needs, so there is only one API and which one a script used is visible * in the listing. */ #include #include #include #include #include #include #include #include #include #include /* ------------------------------------------------------------ the game --- */ /** @brief The game's own struct. Nothing about it is akbasic's business. */ typedef struct { char name[32]; int32_t hp; float x; float y; bool hostile; } game_Enemy; static game_Enemy GOBLIN = { "GOBLIN", 30, 10.0f, 20.0f, true }; static game_Enemy TROLL = { "TROLL", 80, 50.0f, 60.0f, true }; /* * The description akbasic needs: one row per field, with the BASIC name and its * suffix, the C representation, and where it sits. AKBASIC_HOST_FIELD takes the * offset and the width from the member itself, so the two cannot drift apart -- * writing them out by hand is two chances to name the wrong member and no way to * notice. */ static const akbasic_HostField ENEMY_FIELDS[] = { /* struct member BASIC name C representation */ AKBASIC_HOST_FIELD( game_Enemy, name, "NAME$", AKBASIC_HOSTFIELD_CSTRING ), AKBASIC_HOST_FIELD( game_Enemy, hp, "HP#", AKBASIC_HOSTFIELD_INT32 ), AKBASIC_HOST_FIELD( game_Enemy, x, "X%", AKBASIC_HOSTFIELD_FLOAT ), AKBASIC_HOST_FIELD( game_Enemy, y, "Y%", AKBASIC_HOSTFIELD_FLOAT ), AKBASIC_HOST_FIELD( game_Enemy, hostile, "HOSTILE#", AKBASIC_HOSTFIELD_BOOL ) }; static const akbasic_HostType ENEMY_TYPE = { "ENEMY", sizeof(game_Enemy), ENEMY_FIELDS, 5 }; /* ------------------------------------------------------ the interpreter --- */ static akbasic_Runtime SCRIPT; static akbasic_TextSink SINK; static akbasic_StdioSink SINKSTATE; /* * A script written against one name. The host points that name at each enemy in * turn, which is what akbasic_host_rebind() is for -- one script, one binding, * however many enemies. */ static const char *PROGRAM = "10 PRINT FOE@.NAME$ + \" HAS \" + FOE@.HP#\n" "20 FOE@.HP# = FOE@.HP# - 10\n" "30 PRINT \" AFTER THE HIT: \" + FOE@.HP#\n"; static akerr_ErrorContext AKERR_NOIGNORE *run_against(game_Enemy *enemy) { PREPARE_ERROR(errctx); PASS(errctx, akbasic_host_rebind(&SCRIPT, "FOE@", enemy)); PASS(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN)); PASS(errctx, akbasic_runtime_run(&SCRIPT, 0)); SUCCEED_RETURN(errctx); } int main(void) { PREPARE_ERROR(errctx); int rc = 0; ATTEMPT { CATCH(errctx, akbasic_error_register()); CATCH(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL)); CATCH(errctx, akbasic_runtime_init(&SCRIPT, &SINK)); /* * Register the type, then bind an instance. The descriptor is borrowed * rather than copied, which is why it is `static const` -- it has to * outlive the runtime. */ CATCH(errctx, akbasic_host_register_type(&SCRIPT, &ENEMY_TYPE)); CATCH(errctx, akbasic_host_bind(&SCRIPT, "FOE@", "ENEMY", &GOBLIN)); CATCH(errctx, akbasic_runtime_load(&SCRIPT, PROGRAM)); CATCH(errctx, run_against(&GOBLIN)); CATCH(errctx, run_against(&TROLL)); /* * And the game's own structs have changed. No marshalling step, no copy * to write back -- the script was reading and writing these all along. */ printf("host sees GOBLIN.hp = %d\n", GOBLIN.hp); printf("host sees TROLL.hp = %d\n", TROLL.hp); /* * Unbind before the storage would go away. A binding is the only pointer * this interpreter holds that it did not allocate, so this is the one * lifetime a host has to think about -- and after it, a script reading * the name is refused by name rather than reading freed memory. */ CATCH(errctx, akbasic_host_unbind(&SCRIPT, "FOE@")); } CLEANUP { } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "host structure example failed"); rc = 1; } FINISH_NORETURN(errctx); return rc; }