/** * @file hostvars.c * @brief Exchanging variables between a host C program and a BASIC script. * * This is the code in README.md's "Exchanging variables with the host" section, * kept here so it is compiled and run by every build rather than rotting in a * document. * * **Use akbasic_runtime_global().** It finds or creates the variable in the * script's outermost scope, which is what a host means every time. The obvious * alternative, akbasic_environment_get(), lands a new variable in whatever scope * is *active* -- and a script suspended part-way through a bounded run() is * usually inside a FOR or GOSUB body, so the variable dies when that body pops * and the script reads zero from then on, with nothing raised anywhere. * * The bottom of this file demonstrates that difference rather than describing * it, because it was TODO.md section 6 item 17 and it is the sort of thing that * gets rediscovered. */ #include #include #include #include #include #include static akbasic_Runtime SCRIPT; static akbasic_TextSink SINK; static akbasic_StdioSink SINKSTATE; static const char *PROGRAM = "10 PRINT \"HERO \" + NAME$ + \" HAS \" + HP# + \" HP\"\n" "20 SCORE# = HP# * LEVEL#\n" "30 STATUS$ = \"OK\"\n"; /* ------------------------------------------------------- host -> script -- */ /* * Seed an integer the script can read. akbasic_runtime_global() creates the * variable if it does not exist, and the type comes from the name's suffix, so * "HP#" is an integer and "HP$" would be a string. * * The subscript list is {0} with a count of 1 because every scalar is really a * one-element array -- the same shape the interpreter itself uses. */ static 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_runtime_global(obj, name, &variable)); PASS(errctx, akbasic_variable_set_integer(variable, value, subscript, 1)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext AKERR_NOIGNORE *host_set_string(akbasic_Runtime *obj, const char *name, const char *value) { PREPARE_ERROR(errctx); akbasic_Variable *variable = NULL; int64_t subscript[1] = { 0 }; PASS(errctx, akbasic_runtime_global(obj, name, &variable)); PASS(errctx, akbasic_variable_set_string(variable, value, subscript, 1)); SUCCEED_RETURN(errctx); } /* ------------------------------------------------------- script -> host -- */ static 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_runtime_global(obj, name, &variable)); 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); } static akerr_ErrorContext AKERR_NOIGNORE *host_get_string(akbasic_Runtime *obj, const char *name, char *dest, size_t len) { PREPARE_ERROR(errctx); akbasic_Variable *variable = NULL; akbasic_Value *value = NULL; int64_t subscript[1] = { 0 }; PASS(errctx, akbasic_runtime_global(obj, name, &variable)); PASS(errctx, akbasic_variable_get_subscript(variable, subscript, 1, &value)); FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, "%s is not a string", name); FAIL_ZERO_RETURN(errctx, (strlen(value->stringval) < len), AKBASIC_ERR_BOUNDS, "%s does not fit the caller's buffer", name); snprintf(dest, len, "%s", value->stringval); SUCCEED_RETURN(errctx); } /* ------------------------------------------------------------- the demo -- */ /* * The difference between the two entry points, demonstrated rather than * described. The script prints the same variable from inside a loop and from * after it, so a variable that died with the loop's scope is visible as a zero * on the second line rather than as an error anywhere. */ static const char *LOOPING_PROGRAM = "10 FOR I# = 1 TO 3\n" "20 PRINT \" in loop, GIFT# = \" + GIFT#\n" "30 NEXT I#\n" "40 PRINT \" after loop, GIFT# = \" + GIFT#\n"; /* Step until the script is inside the loop body, whatever line that turns out * to be. A step processes one source-line *index*, and lines are filed under * their BASIC line numbers, so a fixed count would be a hostage to them. */ static akerr_ErrorContext AKERR_NOIGNORE *run_into_the_loop(void) { PREPARE_ERROR(errctx); int steps = 0; while ( steps < AKBASIC_MAX_SOURCE_LINES && SCRIPT.mode != AKBASIC_MODE_QUIT && SCRIPT.environment->parent == NULL ) { PASS(errctx, akbasic_runtime_run(&SCRIPT, 1)); steps += 1; } SUCCEED_RETURN(errctx); } static akerr_ErrorContext AKERR_NOIGNORE *demonstrate_scope_rules(void) { PREPARE_ERROR(errctx); akbasic_Environment *root = NULL; akbasic_Variable *variable = NULL; PASS(errctx, akbasic_runtime_init(&SCRIPT, &SINK)); root = SCRIPT.environment; PASS(errctx, akbasic_runtime_load(&SCRIPT, LOOPING_PROGRAM)); PASS(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN)); PASS(errctx, run_into_the_loop()); printf("suspended inside the loop? %s\n", (SCRIPT.environment != root ? "yes" : "no")); /* * The right way. The variable lands in the root scope, so line 40 -- which * runs after NEXT has popped the loop -- still sees it. */ PASS(errctx, host_set_int(&SCRIPT, "GIFT#", 42)); /* * The wrong way, kept because the failure is silent and worth seeing once. * akbasic_environment_get() only auto-creates in the *active* environment, * so reaching for the root while a child is active returns NULL through * dest without raising -- and an unchecked host dereferences it. */ PASS(errctx, akbasic_environment_get(root, "UNREACHABLE#", &variable)); printf("does environment_get() create in the root mid-run? %s\n", (variable != NULL ? "yes" : "no -- it returns NULL, with no error")); PASS(errctx, akbasic_runtime_run(&SCRIPT, 0)); SUCCEED_RETURN(errctx); } int main(void) { PREPARE_ERROR(errctx); char status[64]; int64_t score = 0; int rc = 0; ATTEMPT { CATCH(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, stdin)); CATCH(errctx, akbasic_runtime_init(&SCRIPT, &SINK)); CATCH(errctx, akbasic_runtime_load(&SCRIPT, PROGRAM)); /* * Seed before starting. At this point the root environment is the active * one, so anything created here is global to the script. */ CATCH(errctx, host_set_int(&SCRIPT, "HP#", 100)); 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)); /* Read the results back once it has stopped. */ CATCH(errctx, host_get_int(&SCRIPT, "SCORE#", &score)); CATCH(errctx, host_get_string(&SCRIPT, "STATUS$", status, sizeof(status))); printf("host read back: SCORE# = %lld, STATUS$ = \"%s\"\n", (long long)score, status); printf("\n--- creating a variable mid-run ---\n"); CATCH(errctx, demonstrate_scope_rules()); } CLEANUP { } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "host variable exchange failed"); rc = 1; } FINISH_NORETURN(errctx); return rc; }