Files
akbasic/examples/hoststruct.c

139 lines
4.7 KiB
C
Raw Normal View History

Share a host program's own C structures with a script A BASIC TYPE and a host C struct are the same thing seen from two sides, so both go in one type table and everything the language already does with a structure works across the boundary with no second set of rules. The host describes its struct once as a table of field descriptors and binds an instance: akbasic_host_bind(&SCRIPT, "FOE@", "ENEMY", &GOBLIN); after which `FOE@.HP# = FOE@.HP# - 10` decrements GOBLIN.hp in place, with no marshalling step the host has to remember to run. The sharing is done with shadow slots: a binding takes a run from the same value pool a DIMmed record uses, a field read refreshes its slot from host memory first, and a write converts back and stores. So the script always sees current values and its writes always land, while the rest of the interpreter goes on seeing one storage model instead of two. AKBASIC_HOST_FIELD takes the offset and the width from the same member, which is the only reason it is a macro: writing offsetof and sizeof out by hand is two chances to name the wrong member and no way to notice. A field name's suffix must agree with the C type it describes, refused at registration -- a host writing "HP%" over an int32_t has said two different things about one field and the script would believe the suffix. Conversion refuses rather than truncates. 200 into an int8_t, 70000 into an int16_t, -1 into a uint8_t and thirteen characters into a char[8] are each an error naming the field, because a silent wrap is found three frames later in code that did nothing wrong. Each width is tested separately, since a range check is exactly the thing that is right for int32_t and wrong for int8_t when only one of them is covered. The language's own distinction turns out to be the one a host needs, so there is one API rather than two: assignment copies and gives a script a private snapshot, POINT shares and lets it change the game, and which one happened is visible in the listing. Two things the work required. The prescan runs again on every RUN and used to wipe the whole type table, unregistering the host's types the first time a script ran; it now keeps what the host registered and drops only what the script declared. And akbasic_runtime_start() did not rewind, which cost nothing while a host started a script once and cost everything to a host running one per enemy -- the second start began past the end and silently did nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:56:04 -04:00
/**
* @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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#include <akbasic/error.h>
#include <akbasic/host.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
/* ------------------------------------------------------------ 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;
}