Files
akbasic/src/host.c

364 lines
15 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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 11:56:04 -04:00
/**
* @file host.c
* @brief Implements host structure binding and the conversions it needs.
*
* A host type joins the same table a `TYPE` declaration fills, so everything the
* language already does with a structure works across the boundary with no
* second set of rules. What differs is only where a field's bytes are.
*
* **The sharing is done with shadow slots.** A binding takes a run of value
* slots from the same pool a `DIM`med record uses; a field read refreshes its
* slot from host memory first, and a field write converts back and stores. So a
* script always sees current values and its writes always land, and the rest of
* the interpreter goes on seeing one storage model instead of two.
*
* **Every conversion refuses rather than truncates.** Writing 70000 into an
* `int16_t` is an error naming the field, because a silent wrap is the failure
* mode that gets found three frames later in somebody else's code.
*/
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#include <akbasic/error.h>
#include <akbasic/host.h>
#include <akbasic/runtime.h>
#include <akbasic/structtype.h>
/**
* @brief The BASIC type a host field presents as.
*
* Every integer width is an integer here, because BASIC has one integer type and
* a 64-bit one at that. The width still matters -- it is what a write is range
* checked against -- but it does not change what the script sees.
*/
static akbasic_Type basic_type_of(akbasic_HostFieldKind kind)
{
switch ( kind ) {
case AKBASIC_HOSTFIELD_FLOAT:
case AKBASIC_HOSTFIELD_DOUBLE: return AKBASIC_TYPE_FLOAT;
case AKBASIC_HOSTFIELD_CSTRING: return AKBASIC_TYPE_STRING;
case AKBASIC_HOSTFIELD_STRUCT: return AKBASIC_TYPE_STRUCT;
default: return AKBASIC_TYPE_INTEGER;
}
}
/** @brief The type suffix a field name must carry for the C type it describes. */
static char suffix_for(akbasic_HostFieldKind kind)
{
switch ( basic_type_of(kind) ) {
case AKBASIC_TYPE_FLOAT: return '%';
case AKBASIC_TYPE_STRING: return '$';
case AKBASIC_TYPE_STRUCT: return '@';
default: return '#';
}
}
akerr_ErrorContext *akbasic_host_read_field(akbasic_Runtime *obj, akbasic_StructField *field,
void *hostbase, akbasic_Value *dest)
{
PREPARE_ERROR(errctx);
const char *at = (const char *)hostbase + field->hostoffset;
char text[AKBASIC_MAX_STRING_LENGTH];
(void)obj;
FAIL_ZERO_RETURN(errctx, (hostbase != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in host read");
PASS(errctx, akbasic_value_zero(dest));
switch ( field->hostkind ) {
case AKBASIC_HOSTFIELD_INT8: dest->intval = *(const int8_t *)at; break;
case AKBASIC_HOSTFIELD_INT16: dest->intval = *(const int16_t *)at; break;
case AKBASIC_HOSTFIELD_INT32: dest->intval = *(const int32_t *)at; break;
case AKBASIC_HOSTFIELD_INT64: dest->intval = *(const int64_t *)at; break;
case AKBASIC_HOSTFIELD_UINT8: dest->intval = *(const uint8_t *)at; break;
case AKBASIC_HOSTFIELD_UINT16: dest->intval = *(const uint16_t *)at; break;
case AKBASIC_HOSTFIELD_UINT32: dest->intval = *(const uint32_t *)at; break;
case AKBASIC_HOSTFIELD_BOOL:
/* -1 and 0, the Commodore convention every condition here already uses. */
dest->intval = (*(const bool *)at ? AKBASIC_TRUE : AKBASIC_FALSE);
break;
case AKBASIC_HOSTFIELD_FLOAT: dest->floatval = (double)*(const float *)at; break;
case AKBASIC_HOSTFIELD_DOUBLE: dest->floatval = *(const double *)at; break;
case AKBASIC_HOSTFIELD_CSTRING:
/*
* Copied out rather than pointed at, and NUL-terminated even if the host
* left it full: a BASIC string is inline and fixed, so there is nothing
* to alias and nothing that can outlive the read.
*/
snprintf(text, sizeof(text), "%.*s", (int)field->hostwidth, at);
snprintf(dest->stringval, sizeof(dest->stringval), "%s", text);
break;
default:
FAIL_RETURN(errctx, AKBASIC_ERR_TYPE,
"Host field %s cannot be read as a value", field->name);
}
dest->valuetype = basic_type_of(field->hostkind);
SUCCEED_RETURN(errctx);
}
/**
* @brief Convert a value back into the host's own representation, or refuse.
*
* The range checks are the reason this is not a memcpy. A host `int16_t` holds
* -32768..32767 and a script that assigns 70000 has made a mistake -- wrapping
* it silently would hand the game a number it never asked for and no way to
* find out where it came from.
*/
akerr_ErrorContext *akbasic_host_write_field(akbasic_Runtime *obj, akbasic_StructField *field,
void *hostbase, akbasic_Value *src)
{
PREPARE_ERROR(errctx);
char *at = (char *)hostbase + field->hostoffset;
int64_t n = 0;
double d = 0.0;
(void)obj;
FAIL_ZERO_RETURN(errctx, (hostbase != NULL && src != NULL), AKERR_NULLPOINTER,
"NULL argument in host write");
if ( basic_type_of(field->hostkind) == AKBASIC_TYPE_INTEGER ) {
n = (src->valuetype == AKBASIC_TYPE_FLOAT ? (int64_t)src->floatval : src->intval);
} else if ( basic_type_of(field->hostkind) == AKBASIC_TYPE_FLOAT ) {
d = (src->valuetype == AKBASIC_TYPE_INTEGER ? (double)src->intval : src->floatval);
}
#define AKBASIC_HOST_RANGE(__lo, __hi) \
FAIL_ZERO_RETURN(errctx, (n >= (int64_t)(__lo) && n <= (int64_t)(__hi)), \
AKBASIC_ERR_VALUE, \
"%" PRId64 " does not fit in %s, which holds %" PRId64 \
" to %" PRId64, n, field->name, (int64_t)(__lo), (int64_t)(__hi))
switch ( field->hostkind ) {
case AKBASIC_HOSTFIELD_INT8:
AKBASIC_HOST_RANGE(INT8_MIN, INT8_MAX); *(int8_t *)at = (int8_t)n; break;
case AKBASIC_HOSTFIELD_INT16:
AKBASIC_HOST_RANGE(INT16_MIN, INT16_MAX); *(int16_t *)at = (int16_t)n; break;
case AKBASIC_HOSTFIELD_INT32:
AKBASIC_HOST_RANGE(INT32_MIN, INT32_MAX); *(int32_t *)at = (int32_t)n; break;
case AKBASIC_HOSTFIELD_INT64:
*(int64_t *)at = n; break;
case AKBASIC_HOSTFIELD_UINT8:
AKBASIC_HOST_RANGE(0, UINT8_MAX); *(uint8_t *)at = (uint8_t)n; break;
case AKBASIC_HOSTFIELD_UINT16:
AKBASIC_HOST_RANGE(0, UINT16_MAX); *(uint16_t *)at = (uint16_t)n; break;
case AKBASIC_HOSTFIELD_UINT32:
AKBASIC_HOST_RANGE(0, UINT32_MAX); *(uint32_t *)at = (uint32_t)n; break;
case AKBASIC_HOSTFIELD_BOOL:
/* Any nonzero is true, which is what AKBASIC_TRUE being -1 requires. */
*(bool *)at = (n != 0);
break;
case AKBASIC_HOSTFIELD_FLOAT:
*(float *)at = (float)d;
break;
case AKBASIC_HOSTFIELD_DOUBLE:
*(double *)at = d;
break;
case AKBASIC_HOSTFIELD_CSTRING:
FAIL_ZERO_RETURN(errctx, (src->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"%s is a string field", field->name);
/*
* Refused rather than truncated, and the width is the host's: a
* `char[32]` holds 31 characters and a script assigning 40 has lost
* eight of them. BASIC strings have no width, so this is the one place
* the two models genuinely disagree and the disagreement is reported.
*/
FAIL_ZERO_RETURN(errctx, (strlen(src->stringval) < field->hostwidth),
AKBASIC_ERR_VALUE,
"A string of %zu characters does not fit in %s, which holds %zu",
strlen(src->stringval), field->name, field->hostwidth - 1);
memset(at, 0, field->hostwidth);
memcpy(at, src->stringval, strlen(src->stringval));
break;
default:
FAIL_RETURN(errctx, AKBASIC_ERR_TYPE,
"Host field %s cannot be written from a value", field->name);
}
#undef AKBASIC_HOST_RANGE
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_host_refresh(akbasic_Runtime *obj, int typeindex,
void *hostbase, akbasic_Value *slots)
{
PREPARE_ERROR(errctx);
akbasic_StructType *type = NULL;
int i = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && hostbase != NULL && slots != NULL),
AKERR_NULLPOINTER, "NULL argument in host refresh");
type = &obj->structtypes.types[typeindex];
for ( i = 0; i < type->fieldcount; i++ ) {
akbasic_StructField *field = &type->fields[i];
if ( field->kind == AKBASIC_FIELD_STRUCT ) {
PASS(errctx, akbasic_host_refresh(obj, field->typeindex,
(char *)hostbase + field->hostoffset,
slots + field->offset));
continue;
}
PASS(errctx, akbasic_host_read_field(obj, field, hostbase, slots + field->offset));
}
SUCCEED_RETURN(errctx);
}
/* ------------------------------------------------------------ the API --- */
akerr_ErrorContext *akbasic_host_register_type(akbasic_Runtime *obj, const akbasic_HostType *type)
{
PREPARE_ERROR(errctx);
akbasic_StructType *dest = NULL;
int existing = -1;
int offset = 0;
int i = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && type != NULL && type->name != NULL),
AKERR_NULLPOINTER, "NULL argument in host register_type");
PASS(errctx, akbasic_structtype_find(&obj->structtypes, type->name, &existing));
FAIL_NONZERO_RETURN(errctx, (existing >= 0), AKBASIC_ERR_VALUE,
"TYPE %s is already registered", type->name);
FAIL_ZERO_RETURN(errctx, (obj->structtypes.count < AKBASIC_MAX_STRUCT_TYPES),
AKBASIC_ERR_BOUNDS, "More than %d types", AKBASIC_MAX_STRUCT_TYPES);
FAIL_ZERO_RETURN(errctx, (type->fieldcount > 0 && type->fieldcount <= AKBASIC_MAX_STRUCT_FIELDS),
AKBASIC_ERR_BOUNDS, "%s declares %d fields, which is outside 1..%d",
type->name, type->fieldcount, AKBASIC_MAX_STRUCT_FIELDS);
dest = &obj->structtypes.types[obj->structtypes.count];
memset(dest, 0, sizeof(*dest));
snprintf(dest->name, sizeof(dest->name), "%s", type->name);
dest->used = true;
dest->ishost = true;
dest->hostsize = type->size;
dest->firstline = -1;
dest->lastline = -1;
for ( i = 0; i < type->fieldcount; i++ ) {
const akbasic_HostField *src = &type->fields[i];
akbasic_StructField *field = &dest->fields[i];
size_t len = 0;
FAIL_ZERO_RETURN(errctx, (src->name != NULL), AKERR_NULLPOINTER,
"%s field %d has no name", type->name, i);
len = strlen(src->name);
/*
* The suffix has to agree with the C type. A host that writes "HP%" over
* an int32_t has said two different things about one field, and the
* script would believe the suffix -- so the disagreement is refused here
* rather than discovered as a wrong answer later.
*/
FAIL_ZERO_RETURN(errctx, (len >= 2 && src->name[len - 1] == suffix_for(src->kind)),
AKBASIC_ERR_VALUE,
"%s.%s must end in '%c' for the C type it describes",
type->name, src->name, suffix_for(src->kind));
snprintf(field->name, sizeof(field->name), "%s", src->name);
field->hostkind = src->kind;
field->hostoffset = src->offset;
field->hostwidth = src->width;
field->valuetype = basic_type_of(src->kind);
field->typeindex = -1;
field->offset = offset;
if ( src->kind == AKBASIC_HOSTFIELD_STRUCT ) {
int nested = -1;
FAIL_ZERO_RETURN(errctx, (src->typename_ != NULL), AKERR_NULLPOINTER,
"%s.%s is a nested structure and must name its type",
type->name, src->name);
PASS(errctx, akbasic_structtype_find(&obj->structtypes, src->typename_, &nested));
FAIL_ZERO_RETURN(errctx, (nested >= 0), AKBASIC_ERR_UNDEFINED,
"%s.%s names type %s, which is not registered",
type->name, src->name, src->typename_);
field->kind = AKBASIC_FIELD_STRUCT;
field->typeindex = nested;
field->slotcount = obj->structtypes.types[nested].slotcount;
} else {
field->kind = AKBASIC_FIELD_PRIMITIVE;
field->slotcount = 1;
}
offset += field->slotcount;
}
dest->fieldcount = type->fieldcount;
dest->slotcount = offset;
obj->structtypes.count += 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_host_bind(akbasic_Runtime *obj, const char *name,
const char *typename_, void *instance)
{
PREPARE_ERROR(errctx);
akbasic_Variable *variable = NULL;
int64_t sizes[1] = { 1 };
size_t len = 0;
int typeindex = -1;
FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && typename_ != NULL && instance != NULL),
AKERR_NULLPOINTER, "NULL argument in host bind");
len = strlen(name);
FAIL_ZERO_RETURN(errctx, (len >= 2 && name[len - 1] == '@'), AKBASIC_ERR_VALUE,
"A structure variable's name ends in '@', so \"%s\" cannot be bound", name);
PASS(errctx, akbasic_structtype_find(&obj->structtypes, typename_, &typeindex));
FAIL_ZERO_RETURN(errctx, (typeindex >= 0), AKBASIC_ERR_UNDEFINED,
"TYPE %s is not registered", typename_);
FAIL_ZERO_RETURN(errctx, (obj->structtypes.types[typeindex].ishost), AKBASIC_ERR_TYPE,
"TYPE %s was declared by the script, not registered by the host",
typename_);
/*
* The outermost scope, for the reason akbasic_runtime_global() exists: a
* binding made while a script is suspended would otherwise land in whatever
* FOR or GOSUB body is active and die with it.
*/
PASS(errctx, akbasic_runtime_global(obj, name, &variable));
if ( variable->structtype < 0 ) {
sizes[0] = obj->structtypes.types[typeindex].slotcount;
PASS(errctx, akbasic_variable_init(variable, &obj->valuepool, sizes, 1));
variable->valuetype = AKBASIC_TYPE_STRUCT;
variable->structtype = typeindex;
variable->ispointer = false;
}
FAIL_ZERO_RETURN(errctx, (variable->structtype == typeindex), AKBASIC_ERR_TYPE,
"%s is already bound to a different type", name);
variable->hostbase = instance;
PASS(errctx, akbasic_host_refresh(obj, typeindex, instance, variable->values));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_host_rebind(akbasic_Runtime *obj, const char *name, void *instance)
{
PREPARE_ERROR(errctx);
akbasic_Variable *variable = NULL;
FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && instance != NULL),
AKERR_NULLPOINTER, "NULL argument in host rebind");
PASS(errctx, akbasic_runtime_global(obj, name, &variable));
FAIL_ZERO_RETURN(errctx, (variable->structtype >= 0 && variable->hostbase != NULL),
AKBASIC_ERR_UNDEFINED, "%s is not bound to a host structure", name);
variable->hostbase = instance;
PASS(errctx, akbasic_host_refresh(obj, variable->structtype, instance, variable->values));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_host_unbind(akbasic_Runtime *obj, const char *name)
{
PREPARE_ERROR(errctx);
akbasic_Variable *variable = NULL;
FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL), AKERR_NULLPOINTER,
"NULL argument in host unbind");
PASS(errctx, akbasic_runtime_global(obj, name, &variable));
FAIL_ZERO_RETURN(errctx, (variable->structtype >= 0 && variable->hostbase != NULL),
AKBASIC_ERR_UNDEFINED, "%s is not bound to a host structure", name);
/*
* The shadow slots keep whatever they last held, but the binding is gone --
* so a later read is refused by name rather than following a pointer into
* storage the host may already have freed. That refusal is the whole reason
* this entry point exists.
*/
variable->hostbase = NULL;
variable->structtype = -1;
SUCCEED_RETURN(errctx);
}