260 lines
8.3 KiB
C
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;
|
|
}
|