// loaderMain.c -- DVX bootstrap loader entry point // // Loads all DXE modules from two directories: // libs/ *.lib -- core libraries (libtasks, libdvx, dvxshell) // widgets/ *.wgt -- widget type plugins (box, button, listview, etc.) // // Each module may have a .dep file (same base name, .dep extension) // listing base names of modules that must be loaded before it. // The loader resolves the dependency graph and loads in topological // order. After loading, any module that exports wgtRegister() has // it called. Finally, the loader finds and calls shellMain(). #include "dvxPlat.h" #include "dvxPrefs.h" #include #include #include #include #include #include #include #include #include // The loader is not a DXE — use plain realloc/free for stb_ds so that // all translation units (loaderMain.o, dvxPrefs.o) share the same heap. #define STB_DS_IMPLEMENTATION #include "stb_ds_wrap.h" // ============================================================ // Constants // ============================================================ #define LIBS_DIR "LIBS" #define WIDGET_DIR "WIDGETS" #define LOG_PATH "dvx.log" #define INI_PATH "CONFIG/DVX.INI" #define HLPC_PATH "SYSTEM/DVXHLPC.EXE" // ============================================================ // Splash screen (delegates to platformSplash* in dvxPlatformDos.c) // ============================================================ // Palette indices for progress bar (indices into SPLASH.RAW palette) #define SPLASH_BAR_BG 50 // dark gray (RGB 68,68,68) #define SPLASH_BAR_FG 135 // light gray (RGB 168,168,168) #define SPLASH_BAR_OUT 45 // darker gray (RGB 60,60,60) static int32_t sSplashActive = 0; static int32_t sSplashTotal = 0; static int32_t sSplashLoaded = 0; // Progress bar geometry #define PBAR_X 10 #define PBAR_Y 188 #define PBAR_W 300 #define PBAR_H 6 static void splashDrawScreen(void) { if (platformSplashLoadRaw("SYSTEM/SPLASH.RAW")) { platformSplashFillRect(PBAR_X - 1, PBAR_Y - 1, PBAR_W + 2, PBAR_H + 2, SPLASH_BAR_OUT); platformSplashFillRect(PBAR_X, PBAR_Y, PBAR_W, PBAR_H, SPLASH_BAR_BG); } } static void splashUpdateProgress(void) { if (!sSplashActive || sSplashTotal <= 0) { return; } int32_t fillW = (PBAR_W * sSplashLoaded) / sSplashTotal; if (fillW > PBAR_W) { fillW = PBAR_W; } platformSplashFillRect(PBAR_X, PBAR_Y, fillW, PBAR_H, SPLASH_BAR_FG); } // ============================================================ // dvxLog -- append a line to dvx.log // ============================================================ // // Global logging function exported to all DXE modules. // Opens/closes the file per-write so it's never held open. void dvxLog(const char *fmt, ...) { FILE *f = fopen(LOG_PATH, "a"); if (!f) { return; } va_list ap; va_start(ap, fmt); vfprintf(f, fmt, ap); va_end(ap); fprintf(f, "\n"); fclose(f); } // ============================================================ // Module entry for dependency resolution // ============================================================ typedef struct { char path[DVX_MAX_PATH]; char baseName[16]; char **deps; bool loaded; void *handle; } ModuleT; // ============================================================ // Prototypes // ============================================================ static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods); static int32_t countHcfFilesRecurse(const char *dirPath); static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize); static void *findSymbol(void **handles, const char *symbol); static void freeMods(ModuleT *mods); static void **loadAllModules(void); static void loadInOrder(ModuleT *mods); static void logAndReadDeps(ModuleT *mods); static void processHcfDir(const char *dirPath); static void readDeps(ModuleT *mod); static void scanDir(const char *dirPath, const char *ext, ModuleT **mods); // ============================================================ // extractBaseName -- strip directory and extension, lowercase // ============================================================ static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize) { // Find last directory separator const char *start = path; const char *p = path; while (*p) { if (*p == '/' || *p == '\\') { start = p + 1; } p++; } // Copy up to the extension int32_t extLen = strlen(ext); int32_t len = strlen(start); if (len > extLen && strcasecmp(start + len - extLen, ext) == 0) { len -= extLen; } if (len >= outSize) { len = outSize - 1; } for (int32_t i = 0; i < len; i++) { out[i] = tolower((unsigned char)start[i]); } out[len] = '\0'; } // ============================================================ // readDeps -- parse .dep file for a module // ============================================================ // // The .dep file has the same path as the module but with a .dep // extension. Each line is a dependency base name. Empty lines // and lines starting with # are ignored. static void readDeps(ModuleT *mod) { // Build dep file path: replace extension with .dep char depPath[DVX_MAX_PATH]; strncpy(depPath, mod->path, sizeof(depPath) - 1); depPath[sizeof(depPath) - 1] = '\0'; char *dot = strrchr(depPath, '.'); if (!dot) { return; } strcpy(dot, ".dep"); FILE *f = fopen(depPath, "r"); if (!f) { return; } char line[64]; while (fgets(line, sizeof(line), f)) { // Strip \r and \n int32_t len = strlen(line); while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { line[--len] = '\0'; } // Skip empty lines and comments if (len == 0 || line[0] == '#') { continue; } // Lowercase the dep name for case-insensitive matching for (int32_t i = 0; i < len; i++) { line[i] = tolower((unsigned char)line[i]); } arrput(mod->deps, strdup(line)); } fclose(f); } // ============================================================ // scanDir -- recursively find modules with a given extension // ============================================================ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) { DIR *dir = opendir(dirPath); if (!dir) { return; } struct dirent *ent; while ((ent = readdir(dir)) != NULL) { // Copy d_name — readdir may use a shared buffer across recursion char name[DVX_MAX_PATH]; snprintf(name, sizeof(name), "%s", ent->d_name); // Skip . and .. if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) { continue; } char path[DVX_MAX_PATH]; snprintf(path, sizeof(path), "%s%c%s", dirPath, DVX_PATH_SEP, name); // Check for matching extension int32_t nameLen = strlen(name); int32_t extLen = strlen(ext); if (nameLen > extLen && strcasecmp(name + nameLen - extLen, ext) == 0) { ModuleT mod; memset(&mod, 0, sizeof(mod)); snprintf(mod.path, sizeof(mod.path), "%s", path); extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName)); arrput(*mods, mod); continue; } // Recurse into subdirectories struct stat st; if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { scanDir(path, ext, mods); } } closedir(dir); } // ============================================================ // allDepsLoaded -- check if a module's dependencies are satisfied // ============================================================ // // A dep is satisfied if either: // 1. No module with that base name exists (external, assumed OK) // 2. The module with that base name is already loaded static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods) { for (int32_t d = 0; d < arrlen(mod->deps); d++) { for (int32_t j = 0; j < arrlen(mods); j++) { if (strcasecmp(mods[j].baseName, mod->deps[d]) == 0) { if (!mods[j].loaded) { return false; } break; } } } return true; } // ============================================================ // loadInOrder -- topological sort and load // ============================================================ // // Repeatedly scans the module list for entries whose dependencies // are all satisfied, loads them, and marks them done. Stops when // all modules are loaded or no progress can be made (circular dep). static void loadInOrder(ModuleT *mods) { typedef void (*RegFnT)(void); int32_t total = arrlen(mods); int32_t loaded = 0; bool progress; do { progress = false; for (int32_t i = 0; i < total; i++) { if (mods[i].loaded) { continue; } if (!allDepsLoaded(&mods[i], mods)) { continue; } dvxLog("Loading: %s", mods[i].path); mods[i].handle = dlopen(mods[i].path, RTLD_GLOBAL); if (!mods[i].handle) { const char *err = dlerror(); dvxLog(" FAILED: %s", err ? err : "(unknown)"); platformSplashShutdown(); sSplashActive = 0; fprintf(stderr, "FATAL: Failed to load %s\n %s\n", mods[i].path, err ? err : "(unknown error)"); exit(1); } RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister"); if (regFn) { regFn(); // Record the .wgt path for any newly registered ifaces typedef int32_t (*IfaceCountFnT)(void); typedef const void *(*IfaceAtFnT)(int32_t, const char **); typedef const char *(*IfaceGetPathFnT)(const char *); typedef void (*IfaceSetPathFnT)(const char *, const char *); IfaceCountFnT countFn = (IfaceCountFnT)dlsym(NULL, "_wgtIfaceCount"); IfaceAtFnT atFn = (IfaceAtFnT)dlsym(NULL, "_wgtIfaceAt"); IfaceGetPathFnT getPathFn = (IfaceGetPathFnT)dlsym(NULL, "_wgtIfaceGetPath"); IfaceSetPathFnT setPathFn = (IfaceSetPathFnT)dlsym(NULL, "_wgtIfaceSetPath"); if (countFn && atFn && getPathFn && setPathFn) { int32_t ic = countFn(); for (int32_t k = 0; k < ic; k++) { const char *ifaceName = NULL; atFn(k, &ifaceName); if (ifaceName && !getPathFn(ifaceName)) { setPathFn(ifaceName, mods[i].path); } } } } else if (strstr(mods[i].path, ".wgt") || strstr(mods[i].path, ".WGT")) { dvxLog(" No _wgtRegister in %s", mods[i].baseName); } mods[i].loaded = true; loaded++; progress = true; sSplashLoaded++; splashUpdateProgress(); } } while (progress && loaded < total); if (loaded < total) { fprintf(stderr, "Module loader: %d of %d modules could not be loaded (circular deps or missing deps)\n", (int)(total - loaded), (int)total); for (int32_t i = 0; i < total; i++) { if (!mods[i].loaded) { fprintf(stderr, " %s\n", mods[i].path); } } } } // ============================================================ // loadAllModules -- scan libs/ and widgets/, load in dep order // ============================================================ // // Returns a stb_ds dynamic array of dlopen handles. static void logAndReadDeps(ModuleT *mods) { dvxLog("Discovered %d modules:", arrlen(mods)); for (int32_t i = 0; i < arrlen(mods); i++) { dvxLog(" [%d] %s (base: %s)", i, mods[i].path, mods[i].baseName); } for (int32_t i = 0; i < arrlen(mods); i++) { readDeps(&mods[i]); if (arrlen(mods[i].deps) > 0) { char line[512]; int32_t pos = snprintf(line, sizeof(line), " %s deps:", mods[i].baseName); for (int32_t d = 0; d < arrlen(mods[i].deps); d++) { pos += snprintf(line + pos, sizeof(line) - pos, " %s", mods[i].deps[d]); } dvxLog("%s", line); } } } static void freeMods(ModuleT *mods) { for (int32_t i = 0; i < arrlen(mods); i++) { for (int32_t d = 0; d < arrlen(mods[i].deps); d++) { free(mods[i].deps[d]); } arrfree(mods[i].deps); } arrfree(mods); } // ============================================================ // Help recompilation // ============================================================ static int32_t sLibCount = 0; static int32_t sWidgetCount = 0; // ============================================================ // Glob matching // ============================================================ // // Simple glob pattern matching for filenames. Supports: // * matches zero or more characters // ? matches exactly one character // !pat at the start means "exclude files matching pat" // Case-insensitive. Exported for use by DXE modules. bool platformGlobMatch(const char *pattern, const char *name) { while (*pattern && *name) { if (*pattern == '*') { pattern++; if (!*pattern) { return true; } while (*name) { if (platformGlobMatch(pattern, name)) { return true; } name++; } return false; } else if (*pattern == '?' || tolower((unsigned char)*pattern) == tolower((unsigned char)*name)) { pattern++; name++; } else { return false; } } // Skip trailing * while (*pattern == '*') { pattern++; } return *pattern == '\0' && *name == '\0'; } // ============================================================ // HCF-driven help recompilation // ============================================================ // // Scans APPS/*/ for .hcf (help config) files. Each .hcf defines // an output .hlp file and source patterns: // // output = DVXHELP.HLP // source = LIBS/*.DHS // source = WIDGETS/*.DHS // // Source lines specify a directory/glob pattern. An optional !pattern // excludes matching files. The loader expands these by scanning // directories and matching filenames. // Recursively count .hcf files under the given directory static int32_t countHcfFilesRecurse(const char *dirPath) { int32_t count = 0; DIR *dir = opendir(dirPath); if (!dir) { return 0; } struct dirent *ent; while ((ent = readdir(dir)) != NULL) { if (ent->d_name[0] == '.') { continue; } // Copy d_name before any recursion — readdir may use a shared buffer char name[DVX_MAX_PATH]; snprintf(name, sizeof(name), "%s", ent->d_name); char fullPath[DVX_MAX_PATH]; snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name); struct stat st; if (stat(fullPath, &st) != 0) { continue; } if (S_ISDIR(st.st_mode)) { count += countHcfFilesRecurse(fullPath); } else { int32_t nameLen = (int32_t)strlen(name); if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) { count++; } } } closedir(dir); return count; } // Count .hcf files under APPS/ static int32_t countHcfFiles(void) { return countHcfFilesRecurse("APPS"); } // Process a single .hcf file: parse it and invoke the compiler. #define HELP_RESP_FILE "SYSTEM/HLPFILES.TMP" // Write matching filenames to the response file (one per line). static void writeGlobToResp(FILE *resp, const char *pattern, const char *excludePattern) { // Split pattern into directory and filename glob char dirPart[DVX_MAX_PATH]; const char *globPart = NULL; snprintf(dirPart, sizeof(dirPart), "%s", pattern); char *lastSep = strrchr(dirPart, '/'); if (!lastSep) { lastSep = strrchr(dirPart, '\\'); } if (lastSep) { *lastSep = '\0'; globPart = lastSep + 1; } else { globPart = pattern; dirPart[0] = '.'; dirPart[1] = '\0'; } DIR *d = opendir(dirPart); if (!d) { return; } struct dirent *ent; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') { continue; } char name[DVX_MAX_PATH]; snprintf(name, sizeof(name), "%s", ent->d_name); char fullPath[DVX_MAX_PATH]; snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, name); struct stat st; if (stat(fullPath, &st) != 0) { continue; } if (S_ISDIR(st.st_mode)) { char subPattern[DVX_MAX_PATH]; snprintf(subPattern, sizeof(subPattern), "%s%c%s", fullPath, DVX_PATH_SEP, globPart); writeGlobToResp(resp, subPattern, excludePattern); } else if (platformGlobMatch(globPart, name)) { if (excludePattern && platformGlobMatch(excludePattern, name)) { continue; } fprintf(resp, "%s\n", fullPath); } } closedir(d); } static void processHcf(const char *hcfPath, const char *hcfDir) { FILE *f = fopen(hcfPath, "r"); if (!f) { return; } char outputFile[DVX_MAX_PATH] = {0}; // Write matching source files to a response file FILE *resp = fopen(HELP_RESP_FILE, "w"); if (!resp) { fclose(f); dvxLog("helpRecompile: cannot create %s", HELP_RESP_FILE); return; } bool hasFiles = false; char line[512]; while (fgets(line, (int)sizeof(line), f)) { int32_t len = (int32_t)strlen(line); while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r' || line[len - 1] == ' ')) { line[--len] = '\0'; } if (line[0] == '#' || line[0] == '\0') { continue; } if (strncasecmp(line, "output", 6) == 0) { const char *p = line + 6; while (*p == ' ' || *p == '=') { p++; } snprintf(outputFile, sizeof(outputFile), "%s%c%s", hcfDir, DVX_PATH_SEP, p); } else if (strncasecmp(line, "source", 6) == 0) { const char *p = line + 6; while (*p == ' ' || *p == '=') { p++; } char pattern[DVX_MAX_PATH] = {0}; char exclude[DVX_MAX_PATH] = {0}; const char *bang = strchr(p, '!'); if (bang) { int32_t patLen = (int32_t)(bang - p); while (patLen > 0 && p[patLen - 1] == ' ') { patLen--; } memcpy(pattern, p, patLen); pattern[patLen] = '\0'; bang++; while (*bang == ' ') { bang++; } snprintf(exclude, sizeof(exclude), "%s", bang); } else { snprintf(pattern, sizeof(pattern), "%s", p); } long before = ftell(resp); writeGlobToResp(resp, pattern, exclude[0] ? exclude : NULL); if (ftell(resp) > before) { hasFiles = true; } } } fclose(f); fclose(resp); if (!outputFile[0] || !hasFiles) { remove(HELP_RESP_FILE); return; } // Short command: compiler + output + response file char cmd[DVX_MAX_PATH * 2]; snprintf(cmd, sizeof(cmd), "%s --quiet -o %s @%s", HLPC_PATH, outputFile, HELP_RESP_FILE); dvxLog("helpRecompile: %s -> %s", hcfPath, outputFile); int rc = system(cmd); if (rc != 0) { dvxLog("helpRecompile: FAILED (rc=%d)", rc); } remove(HELP_RESP_FILE); } // Recursively scan a directory for .hcf files and process each one. static void processHcfDir(const char *dirPath) { DIR *dir = opendir(dirPath); if (!dir) { return; } struct dirent *ent; while ((ent = readdir(dir)) != NULL) { if (ent->d_name[0] == '.') { continue; } // Copy d_name before any recursion — readdir may use a shared buffer char name[DVX_MAX_PATH]; snprintf(name, sizeof(name), "%s", ent->d_name); char fullPath[DVX_MAX_PATH]; snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name); struct stat st; if (stat(fullPath, &st) != 0) { continue; } if (S_ISDIR(st.st_mode)) { processHcfDir(fullPath); } else { int32_t nameLen = (int32_t)strlen(name); if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) { processHcf(fullPath, dirPath); sSplashLoaded++; splashUpdateProgress(); } } } closedir(dir); } static void helpRecompileIfNeeded(void) { PrefsHandleT *ini = prefsLoad(INI_PATH); if (!ini) { dvxLog("helpRecompile: cannot load INI"); return; } int32_t storedLibs = prefsGetInt(ini, "help", "libCount", -1); int32_t storedWidgets = prefsGetInt(ini, "help", "widgetCount", -1); // First startup: no [help] section. Save counts, skip recompile. if (storedLibs == -1 && storedWidgets == -1) { dvxLog("helpRecompile: first startup, saving initial counts (libs=%d widgets=%d)", (int)sLibCount, (int)sWidgetCount); prefsSetInt(ini, "help", "libCount", sLibCount); prefsSetInt(ini, "help", "widgetCount", sWidgetCount); prefsSave(ini); prefsClose(ini); return; } // Counts unchanged: no recompile needed if (storedLibs == sLibCount && storedWidgets == sWidgetCount) { prefsClose(ini); return; } dvxLog("helpRecompile: counts changed (libs: %d->%d, widgets: %d->%d)", (int)storedLibs, (int)sLibCount, (int)storedWidgets, (int)sWidgetCount); // Check if compiler exists FILE *test = fopen(HLPC_PATH, "rb"); if (!test) { dvxLog("helpRecompile: %s not found, skipping", HLPC_PATH); prefsSetInt(ini, "help", "libCount", sLibCount); prefsSetInt(ini, "help", "widgetCount", sWidgetCount); prefsSave(ini); prefsClose(ini); return; } fclose(test); // Count .hcf files for progress bar int32_t hcfCount = countHcfFiles(); if (hcfCount == 0) { dvxLog("helpRecompile: no .hcf files found"); prefsSetInt(ini, "help", "libCount", sLibCount); prefsSetInt(ini, "help", "widgetCount", sWidgetCount); prefsSave(ini); prefsClose(ini); return; } // Reset progress bar for recompile phase sSplashLoaded = 0; sSplashTotal = hcfCount; platformSplashFillRect(PBAR_X, PBAR_Y, PBAR_W, PBAR_H, SPLASH_BAR_BG); // Recursively scan APPS/ for .hcf files and process each processHcfDir("APPS"); // Save new counts prefsSetInt(ini, "help", "libCount", sLibCount); prefsSetInt(ini, "help", "widgetCount", sWidgetCount); prefsSave(ini); prefsClose(ini); dvxLog("helpRecompile: done"); } static void **loadAllModules(void) { void **handles = NULL; // Scan both directories first to get total module count for progress bar ModuleT *libs = NULL; scanDir(LIBS_DIR, ".lib", &libs); logAndReadDeps(libs); ModuleT *widgets = NULL; scanDir(WIDGET_DIR, ".wgt", &widgets); logAndReadDeps(widgets); sLibCount = (int32_t)arrlen(libs); sWidgetCount = (int32_t)arrlen(widgets); sSplashTotal = sLibCount + sWidgetCount; // Phase 1: load libraries in dependency order loadInOrder(libs); for (int32_t i = 0; i < arrlen(libs); i++) { if (libs[i].handle) { arrput(handles, libs[i].handle); } } freeMods(libs); // Phase 2: load widgets in dependency order (all libs already loaded) loadInOrder(widgets); for (int32_t i = 0; i < arrlen(widgets); i++) { if (widgets[i].handle) { arrput(handles, widgets[i].handle); } } freeMods(widgets); return handles; } // ============================================================ // findSymbol -- search loaded handles for a symbol // ============================================================ static void *findSymbol(void **handles, const char *symbol) { for (int32_t i = 0; i < arrlen(handles); i++) { void *sym = dlsym(handles[i], symbol); if (sym) { return sym; } } return NULL; } // ============================================================ // main // ============================================================ int main(int argc, char *argv[]) { // Change to the directory containing the executable so relative // paths (LIBS/, WIDGETS/, APPS/, CONFIG/) resolve correctly. char exeDir[DVX_MAX_PATH]; strncpy(exeDir, argv[0], sizeof(exeDir) - 1); exeDir[sizeof(exeDir) - 1] = '\0'; char *sep = platformPathDirEnd(exeDir); if (sep) { *sep = '\0'; platformChdir(exeDir); } // Truncate log, then use append-per-write FILE *logInit = fopen(LOG_PATH, "w"); if (logInit) { fclose(logInit); } // Suppress Ctrl+C before anything else platformInit(); // Switch to VGA mode 13h and show graphical splash platformSplashInit(); sSplashActive = 1; splashDrawScreen(); dvxLog("DVX Loader starting..."); // Register platform + libc/libm/runtime symbols for DXE resolution platformRegisterDxeExports(); dvxLog("Platform exports registered."); // Load all modules from libs/ and widgets/ in dependency order. // Each module may have a .dep file specifying load-before deps. // Widget modules that export wgtRegister() get it called. void **handles = loadAllModules(); if (!handles || arrlen(handles) == 0) { fprintf(stderr, "No modules loaded from %s/ or %s/\n", LIBS_DIR, WIDGET_DIR); arrfree(handles); return 1; } // Check if help files need recompilation (module count changed) helpRecompileIfNeeded(); // Find and call shellMain from whichever module exports it typedef int (*ShellMainFnT)(int, char **); ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, "_shellMain"); if (!shellMain) { dvxLog("ERROR: No module exports shellMain"); for (int32_t i = arrlen(handles) - 1; i >= 0; i--) { dlclose(handles[i]); } arrfree(handles); return 1; } int result = shellMain(argc, argv); // Clean up in reverse load order for (int32_t i = arrlen(handles) - 1; i >= 0; i--) { dlclose(handles[i]); } arrfree(handles); return result; }