// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // basstub.c -- DVX BASIC standalone application stub // // Minimal entry point for compiled BASIC applications. Reads the // MODULE resource (serialized bytecode), FORMn resources (.frm text), // and optional DEBUG resource from its own DXE file. Creates a VM // and form runtime from basrt.lib and runs the program. // // This file is compiled into basstub.app, which is embedded as a // resource in dvxbasic.app. "Make Executable" extracts it, renames // it to the project name, and attaches the compiled resources. #include "dvxApp.h" #include "dvxDlg.h" #include "dvxRes.h" #include "dvxWgt.h" #include "shellApp.h" #include "../basRes.h" #include "../runtime/vm.h" #include "../runtime/serialize.h" #include "../formrt/formrt.h" #include #include #include #include // ============================================================ // App descriptor // ============================================================ #define STUB_STACK_SIZE 65536 AppDescriptorT appDescriptor = { .name = "BASIC App", .hasMainLoop = true, .multiInstance = false, .stackSize = STUB_STACK_SIZE, .priority = TS_PRIORITY_NORMAL }; // ============================================================ // Callbacks // ============================================================ static AppContextT *sAc = NULL; // Function prototypes (alphabetical; main/appMain last) void appShutdown(void); static bool stubDoEvents(void *ctx); static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize); static void stubPrint(void *ctx, const char *text, bool newline); int32_t appMain(DxeAppContextT *ctx); static bool stubDoEvents(void *ctx) { (void)ctx; if (!sAc) { return false; } return dvxUpdate(sAc); } static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize) { (void)ctx; buf[0] = '\0'; if (!sAc) { return false; } return dvxInputBox(sAc, "Input", prompt, "", buf, bufSize); } static void stubPrint(void *ctx, const char *text, bool newline) { (void)ctx; (void)text; (void)newline; } // Resolved by the shell as _appShutdown and called on BOTH graceful reap and // force-kill, before this app's DXE is unmapped. On a normal exit appMain // already ran basFormRtDestroy; on a force-kill it never returns, so this is // the only path that drops the serial/secLink idle pollers from the shell // registry -- otherwise they would keep polling this app's freed terminals. void appShutdown(void) { basFormRtSerialShutdown(); } int32_t appMain(DxeAppContextT *ctx) { sAc = ctx->shellCtx; // Open our own resources DvxResHandleT *res = dvxResOpen(ctx->appPath); if (!res) { dvxMessageBox(sAc, "Error", "No compiled module found in this application.", 0); return 1; } // Read app name and update the shell's app record uint32_t nameSize = 0; char *appName = (char *)dvxResRead(res, BAS_RES_NAME, &nameSize); if (appName) { ShellAppT *app = shellGetApp(ctx->appId); if (app) { snprintf(app->name, SHELL_APP_NAME_MAX, "%s", appName); } free(appName); } // Set help file path if present uint32_t helpNameSize = 0; char *helpName = (char *)dvxResRead(res, BAS_RES_HELPFILE, &helpNameSize); if (helpName) { snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, helpName); free(helpName); } // Load MODULE resource uint32_t modSize = 0; uint8_t *modData = (uint8_t *)dvxResRead(res, BAS_RES_MODULE, &modSize); if (!modData) { dvxMessageBox(sAc, "Error", "MODULE resource not found.", 0); dvxResClose(res); return 1; } BasModuleT *mod = basModuleDeserialize(modData, (int32_t)modSize); free(modData); if (!mod) { dvxMessageBox(sAc, "Error", "Failed to deserialize module.", 0); dvxResClose(res); return 1; } // Load optional DEBUG resource uint32_t dbgSize = 0; uint8_t *dbgData = (uint8_t *)dvxResRead(res, BAS_RES_DEBUG, &dbgSize); if (dbgData) { basDebugDeserialize(mod, dbgData, (int32_t)dbgSize); free(dbgData); } // Create VM BasVmT *vm = basVmCreate(); basVmLoadModule(vm, mod); basVmSetPrintCallback(vm, stubPrint, NULL); basVmSetInputCallback(vm, stubInput, NULL); basVmSetDoEventsCallback(vm, stubDoEvents, NULL); // Set app paths. App.Path is the .app's directory (read-only on CD); // App.Config and App.Data are writable subdirectories created on // demand. The IDE does the same split for project debugging, so // behavior matches between compiled apps and in-IDE runs. snprintf(vm->appPath, DVX_MAX_PATH, "%s", ctx->appDir); snprintf(vm->appConfig, DVX_MAX_PATH, "%s" DVX_PATH_SEP "CONFIG", ctx->appDir); snprintf(vm->appData, DVX_MAX_PATH, "%s" DVX_PATH_SEP "DATA", ctx->appDir); platformMkdirRecursive(vm->appConfig); platformMkdirRecursive(vm->appData); // Set extern call callbacks (required for DECLARE LIBRARY functions) BasExternCallbacksT extCb; memset(&extCb, 0, sizeof(extCb)); extCb.resolveExtern = basExternResolve; extCb.callExtern = basExternCall; basVmSetExternCallbacks(vm, &extCb); // Create form runtime BasFormRtT *rt = basFormRtCreate(sAc, vm, mod); // Register .frm source text for lazy loading for (int32_t i = 0; i < BAS_MAX_FORM_RESOURCES; i++) { char resName[16]; snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)i); uint32_t frmSize = 0; char *frmText = (char *)dvxResRead(res, resName, &frmSize); if (!frmText) { // Stop at the first missing index: basBuild.c numbers the FORMn // resources densely (outIdx), so there is never a gap and the // first miss means there are no more forms. Probing all // BAS_MAX_FORM_RESOURCES indices would just waste startup lookups. break; } // dvxResRead returns exactly frmSize bytes with no terminator; the // form text is not guaranteed NUL-terminated. Append one so the // unbounded scan in basExtractFormName can't read past the buffer. char *frmTextZ = (char *)realloc(frmText, frmSize + 1); if (!frmTextZ) { free(frmText); continue; } frmText = frmTextZ; frmText[frmSize] = '\0'; // Extract form name from "Begin Form " line. Use the full // form-name length so a 32-63 char form name is not truncated here // (which would break name-keyed form-scope variable binding). char frmName[BAS_MAX_FORM_NAME] = ""; basExtractFormName(frmText, frmName, BAS_MAX_FORM_NAME); if (frmName[0]) { basFormRtRegisterFrm(rt, frmName, frmText, (int32_t)frmSize); } free(frmText); } dvxResClose(res); // Load all cached forms and show the startup form basFormRtLoadAllForms(rt, NULL); // Run bytecode + VB-style event loop basFormRtRunSimple(rt); // Cleanup basFormRtDestroy(rt); basVmDestroy(vm); basModuleFree(mod); return 0; }