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.
This commit is contained in:
2026-08-03 19:36:08 -05:00
parent b9d2fe60fd
commit 5f34cb34b2
8 changed files with 1269 additions and 0 deletions
+2
View File
@@ -7,6 +7,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
scriptmanager.c
scriptproto.c
scriptvalue.c
scriptdef.c
)
add_subdirectory(module)
+224
View File
@@ -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, &params[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);
}
}
+167
View File
@@ -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
);
+2
View File
@@ -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);
+279
View File
@@ -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();
}
}
+146
View File
@@ -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 <jerryscript.h>
/** 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);
+2
View File
@@ -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}"
+447
View File
@@ -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);
}