diff --git a/assets/init.js b/assets/init.js index 25147440..ae7ed53f 100644 --- a/assets/init.js +++ b/assets/init.js @@ -10,6 +10,13 @@ const platformNames = { Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown')); +const test = require('test.js'); +Console.print(test.str()); +test.increment(); +Console.print(test.str()); +test.decrement(); +Console.print(test.str()); + // Scene.set('testscene.js'); // Console.print('Loading scene...'); // requireAsync('./testscene.js', function(scene) { diff --git a/assets/test.js b/assets/test.js new file mode 100644 index 00000000..e6ced7fa --- /dev/null +++ b/assets/test.js @@ -0,0 +1,15 @@ +Console.print('test included'); + +var x = 1 + 2; + +module.exports = { + str: function() { + return x.toString(); + }, + increment: function() { + x++; + }, + decrement: function() { + x--; + } +} \ No newline at end of file diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c index b20f8098..8f216fa4 100644 --- a/src/dusk/asset/asset.c +++ b/src/dusk/asset/asset.c @@ -78,6 +78,24 @@ assetentry_t * assetGetEntry( return NULL; } +uint32_t assetGetEntriesOfType( + assetentry_t **outEntries, + const assetloadertype_t type +) { + assertNotNull(outEntries, "Output entries cannot be NULL."); + + uint32_t count = 0; + assetentry_t *entry = ASSET.entries; + do { + if(entry->type == type) { + outEntries[count++] = entry; + } + entry++; + } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); + + return count; +} + errorret_t assetRequireLoaded(assetentry_t *entry) { assertNotNull(entry, "Entry cannot be NULL."); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); @@ -103,6 +121,43 @@ errorret_t assetRequireLoaded(assetentry_t *entry) { errorOk(); } +errorret_t assetRequireDisposed(assetentry_t *entry) { + assertNotNull(entry, "Entry cannot be NULL."); + assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); + assertIsMainThread("Currently only works on main thread."); + + if(entry->type == ASSET_LOADER_TYPE_NULL) { + errorOk(); + } + + if( + entry->state == ASSET_ENTRY_STATE_NOT_STARTED || + entry->state == ASSET_ENTRY_STATE_ERROR + ) { + errorOk(); + } + + assertTrue( + entry->refs.count == 0, + "Cannot require disposal of an entry with active references." + ); + + // Lock to prevent the reaper from collecting the entry mid-spin. + assetEntryLock(entry); + + while(entry->type != ASSET_LOADER_TYPE_NULL) { + usleep(1000); + errorret_t ret = assetUpdate(); + if(errorIsNotOk(ret)) { + assetEntryUnlock(entry); + errorChain(ret); + } + } + + assetEntryUnlock(entry); + errorOk(); +} + assetentry_t * assetLock( const char_t *name, const assetloadertype_t type, @@ -265,9 +320,7 @@ errorret_t assetUpdate(void) { } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); - // Reap entries that have no external locks (refs.count == 0) AND have been - // explicitly locked at least once. Entries that were never locked are left - // alone — raw-pointer callers hold no ref but should not lose the entry. + // Reap unused entries. entry = ASSET.entries; do { if(entry->state != ASSET_ENTRY_STATE_LOADED) { @@ -280,11 +333,6 @@ errorret_t assetUpdate(void) { continue; } - if(!entry->wasLocked) { - entry++; - continue; - } - if(entry->refs.count > 0) { entry++; continue; diff --git a/src/dusk/asset/asset.h b/src/dusk/asset/asset.h index e98697be..03b4e0f8 100644 --- a/src/dusk/asset/asset.h +++ b/src/dusk/asset/asset.h @@ -68,6 +68,18 @@ assetentry_t * assetGetEntry( assetloaderinput_t *input ); +/** + * Gets all asset entries of a given type. + * + * @param outEntries Output array to write the entries to. + * @param type Type of the asset entries to get. + * @return The number of entries written to outEntries. + */ +uint32_t assetGetEntriesOfType( + assetentry_t **outEntries, + const assetloadertype_t type +); + /** * Gets, creates, and locks an asset entry. The asset will begin loading on * the next assetUpdate. Call assetUnlock when done to allow the entry to be @@ -109,6 +121,15 @@ void assetUnlockEntry(assetentry_t *entry); */ errorret_t assetRequireLoaded(assetentry_t *entry); +/** + * Requires an asset entry to be disposed. This will block until the asset entry + * is fully disposed. + * + * @param entry The asset entry to require disposal of. + * @return An error code if the asset entry could not be disposed properly. + */ +errorret_t assetRequireDisposed(assetentry_t *entry); + /** * Updates the asset system. * diff --git a/src/dusk/asset/loader/script/assetscriptloader.c b/src/dusk/asset/loader/script/assetscriptloader.c index eb16de0e..eafe190e 100644 --- a/src/dusk/asset/loader/script/assetscriptloader.c +++ b/src/dusk/asset/loader/script/assetscriptloader.c @@ -25,33 +25,21 @@ errorret_t assetScriptLoaderAsync(assetloading_t *loading) { assertNull(loading->loading.script.buffer, "Buffer already defined?"); assetfile_t *file = &loading->loading.script.file; - assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL)); - assetLoaderErrorChain(loading, assetFileOpen(file)); + assetLoaderErrorChain( + loading, assetFileInit(file, loading->entry->name, NULL, NULL) + ); - size_t capacity = ASSET_SCRIPT_CHUNK_SIZE; - uint8_t *buffer = memoryAllocate(capacity + 1); - size_t offset = 0; - - while(1) { - if(offset + ASSET_SCRIPT_CHUNK_SIZE > capacity) { - size_t oldCapacity = capacity + 1; - capacity += ASSET_SCRIPT_CHUNK_SIZE; - memoryResize((void **)&buffer, oldCapacity, capacity + 1); - } - assetLoaderErrorChain(loading, assetFileRead( - file, buffer + offset, ASSET_SCRIPT_CHUNK_SIZE - )); - size_t chunk = (size_t)file->lastRead; - offset += chunk; - if(chunk == 0) break; - } - - buffer[offset] = '\0'; - assetLoaderErrorChain(loading, assetFileClose(file)); + uint8_t *buffer = NULL; + size_t size = 0; + assetLoaderErrorChain(loading, assetFileReadEntire(file, &buffer, &size)); assetLoaderErrorChain(loading, assetFileDispose(file)); + // Null-terminate for jerry_eval. + memoryResize((void **)&buffer, size, size + 1); + buffer[size] = '\0'; + loading->loading.script.buffer = buffer; - loading->loading.script.size = offset; + loading->loading.script.size = size; loading->loading.script.state = ASSET_SCRIPT_LOADING_STATE_EXEC; loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; errorOk(); @@ -76,51 +64,58 @@ errorret_t assetScriptLoaderSync(assetloading_t *loading) { errorOk(); } + // Get read buffer uint8_t *buffer = loading->loading.script.buffer; assertNotNull(buffer, "Script buffer should have been loaded by now."); - bool_t isModule = ( - loading->entry->input != NULL && - loading->entry->input->script.isModule + // Get the current global script realm + jerry_value_t global = jerry_current_realm(); + + // Replace globalThis.module with a new `module = {}` + jerry_value_t oldModule = jerry_object_get_sz(global, "module"); + + jerry_value_t module = jerry_object(); + jerry_object_set_sz(global, "module", module); + + // Eval the script, we handle failure later down the code. + jerry_value_t result = jerry_eval( + buffer, + loading->loading.script.size, + JERRY_PARSE_NO_OPTS ); - jerry_value_t result; - if(isModule) { - result = moduleRequireExecModule( - loading->entry->name, - (const jerry_char_t *)buffer, - loading->loading.script.size - ); - } else { - moduleRequirePushDir(loading->entry->name); - result = jerry_eval( - (const jerry_char_t *)buffer, - loading->loading.script.size, - JERRY_PARSE_NO_OPTS - ); - moduleRequirePopDir(); - } - + // Free the read buffer memoryFree(buffer); loading->loading.script.buffer = NULL; + // Restore globalThis.module + jerry_object_set_sz(global, "module", oldModule); + jerry_value_free(oldModule); + jerry_value_free(global); + if(jerry_value_is_exception(result)) { - jerry_value_t errVal = jerry_exception_value(result, false); - jerry_value_t errStr = jerry_value_to_string(errVal); + jerry_value_free(module); + + loading->entry->data.script.exports = jerry_undefined(); + loading->entry->state = ASSET_ENTRY_STATE_ERROR; + + // Get error string char_t buf[256]; - jerry_size_t len = jerry_string_to_buffer( - errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1 - ); - buf[len] = '\0'; - jerry_value_free(errStr); - jerry_value_free(errVal); + moduleBaseExceptionMessage(result, buf, sizeof(buf)); jerry_value_free(result); - assetLoaderErrorThrow(loading, "Script error in '%s': %s", - loading->entry->name, buf + assetLoaderErrorThrow( + loading, + "Script execution failed: %s: %s", loading->entry->name, buf ); } + + // Get module.exports + jerry_value_t exports = jerry_object_get_sz(module, "exports"); + jerry_value_free(result); + jerry_value_free(module); - loading->entry->data.script = (assetscriptoutput_t)result; + // Store the exports. + loading->entry->data.script.exports = exports; loading->entry->state = ASSET_ENTRY_STATE_LOADED; errorOk(); } @@ -130,9 +125,12 @@ errorret_t assetScriptDispose(assetentry_t *entry) { assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type."); assertIsMainThread("Must be called from the main thread."); - if(entry->data.script != 0) { - jerry_value_free((jerry_value_t)entry->data.script); - entry->data.script = 0; + if( + entry->data.script.exports != 0 && + !jerry_value_is_undefined(entry->data.script.exports) + ) { + jerry_value_free(entry->data.script.exports); + entry->data.script.exports = 0; } errorOk(); diff --git a/src/dusk/asset/loader/script/assetscriptloader.h b/src/dusk/asset/loader/script/assetscriptloader.h index 869789fa..b38362bf 100644 --- a/src/dusk/asset/loader/script/assetscriptloader.h +++ b/src/dusk/asset/loader/script/assetscriptloader.h @@ -7,19 +7,18 @@ #pragma once #include "asset/assetfile.h" +#include "script/scriptmodule.h" #define ASSET_SCRIPT_CHUNK_SIZE 1024 typedef struct assetloading_s assetloading_t; typedef struct assetentry_s assetentry_t; -/** - * Pass isModule = true to evaluate in isolated module scope. - * The loaded result (entry->data.script) will be module.exports rather than - * the script's return value. - */ -typedef struct { bool_t isModule; } assetscriptloaderinput_t; -typedef uint32_t assetscriptoutput_t; +typedef struct { + void *nothing; +} assetscriptloaderinput_t; + +typedef scriptmodule_t assetscriptoutput_t; typedef enum { ASSET_SCRIPT_LOADING_STATE_INITIAL, @@ -35,6 +34,27 @@ typedef struct { size_t size; } assetscriptloaderloading_t; +/** + * Asynchronous loader for a script asset/module. + * + * @param loading The loading context. + * @returns An error code and state. + */ errorret_t assetScriptLoaderAsync(assetloading_t *loading); + +/** + * Synchronous loader for a script asset/module. This executes the script after + * it has been loaded by the async loader. + * + * @param loading The loading context. + * @returns An error code and state. + */ errorret_t assetScriptLoaderSync(assetloading_t *loading); + +/** + * Disposes of a loaded script asset/module. + * + * @param entry The asset entry to dispose. + * @returns An error code and state. + */ errorret_t assetScriptDispose(assetentry_t *entry); diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c index 3180eb46..ac8f4b8c 100644 --- a/src/dusk/scene/scene.c +++ b/src/dusk/scene/scene.c @@ -84,35 +84,34 @@ errorret_t sceneSet(const char_t *jsFile) { // Dispose any active scene before switching. errorChain(sceneDispose()); - // Lock the asset - assetloaderinput_t input; - memoryZero(&input, sizeof(input)); - input.script.isModule = true; - assetentry_t *entry = assetLock(jsFile, ASSET_LOADER_TYPE_SCRIPT, &input); - if(!entry) errorThrow("sceneSet: failed to lock '%s'", jsFile); + // // Lock the asset + // assetloaderinput_t input; + // memoryZero(&input, sizeof(input)); + // assetentry_t *entry = assetLock(jsFile, ASSET_LOADER_TYPE_SCRIPT, &input); + // if(!entry) errorThrow("sceneSet: failed to lock '%s'", jsFile); - // Wait for loaded. - errorret_t err = assetRequireLoaded(entry); - if(errorIsNotOk(err)) { - assetUnlockEntry(entry); - errorChain(err); - } + // // Wait for loaded. + // errorret_t err = assetRequireLoaded(entry); + // if(errorIsNotOk(err)) { + // assetUnlockEntry(entry); + // errorChain(err); + // } - // Any export? - if(entry->data.script == 0) { - assetUnlockEntry(entry); - errorThrow("sceneSet: '%s' produced no exports", jsFile); - } + // // Any export? + // if(entry->data.script.exportedModule == 0) { + // assetUnlockEntry(entry); + // errorThrow("sceneSet: '%s' produced no exports", jsFile); + // } - // Copy the exports reference before unlocking (entry may be reaped). - jerry_value_t exports = jerry_value_copy((jerry_value_t)entry->data.script); - assetUnlockEntry(entry); + // // Copy the exports reference before unlocking (entry may be reaped). + // jerry_value_t exports = jerry_value_copy(entry->data.script.exportedModule); + // assetUnlockEntry(entry); - // Extract the three lifecycle callbacks. - SCENE.jsInit = sceneExtractFn(exports, "init"); - SCENE.jsUpdate = sceneExtractFn(exports, "update"); - SCENE.jsDispose = sceneExtractFn(exports, "dispose"); - jerry_value_free(exports); + // // Extract the three lifecycle callbacks. + // SCENE.jsInit = sceneExtractFn(exports, "init"); + // SCENE.jsUpdate = sceneExtractFn(exports, "update"); + // SCENE.jsDispose = sceneExtractFn(exports, "dispose"); + // jerry_value_free(exports); SCENE.state |= SCENE_STATE_LOADED; diff --git a/src/dusk/script/module/CMakeLists.txt b/src/dusk/script/module/CMakeLists.txt index b5fa4d0b..dadd3eb7 100644 --- a/src/dusk/script/module/CMakeLists.txt +++ b/src/dusk/script/module/CMakeLists.txt @@ -6,6 +6,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC modulebase.c + modulelist.c ) # Subdirs diff --git a/src/dusk/script/module/modulelist.c b/src/dusk/script/module/modulelist.c new file mode 100644 index 00000000..ed77dd84 --- /dev/null +++ b/src/dusk/script/module/modulelist.c @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulelist.h" +#include "script/module/asset/moduleasset.h" +#include "script/module/console/moduleconsole.h" +#include "script/module/display/modulecolor.h" +#include "script/module/display/modulescreen.h" +#include "script/module/engine/moduleengine.h" +#include "script/module/entity/component/modulecomponentlist.h" +#include "script/module/entity/modulecomponent.h" +#include "script/module/entity/moduleentity.h" +#include "script/module/input/moduleinput.h" +#include "script/module/math/modulevec3.h" +#include "script/module/require/modulerequire.h" +#include "script/module/scene/modulescene.h" +#include "script/module/system/modulesystem.h" + + +void moduleListInit(void) { + moduleTextureInit(); + moduleColorInit(); + moduleAssetInit(); + moduleConsoleInit(); + moduleScreenInit(); + moduleEngineInit(); + moduleVec3Init(); + moduleComponentInit(); + moduleEntityInit(); + moduleComponentListInit(); + moduleInputInit(); + moduleRequireInit(); + moduleSceneInit(); + moduleSystemInit(); +} + +void moduleListDispose(void) { + moduleSystemDispose(); + moduleSceneDispose(); + moduleRequireDispose(); + moduleInputDispose(); + moduleComponentListDispose(); + moduleEntityDispose(); + moduleComponentDispose(); + moduleVec3Dispose(); + moduleEngineDispose(); + moduleScreenDispose(); + moduleConsoleDispose(); + moduleAssetDispose(); + moduleColorDispose(); + moduleTextureDispose(); +} diff --git a/src/dusk/script/module/modulelist.h b/src/dusk/script/module/modulelist.h index 60dcd720..e789033d 100644 --- a/src/dusk/script/module/modulelist.h +++ b/src/dusk/script/module/modulelist.h @@ -6,50 +6,14 @@ */ #pragma once -#include "script/module/asset/moduleasset.h" -#include "script/module/console/moduleconsole.h" -#include "script/module/display/modulecolor.h" -#include "script/module/display/modulescreen.h" -#include "script/module/engine/moduleengine.h" -#include "script/module/entity/component/modulecomponentlist.h" -#include "script/module/entity/modulecomponent.h" -#include "script/module/entity/moduleentity.h" -#include "script/module/input/moduleinput.h" -#include "script/module/math/modulevec3.h" -#include "script/module/require/modulerequire.h" -#include "script/module/scene/modulescene.h" -#include "script/module/system/modulesystem.h" +#include "dusk.h" -static void moduleListInit(void) { - moduleTextureInit(); - moduleColorInit(); - moduleAssetInit(); - moduleConsoleInit(); - moduleScreenInit(); - moduleEngineInit(); - moduleVec3Init(); - moduleComponentInit(); - moduleEntityInit(); - moduleComponentListInit(); - moduleInputInit(); - moduleRequireInit(); - moduleSceneInit(); - moduleSystemInit(); -} +/** + * Initializes all of the internal (C) script modules. + */ +void moduleListInit(void); -static void moduleListDispose(void) { - moduleSystemDispose(); - moduleSceneDispose(); - moduleRequireDispose(); - moduleInputDispose(); - moduleComponentListDispose(); - moduleEntityDispose(); - moduleComponentDispose(); - moduleVec3Dispose(); - moduleEngineDispose(); - moduleScreenDispose(); - moduleConsoleDispose(); - moduleAssetDispose(); - moduleColorDispose(); - moduleTextureDispose(); -} +/** + * Disposes all of the internal (C) script modules. + */ +void moduleListDispose(void); \ No newline at end of file diff --git a/src/dusk/script/module/require/modulerequire.c b/src/dusk/script/module/require/modulerequire.c index c2baca22..35e8a5df 100644 --- a/src/dusk/script/module/require/modulerequire.c +++ b/src/dusk/script/module/require/modulerequire.c @@ -11,617 +11,72 @@ #include "util/string.h" #include "assert/assert.h" #include "console/console.h" -#include - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -#define REQUIRE_CACHE_MAX 32 -#define REQUIRE_DIR_STACK_MAX 16 -#define REQUIRE_SEG_MAX 12 -#define REQUIRE_ASYNC_MULTI_MAX 16 - -// --------------------------------------------------------------------------- -// Static state -// --------------------------------------------------------------------------- - -typedef struct { - char_t path[ASSET_FILE_NAME_MAX]; - jerry_value_t exports; -} requirecacheentry_t; - -static requirecacheentry_t g_cache[REQUIRE_CACHE_MAX]; -static uint16_t g_cacheCount; - -static char_t g_dirStack[REQUIRE_DIR_STACK_MAX][ASSET_FILE_NAME_MAX]; -static uint16_t g_dirDepth; - -// --------------------------------------------------------------------------- -// Async context types (forward-declared so items can back-pointer to ctx) -// --------------------------------------------------------------------------- - -typedef struct requireasyncmulti_s requireasyncmulti_t; - -typedef struct { - requireasyncmulti_t *ctx; - uint16_t index; - assetentry_t *entry; - char_t resolvedPath[ASSET_FILE_NAME_MAX]; -} requireasyncmultiitem_t; - -struct requireasyncmulti_s { - jerry_value_t jscallback; - jerry_value_t resultArray; - uint16_t remaining; - requireasyncmultiitem_t items[REQUIRE_ASYNC_MULTI_MAX]; -}; - -typedef struct { - jerry_value_t jscallback; - assetentry_t *entry; - char_t resolvedPath[ASSET_FILE_NAME_MAX]; -} requireasynctask_t; - -// --------------------------------------------------------------------------- -// Path resolution -// --------------------------------------------------------------------------- - -static void requireResolvePath( - const char_t *path, - char_t *out, - const size_t outLen -) { - // Build the raw combined path (base dir + requested path if relative). - char_t combined[ASSET_FILE_NAME_MAX]; - bool_t isRelative = ( - path[0] == '.' && ( - path[1] == '/' || - (path[1] == '.' && path[2] == '/') - ) - ); - - if(isRelative && g_dirDepth > 0) { - stringFormat(combined, sizeof(combined), "%s/%s", - g_dirStack[g_dirDepth - 1], path); - } else { - stringCopy(combined, path, sizeof(combined)); - } - - // Normalise: split on '/', resolve '.' and '..'. - char_t seg[REQUIRE_SEG_MAX][ASSET_FILE_NAME_MAX]; - int32_t segCnt = 0; - const char_t *p = combined; - - while (*p) { - while (*p == '/') p++; - if(!*p) break; - const char_t *start = p; - while (*p && *p != '/') p++; - size_t len = (size_t)(p - start); - - if(len == 1 && start[0] == '.') { - // skip current-dir marker - } else if(len == 2 && start[0] == '.' && start[1] == '.') { - if(segCnt > 0) segCnt--; - } else if(segCnt < REQUIRE_SEG_MAX) { - size_t copy = len < ASSET_FILE_NAME_MAX - 1 ? len : ASSET_FILE_NAME_MAX - 1; - memoryCopy(seg[segCnt], start, copy); - seg[segCnt][copy] = '\0'; - segCnt++; - } - } - - // Reconstruct normalised path. - size_t pos = 0; - out[0] = '\0'; - for(int32_t i = 0; i < segCnt; i++) { - if(i > 0 && pos < outLen - 1) { - out[pos++] = '/'; - out[pos] = '\0'; - } - size_t slen = strlen(seg[i]); - size_t avail = outLen - pos - 1; - if(slen > avail) slen = avail; - memoryCopy(out + pos, seg[i], slen); - pos += slen; - out[pos] = '\0'; - } - - // Append .js extension when absent. - if(!stringEndsWith(out, ".js")) { - size_t curLen = strlen(out); - size_t avail = outLen - curLen - 1; - size_t extLen = avail < 3u ? avail : 3u; - memoryCopy(out + curLen, ".js", extLen); - out[curLen + extLen] = '\0'; - } -} - -// --------------------------------------------------------------------------- -// Directory stack -// --------------------------------------------------------------------------- - -void moduleRequirePushDir(const char_t *filePath) { - assertTrue(g_dirDepth < REQUIRE_DIR_STACK_MAX, "Require dir stack overflow."); - char_t *lastSlash = stringFindLastChar(filePath, '/'); - if(lastSlash) { - size_t dirLen = (size_t)(lastSlash - filePath); - if(dirLen > ASSET_FILE_NAME_MAX - 1) dirLen = ASSET_FILE_NAME_MAX - 1; - memoryCopy(g_dirStack[g_dirDepth], filePath, dirLen); - g_dirStack[g_dirDepth][dirLen] = '\0'; - } else { - g_dirStack[g_dirDepth][0] = '\0'; - } - g_dirDepth++; -} - -void moduleRequirePopDir(void) { - assertTrue(g_dirDepth > 0, "Require dir stack underflow."); - g_dirDepth--; -} - -// --------------------------------------------------------------------------- -// Module cache -// --------------------------------------------------------------------------- - -static bool_t requireCacheHas(const char_t *resolvedPath) { - for(uint16_t i = 0; i < g_cacheCount; i++) { - if(stringEquals(g_cache[i].path, resolvedPath)) return true; - } - return false; -} - -// Returns a new reference on cache hit, jerry_undefined() on miss. -static jerry_value_t requireCacheLookup(const char_t *resolvedPath) { - for(uint16_t i = 0; i < g_cacheCount; i++) { - if(stringEquals(g_cache[i].path, resolvedPath)) { - return jerry_value_copy(g_cache[i].exports); - } - } - return jerry_undefined(); -} - -// Stores an additional reference to exports in the cache. -static void requireCacheStore( - const char_t *resolvedPath, - jerry_value_t exports -) { - if(g_cacheCount >= REQUIRE_CACHE_MAX) return; - stringCopy(g_cache[g_cacheCount].path, resolvedPath, ASSET_FILE_NAME_MAX); - g_cache[g_cacheCount].exports = jerry_value_copy(exports); - g_cacheCount++; -} - -// --------------------------------------------------------------------------- -// Module execution -// --------------------------------------------------------------------------- - -jerry_value_t moduleRequireExecModule( - const char_t *path, - const jerry_char_t *src, - const size_t size -) { - jerry_value_t global = jerry_current_realm(); - jerry_value_t modKey = jerry_string_sz("module"); - jerry_value_t expKey = jerry_string_sz("exports"); - - // Save existing module/exports globals so nested modules can be restored. - jerry_value_t oldModule = jerry_object_get(global, modKey); - jerry_value_t oldExports = jerry_object_get(global, expKey); - - // Create fresh: module = { exports: {} } - jerry_value_t moduleObj = jerry_object(); - jerry_value_t emptyExports = jerry_object(); - jerry_object_set(moduleObj, expKey, emptyExports); - jerry_value_free(emptyExports); - - jerry_object_set(global, modKey, moduleObj); - - // Also expose exports as a top-level shorthand. - jerry_value_t freshExports = jerry_object_get(moduleObj, expKey); - jerry_object_set(global, expKey, freshExports); - jerry_value_free(freshExports); - jerry_value_free(moduleObj); - - // Push this module's directory so nested require() calls resolve correctly. - moduleRequirePushDir(path); - - jerry_value_t evalResult = jerry_eval(src, size, JERRY_PARSE_NO_OPTS); - - moduleRequirePopDir(); - - // Collect module.exports (the script may have replaced the object entirely). - jerry_value_t result; - if(jerry_value_is_exception(evalResult)) { - result = evalResult; - } else { - jerry_value_free(evalResult); - jerry_value_t currentModule = jerry_object_get(global, modKey); - result = jerry_object_get(currentModule, expKey); - jerry_value_free(currentModule); - } - - // Restore caller's module/exports globals. - jerry_object_set(global, modKey, oldModule); - jerry_object_set(global, expKey, oldExports); - jerry_value_free(oldModule); - jerry_value_free(oldExports); - jerry_value_free(modKey); - jerry_value_free(expKey); - jerry_value_free(global); - - return result; -} - -// --------------------------------------------------------------------------- -// Callback error logging -// --------------------------------------------------------------------------- - -static void requireLogCallbackException( - jerry_value_t ret, - const char_t *modulePath -) { - if(!jerry_value_is_exception(ret)) return; - char_t buf[256]; - moduleBaseExceptionMessage(ret, buf, sizeof(buf)); - consolePrint("requireAsync callback error (in '%s'): %s", modulePath, buf); -} - -// --------------------------------------------------------------------------- -// Async callbacks — single path -// --------------------------------------------------------------------------- - -static void requireAsyncOnLoaded(void *params, void *user) { - assetentry_t *entry = (assetentry_t *)params; - requireasynctask_t *task = (requireasynctask_t *)user; - - jerry_value_t exports = jerry_value_copy((jerry_value_t)entry->data.script); - - if(!requireCacheHas(task->resolvedPath)) { - requireCacheStore(task->resolvedPath, exports); - } - - jerry_value_t callArgs[1] = { exports }; - jerry_value_t ret = jerry_call(task->jscallback, jerry_undefined(), callArgs, 1); - requireLogCallbackException(ret, task->resolvedPath); - jerry_value_free(ret); - jerry_value_free(exports); - jerry_value_free(task->jscallback); - assetUnlockEntry(task->entry); - memoryFree(task); -} - -static void requireAsyncOnError(void *params, void *user) { - (void)params; - requireasynctask_t *task = (requireasynctask_t *)user; - - consolePrint("requireAsync: failed to load '%s'", task->resolvedPath); - - jerry_value_t jsnull = jerry_null(); - jerry_value_t callArgs[1] = { jsnull }; - jerry_value_t ret = jerry_call(task->jscallback, jerry_undefined(), callArgs, 1); - requireLogCallbackException(ret, task->resolvedPath); - jerry_value_free(ret); - jerry_value_free(jsnull); - jerry_value_free(task->jscallback); - assetUnlockEntry(task->entry); - memoryFree(task); -} - -// --------------------------------------------------------------------------- -// Async callbacks — multi-path -// --------------------------------------------------------------------------- - -static void requireAsyncMultiFinish(requireasyncmulti_t *ctx) { - jerry_value_t callArgs[1] = { ctx->resultArray }; - jerry_value_t ret = jerry_call(ctx->jscallback, jerry_undefined(), callArgs, 1); - requireLogCallbackException(ret, "multi-path requireAsync"); - jerry_value_free(ret); - jerry_value_free(ctx->resultArray); - jerry_value_free(ctx->jscallback); - memoryFree(ctx); -} - -static void requireAsyncMultiOnLoaded(void *params, void *user) { - assetentry_t *entry = (assetentry_t *)params; - requireasyncmultiitem_t *item = (requireasyncmultiitem_t *)user; - requireasyncmulti_t *ctx = item->ctx; - - jerry_value_t exports = jerry_value_copy((jerry_value_t)entry->data.script); - - if(!requireCacheHas(item->resolvedPath)) { - requireCacheStore(item->resolvedPath, exports); - } - - jerry_object_set_index(ctx->resultArray, (uint32_t)item->index, exports); - jerry_value_free(exports); - assetUnlockEntry(entry); - item->entry = NULL; - - ctx->remaining--; - if(ctx->remaining == 0) requireAsyncMultiFinish(ctx); -} - -static void requireAsyncMultiOnError(void *params, void *user) { - (void)params; - requireasyncmultiitem_t *item = (requireasyncmultiitem_t *)user; - consolePrint("requireAsync: failed to load '%s'", item->resolvedPath); - requireasyncmulti_t *ctx = item->ctx; - - jerry_value_t jsnull = jerry_null(); - jerry_object_set_index(ctx->resultArray, (uint32_t)item->index, jsnull); - jerry_value_free(jsnull); - assetUnlockEntry(item->entry); - item->entry = NULL; - - ctx->remaining--; - if(ctx->remaining == 0) requireAsyncMultiFinish(ctx); -} - -// --------------------------------------------------------------------------- -// Sync load helper — resolves, checks cache, locks, and blocks until loaded. -// Returns an owned jerry_value_t on success, or an exception on failure. -// --------------------------------------------------------------------------- - -static jerry_value_t requireLoad(const char_t *resolved) { - // Fast path: return cached exports. - jerry_value_t cached = requireCacheLookup(resolved); - if(!jerry_value_is_undefined(cached)) return cached; - jerry_value_free(cached); - - assetloaderinput_t input; - memoryZero(&input, sizeof(input)); - input.script.isModule = true; - - assetentry_t *entry = assetLock(resolved, ASSET_LOADER_TYPE_SCRIPT, &input); - if(!entry) return moduleBaseThrow("require: failed to lock asset"); - - errorret_t err = assetRequireLoaded(entry); - if(errorIsNotOk(err)) { - assetUnlockEntry(entry); - return moduleBaseThrowError(err); - } - - if(entry->data.script == 0) { - assetUnlockEntry(entry); - return moduleBaseThrow("require: module produced no exports"); - } - - jerry_value_t exports = jerry_value_copy((jerry_value_t)entry->data.script); - requireCacheStore(resolved, exports); - assetUnlockEntry(entry); - return exports; -} - -// --------------------------------------------------------------------------- -// JS require() -// --------------------------------------------------------------------------- jerry_value_t moduleRequireFunc( const jerry_call_info_t *callInfo, const jerry_value_t args[], const jerry_length_t argc ) { - (void)callInfo; - if(argc < 1) return moduleBaseThrow("require: expected path argument"); + assertNotNull(callInfo, "callInfo must not be null."); + assertNotNull(args, "args must not be null."); - // Multi-path: require(['a', 'b', ...]) - if(jerry_value_is_array(args[0])) { - uint32_t count = jerry_array_length(args[0]); - jerry_value_t result = jerry_array(count); - - for(uint32_t i = 0; i < count; i++) { - jerry_value_t pathVal = jerry_object_get_index(args[0], i); - if(!jerry_value_is_string(pathVal)) { - jerry_value_free(pathVal); - jerry_value_free(result); - return moduleBaseThrow("require: each path must be a string"); - } - char_t rawPath[ASSET_FILE_NAME_MAX]; - moduleBaseToString(pathVal, rawPath, sizeof(rawPath)); - jerry_value_free(pathVal); - - char_t resolved[ASSET_FILE_NAME_MAX]; - requireResolvePath(rawPath, resolved, sizeof(resolved)); - - jerry_value_t exports = requireLoad(resolved); - if(jerry_value_is_exception(exports)) { - jerry_value_free(result); - return exports; - } - jerry_object_set_index(result, i, exports); - jerry_value_free(exports); - } - return result; + if(argc < 1 || !jerry_value_is_string(args[0])) { + return moduleBaseThrow("Expected a string argument for module name."); + } + + // Is filename too long? + if( + jerry_string_size(args[0], JERRY_ENCODING_UTF8) >= ASSET_FILE_NAME_MAX + ) { + return moduleBaseThrow("Module name too long."); } - // Single path: require('./foo') - if(!jerry_value_is_string(args[0])) { - return moduleBaseThrow("require: path must be a string or array of strings"); + // Get C string + char_t moduleName[ASSET_FILE_NAME_MAX]; + moduleBaseToString(args[0], moduleName, sizeof(moduleName)); + + // Lock and load the asset. + assetloaderinput_t input; + input.script.nothing = NULL; + + assetentry_t *entry = assetLock( + moduleName, + ASSET_LOADER_TYPE_SCRIPT, + &input + ); + + errorret_t err = assetRequireLoaded(entry); + + if(errorIsNotOk(err)) { + assetUnlockEntry(entry); + return moduleBaseThrowError(err); } - char_t rawPath[ASSET_FILE_NAME_MAX]; - moduleBaseToString(args[0], rawPath, sizeof(rawPath)); + // Now the module is loaded, copy it before unlocking. + if(jerry_value_is_undefined(entry->data.script.exports)) { + assetUnlockEntry(entry); + return jerry_undefined(); + } - char_t resolved[ASSET_FILE_NAME_MAX]; - requireResolvePath(rawPath, resolved, sizeof(resolved)); - - return requireLoad(resolved); + jerry_value_t exportsCopy = jerry_value_copy( + entry->data.script.exports + ); + assetUnlockEntry(entry);// Frees entry->data.script.exports + return exportsCopy; } -// --------------------------------------------------------------------------- -// JS requireAsync() -// --------------------------------------------------------------------------- - jerry_value_t moduleRequireAsyncFunc( const jerry_call_info_t *callInfo, const jerry_value_t args[], const jerry_length_t argc ) { - (void)callInfo; - if(argc < 2) { - return moduleBaseThrow("requireAsync: expected (path, callback)"); - } - if(!jerry_value_is_function(args[1])) { - return moduleBaseThrow("requireAsync: second argument must be a function"); - } - - // ---- Single path ------------------------------------------------------- - if(jerry_value_is_string(args[0])) { - char_t rawPath[ASSET_FILE_NAME_MAX]; - moduleBaseToString(args[0], rawPath, sizeof(rawPath)); - - char_t resolved[ASSET_FILE_NAME_MAX]; - requireResolvePath(rawPath, resolved, sizeof(resolved)); - - // Cache hit: invoke callback synchronously. - jerry_value_t cached = requireCacheLookup(resolved); - if(!jerry_value_is_undefined(cached)) { - jerry_value_t callArgs[1] = { cached }; - jerry_value_t ret = jerry_call(args[1], jerry_undefined(), callArgs, 1); - jerry_value_free(ret); - jerry_value_free(cached); - return jerry_undefined(); - } - jerry_value_free(cached); - - assetloaderinput_t input; - memoryZero(&input, sizeof(input)); - input.script.isModule = true; - assetentry_t *entry = assetLock(resolved, ASSET_LOADER_TYPE_SCRIPT, &input); - - if(!entry) { - jerry_value_t jsnull = jerry_null(); - jerry_value_t callArgs[1] = { jsnull }; - jerry_value_t ret = jerry_call(args[1], jerry_undefined(), callArgs, 1); - jerry_value_free(ret); - jerry_value_free(jsnull); - return jerry_undefined(); - } - - // Already loaded (e.g. locked previously and still alive). - if(entry->state == ASSET_ENTRY_STATE_LOADED && entry->data.script != 0) { - jerry_value_t exports = jerry_value_copy((jerry_value_t)entry->data.script); - requireCacheStore(resolved, exports); - jerry_value_t callArgs[1] = { exports }; - jerry_value_t ret = jerry_call(args[1], jerry_undefined(), callArgs, 1); - jerry_value_free(ret); - jerry_value_free(exports); - assetUnlockEntry(entry); - return jerry_undefined(); - } - - requireasynctask_t *task = - (requireasynctask_t *)memoryAllocate(sizeof(requireasynctask_t)); - task->jscallback = jerry_value_copy(args[1]); - task->entry = entry; - stringCopy(task->resolvedPath, resolved, ASSET_FILE_NAME_MAX); - - eventSubscribe(&entry->onLoaded, requireAsyncOnLoaded, task); - eventSubscribe(&entry->onError, requireAsyncOnError, task); - return jerry_undefined(); - } - - // ---- Multi-path -------------------------------------------------------- - if(jerry_value_is_array(args[0])) { - uint32_t count = jerry_array_length(args[0]); - if(count == 0 || count > (uint32_t)REQUIRE_ASYNC_MULTI_MAX) { - return moduleBaseThrow("requireAsync: path array out of range"); - } - - requireasyncmulti_t *ctx = - (requireasyncmulti_t *)memoryAllocate(sizeof(requireasyncmulti_t)); - ctx->jscallback = jerry_value_copy(args[1]); - ctx->resultArray = jerry_array(count); - ctx->remaining = (uint16_t)count; - - for(uint32_t i = 0; i < count; i++) { - ctx->items[i].ctx = ctx; - ctx->items[i].index = (uint16_t)i; - ctx->items[i].entry = NULL; - - jerry_value_t pathVal = jerry_object_get_index(args[0], i); - if(!jerry_value_is_string(pathVal)) { - jerry_value_free(pathVal); - jerry_value_t jsnull = jerry_null(); - jerry_object_set_index(ctx->resultArray, i, jsnull); - jerry_value_free(jsnull); - ctx->remaining--; - continue; - } - - char_t rawPath[ASSET_FILE_NAME_MAX]; - moduleBaseToString(pathVal, rawPath, sizeof(rawPath)); - jerry_value_free(pathVal); - - char_t resolved[ASSET_FILE_NAME_MAX]; - requireResolvePath(rawPath, resolved, sizeof(resolved)); - stringCopy(ctx->items[i].resolvedPath, resolved, ASSET_FILE_NAME_MAX); - - // Cache hit. - jerry_value_t cached = requireCacheLookup(resolved); - if(!jerry_value_is_undefined(cached)) { - jerry_object_set_index(ctx->resultArray, i, cached); - jerry_value_free(cached); - ctx->remaining--; - continue; - } - jerry_value_free(cached); - - assetloaderinput_t input; - memoryZero(&input, sizeof(input)); - input.script.isModule = true; - assetentry_t *entry = assetLock(resolved, ASSET_LOADER_TYPE_SCRIPT, &input); - - if(!entry) { - jerry_value_t jsnull = jerry_null(); - jerry_object_set_index(ctx->resultArray, i, jsnull); - jerry_value_free(jsnull); - ctx->remaining--; - continue; - } - - // Already loaded. - if(entry->state == ASSET_ENTRY_STATE_LOADED && entry->data.script != 0) { - jerry_value_t exports = jerry_value_copy((jerry_value_t)entry->data.script); - requireCacheStore(resolved, exports); - jerry_object_set_index(ctx->resultArray, i, exports); - jerry_value_free(exports); - assetUnlockEntry(entry); - ctx->remaining--; - continue; - } - - ctx->items[i].entry = entry; - eventSubscribe(&entry->onLoaded, requireAsyncMultiOnLoaded, &ctx->items[i]); - eventSubscribe(&entry->onError, requireAsyncMultiOnError, &ctx->items[i]); - } - - // All resolved immediately. - if(ctx->remaining == 0) requireAsyncMultiFinish(ctx); - return jerry_undefined(); - } - - return moduleBaseThrow("requireAsync: path must be a string or array of strings"); + return jerry_undefined(); } -// --------------------------------------------------------------------------- -// Init / Dispose -// --------------------------------------------------------------------------- - void moduleRequireInit(void) { - memoryZero(g_cache, sizeof(g_cache)); - g_cacheCount = 0; - g_dirDepth = 0; - moduleBaseDefineGlobalMethod("require", moduleRequireFunc); + moduleBaseDefineGlobalMethod("require", moduleRequireFunc); moduleBaseDefineGlobalMethod("requireAsync", moduleRequireAsyncFunc); } void moduleRequireDispose(void) { - for(uint16_t i = 0; i < g_cacheCount; i++) { - jerry_value_free(g_cache[i].exports); - } - memoryZero(g_cache, sizeof(g_cache)); - g_cacheCount = 0; } diff --git a/src/dusk/script/module/require/modulerequire.h b/src/dusk/script/module/require/modulerequire.h index 85aa65c3..f4c24b1d 100644 --- a/src/dusk/script/module/require/modulerequire.h +++ b/src/dusk/script/module/require/modulerequire.h @@ -9,46 +9,11 @@ #include "script/module/modulebase.h" /** - * Initializes the require() module system: creates the module cache and - * registers the global require() and requireAsync() functions. + * Initializes the require module. */ void moduleRequireInit(void); /** - * Disposes of the require() module system, releasing the module cache. + * Disposes the require module. */ -void moduleRequireDispose(void); - -/** - * Records the directory of the script about to be evaluated so that relative - * require() paths can be resolved correctly. Call before jerry_eval; pair each - * call with moduleRequirePopDir(). - * - * @param filePath Full asset path of the script (e.g. "scripts/Main.js"). - */ -void moduleRequirePushDir(const char_t *filePath); - -/** - * Pops the most recently pushed directory off the resolution stack. - */ -void moduleRequirePopDir(void); - -/** - * Evaluates a script source in an isolated module scope. - * - * Sets up fresh module/exports globals, pushes the directory for nested - * require() calls, evaluates the source, then restores everything. - * - * On success returns module.exports (caller owns the reference). - * On eval error returns the exception value (caller must check with - * jerry_value_is_exception and free appropriately). - * - * @param path Full asset path — used only for directory resolution. - * @param src Source bytes. - * @param size Byte count of src (not including any null terminator). - */ -jerry_value_t moduleRequireExecModule( - const char_t *path, - const jerry_char_t *src, - const size_t size -); +void moduleRequireDispose(void); \ No newline at end of file diff --git a/src/dusk/script/script.c b/src/dusk/script/script.c index a08ba1c4..b1a4358e 100644 --- a/src/dusk/script/script.c +++ b/src/dusk/script/script.c @@ -69,8 +69,26 @@ errorret_t scriptExecFile(const char_t *path) { errorret_t scriptDispose(void) { if(!SCRIPT.initialized) errorOk(); + + // Make a long story short we need to dispose script assets here, because the + // asset reaper isn't called until later. + assetentry_t *entries[ASSET_ENTRY_COUNT_MAX]; + uint32_t count = assetGetEntriesOfType(entries, ASSET_LOADER_TYPE_SCRIPT); + + // Release the locks + for(size_t i = 0; i < count; i++) { + assetUnlockEntry(entries[i]); + assetRequireDisposed(entries[i]); + } + + assertTrue( + assetGetEntriesOfType(entries, ASSET_LOADER_TYPE_SCRIPT) == 0, + "All script assets should be disposed by now." + ); + moduleListDispose(); jerry_cleanup(); SCRIPT.initialized = false; + errorOk(); } diff --git a/src/dusk/script/scriptmodule.h b/src/dusk/script/scriptmodule.h new file mode 100644 index 00000000..fef0f58f --- /dev/null +++ b/src/dusk/script/scriptmodule.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "scriptproto.h" + +typedef struct { + jerry_value_t exports; +} scriptmodule_t; \ No newline at end of file