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>
This commit is contained in:
@@ -372,8 +372,9 @@ static akerr_ErrorContext *assign_field(akbasic_Environment *obj, akbasic_ASTLea
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_StructField *field = NULL;
|
||||
akbasic_Value *slot = NULL;
|
||||
void *hostbase = NULL;
|
||||
|
||||
PASS(errctx, akbasic_struct_resolve(obj->runtime, lval, &field, &slot));
|
||||
PASS(errctx, akbasic_struct_resolve(obj->runtime, lval, &field, &slot, &hostbase));
|
||||
|
||||
switch ( field->kind ) {
|
||||
case AKBASIC_FIELD_STRUCT:
|
||||
@@ -413,6 +414,16 @@ static akerr_ErrorContext *assign_field(akbasic_Environment *obj, akbasic_ASTLea
|
||||
PASS(errctx, akbasic_value_clone(rval, slot));
|
||||
break;
|
||||
}
|
||||
/*
|
||||
* And straight back into the host's own memory, so `FOE@.HP# = 0` changes
|
||||
* the game's enemy rather than a copy of it. The conversion refuses what
|
||||
* will not fit -- an int16_t field handed 70000 is an error naming the
|
||||
* field, because a silent wrap is found three frames later in code that did
|
||||
* nothing wrong.
|
||||
*/
|
||||
if ( hostbase != NULL && field->kind == AKBASIC_FIELD_PRIMITIVE ) {
|
||||
PASS(errctx, akbasic_host_write_field(obj->runtime, field, hostbase, slot));
|
||||
}
|
||||
*dest = slot;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
363
src/host.c
Normal file
363
src/host.c
Normal file
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
@@ -480,10 +480,22 @@ static akerr_ErrorContext *evaluate_identifier(akbasic_Runtime *obj, akbasic_AST
|
||||
PASS(errctx, akbasic_environment_new_value(obj->environment, ©));
|
||||
PASS(errctx, akbasic_struct_describe(obj, variable->structtype, slot,
|
||||
variable->ispointer, copy));
|
||||
/*
|
||||
* A host binding carries its instance along, so a field read can go to
|
||||
* the game's memory rather than to the shadow slot. Refreshed here too,
|
||||
* so `B@ = FOE@` copies what the game holds now rather than what it held
|
||||
* when the binding was made.
|
||||
*/
|
||||
if ( variable->hostbase != NULL ) {
|
||||
copy->hostbase = variable->hostbase;
|
||||
PASS(errctx, akbasic_host_refresh(obj, variable->structtype,
|
||||
variable->hostbase, slot));
|
||||
}
|
||||
if ( variable->ispointer ) {
|
||||
/* A pointer variable holds its reference in its own slot. */
|
||||
copy->structtype = slot->structtype;
|
||||
copy->structbase = slot->structbase;
|
||||
copy->hostbase = slot->hostbase;
|
||||
}
|
||||
*dest = copy;
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -1285,6 +1297,21 @@ akerr_ErrorContext *akbasic_runtime_start(akbasic_Runtime *obj, int mode)
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in start");
|
||||
obj->run_finished_mode = (mode == AKBASIC_MODE_REPL ? AKBASIC_MODE_REPL : AKBASIC_MODE_QUIT);
|
||||
/*
|
||||
* Start means start, so a run begins at the first line and is not something
|
||||
* CONT may resume into -- the same two lines `RUN` has always set.
|
||||
*
|
||||
* It was missing here, which cost nothing while a host started a script
|
||||
* once: `nextline` is already 0 the first time. It costs a host that runs a
|
||||
* script *again* everything, because the second start began past the end and
|
||||
* silently did nothing. A game rebinding a structure per enemy and running
|
||||
* one script over each of them is exactly that shape, which is how this was
|
||||
* found.
|
||||
*/
|
||||
if ( mode == AKBASIC_MODE_RUN && obj->environment != NULL ) {
|
||||
obj->environment->nextline = 0;
|
||||
obj->stopped = false;
|
||||
}
|
||||
PASS(errctx, akbasic_runtime_set_mode(obj, mode));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,8 @@ akerr_ErrorContext *akbasic_struct_copy(akbasic_Runtime *obj, int typeindex,
|
||||
* name at all.
|
||||
*/
|
||||
akerr_ErrorContext *akbasic_struct_resolve(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
||||
akbasic_StructField **fielddest, akbasic_Value **slotdest)
|
||||
akbasic_StructField **fielddest, akbasic_Value **slotdest,
|
||||
void **hostdest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Value *basevalue = NULL;
|
||||
@@ -136,6 +137,9 @@ akerr_ErrorContext *akbasic_struct_resolve(akbasic_Runtime *obj, akbasic_ASTLeaf
|
||||
if ( fielddest != NULL ) {
|
||||
*fielddest = field;
|
||||
}
|
||||
if ( hostdest != NULL ) {
|
||||
*hostdest = basevalue->hostbase;
|
||||
}
|
||||
*slotdest = slot;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -146,8 +150,9 @@ akerr_ErrorContext *akbasic_struct_evaluate_field(akbasic_Runtime *obj, akbasic_
|
||||
akbasic_StructField *field = NULL;
|
||||
akbasic_Value *slot = NULL;
|
||||
akbasic_Value *out = NULL;
|
||||
void *hostbase = NULL;
|
||||
|
||||
PASS(errctx, akbasic_struct_resolve(obj, expr, &field, &slot));
|
||||
PASS(errctx, akbasic_struct_resolve(obj, expr, &field, &slot, &hostbase));
|
||||
|
||||
/*
|
||||
* A nested structure evaluates to a description of where it lives, not to a
|
||||
@@ -158,9 +163,20 @@ akerr_ErrorContext *akbasic_struct_evaluate_field(akbasic_Runtime *obj, akbasic_
|
||||
if ( field->kind == AKBASIC_FIELD_STRUCT ) {
|
||||
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
|
||||
PASS(errctx, akbasic_struct_describe(obj, field->typeindex, slot, false, out));
|
||||
if ( hostbase != NULL ) {
|
||||
out->hostbase = (char *)hostbase + field->hostoffset;
|
||||
}
|
||||
*dest = out;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
/*
|
||||
* A host-backed field is read from the host's memory every time, not from
|
||||
* the shadow slot -- the game may have moved it since the script last
|
||||
* looked, and "shared" has to mean shared in both directions.
|
||||
*/
|
||||
if ( hostbase != NULL ) {
|
||||
PASS(errctx, akbasic_host_read_field(obj, field, hostbase, slot));
|
||||
}
|
||||
*dest = slot;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -275,7 +291,7 @@ static akerr_ErrorContext *pointer_slot(akbasic_Runtime *obj, akbasic_ASTLeaf *l
|
||||
FAIL_ZERO_RETURN(errctx, (leaf != NULL), AKERR_NULLPOINTER, "NULL leaf in pointer_slot");
|
||||
|
||||
if ( leaf->leaftype == AKBASIC_LEAF_FIELD ) {
|
||||
PASS(errctx, akbasic_struct_resolve(obj, leaf, &field, &slot));
|
||||
PASS(errctx, akbasic_struct_resolve(obj, leaf, &field, &slot, NULL));
|
||||
FAIL_ZERO_RETURN(errctx, (field->kind == AKBASIC_FIELD_POINTER), AKBASIC_ERR_TYPE,
|
||||
"%s is not a pointer; only a field declared PTR TO can be POINTed",
|
||||
field->name);
|
||||
@@ -338,6 +354,13 @@ akerr_ErrorContext *akbasic_cmd_point(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
|
||||
slot->valuetype = AKBASIC_TYPE_POINTER;
|
||||
slot->structtype = target->structtype;
|
||||
slot->structbase = target->structbase;
|
||||
/*
|
||||
* And the host instance, when there is one. Without it a pointer aimed at a
|
||||
* host binding would write the shadow slot and stop there -- the script
|
||||
* would see its own change and the game would not, which is the one outcome
|
||||
* worse than refusing outright.
|
||||
*/
|
||||
slot->hostbase = target->hostbase;
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -425,7 +425,26 @@ akerr_ErrorContext *akbasic_structtype_scan(akbasic_Runtime *obj)
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in structtype scan");
|
||||
table = &obj->structtypes;
|
||||
PASS(errctx, akbasic_structtype_table_init(table));
|
||||
|
||||
/*
|
||||
* Drop what the *script* declared and keep what the *host* registered.
|
||||
*
|
||||
* This prescan runs again on every RUN, so wiping the table outright would
|
||||
* unregister a host's types the first time a script was run -- and the host
|
||||
* registered them before loading anything, with no way to know it had to do
|
||||
* it again. Host types are registered before a program exists, so they are
|
||||
* always a prefix; truncating to that prefix keeps every index a field
|
||||
* already recorded pointing at the same type.
|
||||
*/
|
||||
for ( t = 0; t < table->count; t++ ) {
|
||||
if ( !table->types[t].ishost ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for ( i = t; i < table->count; i++ ) {
|
||||
memset(&table->types[i], 0, sizeof(table->types[i]));
|
||||
}
|
||||
table->count = t;
|
||||
|
||||
PASS(errctx, scan_names(obj));
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ akerr_ErrorContext *akbasic_value_zero(akbasic_Value *obj)
|
||||
obj->boolvalue = AKBASIC_FALSE;
|
||||
obj->structtype = -1;
|
||||
obj->structbase = NULL;
|
||||
obj->hostbase = NULL;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -214,6 +215,7 @@ akerr_ErrorContext *akbasic_value_clone(akbasic_Value *self, akbasic_Value *dest
|
||||
*/
|
||||
dest->structtype = self->structtype;
|
||||
dest->structbase = self->structbase;
|
||||
dest->hostbase = self->hostbase;
|
||||
/* mutable_ is deliberately not copied: the reference's clone() does not. */
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user