From 5f34cb34b25c62b5a337de063d2882ffb8776b02 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 3 Aug 2026 19:36:08 -0500 Subject: [PATCH] Add a thin declarative API for defining script methods/properties scriptvalue.h/.c holds the scriptvalue_t tagged-union type and its JS<->C decode/encode helpers; scriptdef.h/.c holds the declarative scriptfuncdef_t/scriptpropdef_t definitions, the binding registry, and the JerryScript trampolines. Lets a module declare its JS-facing methods/properties/globals as typed data instead of hand-writing argument-checking boilerplate per method. Framework only for now -- no existing module has been migrated onto it yet. --- src/dusk/script/CMakeLists.txt | 2 + src/dusk/script/scriptdef.c | 224 ++++++++++++++++ src/dusk/script/scriptdef.h | 167 ++++++++++++ src/dusk/script/scriptmanager.c | 2 + src/dusk/script/scriptvalue.c | 279 ++++++++++++++++++++ src/dusk/script/scriptvalue.h | 146 +++++++++++ test/script/CMakeLists.txt | 2 + test/script/test_scriptdef.c | 447 ++++++++++++++++++++++++++++++++ 8 files changed, 1269 insertions(+) create mode 100644 src/dusk/script/scriptdef.c create mode 100644 src/dusk/script/scriptdef.h create mode 100644 src/dusk/script/scriptvalue.c create mode 100644 src/dusk/script/scriptvalue.h create mode 100644 test/script/test_scriptdef.c diff --git a/src/dusk/script/CMakeLists.txt b/src/dusk/script/CMakeLists.txt index bd368ab4..45cc67d5 100644 --- a/src/dusk/script/CMakeLists.txt +++ b/src/dusk/script/CMakeLists.txt @@ -7,6 +7,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC scriptmanager.c scriptproto.c + scriptvalue.c + scriptdef.c ) add_subdirectory(module) diff --git a/src/dusk/script/scriptdef.c b/src/dusk/script/scriptdef.c new file mode 100644 index 00000000..0497788b --- /dev/null +++ b/src/dusk/script/scriptdef.c @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scriptdef.h" +#include "script/module/modulebase.h" +#include "assert/assert.h" +#include "util/memory.h" + +#define SCRIPT_DEF_BINDING_MAX 256 + +typedef struct { + const scriptproto_t *proto; + const scriptfuncdef_t *funcDef; + const scriptpropdef_t *propDef; +} scriptdefbinding_t; + +static scriptdefbinding_t SCRIPT_DEF_BINDINGS[SCRIPT_DEF_BINDING_MAX]; +static size_t SCRIPT_DEF_BINDING_COUNT = 0; + +static const jerry_object_native_info_t SCRIPT_DEF_NATIVE_INFO = { + .free_cb = NULL, + .number_of_references = 0, + .offset_of_references = 0 +}; + +void scriptProtoDefineFuncDefs( + scriptproto_t *proto, + const scriptfuncdef_t *defs +) { + assertNotNull(proto, "Script prototype struct must not be null"); + scriptDefDefineFuncsOnTarget(proto->prototype, proto, defs); +} + +void scriptProtoDefinePropDefs( + scriptproto_t *proto, + const scriptpropdef_t *defs +) { + assertNotNull(proto, "Script prototype struct must not be null"); + scriptDefDefinePropsOnTarget(proto->prototype, proto, defs); +} + +void scriptDefDefineGlobalFuncs( + const jerry_value_t target, + const scriptfuncdef_t *defs +) { + scriptDefDefineFuncsOnTarget(target, NULL, defs); +} + +void scriptDefDefineGlobalProps( + const jerry_value_t target, + const scriptpropdef_t *defs +) { + scriptDefDefinePropsOnTarget(target, NULL, defs); +} + +void scriptDefDisposeAll(void) { + SCRIPT_DEF_BINDING_COUNT = 0; +} + +void *scriptDefResolveHandle( + const scriptproto_t *proto, + const jerry_value_t thisValue +) { + if(!proto) return NULL; + return scriptProtoGetValue(proto, thisValue); +} + +moduleBaseFunction(scriptDefFuncTrampoline) { + const scriptdefbinding_t *binding = + (const scriptdefbinding_t *)jerry_object_get_native_ptr( + callInfo->function, &SCRIPT_DEF_NATIVE_INFO + ); + assertNotNull(binding, "Missing script function binding"); + const scriptfuncdef_t *def = binding->funcDef; + + void *handle = scriptDefResolveHandle(binding->proto, callInfo->this_value); + if(binding->proto && !handle) return jerry_undefined(); + + scriptvalue_t params[SCRIPT_DEF_PARAMS_MAX]; + uint32_t consumed = 0; + for(uint32_t i = 0; i < def->paramCount; i++) { + jerry_value_t thrown; + if(!scriptDefReadArg( + def->paramTypes[i], args, argc, &consumed, ¶ms[i], &thrown + )) { + return thrown; + } + } + + scriptvalue_t ret = def->fn(handle, params, def->paramCount); + return scriptDefValueToJerry(&ret); +} + +moduleBaseFunction(scriptDefPropGetTrampoline) { + const scriptdefbinding_t *binding = + (const scriptdefbinding_t *)jerry_object_get_native_ptr( + callInfo->function, &SCRIPT_DEF_NATIVE_INFO + ); + assertNotNull(binding, "Missing script property binding"); + + void *handle = scriptDefResolveHandle(binding->proto, callInfo->this_value); + if(binding->proto && !handle) return jerry_undefined(); + + scriptvalue_t ret = binding->propDef->getter(handle); + return scriptDefValueToJerry(&ret); +} + +moduleBaseFunction(scriptDefPropSetTrampoline) { + const scriptdefbinding_t *binding = + (const scriptdefbinding_t *)jerry_object_get_native_ptr( + callInfo->function, &SCRIPT_DEF_NATIVE_INFO + ); + assertNotNull(binding, "Missing script property binding"); + + void *handle = scriptDefResolveHandle(binding->proto, callInfo->this_value); + if(binding->proto && !handle) return jerry_undefined(); + + if(argc < 1) return moduleBaseThrow("Expected a value to set"); + + scriptvalue_t value; + jerry_value_t thrown; + if(!scriptDefReadValue(binding->propDef->type, args[0], &value, &thrown)) { + return thrown; + } + + binding->propDef->setter(handle, &value); + return jerry_undefined(); +} + +void scriptDefDefineFuncsOnTarget( + const jerry_value_t target, + const scriptproto_t *proto, + const scriptfuncdef_t *defs +) { + assertNotNull(defs, "Function definitions must not be null"); + + for(size_t i = 0; defs[i].name != NULL; i++) { + assertTrue( + defs[i].paramCount <= SCRIPT_DEF_PARAMS_MAX, + "Too many params declared for script function" + ); + assertTrue( + SCRIPT_DEF_BINDING_COUNT < SCRIPT_DEF_BINDING_MAX, + "Script def binding capacity exceeded" + ); + + scriptdefbinding_t *binding = + &SCRIPT_DEF_BINDINGS[SCRIPT_DEF_BINDING_COUNT++]; + binding->proto = proto; + binding->funcDef = &defs[i]; + binding->propDef = NULL; + + jerry_value_t fn = jerry_function_external(scriptDefFuncTrampoline); + jerry_object_set_native_ptr(fn, &SCRIPT_DEF_NATIVE_INFO, binding); + + jerry_value_t key = jerry_string_sz(defs[i].name); + jerry_object_set(target, key, fn); + jerry_value_free(key); + jerry_value_free(fn); + } +} + +void scriptDefDefinePropsOnTarget( + const jerry_value_t target, + const scriptproto_t *proto, + const scriptpropdef_t *defs +) { + assertNotNull(defs, "Property definitions must not be null"); + + for(size_t i = 0; defs[i].name != NULL; i++) { + assertTrue( + SCRIPT_DEF_BINDING_COUNT < SCRIPT_DEF_BINDING_MAX, + "Script def binding capacity exceeded" + ); + + scriptdefbinding_t *getBinding = + &SCRIPT_DEF_BINDINGS[SCRIPT_DEF_BINDING_COUNT++]; + getBinding->proto = proto; + getBinding->funcDef = NULL; + getBinding->propDef = &defs[i]; + + 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(scriptDefPropGetTrampoline); + jerry_object_set_native_ptr( + desc.getter, &SCRIPT_DEF_NATIVE_INFO, getBinding + ); + + if(defs[i].setter != NULL) { + assertTrue( + SCRIPT_DEF_BINDING_COUNT < SCRIPT_DEF_BINDING_MAX, + "Script def binding capacity exceeded" + ); + + scriptdefbinding_t *setBinding = + &SCRIPT_DEF_BINDINGS[SCRIPT_DEF_BINDING_COUNT++]; + setBinding->proto = proto; + setBinding->funcDef = NULL; + setBinding->propDef = &defs[i]; + + desc.flags |= JERRY_PROP_IS_SET_DEFINED; + desc.setter = jerry_function_external(scriptDefPropSetTrampoline); + jerry_object_set_native_ptr( + desc.setter, &SCRIPT_DEF_NATIVE_INFO, setBinding + ); + } + + jerry_value_t key = jerry_string_sz(defs[i].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(defs[i].setter != NULL) jerry_value_free(desc.setter); + } +} diff --git a/src/dusk/script/scriptdef.h b/src/dusk/script/scriptdef.h new file mode 100644 index 00000000..c19e0e6c --- /dev/null +++ b/src/dusk/script/scriptdef.h @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "scriptproto.h" +#include "scriptvalue.h" + +/** Max params a single scriptfuncdef_t may declare. */ +#define SCRIPT_DEF_PARAMS_MAX 8 + +/** + * Native handler for a scriptfuncdef_t. handle is the native pointer the + * owning scriptproto_t wraps (or NULL for a proto-less/global + * function), already resolved by the trampoline. args holds argc + * already-decoded, already-type-checked values (one per paramTypes + * entry -- a SCRIPT_TYPE_VEC3 param still counts as a single args[] + * entry even though it consumed 3 raw JS arguments). + */ +typedef scriptvalue_t (*scriptfuncdeffn_t)( + void *handle, + const scriptvalue_t *args, + const uint32_t argc +); + +/** + * Declarative description of one JS-callable method. Terminate an array + * of these with a zeroed entry ({ .name = NULL }). + */ +typedef struct { + const char_t *name; + scriptvaluetype_t returnType; + scriptvaluetype_t paramTypes[SCRIPT_DEF_PARAMS_MAX]; + uint32_t paramCount; + scriptfuncdeffn_t fn; +} scriptfuncdef_t; + +/** + * Native getter for a scriptpropdef_t. handle is as in + * scriptfuncdeffn_t. + */ +typedef scriptvalue_t (*scriptpropdefgetter_t)(void *handle); + +/** + * Native setter for a scriptpropdef_t. value is already decoded/type- + * checked according to the property's declared type. + */ +typedef void (*scriptpropdefsetter_t)( + void *handle, + const scriptvalue_t *value +); + +/** + * Declarative description of one JS-visible property. setter may be + * NULL for a read-only property. Terminate an array of these with a + * zeroed entry ({ .name = NULL }). + */ +typedef struct { + const char_t *name; + scriptvaluetype_t type; + scriptpropdefgetter_t getter; + scriptpropdefsetter_t setter; +} scriptpropdef_t; + +/** + * Define instance methods on a scriptproto_t's prototype. Each + * handler's handle param resolves via scriptProtoGetValue(proto, this). + * + * @param proto The class prototype to attach methods to. + * @param defs Sentinel-terminated array of method definitions. + */ +void scriptProtoDefineFuncDefs( + scriptproto_t *proto, + const scriptfuncdef_t *defs +); + +/** + * Define instance properties on a scriptproto_t's prototype. Each + * getter/setter's handle param resolves via scriptProtoGetValue(proto, + * this). + * + * @param proto The class prototype to attach properties to. + * @param defs Sentinel-terminated array of property definitions. + */ +void scriptProtoDefinePropDefs( + scriptproto_t *proto, + const scriptpropdef_t *defs +); + +/** + * Define functions directly on an arbitrary JS object (e.g. a plain + * jerry_object() used as a global namespace, or proto->constructor for + * statics) with no owning scriptproto_t -- every handler's handle param + * is always NULL. + * + * @param target The JS object to attach functions to. + * @param defs Sentinel-terminated array of method definitions. + */ +void scriptDefDefineGlobalFuncs( + const jerry_value_t target, + const scriptfuncdef_t *defs +); + +/** + * Define properties directly on an arbitrary JS object, with no owning + * scriptproto_t -- every getter/setter's handle param is always NULL. + * + * @param target The JS object to attach properties to. + * @param defs Sentinel-terminated array of property definitions. + */ +void scriptDefDefineGlobalProps( + const jerry_value_t target, + const scriptpropdef_t *defs +); + +/** + * Forget every binding registered via the functions above. Must be + * called during script manager teardown (alongside + * scriptProtoDisposeAll()) so a later scriptManagerInit() cycle can + * re-register from a clean slate instead of overflowing the binding + * registry. + */ +void scriptDefDisposeAll(void); + +/** + * Internal. Resolves the native handle for a call, or NULL if proto is + * NULL (the global-target case). + * + * @param proto The owning prototype, or NULL. + * @param thisValue The this_value from the call's jerry_call_info_t. + * @return The resolved native handle, or NULL. + */ +void *scriptDefResolveHandle( + const scriptproto_t *proto, + const jerry_value_t thisValue +); + +/** + * Internal. Shared implementation behind scriptProtoDefineFuncDefs() + * and scriptDefDefineGlobalFuncs(). + * + * @param target The JS object to attach functions to. + * @param proto The owning prototype for handle resolution, or NULL. + * @param defs Sentinel-terminated array of method definitions. + */ +void scriptDefDefineFuncsOnTarget( + const jerry_value_t target, + const scriptproto_t *proto, + const scriptfuncdef_t *defs +); + +/** + * Internal. Shared implementation behind scriptProtoDefinePropDefs() + * and scriptDefDefineGlobalProps(). + * + * @param target The JS object to attach properties to. + * @param proto The owning prototype for handle resolution, or NULL. + * @param defs Sentinel-terminated array of property definitions. + */ +void scriptDefDefinePropsOnTarget( + const jerry_value_t target, + const scriptproto_t *proto, + const scriptpropdef_t *defs +); diff --git a/src/dusk/script/scriptmanager.c b/src/dusk/script/scriptmanager.c index 4c138f8f..31b18f1f 100644 --- a/src/dusk/script/scriptmanager.c +++ b/src/dusk/script/scriptmanager.c @@ -12,6 +12,7 @@ #include "util/memory.h" #include "util/string.h" #include "scriptproto.h" +#include "scriptdef.h" #include "script/module/modulelist.h" #include "script/module/require/modulerequire.h" @@ -163,6 +164,7 @@ errorret_t scriptManagerCallValue( errorret_t scriptManagerDispose(void) { moduleListDispose(); scriptProtoDisposeAll(); + scriptDefDisposeAll(); for(uint8_t i = 0; i < SCRIPT_MANAGER.globalKeyCacheCount; i++) { jerry_value_free(SCRIPT_MANAGER.globalKeyCache[i].key); diff --git a/src/dusk/script/scriptvalue.c b/src/dusk/script/scriptvalue.c new file mode 100644 index 00000000..d3d67b34 --- /dev/null +++ b/src/dusk/script/scriptvalue.c @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scriptvalue.h" +#include "script/module/modulebase.h" +#include "util/memory.h" +#include "util/string.h" + +scriptvalue_t scriptValueVoid(void) { + scriptvalue_t v; + memoryZero(&v, sizeof(v)); + v.type = SCRIPT_TYPE_VOID; + return v; +} + +scriptvalue_t scriptValueNumber(const double value) { + scriptvalue_t v; + memoryZero(&v, sizeof(v)); + v.type = SCRIPT_TYPE_NUMBER; + v.as.number = value; + return v; +} + +scriptvalue_t scriptValueInt(const int32_t value) { + scriptvalue_t v; + memoryZero(&v, sizeof(v)); + v.type = SCRIPT_TYPE_INT; + v.as.intValue = value; + return v; +} + +scriptvalue_t scriptValueBool(const bool_t value) { + scriptvalue_t v; + memoryZero(&v, sizeof(v)); + v.type = SCRIPT_TYPE_BOOL; + v.as.boolValue = value; + return v; +} + +scriptvalue_t scriptValueString(const char_t *value) { + scriptvalue_t v; + memoryZero(&v, sizeof(v)); + v.type = SCRIPT_TYPE_STRING; + stringCopy(v.as.string, value, SCRIPT_VALUE_STRING_MAX - 1); + return v; +} + +scriptvalue_t scriptValueVec3(const vec3 value) { + scriptvalue_t v; + memoryZero(&v, sizeof(v)); + v.type = SCRIPT_TYPE_VEC3; + v.as.vec3Value[0] = value[0]; + v.as.vec3Value[1] = value[1]; + v.as.vec3Value[2] = value[2]; + return v; +} + +scriptvalue_t scriptValuePointer(void *value) { + scriptvalue_t v; + memoryZero(&v, sizeof(v)); + v.type = SCRIPT_TYPE_POINTER; + v.as.pointer = value; + return v; +} + +bool_t scriptDefReadArg( + const scriptvaluetype_t type, + const jerry_value_t args[], + const jerry_length_t argc, + uint32_t *consumed, + scriptvalue_t *out, + jerry_value_t *outThrown +) { + switch(type) { + case SCRIPT_TYPE_NUMBER: + if(*consumed >= argc || !jerry_value_is_number(args[*consumed])) { + *outThrown = moduleBaseThrow("Expected number argument"); + return false; + } + out->type = SCRIPT_TYPE_NUMBER; + out->as.number = jerry_value_as_number(args[(*consumed)++]); + return true; + + case SCRIPT_TYPE_INT: + if(*consumed >= argc || !jerry_value_is_number(args[*consumed])) { + *outThrown = moduleBaseThrow("Expected number argument"); + return false; + } + out->type = SCRIPT_TYPE_INT; + out->as.intValue = (int32_t)jerry_value_as_number(args[(*consumed)++]); + return true; + + case SCRIPT_TYPE_BOOL: + if(*consumed >= argc) { + *outThrown = moduleBaseThrow("Expected boolean argument"); + return false; + } + out->type = SCRIPT_TYPE_BOOL; + out->as.boolValue = jerry_value_is_true(args[(*consumed)++]); + return true; + + case SCRIPT_TYPE_STRING: + if(*consumed >= argc || !jerry_value_is_string(args[*consumed])) { + *outThrown = moduleBaseThrow("Expected string argument"); + return false; + } + out->type = SCRIPT_TYPE_STRING; + moduleBaseToString( + args[(*consumed)++], out->as.string, SCRIPT_VALUE_STRING_MAX + ); + return true; + + case SCRIPT_TYPE_VEC3: { + if(*consumed + 3 > argc) { + *outThrown = moduleBaseThrow("Expected 3 number arguments"); + return false; + } + for(uint32_t i = 0; i < 3; i++) { + if(!jerry_value_is_number(args[*consumed + i])) { + *outThrown = moduleBaseThrow("Expected number argument"); + return false; + } + } + out->type = SCRIPT_TYPE_VEC3; + out->as.vec3Value[0] = (float_t)jerry_value_as_number(args[*consumed]); + out->as.vec3Value[1] = + (float_t)jerry_value_as_number(args[*consumed + 1]); + out->as.vec3Value[2] = + (float_t)jerry_value_as_number(args[*consumed + 2]); + *consumed += 3; + return true; + } + + case SCRIPT_TYPE_POINTER: + if(*consumed >= argc || !jerry_value_is_object(args[*consumed])) { + *outThrown = moduleBaseThrow("Expected object argument"); + return false; + } + out->type = SCRIPT_TYPE_POINTER; + out->as.pointer = moduleBaseUnwrapPointer(args[(*consumed)++]); + return true; + + case SCRIPT_TYPE_CALLBACK: + if(*consumed >= argc || !jerry_value_is_function(args[*consumed])) { + *outThrown = moduleBaseThrow("Expected function argument"); + return false; + } + out->type = SCRIPT_TYPE_CALLBACK; + out->as.value = jerry_value_copy(args[(*consumed)++]); + return true; + + case SCRIPT_TYPE_VALUE: + if(*consumed >= argc) { + *outThrown = moduleBaseThrow("Expected argument"); + return false; + } + out->type = SCRIPT_TYPE_VALUE; + out->as.value = args[(*consumed)++]; + return true; + + default: + *outThrown = moduleBaseThrow("Unsupported script argument type"); + return false; + } +} + +bool_t scriptDefReadValue( + const scriptvaluetype_t type, + const jerry_value_t value, + scriptvalue_t *out, + jerry_value_t *outThrown +) { + switch(type) { + case SCRIPT_TYPE_NUMBER: + if(!jerry_value_is_number(value)) { + *outThrown = moduleBaseThrow("Expected number value"); + return false; + } + out->type = SCRIPT_TYPE_NUMBER; + out->as.number = jerry_value_as_number(value); + return true; + + case SCRIPT_TYPE_INT: + if(!jerry_value_is_number(value)) { + *outThrown = moduleBaseThrow("Expected number value"); + return false; + } + out->type = SCRIPT_TYPE_INT; + out->as.intValue = (int32_t)jerry_value_as_number(value); + return true; + + case SCRIPT_TYPE_BOOL: + out->type = SCRIPT_TYPE_BOOL; + out->as.boolValue = jerry_value_is_true(value); + return true; + + case SCRIPT_TYPE_STRING: + if(!jerry_value_is_string(value)) { + *outThrown = moduleBaseThrow("Expected string value"); + return false; + } + out->type = SCRIPT_TYPE_STRING; + moduleBaseToString(value, out->as.string, SCRIPT_VALUE_STRING_MAX); + return true; + + case SCRIPT_TYPE_VEC3: { + if(!jerry_value_is_object(value)) { + *outThrown = moduleBaseThrow("Expected {x, y, z} object value"); + return false; + } + jerry_value_t xVal = moduleBaseGetProp(value, "x"); + jerry_value_t yVal = moduleBaseGetProp(value, "y"); + jerry_value_t zVal = moduleBaseGetProp(value, "z"); + out->type = SCRIPT_TYPE_VEC3; + out->as.vec3Value[0] = moduleBaseValueFloat(xVal); + out->as.vec3Value[1] = moduleBaseValueFloat(yVal); + out->as.vec3Value[2] = moduleBaseValueFloat(zVal); + jerry_value_free(xVal); + jerry_value_free(yVal); + jerry_value_free(zVal); + return true; + } + + case SCRIPT_TYPE_POINTER: + if(!jerry_value_is_object(value)) { + *outThrown = moduleBaseThrow("Expected object value"); + return false; + } + out->type = SCRIPT_TYPE_POINTER; + out->as.pointer = moduleBaseUnwrapPointer(value); + return true; + + case SCRIPT_TYPE_CALLBACK: + if(!jerry_value_is_function(value)) { + *outThrown = moduleBaseThrow("Expected function value"); + return false; + } + out->type = SCRIPT_TYPE_CALLBACK; + out->as.value = jerry_value_copy(value); + return true; + + case SCRIPT_TYPE_VALUE: + out->type = SCRIPT_TYPE_VALUE; + out->as.value = value; + return true; + + default: + *outThrown = moduleBaseThrow("Unsupported script value type"); + return false; + } +} + +jerry_value_t scriptDefValueToJerry(const scriptvalue_t *value) { + switch(value->type) { + case SCRIPT_TYPE_VOID: + return jerry_undefined(); + case SCRIPT_TYPE_NUMBER: + return jerry_number(value->as.number); + case SCRIPT_TYPE_INT: + return jerry_number(value->as.intValue); + case SCRIPT_TYPE_BOOL: + return jerry_boolean(value->as.boolValue); + case SCRIPT_TYPE_STRING: + return jerry_string_sz(value->as.string); + case SCRIPT_TYPE_VEC3: + return moduleBaseVec3ToObject(value->as.vec3Value); + case SCRIPT_TYPE_POINTER: + return moduleBaseWrapPointer(value->as.pointer); + case SCRIPT_TYPE_CALLBACK: + case SCRIPT_TYPE_VALUE: + return value->as.value; + default: + return jerry_undefined(); + } +} diff --git a/src/dusk/script/scriptvalue.h b/src/dusk/script/scriptvalue.h new file mode 100644 index 00000000..2dbdb70e --- /dev/null +++ b/src/dusk/script/scriptvalue.h @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include + +/** Max chars (incl. null terminator) a SCRIPT_TYPE_STRING value holds. */ +#define SCRIPT_VALUE_STRING_MAX 128 + +/** + * The type of a scriptvalue_t, and of a single function param/property. + * + * SCRIPT_TYPE_VEC3 is read differently depending on context: as a + * function param it consumes 3 consecutive JS number arguments (e.g. + * `setPosition(x, y, z)`); as a property value it reads/writes a single + * `{x, y, z}` JS object. + * + * SCRIPT_TYPE_CALLBACK validates the incoming value is a JS function and + * takes a jerry_value_copy() of it for you -- the receiving handler owns + * that reference (store it, invoke it later with the existing + * scriptManagerCallValue(), and jerry_value_free() it when done). + * + * SCRIPT_TYPE_VALUE is a raw passthrough jerry_value_t, borrowed only + * for the duration of the call -- do not store it without copying it + * yourself first. + */ +typedef enum { + SCRIPT_TYPE_VOID, + SCRIPT_TYPE_NUMBER, + SCRIPT_TYPE_INT, + SCRIPT_TYPE_BOOL, + SCRIPT_TYPE_STRING, + SCRIPT_TYPE_VEC3, + SCRIPT_TYPE_POINTER, + SCRIPT_TYPE_CALLBACK, + SCRIPT_TYPE_VALUE +} scriptvaluetype_t; + +/** + * A typed value crossing the JS/C boundary -- a function param/return + * value, or a property's get/set value. + */ +typedef struct { + scriptvaluetype_t type; + union { + double number; + int32_t intValue; + bool_t boolValue; + char_t string[SCRIPT_VALUE_STRING_MAX]; + vec3 vec3Value; + void *pointer; + jerry_value_t value; + } as; +} scriptvalue_t; + +/** + * Build a SCRIPT_TYPE_VOID value (e.g. to return from a fn with no + * meaningful result). + */ +scriptvalue_t scriptValueVoid(void); + +/** + * Build a SCRIPT_TYPE_NUMBER value. + */ +scriptvalue_t scriptValueNumber(const double value); + +/** + * Build a SCRIPT_TYPE_INT value. + */ +scriptvalue_t scriptValueInt(const int32_t value); + +/** + * Build a SCRIPT_TYPE_BOOL value. + */ +scriptvalue_t scriptValueBool(const bool_t value); + +/** + * Build a SCRIPT_TYPE_STRING value. Copies value (truncating to + * SCRIPT_VALUE_STRING_MAX - 1 chars) into the returned scriptvalue_t -- + * value need not outlive this call. + */ +scriptvalue_t scriptValueString(const char_t *value); + +/** + * Build a SCRIPT_TYPE_VEC3 value. + */ +scriptvalue_t scriptValueVec3(const vec3 value); + +/** + * Build a SCRIPT_TYPE_POINTER value. + */ +scriptvalue_t scriptValuePointer(void *value); + +/** + * Internal. Decodes one function-call argument (advancing *consumed by + * 1, or by 3 for SCRIPT_TYPE_VEC3) into a typed scriptvalue_t. + * + * @param type The declared param type. + * @param args The raw JS arguments array. + * @param argc Number of raw JS arguments. + * @param consumed In/out cursor into args. + * @param out Receives the decoded value on success. + * @param outThrown Receives a thrown JS error value on failure. + * @return true on success, false if outThrown was set. + */ +bool_t scriptDefReadArg( + const scriptvaluetype_t type, + const jerry_value_t args[], + const jerry_length_t argc, + uint32_t *consumed, + scriptvalue_t *out, + jerry_value_t *outThrown +); + +/** + * Internal. Decodes a single incoming JS value (e.g. a property + * setter's argument) into a typed scriptvalue_t. Unlike + * scriptDefReadArg, SCRIPT_TYPE_VEC3 here decodes a single {x, y, z} + * object rather than 3 raw arguments. + * + * @param type The declared value type. + * @param value The raw incoming JS value. + * @param out Receives the decoded value on success. + * @param outThrown Receives a thrown JS error value on failure. + * @return true on success, false if outThrown was set. + */ +bool_t scriptDefReadValue( + const scriptvaluetype_t type, + const jerry_value_t value, + scriptvalue_t *out, + jerry_value_t *outThrown +); + +/** + * Internal. Converts a typed scriptvalue_t into a JS value (the + * return/get direction). + * + * @param value The value to convert. + * @return The equivalent JS value. + */ +jerry_value_t scriptDefValueToJerry(const scriptvalue_t *value); diff --git a/test/script/CMakeLists.txt b/test/script/CMakeLists.txt index 877e32b7..bf893c35 100644 --- a/test/script/CMakeLists.txt +++ b/test/script/CMakeLists.txt @@ -7,6 +7,8 @@ include(dusktest) dusktest(test_modulerequire.c) +dusktest(test_scriptdef.c) + dusktest(test_overworldscene.c) target_compile_definitions(test_overworldscene PRIVATE DUSK_ASSETS_DIR="${DUSK_ASSETS_DIR}" diff --git a/test/script/test_scriptdef.c b/test/script/test_scriptdef.c new file mode 100644 index 00000000..7485617d --- /dev/null +++ b/test/script/test_scriptdef.c @@ -0,0 +1,447 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "script/scriptmanager.h" +#include "script/scriptdef.h" +#include "script/module/modulebase.h" +#include "util/memory.h" +#include "util/string.h" + +typedef struct { + double numberValue; + int32_t intValue; + bool_t boolValue; + char_t stringValue[SCRIPT_VALUE_STRING_MAX]; + vec3 vec3Value; + void *pointerValue; + jerry_value_t callbackValue; +} testwidgethandle_t; + +static scriptproto_t TEST_WIDGET_PROTO; +static uint8_t TEST_SENTINEL; +static double TEST_GLOBAL_VALUE; + +moduleBaseFunction(testWidgetConstructor) { + testwidgethandle_t *inst = (testwidgethandle_t *)memoryAllocate( + sizeof(testwidgethandle_t) + ); + memoryZero(inst, sizeof(testwidgethandle_t)); + inst->callbackValue = jerry_undefined(); + + jerry_object_set_native_ptr( + callInfo->this_value, &TEST_WIDGET_PROTO.info, inst + ); + return jerry_undefined(); +} + +static scriptvalue_t testWidgetSetNumber( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + ((testwidgethandle_t *)handle)->numberValue = args[0].as.number; + return scriptValueVoid(); +} + +static scriptvalue_t testWidgetGetNumberValue(void *handle) { + return scriptValueNumber(((testwidgethandle_t *)handle)->numberValue); +} + +static scriptvalue_t testWidgetGetNumberMethod( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + return testWidgetGetNumberValue(handle); +} + +static scriptvalue_t testWidgetSetInt( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + ((testwidgethandle_t *)handle)->intValue = args[0].as.intValue; + return scriptValueVoid(); +} + +static scriptvalue_t testWidgetSetBool( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + ((testwidgethandle_t *)handle)->boolValue = args[0].as.boolValue; + return scriptValueVoid(); +} + +static scriptvalue_t testWidgetGetBoolMethod( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + return scriptValueBool(((testwidgethandle_t *)handle)->boolValue); +} + +static scriptvalue_t testWidgetSetString( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + stringCopy( + ((testwidgethandle_t *)handle)->stringValue, args[0].as.string, + SCRIPT_VALUE_STRING_MAX - 1 + ); + return scriptValueVoid(); +} + +static scriptvalue_t testWidgetGetStringMethod( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + return scriptValueString(((testwidgethandle_t *)handle)->stringValue); +} + +static scriptvalue_t testWidgetSetVec3( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + testwidgethandle_t *inst = (testwidgethandle_t *)handle; + inst->vec3Value[0] = args[0].as.vec3Value[0]; + inst->vec3Value[1] = args[0].as.vec3Value[1]; + inst->vec3Value[2] = args[0].as.vec3Value[2]; + return scriptValueVoid(); +} + +static scriptvalue_t testWidgetGetVec3Value(void *handle) { + return scriptValueVec3(((testwidgethandle_t *)handle)->vec3Value); +} + +static scriptvalue_t testWidgetGetVec3Method( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + return testWidgetGetVec3Value(handle); +} + +static scriptvalue_t testWidgetSetPointer( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + ((testwidgethandle_t *)handle)->pointerValue = args[0].as.pointer; + return scriptValueVoid(); +} + +static scriptvalue_t testWidgetGetSentinelPointer( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + return scriptValuePointer(&TEST_SENTINEL); +} + +static scriptvalue_t testWidgetPointerMatches( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + return scriptValueBool( + ((testwidgethandle_t *)handle)->pointerValue == args[0].as.pointer + ); +} + +static scriptvalue_t testWidgetSetCallback( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + testwidgethandle_t *inst = (testwidgethandle_t *)handle; + if(jerry_value_is_function(inst->callbackValue)) { + jerry_value_free(inst->callbackValue); + } + inst->callbackValue = args[0].as.value; + return scriptValueVoid(); +} + +static scriptvalue_t testWidgetInvokeCallback( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + testwidgethandle_t *inst = (testwidgethandle_t *)handle; + if(jerry_value_is_function(inst->callbackValue)) { + errorret_t ret = scriptManagerCallValue( + jerry_undefined(), inst->callbackValue, "TestWidget", "callback" + ); + errorCatch(ret); + } + return scriptValueVoid(); +} + +static scriptvalue_t testWidgetDispose( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + testwidgethandle_t *inst = (testwidgethandle_t *)handle; + if(jerry_value_is_function(inst->callbackValue)) { + jerry_value_free(inst->callbackValue); + } + inst->callbackValue = jerry_undefined(); + return scriptValueVoid(); +} + +static void testWidgetSetNumberProp(void *handle, const scriptvalue_t *value) { + ((testwidgethandle_t *)handle)->numberValue = value->as.number; +} + +static void testWidgetSetVec3Prop(void *handle, const scriptvalue_t *value) { + testwidgethandle_t *inst = (testwidgethandle_t *)handle; + inst->vec3Value[0] = value->as.vec3Value[0]; + inst->vec3Value[1] = value->as.vec3Value[1]; + inst->vec3Value[2] = value->as.vec3Value[2]; +} + +static scriptfuncdef_t TEST_WIDGET_FUNCS[] = { + { "setNumber", SCRIPT_TYPE_VOID, { SCRIPT_TYPE_NUMBER }, 1, + testWidgetSetNumber }, + { "getNumber", SCRIPT_TYPE_NUMBER, { 0 }, 0, testWidgetGetNumberMethod }, + { "setInt", SCRIPT_TYPE_VOID, { SCRIPT_TYPE_INT }, 1, testWidgetSetInt }, + { "setBool", SCRIPT_TYPE_VOID, { SCRIPT_TYPE_BOOL }, 1, testWidgetSetBool }, + { "getBool", SCRIPT_TYPE_BOOL, { 0 }, 0, testWidgetGetBoolMethod }, + { "setString", SCRIPT_TYPE_VOID, { SCRIPT_TYPE_STRING }, 1, + testWidgetSetString }, + { "getString", SCRIPT_TYPE_STRING, { 0 }, 0, testWidgetGetStringMethod }, + { "setVec3", SCRIPT_TYPE_VOID, { SCRIPT_TYPE_VEC3 }, 1, testWidgetSetVec3 }, + { "getVec3", SCRIPT_TYPE_VEC3, { 0 }, 0, testWidgetGetVec3Method }, + { "setPointer", SCRIPT_TYPE_VOID, { SCRIPT_TYPE_POINTER }, 1, + testWidgetSetPointer }, + { "getSentinelPointer", SCRIPT_TYPE_POINTER, { 0 }, 0, + testWidgetGetSentinelPointer }, + { "pointerMatches", SCRIPT_TYPE_BOOL, { SCRIPT_TYPE_POINTER }, 1, + testWidgetPointerMatches }, + { "setCallback", SCRIPT_TYPE_VOID, { SCRIPT_TYPE_CALLBACK }, 1, + testWidgetSetCallback }, + { "invokeCallback", SCRIPT_TYPE_VOID, { 0 }, 0, testWidgetInvokeCallback }, + { "dispose", SCRIPT_TYPE_VOID, { 0 }, 0, testWidgetDispose }, + { NULL, SCRIPT_TYPE_VOID, { 0 }, 0, NULL } +}; + +static scriptpropdef_t TEST_WIDGET_PROPS[] = { + { "numberProp", SCRIPT_TYPE_NUMBER, testWidgetGetNumberValue, + testWidgetSetNumberProp }, + { "vec3Prop", SCRIPT_TYPE_VEC3, testWidgetGetVec3Value, + testWidgetSetVec3Prop }, + { NULL, SCRIPT_TYPE_VOID, NULL, NULL } +}; + +static scriptvalue_t testGlobalGetValue(void *handle) { + return scriptValueNumber(TEST_GLOBAL_VALUE); +} + +static void testGlobalSetValue(void *handle, const scriptvalue_t *value) { + TEST_GLOBAL_VALUE = value->as.number; +} + +static scriptvalue_t testGlobalCompute( + void *handle, const scriptvalue_t *args, const uint32_t argc +) { + return scriptValueNumber(args[0].as.number + args[1].as.number); +} + +static scriptpropdef_t TEST_GLOBAL_PROPS[] = { + { "value", SCRIPT_TYPE_NUMBER, testGlobalGetValue, testGlobalSetValue }, + { NULL, SCRIPT_TYPE_VOID, NULL, NULL } +}; + +static scriptfuncdef_t TEST_GLOBAL_FUNCS[] = { + { "compute", SCRIPT_TYPE_NUMBER, + { SCRIPT_TYPE_NUMBER, SCRIPT_TYPE_NUMBER }, 2, testGlobalCompute }, + { NULL, SCRIPT_TYPE_VOID, { 0 }, 0, NULL } +}; + +static int scriptdef_setup(void **state) { + errorret_t ret = scriptManagerInit(); + if(errorIsNotOk(ret)) { errorCatch(ret); return -1; } + + scriptProtoInit( + &TEST_WIDGET_PROTO, "TestWidget", sizeof(testwidgethandle_t), + testWidgetConstructor + ); + scriptProtoDefineFuncDefs(&TEST_WIDGET_PROTO, TEST_WIDGET_FUNCS); + scriptProtoDefinePropDefs(&TEST_WIDGET_PROTO, TEST_WIDGET_PROPS); + + TEST_GLOBAL_VALUE = 0.0; + jerry_value_t globalObj = jerry_object(); + scriptDefDefineGlobalProps(globalObj, TEST_GLOBAL_PROPS); + scriptDefDefineGlobalFuncs(globalObj, TEST_GLOBAL_FUNCS); + moduleBaseSetValue("TestGlobal", globalObj); + jerry_value_free(globalObj); + + return 0; +} + +static int scriptdef_teardown(void **state) { + errorret_t ret = scriptManagerDispose(); + if(errorIsNotOk(ret)) errorCatch(ret); + + assert_int_equal(memoryGetAllocatedCount(), 0); + return 0; +} + +static void test_scriptdef_number_and_int_and_bool_roundtrip(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "var w = new TestWidget();" + "w.setNumber(3.5); w.setInt(7); w.setBool(true);" + "w.getNumber() + ',' + w.getBool();", + &result + ); + assert_true(errorIsOk(ret)); + char_t buf[64]; + moduleBaseToString(result, buf, sizeof(buf)); + assert_string_equal(buf, "3.5,true"); + jerry_value_free(result); +} + +static void test_scriptdef_string_method_roundtrip(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "var w = new TestWidget();" + "w.setString('hello'); w.getString();", + &result + ); + assert_true(errorIsOk(ret)); + char_t buf[32]; + moduleBaseToString(result, buf, sizeof(buf)); + assert_string_equal(buf, "hello"); + jerry_value_free(result); +} + +static void test_scriptdef_vec3_method_roundtrip(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "var w = new TestWidget();" + "w.setVec3(4, 5, 6);" + "var v = w.getVec3();" + "v.x + ',' + v.y + ',' + v.z;", + &result + ); + assert_true(errorIsOk(ret)); + char_t buf[32]; + moduleBaseToString(result, buf, sizeof(buf)); + assert_string_equal(buf, "4,5,6"); + jerry_value_free(result); +} + +static void test_scriptdef_vec3_property_roundtrip(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "var w = new TestWidget();" + "w.vec3Prop = { x: 1, y: 2, z: 3 };" + "var v = w.vec3Prop;" + "v.x + ',' + v.y + ',' + v.z;", + &result + ); + assert_true(errorIsOk(ret)); + char_t buf[32]; + moduleBaseToString(result, buf, sizeof(buf)); + assert_string_equal(buf, "1,2,3"); + jerry_value_free(result); +} + +static void test_scriptdef_number_property_roundtrip(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "var w = new TestWidget(); w.numberProp = 42; w.numberProp;", &result + ); + assert_true(errorIsOk(ret)); + assert_true(jerry_value_is_number(result)); + assert_int_equal((int)jerry_value_as_number(result), 42); + jerry_value_free(result); +} + +static void test_scriptdef_pointer_roundtrip(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "var w = new TestWidget();" + "var p = w.getSentinelPointer();" + "w.setPointer(p);" + "w.pointerMatches(p);", + &result + ); + assert_true(errorIsOk(ret)); + assert_true(jerry_value_is_true(result)); + jerry_value_free(result); +} + +static void test_scriptdef_callback_can_be_invoked_later(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "var calls = 0;" + "var w = new TestWidget();" + "w.setCallback(function() { calls++; });" + "w.invokeCallback(); w.invokeCallback();" + "w.dispose();" + "calls;", + &result + ); + assert_true(errorIsOk(ret)); + assert_true(jerry_value_is_number(result)); + assert_int_equal((int)jerry_value_as_number(result), 2); + jerry_value_free(result); +} + +static void test_scriptdef_wrong_type_args_throw(void **state) { + errorret_t ret = scriptManagerExec( + "var w = new TestWidget(); w.setNumber('nope');", NULL + ); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); + + ret = scriptManagerExec( + "var w = new TestWidget(); w.setString(123);", NULL + ); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); + + ret = scriptManagerExec( + "var w = new TestWidget(); w.setPointer(42);", NULL + ); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); +} + +static void test_scriptdef_global_object_props_and_funcs(void **state) { + jerry_value_t result; + errorret_t ret = scriptManagerExec( + "TestGlobal.value = 10;" + "TestGlobal.value + ',' + TestGlobal.compute(2, 3);", + &result + ); + assert_true(errorIsOk(ret)); + char_t buf[32]; + moduleBaseToString(result, buf, sizeof(buf)); + assert_string_equal(buf, "10,5"); + jerry_value_free(result); +} + +int main(void) { + assertInit(); + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown( + test_scriptdef_number_and_int_and_bool_roundtrip, + scriptdef_setup, scriptdef_teardown + ), + cmocka_unit_test_setup_teardown( + test_scriptdef_string_method_roundtrip, + scriptdef_setup, scriptdef_teardown + ), + cmocka_unit_test_setup_teardown( + test_scriptdef_vec3_method_roundtrip, + scriptdef_setup, scriptdef_teardown + ), + cmocka_unit_test_setup_teardown( + test_scriptdef_vec3_property_roundtrip, + scriptdef_setup, scriptdef_teardown + ), + cmocka_unit_test_setup_teardown( + test_scriptdef_number_property_roundtrip, + scriptdef_setup, scriptdef_teardown + ), + cmocka_unit_test_setup_teardown( + test_scriptdef_pointer_roundtrip, + scriptdef_setup, scriptdef_teardown + ), + cmocka_unit_test_setup_teardown( + test_scriptdef_callback_can_be_invoked_later, + scriptdef_setup, scriptdef_teardown + ), + cmocka_unit_test_setup_teardown( + test_scriptdef_wrong_type_args_throw, + scriptdef_setup, scriptdef_teardown + ), + cmocka_unit_test_setup_teardown( + test_scriptdef_global_object_props_and_funcs, + scriptdef_setup, scriptdef_teardown + ), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}