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 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 09:06:34 -05:00
parent 7b4f0abfb8
commit 015d90519f
12 changed files with 1413 additions and 0 deletions
+96
View File
@@ -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})
+10
View File
@@ -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)
+12
View File
@@ -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)
@@ -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
);
}
@@ -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 <jerryscript.h>
/**
* 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
);
@@ -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 <jerryscript.h>
/** 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);
+396
View File
@@ -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 <stdlib.h>
#include <string.h>
/**
* 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);
}
+24
View File
@@ -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
}
+200
View File
@@ -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;
}
+132
View File
@@ -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 <jerryscript.h>
#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);
+215
View File
@@ -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;
}
+154
View File
@@ -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 <jerryscript.h>
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);