From 015d90519febd6a97393bfd03f568d04dbf512f3 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 30 Jul 2026 09:06:34 -0500 Subject: [PATCH] WIP: start JerryScript integration (core engine + Component wrapper) Ports the core scripting engine (scriptmanager, scriptproto, modulebase, moduleplatform) and a generic Component JS wrapper from the old we-ball branch, adapted to the current entitymanager_t/multi-scene architecture. Not yet buildable: moduleentity.c, modulescene, modulelist.h/.c, and the CMakeLists.txt for the new module subdirectories are still missing, and engine.c isn't wired up yet. Co-Authored-By: Claude Sonnet 5 --- cmake/modules/Findjerryscript.cmake | 96 +++++ src/dusk/CMakeLists.txt | 10 + src/dusk/script/CMakeLists.txt | 12 + .../module/entity/component/modulecomponent.c | 74 ++++ .../module/entity/component/modulecomponent.h | 58 +++ src/dusk/script/module/entity/moduleentity.h | 42 ++ src/dusk/script/module/modulebase.h | 396 ++++++++++++++++++ src/dusk/script/module/moduleplatform.h | 24 ++ src/dusk/script/scriptmanager.c | 200 +++++++++ src/dusk/script/scriptmanager.h | 132 ++++++ src/dusk/script/scriptproto.c | 215 ++++++++++ src/dusk/script/scriptproto.h | 154 +++++++ 12 files changed, 1413 insertions(+) create mode 100644 cmake/modules/Findjerryscript.cmake create mode 100644 src/dusk/script/CMakeLists.txt create mode 100644 src/dusk/script/module/entity/component/modulecomponent.c create mode 100644 src/dusk/script/module/entity/component/modulecomponent.h create mode 100644 src/dusk/script/module/entity/moduleentity.h create mode 100644 src/dusk/script/module/modulebase.h create mode 100644 src/dusk/script/module/moduleplatform.h create mode 100644 src/dusk/script/scriptmanager.c create mode 100644 src/dusk/script/scriptmanager.h create mode 100644 src/dusk/script/scriptproto.c create mode 100644 src/dusk/script/scriptproto.h diff --git a/cmake/modules/Findjerryscript.cmake b/cmake/modules/Findjerryscript.cmake new file mode 100644 index 00000000..c1f8161f --- /dev/null +++ b/cmake/modules/Findjerryscript.cmake @@ -0,0 +1,96 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Turn things off we don't need +set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE) +set(JERRY_EXT ON CACHE BOOL "" FORCE) +set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE) +set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE) +set(ENABLE_LTO OFF CACHE BOOL "" FORCE) + +# Fetch Jerry +include(FetchContent) +FetchContent_Declare( + jerryscript + GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript + GIT_TAG float32-fix +) +FetchContent_MakeAvailable(jerryscript) + +# Mark found +set(jerryscript_FOUND ON) + +# Define targets +if(TARGET jerryscript-core) + set(JERRY_CORE_TARGET jerryscript-core) +elseif(TARGET jerry-core) + set(JERRY_CORE_TARGET jerry-core) +endif() + +if(TARGET jerryscript-ext) + set(JERRY_EXT_TARGET jerryscript-ext) +elseif(TARGET jerry-ext) + set(JERRY_EXT_TARGET jerry-ext) +endif() + +if(TARGET jerryscript-port-default) + set(JERRY_PORT_TARGET jerryscript-port-default) +elseif(TARGET jerry-port-default) + set(JERRY_PORT_TARGET jerry-port-default) +elseif(TARGET jerryscript-port) + set(JERRY_PORT_TARGET jerryscript-port) +elseif(TARGET jerry-port) + set(JERRY_PORT_TARGET jerry-port) +endif() + +if(NOT JERRY_CORE_TARGET) + message(FATAL_ERROR "JerryScript core target not found") +endif() + +if(NOT JERRY_EXT_TARGET) + message(FATAL_ERROR "JerryScript ext target not found") +endif() + +if(NOT JERRY_PORT_TARGET) + message(FATAL_ERROR "JerryScript port target not found") +endif() + +foreach(tgt IN ITEMS + ${JERRY_CORE_TARGET} + ${JERRY_EXT_TARGET} + ${JERRY_PORT_TARGET} +) + if(TARGET ${tgt}) + set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF) + target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE + JERRY_NUMBER_TYPE_FLOAT64=0 + JERRY_BUILTIN_DATE=0 + ) + endif() +endforeach() + +# Export include dirs through the targets +target_include_directories(${JERRY_CORE_TARGET} INTERFACE + ${jerryscript_SOURCE_DIR}/jerry-core/include +) + +target_include_directories(${JERRY_EXT_TARGET} INTERFACE + ${jerryscript_SOURCE_DIR}/jerry-ext/include +) + +target_include_directories(${JERRY_PORT_TARGET} INTERFACE + ${jerryscript_SOURCE_DIR}/jerry-port/default/include +) + +# Suppress JerryScript-only warning +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(${JERRY_CORE_TARGET} PRIVATE + -Wno-error + ) +endif() + +add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET}) +add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET}) +add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET}) diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index ec8dc97c..df839ccf 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -32,6 +32,15 @@ if(NOT yyjson_FOUND) endif() endif() +if(NOT jerryscript_FOUND) + find_package(jerryscript REQUIRED) + target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + jerryscript::core + jerryscript::ext + jerryscript::port + ) +endif() + if(DUSK_BACKTRACE) target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic) target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC @@ -72,5 +81,6 @@ add_subdirectory(ui) add_subdirectory(network) add_subdirectory(physics) add_subdirectory(save) +add_subdirectory(script) add_subdirectory(util) add_subdirectory(thread) \ No newline at end of file diff --git a/src/dusk/script/CMakeLists.txt b/src/dusk/script/CMakeLists.txt new file mode 100644 index 00000000..bd368ab4 --- /dev/null +++ b/src/dusk/script/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + scriptmanager.c + scriptproto.c +) + +add_subdirectory(module) diff --git a/src/dusk/script/module/entity/component/modulecomponent.c b/src/dusk/script/module/entity/component/modulecomponent.c new file mode 100644 index 00000000..6d0ae99a --- /dev/null +++ b/src/dusk/script/module/entity/component/modulecomponent.c @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulecomponent.h" +#include "script/module/modulebase.h" +#include "util/string.h" + +scriptproto_t MODULE_COMPONENT_PROTO; + +moduleBaseFunction(moduleComponentGetType) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleComponentGet); + return jerry_number(h->type); +} + +moduleBaseFunction(moduleComponentGetEntityId) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleComponentGet); + return jerry_number(h->entityId); +} + +moduleBaseFunction(moduleComponentDisposeMethod) { + moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleComponentGet); + componentDispose(h->mgr, h->entityId, h->componentId); + return jerry_undefined(); +} + +moduleBaseFunction(moduleComponentToString) { + modulecomponenthandle_t *h = moduleComponentGet(callInfo); + if(!h) return jerry_string_sz("Component(?)"); + + char_t buf[64]; + stringFormat( + buf, sizeof(buf), "Component(type=%d, entityId=%d)", h->type, h->entityId + ); + return jerry_string_sz(buf); +} + +void moduleComponentInit(void) { + scriptProtoInit( + &MODULE_COMPONENT_PROTO, + "Component", + sizeof(modulecomponenthandle_t), + NULL + ); + + scriptProtoDefineProp( + &MODULE_COMPONENT_PROTO, "type", moduleComponentGetType, NULL + ); + scriptProtoDefineProp( + &MODULE_COMPONENT_PROTO, "entityId", moduleComponentGetEntityId, NULL + ); + scriptProtoDefineFunc( + &MODULE_COMPONENT_PROTO, "dispose", moduleComponentDisposeMethod + ); + scriptProtoDefineToString(&MODULE_COMPONENT_PROTO, moduleComponentToString); +} + +void moduleComponentDispose(void) { +} + +jerry_value_t moduleComponentCreate(const modulecomponenthandle_t *handle) { + return scriptProtoCreateValue(&MODULE_COMPONENT_PROTO, handle); +} + +modulecomponenthandle_t *moduleComponentGet( + const jerry_call_info_t *callInfo +) { + return (modulecomponenthandle_t *)scriptProtoGetValue( + &MODULE_COMPONENT_PROTO, callInfo->this_value + ); +} diff --git a/src/dusk/script/module/entity/component/modulecomponent.h b/src/dusk/script/module/entity/component/modulecomponent.h new file mode 100644 index 00000000..fa9c5471 --- /dev/null +++ b/src/dusk/script/module/entity/component/modulecomponent.h @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/scriptproto.h" +#include "entity/entitybase.h" +#include "entity/component.h" +#include + +/** + * Native data wrapped by a JS Component instance. Generic across every + * componenttype_t -- there are no per-type property wrappers yet (e.g. + * no `.position` on a POSITION component), just the type-agnostic + * operations every component supports. + */ +typedef struct { + entitymanager_t *mgr; + entityid_t entityId; + componentid_t componentId; + componenttype_t type; +} modulecomponenthandle_t; + +extern scriptproto_t MODULE_COMPONENT_PROTO; + +/** + * Registers the Component class. Has no constructor -- instances are + * only ever created via moduleComponentCreate(), e.g. from + * Entity.add()/getComponent(). + */ +void moduleComponentInit(void); + +/** + * Disposes the Component class's script resources. + */ +void moduleComponentDispose(void); + +/** + * Wraps a component handle as a new JS Component instance. + * + * @param handle The handle to copy into the new instance. + * @return The new JS object. + */ +jerry_value_t moduleComponentCreate(const modulecomponenthandle_t *handle); + +/** + * Internal. Gets the native handle wrapped by a Component instance's + * `this` value. + * + * @param callInfo The JS call info, whose this_value is the instance. + * @return The wrapped handle, or NULL if this_value isn't a Component. + */ +modulecomponenthandle_t *moduleComponentGet( + const jerry_call_info_t *callInfo +); diff --git a/src/dusk/script/module/entity/moduleentity.h b/src/dusk/script/module/entity/moduleentity.h new file mode 100644 index 00000000..70fed92c --- /dev/null +++ b/src/dusk/script/module/entity/moduleentity.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/scriptproto.h" +#include "entity/entitybase.h" +#include + +/** Native data wrapped by a JS Entity instance. */ +typedef struct { + entitymanager_t *mgr; + entityid_t id; +} moduleentityhandle_t; + +extern scriptproto_t MODULE_ENTITY_PROTO; + +/** + * Registers the Entity class (constructible via `new Entity()`, adding + * to the currently active scene's entity manager -- see + * sceneGetActive()) and every currently-registered component type name + * (see componentlist.h) as a global integer constant usable with + * `entity.add(TYPE)`/`entity.getComponent(TYPE)`. + */ +void moduleEntityInit(void); + +/** + * Disposes the Entity class's script resources. + */ +void moduleEntityDispose(void); + +/** + * Internal. Gets the native handle wrapped by an Entity instance's + * `this` value. + * + * @param callInfo The JS call info, whose this_value is the instance. + * @return The wrapped handle, or NULL if this_value isn't an Entity. + */ +moduleentityhandle_t *moduleEntityGet(const jerry_call_info_t *callInfo); diff --git a/src/dusk/script/module/modulebase.h b/src/dusk/script/module/modulebase.h new file mode 100644 index 00000000..1367f470 --- /dev/null +++ b/src/dusk/script/module/modulebase.h @@ -0,0 +1,396 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/scriptmanager.h" +#include "assert/assert.h" +#include "util/string.h" +#include "util/memory.h" +#include +#include + +/** + * Define a function for a module in JavaScript. + * + * @param name Name of the method (in C, not JS) + * @return A C function with that name, containing the standard JS sig. + */ +#define moduleBaseFunction(name) \ + static jerry_value_t name( \ + const jerry_call_info_t *callInfo, \ + const jerry_value_t args[], \ + const jerry_length_t argc) + +/** + * Gets the pointer to what is otherwise a prototype in the JerryScript + * code. This, for example, allows you to define a pointer in C and have + * it be a prototype for objects in JavaScript. + * + * @param object The JavaScript object to get the native pointer from. + * @param info The native info "prototype" struct used to get the + * pointer. + * @return The pointer to the proto (or NULL). + */ +static inline void *moduleBaseGetProto( + const jerry_value_t object, + const jerry_object_native_info_t *info +) { + assertNotNull(info, "Native info must not be null"); + if(!jerry_value_is_object(object)) return NULL; + + return (void *)jerry_object_get_native_ptr(object, info); +} + +/** + * Standard JerryScript free callback. + * + * @param ptr The pointer to free. + * @param info The native info struct associated with the pointer. + */ +static inline void moduleBaseFreeProto( + void *ptr, jerry_object_native_info_t *info +) { + assertNotNull(ptr, "Pointer must not be null"); + assertNotNull(info, "Native info must not be null"); + memoryFree(ptr); +} + +/** + * Evaluates a script in the global scope. + * + * @param script The script to evaluate. + */ +static inline void moduleBaseEval(const char_t *script) { + assertNotNull(script, "Script must not be null"); + + jerry_value_t result = jerry_eval( + (const jerry_char_t *)script, + strlen(script), + JERRY_PARSE_NO_OPTS + ); + jerry_value_free(result); +} + +/** + * Throw a type error from a module function. + * + * @param message The error message to throw. + * @return A JerryScript error value. + */ +static inline jerry_value_t moduleBaseThrow(const char_t *message) { + assertStrLenMin(message, 1, "Error message must not be empty"); + return jerry_throw_sz(JERRY_ERROR_TYPE, message); +} + +/** + * Converts a C errorret_t into a JS exception, forwarding the error + * message so that try/catch in JS sees the real error text. Clears the + * C error state. + * + * @param err The errorret_t returned by a failing C function. + * @return A JerryScript error value carrying the C error message. + */ +static inline jerry_value_t moduleBaseThrowError(const errorret_t err) { + assertNotNull(err.state, "Error state must not be NULL"); + assertNotNull(err.state->message, "Error message must not be NULL"); + jerry_value_t jsErr = jerry_throw_sz(JERRY_ERROR_TYPE, err.state->message); + errorCatch(err); + return jsErr; +} + +/** + * Assert an argument is a number; return type error if not. + */ +#define moduleBaseRequireNumber(i) do { \ + if(!jerry_value_is_number(args[(i)])) { \ + return moduleBaseThrow("Expected number argument"); \ + } \ +} while(0) + +/** + * Assert an argument is a string; return type error if not. + */ +#define moduleBaseRequireString(i) do { \ + if(!jerry_value_is_string(args[(i)])) { \ + return moduleBaseThrow("Expected string argument"); \ + } \ +} while(0) + +/** + * Assert an argument is a function; return type error if not. + */ +#define moduleBaseRequireFunction(i) do { \ + if(!jerry_value_is_function(args[(i)])) { \ + return moduleBaseThrow("Expected function argument"); \ + } \ +} while(0) + +/** + * Assert an argument is an object; return type error if not. + */ +#define moduleBaseRequireObject(i) do { \ + if(!jerry_value_is_object(args[(i)])) { \ + return moduleBaseThrow("Expected object argument"); \ + } \ +} while(0) + +/** + * Require at least N arguments; throw a TypeError if fewer were + * provided. + * + * Example: moduleBaseRequireArgs(2); + */ +#define moduleBaseRequireArgs(n) do { \ + if(argc < (jerry_length_t)(n)) { \ + return moduleBaseThrow("Expected at least " #n " argument(s)"); \ + } \ +} while(0) + +/** + * Declare a typed pointer from a getter and immediately return undefined + * if it is NULL. The named variable is available for the rest of the + * function. + * + * Example: + * moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + * return jerry_number(cam->nearClip); + */ +#define moduleBaseGetOrReturn(type, var, getter) \ + type *var = (getter)(callInfo); \ + if(!(var)) return jerry_undefined() + +/** + * Cast argument i to float_t. Call after validating the arg is a number. + */ +#define moduleBaseArgFloat(i) ((float_t)jerry_value_as_number(args[(i)])) + +/** + * Cast argument i to int32_t. Call after validating the arg is a number. + */ +#define moduleBaseArgInt(i) ((int32_t)jerry_value_as_number(args[(i)])) + +/** + * Read argument i as a boolean (true/false). + */ +#define moduleBaseArgBool(i) (jerry_value_is_true(args[(i)])) + +/** + * Read optional argument i as float_t. Returns def if the argument is + * missing or not a number. + */ +#define moduleBaseOptFloat(i, def) \ + ((jerry_length_t)(i) < argc && jerry_value_is_number(args[(i)]) \ + ? (float_t)jerry_value_as_number(args[(i)]) : (def)) + +/** + * Read optional argument i as int32_t. Returns def if the argument is + * missing or not a number. + */ +#define moduleBaseOptInt(i, def) \ + ((jerry_length_t)(i) < argc && jerry_value_is_number(args[(i)]) \ + ? (int32_t)jerry_value_as_number(args[(i)]) : (def)) + +/** + * Set a global numeric constant. + */ +static inline void moduleBaseSetNumber(const char_t *name, double value) { + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_value_t val = jerry_number(value); + jerry_object_set(global, key, val); + jerry_value_free(val); + jerry_value_free(key); + jerry_value_free(global); +} + +/** + * Set a global integer constant. + */ +static inline void moduleBaseSetInt(const char_t *name, int32_t value) { + moduleBaseSetNumber(name, (double)value); +} + +/** + * Set a global JS value. Caller retains ownership of the value and must + * free it independently. + */ +static inline void moduleBaseSetValue( + const char_t *name, jerry_value_t value +) { + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_object_set(global, key, value); + jerry_value_free(key); + jerry_value_free(global); +} + +/** + * Wrap an engine-owned C pointer as a JS object (no GC free callback). + * Used for global singletons. + */ +static inline jerry_value_t moduleBaseWrapPointer(void *ptr) { + jerry_value_t obj = jerry_object(); + jerry_object_set_native_ptr(obj, &JS_PTR_NATIVE_INFO, ptr); + return obj; +} + +/** + * Set a named global to a wrapped engine-owned C pointer. Combines + * moduleBaseWrapPointer and moduleBaseSetValue in one call. + */ +static inline void moduleBaseSetWrappedPointer( + const char_t *name, void *ptr +) { + jerry_value_t val = moduleBaseWrapPointer(ptr); + moduleBaseSetValue(name, val); + jerry_value_free(val); +} + +/** + * Unwrap a C pointer from a JS object created by moduleBaseWrapPointer. + * Returns NULL if the object does not carry a matching native pointer. + */ +static inline void *moduleBaseUnwrapPointer(jerry_value_t val) { + if(!jerry_value_is_object(val)) return NULL; + return jerry_object_get_native_ptr(val, &JS_PTR_NATIVE_INFO); +} + +/** + * Copy a JerryScript string value into a C buffer (null-terminated). + * + * @param val Jerry string value. + * @param buf Output buffer. + * @param buflen Buffer capacity including the null terminator. + */ +static inline void moduleBaseToString( + jerry_value_t val, + char_t *buf, + jerry_size_t buflen +) { + jerry_size_t len = jerry_string_to_buffer( + val, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, buflen - 1 + ); + buf[len] = '\0'; +} + +/** + * Define a named property on a JS object with getter and optional + * setter. + * + * @param obj Target object (e.g. a prototype). + * @param name Property name. + * @param getter C getter handler. + * @param setter C setter handler, or NULL for read-only property. + */ +static inline void moduleBaseDefineProperty( + jerry_value_t obj, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +) { + jerry_property_descriptor_t desc; + memset(&desc, 0, sizeof(desc)); + desc.flags = (uint16_t)( + JERRY_PROP_IS_GET_DEFINED | + JERRY_PROP_IS_ENUMERABLE_DEFINED | JERRY_PROP_IS_ENUMERABLE | + JERRY_PROP_IS_CONFIGURABLE_DEFINED | JERRY_PROP_IS_CONFIGURABLE + ); + desc.getter = jerry_function_external(getter); + if(setter != NULL) { + desc.flags |= JERRY_PROP_IS_SET_DEFINED; + desc.setter = jerry_function_external(setter); + } + jerry_value_t key = jerry_string_sz(name); + jerry_value_t result = jerry_object_define_own_prop(obj, key, &desc); + jerry_value_free(result); + jerry_value_free(key); + jerry_value_free(desc.getter); + if(setter != NULL) jerry_value_free(desc.setter); +} + +/** + * Set a named method (C function) on a JS object. + * + * @param obj Target object (e.g. a prototype). + * @param name Method name. + * @param fn C handler function. + */ +static inline void moduleBaseDefineMethod( + jerry_value_t obj, + const char_t *name, + jerry_external_handler_t fn +) { + jerry_value_t key = jerry_string_sz(name); + jerry_value_t func = jerry_function_external(fn); + jerry_object_set(obj, key, func); + jerry_value_free(func); + jerry_value_free(key); +} + +/** + * Define a named global function. + * + * @param name The name of the function as seen in JavaScript. + * @param fn The C handler function for the method. + */ +static inline void moduleBaseDefineGlobalMethod( + const char_t *name, + jerry_external_handler_t fn +) { + jerry_value_t global = jerry_current_realm(); + moduleBaseDefineMethod(global, name, fn); + jerry_value_free(global); +} + +/** + * Format an error message from a JerryScript exception value. Caller + * must ensure buf is large enough. + */ +static inline void moduleBaseExceptionMessage( + jerry_value_t exception, + char_t *buf, + size_t buflen +) { + jerry_value_t errVal = jerry_exception_value(exception, false); + jerry_value_t errStr = jerry_value_to_string(errVal); + jerry_size_t len = jerry_string_to_buffer( + errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, + (jerry_size_t)(buflen - 1) + ); + buf[len] = '\0'; + jerry_value_free(errStr); + jerry_value_free(errVal); +} + +/** + * Get a named property from a JS object. Caller must free the returned + * value. + */ +static inline jerry_value_t moduleBaseGetProp( + jerry_value_t obj, const char_t *name +) { + jerry_value_t key = jerry_string_sz(name); + jerry_value_t val = jerry_object_get(obj, key); + jerry_value_free(key); + return val; +} + +/** + * Cast a JS value to float_t. Use for non-args[] values (e.g. object + * properties). For args[], prefer moduleBaseArgFloat. + */ +static inline float_t moduleBaseValueFloat(jerry_value_t val) { + return (float_t)jerry_value_as_number(val); +} + +/** + * Cast a JS value to int32_t. Use for non-args[] values (e.g. object + * properties). For args[], prefer moduleBaseArgInt. + */ +static inline int32_t moduleBaseValueInt(jerry_value_t val) { + return (int32_t)jerry_value_as_number(val); +} diff --git a/src/dusk/script/module/moduleplatform.h b/src/dusk/script/module/moduleplatform.h new file mode 100644 index 00000000..a3dc799d --- /dev/null +++ b/src/dusk/script/module/moduleplatform.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/module/moduleplatformplatform.h" + +#ifndef DUSK_TARGET_SYSTEM + #error "DUSK_TARGET_SYSTEM must be defined" +#endif + +#define MODULE_PLATFORM_VALUE "var PLATFORM = '" DUSK_TARGET_SYSTEM "';\n" + +static inline void modulePlatform(void) { + moduleBaseEval(MODULE_PLATFORM_VALUE); + + #ifdef modulePlatformPlatform + modulePlatformPlatform(); + #endif +} diff --git a/src/dusk/script/scriptmanager.c b/src/dusk/script/scriptmanager.c new file mode 100644 index 00000000..7ed3f2f9 --- /dev/null +++ b/src/dusk/script/scriptmanager.c @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scriptmanager.h" +#include "assert/assert.h" +#include "asset/asset.h" +#include "asset/assetfile.h" +#include "util/memory.h" +#include "util/string.h" +#include "scriptproto.h" +#include "script/module/modulelist.h" + +scriptmanager_t SCRIPT_MANAGER; + +const jerry_object_native_info_t JS_PTR_NATIVE_INFO = { + .free_cb = NULL, + .number_of_references = 0, + .offset_of_references = 0 +}; + +errorret_t scriptManagerInit(void) { + memoryZero(&SCRIPT_MANAGER, sizeof(scriptmanager_t)); + + jerry_init(JERRY_INIT_EMPTY); + + moduleListInit(); + + errorOk(); +} + +errorret_t scriptManagerExec(const char_t *script, jerry_value_t *resultOut) { + assertNotNull(script, "Script cannot be NULL"); + + jerry_value_t result = jerry_eval( + (const jerry_char_t *)script, + strlen(script), + JERRY_PARSE_NO_OPTS + ); + + if(jerry_value_is_exception(result)) { + jerry_value_t errVal = jerry_exception_value(result, false); + jerry_value_t errStr = jerry_value_to_string(errVal); + 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); + jerry_value_free(result); + errorThrow("Failed to execute script: %s", buf); + } + + if(resultOut != NULL) { + *resultOut = result; + } else { + jerry_value_free(result); + } + errorOk(); +} + +errorret_t scriptManagerExecFile( + const char_t *fname, + jerry_value_t *resultOut +) { + assertNotNull(fname, "Filename cannot be NULL"); + + assetfile_t file; + errorChain(assetFileInit(&file, fname, NULL, NULL)); + + uint8_t *buffer = NULL; + size_t size = 0; + errorChain(assetFileReadEntire(&file, &buffer, &size)); + errorChain(assetFileDispose(&file)); + + char_t *src = (char_t *)memoryAllocate(size + 1); + memoryCopy(src, buffer, size); + src[size] = '\0'; + memoryFree(buffer); + + errorret_t ret = scriptManagerExec(src, resultOut); + memoryFree(src); + errorChain(ret); + errorOk(); +} + +errorret_t scriptManagerCallGlobal(const char_t *name) { + assertNotNull(name, "Function name cannot be NULL"); + + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = scriptManagerGetGlobalKey(name); + jerry_value_t fn = jerry_object_get(global, key); + jerry_value_free(global); + + if(!jerry_value_is_function(fn)) { + jerry_value_free(fn); + errorOk(); + } + + jerry_value_t result = jerry_call(fn, jerry_undefined(), NULL, 0); + jerry_value_free(fn); + + if(jerry_value_is_exception(result)) { + errorret_t err = scriptManagerFormatException( + "Global function", name, result + ); + jerry_value_free(result); + errorChain(err); + } + + // If this was an `async function`, its work (including anything it + // awaited) isn't actually done yet - it just returned a pending + // promise. Drive both the JerryScript job queue and the asset system + // forward until it settles, so the caller can rely on the function + // being fully complete. + if(jerry_value_is_promise(result)) { + while(jerry_promise_state(result) == JERRY_PROMISE_STATE_PENDING) { + errorret_t updateErr = assetUpdate(); + if(errorIsNotOk(updateErr)) { + jerry_value_free(result); + errorChain(updateErr); + } + jerry_value_t jobsResult = jerry_run_jobs(); + jerry_value_free(jobsResult); + } + + if(jerry_promise_state(result) == JERRY_PROMISE_STATE_REJECTED) { + jerry_value_t rejectVal = jerry_promise_result(result); + errorret_t err = scriptManagerFormatValueError( + "Global async function", name, rejectVal + ); + jerry_value_free(rejectVal); + jerry_value_free(result); + errorChain(err); + } + } + + jerry_value_free(result); + errorOk(); +} + +errorret_t scriptManagerDispose(void) { + scriptProtoDisposeAll(); + + for(uint8_t i = 0; i < SCRIPT_MANAGER.globalKeyCacheCount; i++) { + jerry_value_free(SCRIPT_MANAGER.globalKeyCache[i].key); + } + SCRIPT_MANAGER.globalKeyCacheCount = 0; + + jerry_cleanup(); + errorOk(); +} + +errorret_t scriptManagerFormatValueError( + const char_t *context, + const char_t *name, + jerry_value_t value +) { + jerry_value_t errStr = jerry_value_to_string(value); + 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); + errorThrow("%s '%s': %s", context, name, buf); +} + +errorret_t scriptManagerFormatException( + const char_t *context, + const char_t *name, + jerry_value_t exception +) { + jerry_value_t errVal = jerry_exception_value(exception, false); + errorret_t err = scriptManagerFormatValueError(context, name, errVal); + jerry_value_free(errVal); + return err; +} + +jerry_value_t scriptManagerGetGlobalKey(const char_t *name) { + for(uint8_t i = 0; i < SCRIPT_MANAGER.globalKeyCacheCount; i++) { + if(stringCompare(name, SCRIPT_MANAGER.globalKeyCache[i].name) == 0) { + return SCRIPT_MANAGER.globalKeyCache[i].key; + } + } + + jerry_value_t key = jerry_string_sz(name); + assertTrue( + SCRIPT_MANAGER.globalKeyCacheCount < SCRIPT_MANAGER_MAX_CACHED_GLOBAL_KEYS, + "Global function key cache is full." + ); + SCRIPT_MANAGER.globalKeyCache[SCRIPT_MANAGER.globalKeyCacheCount].name = name; + SCRIPT_MANAGER.globalKeyCache[SCRIPT_MANAGER.globalKeyCacheCount].key = key; + SCRIPT_MANAGER.globalKeyCacheCount++; + return key; +} diff --git a/src/dusk/script/scriptmanager.h b/src/dusk/script/scriptmanager.h new file mode 100644 index 00000000..c8ae7097 --- /dev/null +++ b/src/dusk/script/scriptmanager.h @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include + +#define SCRIPT_MANAGER_MAX_EVENT_SUBSCRIPTIONS 64 + +/** Max distinct global function names cached by scriptManagerCallGlobal. */ +#define SCRIPT_MANAGER_MAX_CACHED_GLOBAL_KEYS 8 + +typedef struct { + struct { + const char_t *name; + jerry_value_t key; + } globalKeyCache[SCRIPT_MANAGER_MAX_CACHED_GLOBAL_KEYS]; + uint8_t globalKeyCacheCount; +} scriptmanager_t; + +extern scriptmanager_t SCRIPT_MANAGER; + +/** + * Singleton native-info tag for engine-owned C pointers wrapped in JS + * objects. A single global instance ensures jerry_object_get_native_ptr() + * matches across all compilation units (including module headers). + */ +extern const jerry_object_native_info_t JS_PTR_NATIVE_INFO; + +/** + * Initialize the script manager (and the underlying JerryScript context). + * + * @return The error return value. + */ +errorret_t scriptManagerInit(void); + +/** + * Execute a JS string in the active script context. + * + * @param script The JS source to execute. + * @param result Optional out-parameter for the script's return value. + * Caller must call jerry_value_free() on it when done. Pass NULL to + * discard the return value. + * @return The error return value. + */ +errorret_t scriptManagerExec(const char_t *script, jerry_value_t *result); + +/** + * Execute a JS file in the active script context. + * + * @param fname The filename of the script to execute. + * @param result Optional out-parameter for the script's return value. + * Caller must call jerry_value_free() on it when done. Pass NULL to + * discard the return value. + * @return The error return value. + */ +errorret_t scriptManagerExecFile( + const char_t *fname, + jerry_value_t *result +); + +/** + * Calls a global JS function by name, if one is defined. Silently does + * nothing if the global isn't a function (e.g. the script never defined + * it). + * + * If the function returns a Promise (e.g. it's an `async function`), the + * JerryScript job queue is pumped (alongside assetUpdate(), so pending + * asset loads can actually progress) until it settles, and a rejection + * is surfaced as an error - callers can rely on the function's work, + * including anything it awaited, being complete by the time this + * returns. + * + * @param name The name of the global function to call. + * @return The error return value. An error is thrown if the JS function + * itself throws, or if its returned promise rejects. + */ +errorret_t scriptManagerCallGlobal(const char_t *name); + +/** + * Dispose of the script manager. + * + * @return The error return value. + */ +errorret_t scriptManagerDispose(void); + +/** + * Internal. Formats a thrown JS value's string representation into an + * errorret_t (e.g. a promise rejection's value, which may not itself be + * an Error/exception value). + * + * @param context Short label for what was being called, e.g. "Global + * function". + * @param name The name of the function/global that produced value. + * @param value The JS value to format (not necessarily an exception). + * @return The error return value. + */ +errorret_t scriptManagerFormatValueError( + const char_t *context, + const char_t *name, + jerry_value_t value +); + +/** + * Internal. Formats a JerryScript exception value into an errorret_t. + * + * @param context Short label for what was being called, e.g. "Global + * function". + * @param name The name of the function/global that threw. + * @param exception The exception value (see jerry_value_is_exception()). + * @return The error return value. + */ +errorret_t scriptManagerFormatException( + const char_t *context, + const char_t *name, + jerry_value_t exception +); + +/** + * Internal. Returns a cached jerry_value_t string key for a global + * function name, creating and caching it on first use. Avoids + * re-allocating a JS string every call for names looked up every + * frame/tick (e.g. "update"). + * + * @param name The global function name to get a cached key for. + * @return The cached (or newly created) JS string key. + */ +jerry_value_t scriptManagerGetGlobalKey(const char_t *name); diff --git a/src/dusk/script/scriptproto.c b/src/dusk/script/scriptproto.c new file mode 100644 index 00000000..a386a8e9 --- /dev/null +++ b/src/dusk/script/scriptproto.c @@ -0,0 +1,215 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scriptproto.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "script/module/modulebase.h" + +#define SCRIPT_PROTO_REGISTRY_MAX 64 + +static scriptproto_t *SCRIPT_PROTO_REGISTRY[SCRIPT_PROTO_REGISTRY_MAX]; +static size_t SCRIPT_PROTO_REGISTRY_COUNT = 0; + +void scriptProtoInit( + scriptproto_t *proto, + const char_t *name, + const size_t size, + jerry_external_handler_t constructor +) { + assertNotNull(proto, "Script prototype struct must not be null"); + memoryZero(proto, sizeof(scriptproto_t)); + + assertTrue( + SCRIPT_PROTO_REGISTRY_COUNT < SCRIPT_PROTO_REGISTRY_MAX, + "Script prototype registry capacity exceeded" + ); + SCRIPT_PROTO_REGISTRY[SCRIPT_PROTO_REGISTRY_COUNT++] = proto; + + proto->info = (jerry_object_native_info_t){ + .free_cb = moduleBaseFreeProto, + .number_of_references = 0, + .offset_of_references = 0 + }; + proto->prototype = jerry_object(); + proto->size = size; + + if(constructor != NULL) { + proto->constructor = jerry_function_external(constructor); + jerry_value_t protoKey = jerry_string_sz("prototype"); + jerry_object_set(proto->constructor, protoKey, proto->prototype); + jerry_value_free(protoKey); + jerry_value_t ctorKey = jerry_string_sz("constructor"); + jerry_object_set(proto->prototype, ctorKey, proto->constructor); + jerry_value_free(ctorKey); + } + + if(name != NULL) { + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_value_t val; + if(proto->constructor) { + val = proto->constructor; + } else { + val = proto->prototype; + } + jerry_object_set(global, key, val); + jerry_value_free(key); + jerry_value_free(global); + } +} + +void scriptProtoDefineProp( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertStrLenMin(name, 1, "Property name must not be empty"); + assertNotNull(getter, "Getter must not be null"); + + jerry_property_descriptor_t desc; + memoryZero(&desc, sizeof(desc)); + desc.flags = (uint16_t)( + JERRY_PROP_IS_GET_DEFINED | + JERRY_PROP_IS_ENUMERABLE_DEFINED | JERRY_PROP_IS_ENUMERABLE | + JERRY_PROP_IS_CONFIGURABLE_DEFINED | JERRY_PROP_IS_CONFIGURABLE + ); + + desc.getter = jerry_function_external(getter); + + if(setter != NULL) { + desc.flags |= JERRY_PROP_IS_SET_DEFINED; + desc.setter = jerry_function_external(setter); + } + + jerry_value_t key = jerry_string_sz(name); + jerry_value_t result = jerry_object_define_own_prop( + proto->prototype, key, &desc + ); + jerry_value_free(result); + jerry_value_free(key); + jerry_value_free(desc.getter); + if(setter != NULL) jerry_value_free(desc.setter); +} + +void scriptProtoDefineFunc( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t fn +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertStrLenMin(name, 1, "Method name must not be empty"); + assertNotNull(fn, "Function handler must not be null"); + + jerry_value_t key = jerry_string_sz(name); + jerry_value_t func = jerry_function_external(fn); + jerry_object_set(proto->prototype, key, func); + jerry_value_free(func); + jerry_value_free(key); +} + +void scriptProtoDefineStaticProp( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertStrLenMin(name, 1, "Property name must not be empty"); + assertNotNull(getter, "Getter must not be null"); + + jerry_value_t target = ( + proto->constructor ? proto->constructor : proto->prototype + ); + + jerry_property_descriptor_t desc; + memoryZero(&desc, sizeof(desc)); + desc.flags = (uint16_t)( + JERRY_PROP_IS_GET_DEFINED | + JERRY_PROP_IS_ENUMERABLE_DEFINED | JERRY_PROP_IS_ENUMERABLE | + JERRY_PROP_IS_CONFIGURABLE_DEFINED | JERRY_PROP_IS_CONFIGURABLE + ); + + desc.getter = jerry_function_external(getter); + + if(setter != NULL) { + desc.flags |= JERRY_PROP_IS_SET_DEFINED; + desc.setter = jerry_function_external(setter); + } + + jerry_value_t key = jerry_string_sz(name); + jerry_value_t result = jerry_object_define_own_prop(target, key, &desc); + jerry_value_free(result); + jerry_value_free(key); + jerry_value_free(desc.getter); + if(setter != NULL) jerry_value_free(desc.setter); +} + +void scriptProtoDefineStaticFunc( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t fn +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertStrLenMin(name, 1, "Method name must not be empty"); + assertNotNull(fn, "Function handler must not be null"); + + jerry_value_t target = ( + proto->constructor ? proto->constructor : proto->prototype + ); + jerry_value_t key = jerry_string_sz(name); + jerry_value_t func = jerry_function_external(fn); + jerry_object_set(target, key, func); + jerry_value_free(func); + jerry_value_free(key); +} + +jerry_value_t scriptProtoCreateValue( + const scriptproto_t *proto, + const void *value +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertNotNull(value, "Value pointer must not be null"); + + void *ptr = memoryAllocate(proto->size); + memoryCopy(ptr, value, proto->size); + jerry_value_t obj = jerry_object(); + jerry_object_set_native_ptr(obj, &proto->info, ptr); + jerry_object_set_proto(obj, proto->prototype); + return obj; +} + +void *scriptProtoGetValue( + const scriptproto_t *proto, + const jerry_value_t obj +) { + assertNotNull(proto, "Script prototype struct must not be null"); + if(!jerry_value_is_object(obj)) return NULL; + return jerry_object_get_native_ptr(obj, &proto->info); +} + +void scriptProtoDefineToString( + scriptproto_t *proto, + jerry_external_handler_t fn +) { + scriptProtoDefineFunc(proto, "toString", fn); +} + +void scriptProtoDispose(scriptproto_t *proto) { + assertNotNull(proto, "Script prototype struct must not be null"); + jerry_value_free(proto->prototype); + if(proto->constructor) jerry_value_free(proto->constructor); +} + +void scriptProtoDisposeAll(void) { + for(size_t i = 0; i < SCRIPT_PROTO_REGISTRY_COUNT; i++) { + scriptProtoDispose(SCRIPT_PROTO_REGISTRY[i]); + } + SCRIPT_PROTO_REGISTRY_COUNT = 0; +} diff --git a/src/dusk/script/scriptproto.h b/src/dusk/script/scriptproto.h new file mode 100644 index 00000000..ba8139a8 --- /dev/null +++ b/src/dusk/script/scriptproto.h @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include + +typedef struct { + jerry_object_native_info_t info; + jerry_value_t prototype; + jerry_value_t constructor; + size_t size; +} scriptproto_t; + +/** + * Initialize a JS class prototype. + * + * If name is non-NULL the class is registered as a global. When ctor is + * also non-NULL the global is the constructor function (enabling + * `new Name(...)`); otherwise the prototype object itself becomes the + * global. + * + * @param proto The struct to initialize. + * @param name JS global name, or NULL to skip global registration. + * @param size sizeof the C struct this class wraps. + * @param ctor Constructor handler, or NULL if the class has no + * constructor. + */ +void scriptProtoInit( + scriptproto_t *proto, + const char_t *name, + const size_t size, + jerry_external_handler_t ctor +); + +/** + * Define an instance property with a getter and optional setter. + * + * @param proto The class prototype. + * @param name Property name. + * @param getter Getter handler (must not be NULL). + * @param setter Setter handler, or NULL for a read-only property. + */ +void scriptProtoDefineProp( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +); + +/** + * Define an instance method on the class prototype. + * + * @param proto The class prototype. + * @param name Method name. + * @param fn C handler called when the method is invoked. + */ +void scriptProtoDefineFunc( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t fn +); + +/** + * Define a static property on the class (e.g. Scene.current). + * + * Attaches to the constructor function when one exists, otherwise + * attaches directly to the prototype object (which is the global in + * that case). + * + * @param proto The class prototype. + * @param name Property name. + * @param getter Getter handler (must not be NULL). + * @param setter Setter handler, or NULL for a read-only property. + */ +void scriptProtoDefineStaticProp( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +); + +/** + * Define a static method on the class (e.g. Color.fromRGBA). + * + * Attaches to the constructor function when one exists, otherwise + * attaches directly to the prototype object (which is the global in + * that case). + * + * @param proto The class prototype. + * @param name Method name. + * @param fn C handler called when the static method is invoked. + */ +void scriptProtoDefineStaticFunc( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t fn +); + +/** + * Create a JS instance wrapping a copy of a C value. + * + * @param proto The class prototype. + * @param value Pointer to the C value to copy into the new JS object. + * @return A new JS object with the class prototype and native pointer + * set. + */ +jerry_value_t scriptProtoCreateValue( + const scriptproto_t *proto, + const void *value +); + +/** + * Unwrap the native C pointer from a JS object. + * + * @param proto The class prototype. + * @param obj The JS object to inspect. + * @return Pointer to the wrapped C value, or NULL if not an instance. + */ +void *scriptProtoGetValue( + const scriptproto_t *proto, + const jerry_value_t obj +); + +/** + * Define the toString() method on the class prototype. + * + * @param proto The class prototype. + * @param fn C handler called when toString() is invoked on an instance. + */ +void scriptProtoDefineToString( + scriptproto_t *proto, + jerry_external_handler_t fn +); + +/** + * Release all JerryScript resources held by the prototype. + * + * @param proto The class prototype to dispose. + */ +void scriptProtoDispose(scriptproto_t *proto); + +/** + * Disposes every prototype ever initialized via scriptProtoInit. Must be + * called before jerry_cleanup() - this JerryScript build fatally asserts + * during cleanup if any jerry_value_t handle is still held by the + * embedder, and every scriptproto_t retains its prototype/constructor + * handles for the lifetime of the module system. + */ +void scriptProtoDisposeAll(void);