Document host/script variable exchange, and file the scope hazard it exposes
The host can expose variables to a BASIC program, in both directions and for every type, with no marshalling layer -- akbasic_environment_get() finds or creates by name, the type comes from the suffix as it does for BASIC code, and host and script share the same akbasic_Value. Nothing new was needed to make that work. Testing it before writing it up turned up a hazard worth stating plainly, so the README documents the pattern and section 6 item 17 records the gap. Seeding before akbasic_runtime_start() and reading after the script stops is safe, and so is updating an existing global at any time, because the parent chain is searched. Creating one mid-run is not, in two ways, and both are silent. A script suspended part-way through a bounded run() is usually inside a FOR or GOSUB scope, so a variable created through obj->environment lands in that scope and dies when it pops -- the script reads it correctly inside the loop and gets 0 immediately after. And reaching for the root explicitly does not help: akbasic_environment_get only auto-creates when the environment it is given is the active one, so with a child active it returns NULL through dest without raising, and an unchecked host dereferences it. This one is ours rather than inherited. It falls out of the environment pool meeting the bounded run(), a combination the reference never had because its run() never returned. examples/hostvars.c demonstrates both hazards rather than describing them, and prints what it observes, so the day this is fixed that output changes and the example needs revisiting. Like examples/embed.c it is built and run by every build. Both README snippets were extracted and compiled as a check. Not fixed here: akbasic_runtime_global() would close it in about fifteen lines, but what it should do when the script is suspended inside a user function's scope is a design question worth settling deliberately rather than discovering. Filed with the proposed signature and the tests it wants. ctest 61/61; ASan+UBSan 61/61; no warnings under -Wall -Wextra. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
83
README.md
83
README.md
@@ -203,6 +203,89 @@ akerr_ErrorContext AKERR_NOIGNORE *script_tick(void)
|
||||
}
|
||||
```
|
||||
|
||||
## Exchanging variables with the host
|
||||
|
||||
Yes — in both directions, for integers, floats and strings, using the same variable pool the
|
||||
script itself uses. There is no marshalling layer and no copy: the host and the script are
|
||||
looking at the same `akbasic_Value`.
|
||||
|
||||
`akbasic_environment_get()` finds a variable by name and creates it if it does not exist. The
|
||||
type comes from the name's suffix, exactly as it does for BASIC code, so `HP#` is an integer
|
||||
and `NAME$` is a string. Every scalar is really a one-element array, which is why the subscript
|
||||
list is `{0}` with a count of 1.
|
||||
|
||||
```c
|
||||
/* host -> script, before the script starts */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *host_set_int(akbasic_Runtime *obj, const char *name, int64_t value)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Variable *variable = NULL;
|
||||
int64_t subscript[1] = { 0 };
|
||||
|
||||
PASS(errctx, akbasic_environment_get(obj->environment, name, &variable));
|
||||
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKERR_KEY,
|
||||
"could not reach variable %s from the active scope", name);
|
||||
PASS(errctx, akbasic_variable_set_integer(variable, value, subscript, 1));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* script -> host, after it stops */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *host_get_int(akbasic_Runtime *obj, const char *name, int64_t *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Variable *variable = NULL;
|
||||
akbasic_Value *value = NULL;
|
||||
int64_t subscript[1] = { 0 };
|
||||
|
||||
PASS(errctx, akbasic_environment_get(obj->environment, name, &variable));
|
||||
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKERR_KEY, "no variable %s", name);
|
||||
PASS(errctx, akbasic_variable_get_subscript(variable, subscript, 1, &value));
|
||||
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
||||
"%s is not an integer", name);
|
||||
*dest = value->intval;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
Used like this, with `akbasic_variable_set_string` and `set_float` as the other two:
|
||||
|
||||
```c
|
||||
CATCH(errctx, akbasic_runtime_load(&SCRIPT, PROGRAM));
|
||||
|
||||
CATCH(errctx, host_set_int(&SCRIPT, "HP#", 100)); /* seed */
|
||||
CATCH(errctx, host_set_int(&SCRIPT, "LEVEL#", 7));
|
||||
CATCH(errctx, host_set_string(&SCRIPT, "NAME$", "LINK"));
|
||||
|
||||
CATCH(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
|
||||
CATCH(errctx, akbasic_runtime_run(&SCRIPT, 0));
|
||||
|
||||
CATCH(errctx, host_get_int(&SCRIPT, "SCORE#", &score)); /* collect */
|
||||
```
|
||||
|
||||
The full thing, including the string and float forms, is in
|
||||
[`examples/hostvars.c`](examples/hostvars.c). It is built and run by every build.
|
||||
|
||||
### The one rule: seed before, read after
|
||||
|
||||
**Do this between runs, not during one.** Seed before `akbasic_runtime_start()` and read after
|
||||
the script has stopped. Poking a variable while a script is suspended part-way through a
|
||||
bounded `akbasic_runtime_run()` is sharp in two ways, and both are silent:
|
||||
|
||||
* A suspended script is usually inside a `FOR` or `GOSUB` scope, and `obj->environment` is that
|
||||
scope rather than the global one. A variable *created* there dies when the scope pops — the
|
||||
script reads it correctly inside the loop and gets `0` the moment the loop ends.
|
||||
* Reaching for the global scope explicitly does not help. Only the currently active environment
|
||||
auto-creates, so `akbasic_environment_get(root, "NEW#", &var)` with a child active hands back
|
||||
`NULL` and no error, and an unchecked host dereferences it.
|
||||
|
||||
Reading and *updating an existing* global is safe at any time — the parent chain is searched,
|
||||
so a variable that already exists in the global scope is found from anywhere. It is only
|
||||
*creation* that lands in the wrong place.
|
||||
|
||||
`examples/hostvars.c` demonstrates both hazards rather than describing them. This is filed as
|
||||
[`TODO.md`](TODO.md) section 6 item 17; the fix is a small `akbasic_runtime_global()` that
|
||||
resolves against the root scope regardless of what is active.
|
||||
|
||||
## Where the output goes
|
||||
|
||||
`PRINT` writes through an `akbasic_TextSink`, which is a record of function pointers plus whatever state you hang off `self`:
|
||||
|
||||
Reference in New Issue
Block a user