DVX_GUI/src/apps/kpunch/dvxbasic/stub/basstub.c
2026-08-25 16:42:49 -05:00

260 lines
8.3 KiB
C

// 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// 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 <name>" 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;
}