diff --git a/assets/init.js b/assets/init.js index 3b3e7814..25147440 100644 --- a/assets/init.js +++ b/assets/init.js @@ -1,3 +1,5 @@ + + const platformNames = { [System.PLATFORM_LINUX]: 'Linux', [System.PLATFORM_KNULLI]: 'Knulli', @@ -9,3 +11,12 @@ const platformNames = { Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown')); // Scene.set('testscene.js'); +// Console.print('Loading scene...'); +// requireAsync('./testscene.js', function(scene) { +// throw "test"; +// Console.print('Initializing scene...'); +// var batch = scene.load(); +// batch.lock(); +// batch.requireLoaded(); +// scene.init(); +// }); \ No newline at end of file diff --git a/assets/testscene.js b/assets/testscene.js index 875547fd..0717d51a 100644 --- a/assets/testscene.js +++ b/assets/testscene.js @@ -1,6 +1,8 @@ -var scene = {}; +var scene = { + 'test': 'teststring' +}; -scene.batch = AssetBatch([ +var assets = AssetBatch([ { path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA } ]); @@ -11,12 +13,13 @@ var testPos; var testRenderable; var texEntry; -scene.load = function() { - return scene.batch; +scene.init = function() { + assets.lock(); + assets.onLoaded[0] = scene.loaded; }; -scene.init = function() { - texEntry = scene.batch.entry(0); +scene.loaded = function() { + texEntry = assets.entry(0); // Camera at (3, 3, 3) looking at origin cam = Entity.create(); @@ -35,14 +38,13 @@ scene.init = function() { [0, 0, 1, 1, 0, 1, 1, 0] ]; testPos.localPosition = new Vec3(0, 0, 0); -}; +} scene.dispose = function() { + Console.print('Scene Dispose'); Entity.dispose(cam); Entity.dispose(testEntity); - - texEntry.unlock(); - scene.batch.unlock(); + assets.unlock(); }; module.exports = scene; diff --git a/src/dusk/assert/assert.c b/src/dusk/assert/assert.c index 9462a235..52de7d90 100644 --- a/src/dusk/assert/assert.c +++ b/src/dusk/assert/assert.c @@ -9,8 +9,15 @@ #include "log/log.h" #include "util/string.h" #include "util/memory.h" +#ifdef DUSK_THREAD_PTHREAD + #include "thread/thread.h" +#endif #ifndef DUSK_ASSERTIONS_FAKED + void assertInit(void) { + assertMainThreadInit(); + } + #ifdef DUSK_TEST_ASSERT void assertTrueImpl( const char *file, @@ -132,4 +139,26 @@ ) { assertTrueImpl(file, line, stringCompare(a, b) == 0, message); } + + #ifdef DUSK_THREAD_PTHREAD + void assertMainThreadInit(void) { + THREAD_MAIN_THREAD_ID = pthread_self(); + } + + void assertIsMainThreadImpl( + const char *file, + const int32_t line, + const char *message + ) { + assertTrueImpl(file, line, pthread_self() == THREAD_MAIN_THREAD_ID, message); + } + + void assertNotMainThreadImpl( + const char *file, + const int32_t line, + const char *message + ) { + assertTrueImpl(file, line, pthread_self() != THREAD_MAIN_THREAD_ID, message); + } + #endif #endif \ No newline at end of file diff --git a/src/dusk/assert/assert.h b/src/dusk/assert/assert.h index 7d9736f2..1a0f3c57 100644 --- a/src/dusk/assert/assert.h +++ b/src/dusk/assert/assert.h @@ -17,6 +17,57 @@ #endif #ifndef DUSK_ASSERTIONS_FAKED + /** + * Initializes the assert system. Must be the very first call in engine + * startup. + */ + void assertInit(void); + + /** + * Records the calling thread as the main (game) thread. Call once, early + * in engine startup, before any assertIsMainThread / assertNotMainThread + * checks are made. + */ + #ifdef DUSK_THREAD_PTHREAD + void assertMainThreadInit(void); + #else + #define assertMainThreadInit() ((void)0) + #endif + + /** + * Asserts that the current thread is the main thread. + * + * @param message Message to throw against assertion failure. + */ + #ifdef DUSK_THREAD_PTHREAD + void assertIsMainThreadImpl( + const char *file, + const int32_t line, + const char *message + ); + #define assertIsMainThread(message) \ + assertIsMainThreadImpl(__FILE__, __LINE__, message) + #else + #define assertIsMainThread(message) ((void)0) + #endif + + /** + * Asserts that the current thread is NOT the main thread. + * + * @param message Message to throw against assertion failure. + */ + #ifdef DUSK_THREAD_PTHREAD + void assertNotMainThreadImpl( + const char *file, + const int32_t line, + const char *message + ); + #define assertNotMainThread(message) \ + assertNotMainThreadImpl(__FILE__, __LINE__, message) + #else + #define assertNotMainThread(message) ((void)0) + #endif + /** * Assert a given value to be true. * @@ -207,6 +258,10 @@ #else // If assertions are faked, we define the macros to do nothing. + #define assertInit() ((void)0) + #define assertMainThreadInit() ((void)0) + #define assertIsMainThread(message) ((void)0) + #define assertNotMainThread(message) ((void)0) #define assertTrue(x, message) ((void)0) #define assertFalse(x, message) ((void)0) #define assertUnreachable(message) ((void)0) diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c index 77c03d02..b20f8098 100644 --- a/src/dusk/asset/asset.c +++ b/src/dusk/asset/asset.c @@ -81,6 +81,7 @@ assetentry_t * assetGetEntry( errorret_t assetRequireLoaded(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->state == ASSET_ENTRY_STATE_LOADED) { errorOk(); @@ -114,6 +115,7 @@ assetentry_t * assetLock( void assetUnlock(const char_t *name) { assertNotNull(name, "Name cannot be NULL."); + assetentry_t *entry = ASSET.entries; do { if(entry->type != ASSET_LOADER_TYPE_NULL && stringEquals(entry->name, name)) { @@ -122,6 +124,7 @@ void assetUnlock(const char_t *name) { } entry++; } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); + assertUnreachable("Asset entry not found for unlock."); } @@ -131,6 +134,8 @@ void assetUnlockEntry(assetentry_t *entry) { } errorret_t assetUpdate(void) { + assertIsMainThread("assetUpdate must be called from the main thread."); + // Determine how many available loading slots we have. assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX]; uint8_t availableLoadingCount = 0; @@ -294,6 +299,8 @@ errorret_t assetUpdate(void) { } void assetUpdateAsync(thread_t *thread) { + assertNotMainThread("assetUpdateAsync must not run on the main thread."); + while(!threadShouldStop(thread)) { // Walk over each asset assetloading_t *loading; @@ -350,12 +357,35 @@ void assetUpdateAsync(thread_t *thread) { } errorret_t assetDispose(void) { + assertIsMainThread("Must be called from the main thread."); threadStop(&ASSET.loadThread); + // Free any script read-buffers left behind by an in-flight async load + // that was interrupted before the sync eval phase ran. for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { - threadMutexDispose(&ASSET.loading[i].mutex); + assetloading_t *loading = &ASSET.loading[i]; + if( + loading->entry != NULL && + loading->type == ASSET_LOADER_TYPE_SCRIPT && + loading->loading.script.buffer != NULL + ) { + memoryFree(loading->loading.script.buffer); + loading->loading.script.buffer = NULL; + } + threadMutexDispose(&loading->mutex); } + // Dispose every non-null entry so type-specific dispose callbacks + // (e.g. assetScriptDispose freeing jerry values) run before the + // scripting engine is torn down. + assetentry_t *entry = ASSET.entries; + do { + if(entry->type != ASSET_LOADER_TYPE_NULL) { + errorChain(assetEntryDispose(entry)); + } + entry++; + } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); + // Cleanup zip file. if(ASSET.zip != NULL) { if(zip_close(ASSET.zip) != 0) { diff --git a/src/dusk/asset/loader/assetentry.c b/src/dusk/asset/loader/assetentry.c index a2dbac8d..10a7644b 100644 --- a/src/dusk/asset/loader/assetentry.c +++ b/src/dusk/asset/loader/assetentry.c @@ -21,6 +21,7 @@ void assetEntryInit( assertStrLenMax(name, ASSET_FILE_NAME_MAX - 1, "Name too long"); assertTrue(type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); assertTrue(type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type."); + assertIsMainThread("Must be called from the main thread."); memoryZero(entry, sizeof(assetentry_t)); stringCopy(entry->name, name, ASSET_FILE_NAME_MAX); @@ -52,13 +53,16 @@ void assetEntryInit( void assetEntryLock(assetentry_t *entry) { assertNotNull(entry, "Entry cannot be NULL"); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); + entry->wasLocked = true; + refLock(&entry->refs); } void assetEntryUnlock(assetentry_t *entry) { assertNotNull(entry, "Entry cannot be NULL"); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); + refUnlock(&entry->refs); } @@ -73,6 +77,7 @@ void assetEntryStartLoading( entry->state == ASSET_ENTRY_STATE_NOT_STARTED, "Can only start loading from NOT_STARTED state." ); + assertIsMainThread("Must be called from the main thread."); entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; memoryZero(&loading->loading, sizeof(assetloaderloading_t)); @@ -85,6 +90,11 @@ errorret_t assetEntryDispose(assetentry_t *entry) { assertNotNull(entry, "Entry cannot be NULL"); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); assertTrue(entry->type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type."); + assertIsMainThread("Must be called from the main thread."); + assertTrue( + entry->refs.count == 0, + "Asset entry still refed at dispose time." + ); eventInvoke(&entry->onUnloaded, entry); errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry)); diff --git a/src/dusk/asset/loader/display/assettextureloader.c b/src/dusk/asset/loader/display/assettextureloader.c index 1b264f8a..bde00990 100644 --- a/src/dusk/asset/loader/display/assettextureloader.c +++ b/src/dusk/asset/loader/display/assettextureloader.c @@ -54,6 +54,7 @@ int assetTextureEOF(void *user) { errorret_t assetTextureLoaderAsync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); + assertNotMainThread("Should be called from an async thread."); // Only care about loading pixels. if(loading->loading.texture.state != ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS){ @@ -122,6 +123,7 @@ errorret_t assetTextureLoaderAsync(assetloading_t *loading) { errorret_t assetTextureLoaderSync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); + assertIsMainThread("Must be called from the main thread."); switch(loading->loading.texture.state) { case ASSET_TEXTURE_LOADING_STATE_INITIAL: @@ -161,5 +163,7 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) { errorret_t assetTextureDispose(assetentry_t *entry) { assertNotNull(entry, "Asset entry cannot be NULL"); + assertIsMainThread("Must be called from the main thread."); + return textureDispose(&entry->data.texture); } \ No newline at end of file diff --git a/src/dusk/asset/loader/display/assettilesetloader.c b/src/dusk/asset/loader/display/assettilesetloader.c index 3803baf8..74b05c2e 100644 --- a/src/dusk/asset/loader/display/assettilesetloader.c +++ b/src/dusk/asset/loader/display/assettilesetloader.c @@ -14,6 +14,7 @@ errorret_t assetTilesetLoaderAsync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); + assertNotMainThread("Should be called from an async thread."); if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) { errorOk(); @@ -40,6 +41,7 @@ errorret_t assetTilesetLoaderAsync(assetloading_t *loading) { errorret_t assetTilesetLoaderSync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); switch(loading->loading.tileset.state) { case ASSET_TILESET_LOADING_STATE_INITIAL: @@ -111,5 +113,7 @@ errorret_t assetTilesetLoaderSync(assetloading_t *loading) { errorret_t assetTilesetDispose(assetentry_t *entry) { assertNotNull(entry, "Entry cannot be NULL"); assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); + errorOk(); } diff --git a/src/dusk/asset/loader/json/assetjsonloader.c b/src/dusk/asset/loader/json/assetjsonloader.c index 83593d3b..6b04ae37 100644 --- a/src/dusk/asset/loader/json/assetjsonloader.c +++ b/src/dusk/asset/loader/json/assetjsonloader.c @@ -13,6 +13,7 @@ errorret_t assetJsonLoaderAsync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); + assertNotMainThread("Async loader should not be on main thread."); if(loading->loading.json.state != ASSET_JSON_LOADING_STATE_READ_FILE) { errorOk(); @@ -45,6 +46,7 @@ errorret_t assetJsonLoaderAsync(assetloading_t *loading) { errorret_t assetJsonLoaderSync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); switch(loading->loading.json.state) { case ASSET_JSON_LOADING_STATE_INITIAL: @@ -82,7 +84,10 @@ errorret_t assetJsonLoaderSync(assetloading_t *loading) { errorret_t assetJsonDispose(assetentry_t *entry) { assertNotNull(entry, "Asset entry cannot be NULL"); assertTrue(entry->type == ASSET_LOADER_TYPE_JSON, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); + yyjson_doc_free(entry->data.json); entry->data.json = NULL; + errorOk(); } diff --git a/src/dusk/asset/loader/locale/assetlocaleloader.c b/src/dusk/asset/loader/locale/assetlocaleloader.c index d6840df5..aabd630e 100644 --- a/src/dusk/asset/loader/locale/assetlocaleloader.c +++ b/src/dusk/asset/loader/locale/assetlocaleloader.c @@ -16,6 +16,7 @@ errorret_t assetLocaleLoaderAsync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); + assertNotMainThread("Async loader should not be on main thread."); if(loading->loading.locale.state != ASSET_LOCALE_LOADER_STATE_LOAD_HEADER) { errorOk(); @@ -38,6 +39,7 @@ errorret_t assetLocaleLoaderAsync(assetloading_t *loading) { errorret_t assetLocaleLoaderSync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); assertTrue(loading->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); switch(loading->loading.locale.state) { case ASSET_LOCALE_LOADER_STATE_INITIAL: @@ -60,11 +62,14 @@ errorret_t assetLocaleLoaderSync(assetloading_t *loading) { errorret_t assetLocaleDispose(assetentry_t *entry) { assertNotNull(entry, "Asset entry cannot be NULL"); assertTrue(entry->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); + assetlocalefile_t *localeFile = &entry->data.locale; errorChain(assetFileClose(&localeFile->file)); return assetFileDispose(&localeFile->file); } +// These functions probably need some cleaning; errorret_t assetLocaleParseHeader( assetlocalefile_t *localeFile, char_t *headerBuffer, diff --git a/src/dusk/asset/loader/script/assetscriptloader.c b/src/dusk/asset/loader/script/assetscriptloader.c index 93f2b837..eb16de0e 100644 --- a/src/dusk/asset/loader/script/assetscriptloader.c +++ b/src/dusk/asset/loader/script/assetscriptloader.c @@ -16,6 +16,7 @@ errorret_t assetScriptLoaderAsync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); + assertNotMainThread("Async loader should not be on main thread."); if(loading->loading.script.state != ASSET_SCRIPT_LOADING_STATE_READ_FILE) { errorOk(); @@ -59,6 +60,7 @@ errorret_t assetScriptLoaderAsync(assetloading_t *loading) { errorret_t assetScriptLoaderSync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); assertTrue(loading->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); switch(loading->loading.script.state) { case ASSET_SCRIPT_LOADING_STATE_INITIAL: @@ -77,11 +79,28 @@ errorret_t assetScriptLoaderSync(assetloading_t *loading) { uint8_t *buffer = loading->loading.script.buffer; assertNotNull(buffer, "Script buffer should have been loaded by now."); - jerry_value_t result = jerry_eval( - (const jerry_char_t *)buffer, - loading->loading.script.size, - JERRY_PARSE_NO_OPTS + bool_t isModule = ( + loading->entry->input != NULL && + loading->entry->input->script.isModule ); + + 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(); + } + memoryFree(buffer); loading->loading.script.buffer = NULL; @@ -109,9 +128,12 @@ errorret_t assetScriptLoaderSync(assetloading_t *loading) { errorret_t assetScriptDispose(assetentry_t *entry) { assertNotNull(entry, "Asset entry cannot be NULL"); 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; } + errorOk(); } diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 5618bba1..b82415c6 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -30,6 +30,7 @@ engine_t ENGINE; errorret_t engineInit(const int32_t argc, const char_t **argv) { + assertInit(); memoryZero(&ENGINE, sizeof(engine_t)); ENGINE.running = true; ENGINE.argc = argc; @@ -58,6 +59,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { consolePrint("Engine initialized"); errorChain(scriptExecFile("init.js")); + errorChain(sceneSet("testscene.js")); errorOk(); } @@ -74,7 +76,7 @@ errorret_t engineUpdate(void) { physicsManagerUpdate(); errorChain(displayUpdate()); errorChain(cutsceneUpdate()); - errorChain(sceneUpdate()); + sceneUpdate(); errorChain(assetUpdate()); if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false; diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c index 7a471da1..3180eb46 100644 --- a/src/dusk/scene/scene.c +++ b/src/dusk/scene/scene.c @@ -14,6 +14,10 @@ #include "ui/ui.h" #include "scene/scenerenderpipeline.h" #include "entity/component.h" +#include "asset/asset.h" +#include "asset/loader/assetloader.h" +#include "script/module/modulebase.h" +#include "console/console.h" scene_t SCENE; @@ -22,8 +26,21 @@ errorret_t sceneInit(void) { errorOk(); } -errorret_t sceneUpdate(void) { - errorOk(); +void sceneUpdate(void) { + if((SCENE.state & SCENE_STATE_LOADED) == 0) return; + + if((SCENE.state & SCENE_STATE_INITIALIZED) == 0) { + errorret_t ret = sceneCallScriptMethod(SCENE.jsInit, "init"); + if(errorIsNotOk(ret)) { + errorCatch(errorPrint(ret)); + errorCatch(errorPrint(sceneDispose())); + return; + } + + SCENE.state |= SCENE_STATE_INITIALIZED; + } + + errorCatch(errorPrint(sceneCallScriptMethod(SCENE.jsUpdate, "update"))); } errorret_t sceneRender(void) { @@ -61,14 +78,88 @@ errorret_t sceneRender(void) { errorOk(); } -void sceneSet(void) { +errorret_t sceneSet(const char_t *jsFile) { + assertStrLenMin(jsFile, 1, "jsFile cannot be empty"); + + // 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); + + // 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); + } + + // 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); + + // 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; + + errorOk(); +} + +errorret_t sceneCallScriptMethod(jerry_value_t fn, const char_t *name) { + assertStrLenMin(name, 1, "name cannot be empty"); + + if(!jerry_value_is_function(fn)) { + errorOk(); + } + + jerry_value_t ret = jerry_call(fn, jerry_undefined(), NULL, 0); + if(jerry_value_is_exception(ret)) { + char_t buf[256]; + moduleBaseExceptionMessage(ret, buf, sizeof(buf)); + errorThrow("Scene script exception in %s: %s", name, buf); + } + jerry_value_free(ret); + errorOk(); +} + +jerry_value_t sceneExtractFn(jerry_value_t obj, const char_t *name) { + jerry_value_t val = moduleBaseGetProp(obj, name); + if(jerry_value_is_function(val)) return val; + jerry_value_free(val); + return jerry_undefined(); } errorret_t sceneDispose(void) { - // if(SCENE.active) { - // scriptSceneDispose(); - // SCENE.active = false; - // } - // SCENE.transitioning = false; + if((SCENE.state & SCENE_STATE_LOADED) == 0) { + errorOk(); + } + + if((SCENE.state & SCENE_STATE_INITIALIZED) != 0) { + errorChain(sceneCallScriptMethod(SCENE.jsDispose, "dispose")); + } + + jerry_value_free(SCENE.jsDispose); + jerry_value_free(SCENE.jsInit); + jerry_value_free(SCENE.jsUpdate); + + SCENE.jsDispose = jerry_undefined(); + SCENE.jsInit = jerry_undefined(); + SCENE.jsUpdate = jerry_undefined(); + + SCENE.state = 0; errorOk(); } diff --git a/src/dusk/scene/scene.h b/src/dusk/scene/scene.h index 4db421b7..f639762b 100644 --- a/src/dusk/scene/scene.h +++ b/src/dusk/scene/scene.h @@ -7,9 +7,16 @@ #pragma once #include "error/error.h" +#include + +#define SCENE_STATE_LOADED 0x01 +#define SCENE_STATE_INITIALIZED 0x02 typedef struct { - void *nothing; + jerry_value_t jsInit; + jerry_value_t jsUpdate; + jerry_value_t jsDispose; + uint8_t state; } scene_t; extern scene_t SCENE; @@ -23,7 +30,7 @@ errorret_t sceneInit(void); * Ticks the scene manager. Processes any pending scene transition, then * calls scriptSceneUpdate on the active scene. */ -errorret_t sceneUpdate(void); +void sceneUpdate(void); /** * Renders the current scene (entities, render pipeline, UI). @@ -37,6 +44,34 @@ errorret_t sceneRender(void); */ // void sceneSet(void); +/** + * Loads a JS file as a module, extracts the .init / .update / .dispose + * callbacks from its exports, and stores them ready for the scene loop. + * Disposes any previously active scene first. + * + * @param jsFile Asset path of the script (e.g. "testscene.js"). + */ +errorret_t sceneSet(const char_t *jsFile); + +/** + * Calls a scene script method and logs any exception. + * + * @param fn The function to call. If not a function, this is a no-op. + * @param name The name of the method (for logging purposes). + * @return Error state (if any). + */ +errorret_t sceneCallScriptMethod(jerry_value_t fn, const char_t *name); + +/** + * Extracts a function from a scene module export. If the property doesn't + * exist or isn't a function, returns undefined. + * + * @param obj The module exports object. + * @param name The name of the property to extract. + * @return The extracted function, or undefined. + */ +jerry_value_t sceneExtractFn(jerry_value_t obj, const char_t *name); + /** * Disposes the active scene immediately. */ diff --git a/src/dusk/script/module/require/modulerequire.c b/src/dusk/script/module/require/modulerequire.c index 90eae20c..c2baca22 100644 --- a/src/dusk/script/module/require/modulerequire.c +++ b/src/dusk/script/module/require/modulerequire.c @@ -7,28 +7,621 @@ #include "modulerequire.h" #include "asset/asset.h" -#include "asset/assetfile.h" +#include "util/memory.h" +#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 ) { - return jerry_undefined(); + (void)callInfo; + if(argc < 1) return moduleBaseThrow("require: expected path argument"); + + // 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; + } + + // Single path: require('./foo') + if(!jerry_value_is_string(args[0])) { + return moduleBaseThrow("require: path must be a string or array of strings"); + } + + 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)); + + return requireLoad(resolved); } +// --------------------------------------------------------------------------- +// JS requireAsync() +// --------------------------------------------------------------------------- + jerry_value_t moduleRequireAsyncFunc( const jerry_call_info_t *callInfo, const jerry_value_t args[], const jerry_length_t argc ) { - return jerry_undefined(); + (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"); } +// --------------------------------------------------------------------------- +// Init / Dispose +// --------------------------------------------------------------------------- + void moduleRequireInit(void) { - moduleBaseDefineGlobalMethod("require", moduleRequireFunc); + memoryZero(g_cache, sizeof(g_cache)); + g_cacheCount = 0; + g_dirDepth = 0; + 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/thread/thread.c b/src/dusk/thread/thread.c index aed2e937..54fefbee 100644 --- a/src/dusk/thread/thread.c +++ b/src/dusk/thread/thread.c @@ -96,6 +96,8 @@ bool_t threadShouldStop(thread_t *thread) { } #ifdef DUSK_THREAD_PTHREAD + pthread_t THREAD_MAIN_THREAD_ID = 0; + void * threadHandler(thread_t *thread) { assertNotNull(thread, "Thread cannot be NULL."); diff --git a/src/dusk/thread/thread.h b/src/dusk/thread/thread.h index 61c4454a..97b29290 100644 --- a/src/dusk/thread/thread.h +++ b/src/dusk/thread/thread.h @@ -89,9 +89,11 @@ void threadStop(thread_t *thread); bool_t threadShouldStop(thread_t *thread); #ifdef DUSK_THREAD_PTHREAD + extern pthread_t THREAD_MAIN_THREAD_ID; + /** * Handles the thread's lifecycle for pthreads. * @param thread Pointer to the thread structure to handle. - */ + */ void * threadHandler(thread_t *thread); #endif \ No newline at end of file diff --git a/test/assert/test_assert.c b/test/assert/test_assert.c index cf6611fc..e65a1f5e 100644 --- a/test/assert/test_assert.c +++ b/test/assert/test_assert.c @@ -107,6 +107,27 @@ static void test_assertStrLenMin(void **state) { expect_assert_failure(assertStrLenMin("tiny", 10, "asserts")); } +static void test_assertIsMainThread(void **state) { + // The group setup recorded this thread as main — assertIsMainThread must pass. + assertIsMainThread("test thread is main, should not assert"); + + // assertNotMainThread must fail when called from the main thread. + expect_assert_failure(assertNotMainThread("asserts")); +} + +static void test_assertNotMainThread(void **state) { + // assertNotMainThread must fail because we are still on the main thread. + expect_assert_failure(assertNotMainThread("asserts")); + + // assertIsMainThread must pass. + assertIsMainThread("test thread is main, should not assert"); +} + +static int groupSetup(void **state) { + assertInit(); + return 0; +} + int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_assertTrueImpl), @@ -122,7 +143,9 @@ int main(void) { cmocka_unit_test(test_assertDeprecated), cmocka_unit_test(test_assertStrLenMax), cmocka_unit_test(test_assertStrLenMin), + cmocka_unit_test(test_assertIsMainThread), + cmocka_unit_test(test_assertNotMainThread), }; - return cmocka_run_group_tests(tests, NULL, NULL); + return cmocka_run_group_tests(tests, groupSetup, NULL); } \ No newline at end of file diff --git a/types/system/system.d.ts b/types/system/system.d.ts index 870e0ca1..d2d77649 100644 --- a/types/system/system.d.ts +++ b/types/system/system.d.ts @@ -12,7 +12,7 @@ interface SystemNamespace { * Compare against the `System.PLATFORM_*` constants. * * @example - * if (System.platform === System.PLATFORM_PSP) { useLowResAssets(); } + * if(System.platform === System.PLATFORM_PSP) { useLowResAssets(); } */ readonly platform: number;