819 lines
25 KiB
C
819 lines
25 KiB
C
/**
|
|
* interface.c
|
|
*
|
|
* We primarily use JSValue* (pointer to JSValue) when communicating with the
|
|
* host javascript environment, because pointers are trivial to use for calls
|
|
* into emscripten because they're just a number!
|
|
*
|
|
* As with the quickjs.h API, a JSValueConst* value is "borrowed" and should
|
|
* not be freed. A JSValue* is "owned" and should be freed by the owner.
|
|
*
|
|
* Functions starting with "QTS_" are exported by generate.ts to:
|
|
* - interface.h for native C code.
|
|
* - ffi.ts for emscripten.
|
|
*
|
|
* We support building the following build outputs:
|
|
*
|
|
* ## 1. Native machine code
|
|
* For internal development testing purposes.
|
|
*
|
|
* ## 2. WASM via Emscripten
|
|
* For general production use.
|
|
*
|
|
* ## 3. Experimental: Asyncified WASM via Emscripten with -s ASYNCIFY=1.
|
|
* This variant supports treating async host Javascript calls as synchronous
|
|
* from the perspective of the WASM c code.
|
|
*
|
|
* The way this works is described here:
|
|
* https://emscripten.org/docs/porting/asyncify.html
|
|
*
|
|
* In this variant, any call into our C code could return a promise if it ended
|
|
* up suspended. We mark the methods we suspect might suspend due to users' code
|
|
* as returning MaybeAsync(T). This information is ignored for the regular
|
|
* build.
|
|
*/
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
#include <emscripten.h>
|
|
#endif
|
|
|
|
#include <math.h> // For NAN
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#ifdef QTS_SANITIZE_LEAK
|
|
#include <sanitizer/lsan_interface.h>
|
|
#endif
|
|
|
|
#include "../quickjs/cutils.h"
|
|
#include "../quickjs/quickjs-libc.h"
|
|
#include "../quickjs/quickjs.h"
|
|
|
|
#define PKG "quickjs-emscripten: "
|
|
|
|
#ifdef QTS_DEBUG_MODE
|
|
#define QTS_DEBUG(msg) qts_log(msg);
|
|
#define QTS_DUMP(value) qts_dump(ctx, value);
|
|
#else
|
|
#define QTS_DEBUG(msg) ;
|
|
#define QTS_DUMP(value) ;
|
|
#endif
|
|
|
|
/**
|
|
* Signal to our FFI code generator that this string argument should be passed as a pointer
|
|
* allocated by the caller on the heap, not a JS string on the stack.
|
|
* https://github.com/emscripten-core/emscripten/issues/6860#issuecomment-405818401
|
|
*/
|
|
#define BorrowedHeapChar const char
|
|
#define OwnedHeapChar char
|
|
#define JSBorrowedChar const char
|
|
|
|
/**
|
|
* Signal to our FFI code generator that this function should be called
|
|
* asynchronously when compiled with ASYNCIFY.
|
|
*/
|
|
#define MaybeAsync(T) T
|
|
|
|
/**
|
|
* Signal to our FFI code generator that this function is only available in
|
|
* ASYNCIFY builds.
|
|
*/
|
|
#define AsyncifyOnly(T) T
|
|
|
|
#define JSVoid void
|
|
|
|
#define EvalFlags int
|
|
#define EvalDetectModule int
|
|
|
|
void qts_log(char *msg) {
|
|
fputs(PKG, stderr);
|
|
fputs(msg, stderr);
|
|
fputs("\n", stderr);
|
|
}
|
|
|
|
void qts_dump(JSContext *ctx, JSValueConst value) {
|
|
const char *str = JS_ToCString(ctx, value);
|
|
if (!str) {
|
|
QTS_DEBUG("QTS_DUMP: can't dump");
|
|
return;
|
|
}
|
|
fputs(str, stderr);
|
|
JS_FreeCString(ctx, str);
|
|
putchar('\n');
|
|
}
|
|
|
|
void copy_prop_if_needed(JSContext *ctx, JSValueConst dest, JSValueConst src, const char *prop_name) {
|
|
JSAtom prop_atom = JS_NewAtom(ctx, prop_name);
|
|
JSValue dest_prop = JS_GetProperty(ctx, dest, prop_atom);
|
|
if (JS_IsUndefined(dest_prop)) {
|
|
JSValue src_prop = JS_GetProperty(ctx, src, prop_atom);
|
|
if (!JS_IsUndefined(src_prop) && !JS_IsException(src_prop)) {
|
|
JS_SetProperty(ctx, dest, prop_atom, src_prop);
|
|
}
|
|
} else {
|
|
JS_FreeValue(ctx, dest_prop);
|
|
}
|
|
JS_FreeAtom(ctx, prop_atom);
|
|
}
|
|
|
|
JSValue *jsvalue_to_heap(JSValueConst value) {
|
|
JSValue *result = malloc(sizeof(JSValue));
|
|
if (result) {
|
|
// Could be better optimized, but at -0z / -ftlo, it
|
|
// appears to produce the same binary code as a memcpy.
|
|
*result = value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
JSValue *QTS_Throw(JSContext *ctx, JSValueConst *error) {
|
|
JSValue copy = JS_DupValue(ctx, *error);
|
|
return jsvalue_to_heap(JS_Throw(ctx, copy));
|
|
}
|
|
|
|
JSValue *QTS_NewError(JSContext *ctx) {
|
|
return jsvalue_to_heap(JS_NewError(ctx));
|
|
}
|
|
|
|
/**
|
|
* Limits.
|
|
*/
|
|
|
|
/**
|
|
* Memory limit. Set to -1 to disable.
|
|
*/
|
|
void QTS_RuntimeSetMemoryLimit(JSRuntime *rt, size_t limit) {
|
|
JS_SetMemoryLimit(rt, limit);
|
|
}
|
|
|
|
/**
|
|
* Memory diagnostics
|
|
*/
|
|
|
|
JSValue *QTS_RuntimeComputeMemoryUsage(JSRuntime *rt, JSContext *ctx) {
|
|
JSMemoryUsage s;
|
|
JS_ComputeMemoryUsage(rt, &s);
|
|
|
|
// Note that we're going to allocate more memory just to report the memory usage.
|
|
// A more sound approach would be to bind JSMemoryUsage struct directly - but that's
|
|
// a lot of work. This should be okay in the mean time.
|
|
JSValue result = JS_NewObject(ctx);
|
|
|
|
// Manually generated via editor-fu from JSMemoryUsage struct definition in quickjs.h
|
|
JS_SetPropertyStr(ctx, result, "malloc_limit", JS_NewInt64(ctx, s.malloc_limit));
|
|
JS_SetPropertyStr(ctx, result, "memory_used_size", JS_NewInt64(ctx, s.memory_used_size));
|
|
JS_SetPropertyStr(ctx, result, "malloc_count", JS_NewInt64(ctx, s.malloc_count));
|
|
JS_SetPropertyStr(ctx, result, "memory_used_count", JS_NewInt64(ctx, s.memory_used_count));
|
|
JS_SetPropertyStr(ctx, result, "atom_count", JS_NewInt64(ctx, s.atom_count));
|
|
JS_SetPropertyStr(ctx, result, "atom_size", JS_NewInt64(ctx, s.atom_size));
|
|
JS_SetPropertyStr(ctx, result, "str_count", JS_NewInt64(ctx, s.str_count));
|
|
JS_SetPropertyStr(ctx, result, "str_size", JS_NewInt64(ctx, s.str_size));
|
|
JS_SetPropertyStr(ctx, result, "obj_count", JS_NewInt64(ctx, s.obj_count));
|
|
JS_SetPropertyStr(ctx, result, "obj_size", JS_NewInt64(ctx, s.obj_size));
|
|
JS_SetPropertyStr(ctx, result, "prop_count", JS_NewInt64(ctx, s.prop_count));
|
|
JS_SetPropertyStr(ctx, result, "prop_size", JS_NewInt64(ctx, s.prop_size));
|
|
JS_SetPropertyStr(ctx, result, "shape_count", JS_NewInt64(ctx, s.shape_count));
|
|
JS_SetPropertyStr(ctx, result, "shape_size", JS_NewInt64(ctx, s.shape_size));
|
|
JS_SetPropertyStr(ctx, result, "js_func_count", JS_NewInt64(ctx, s.js_func_count));
|
|
JS_SetPropertyStr(ctx, result, "js_func_size", JS_NewInt64(ctx, s.js_func_size));
|
|
JS_SetPropertyStr(ctx, result, "js_func_code_size", JS_NewInt64(ctx, s.js_func_code_size));
|
|
JS_SetPropertyStr(ctx, result, "js_func_pc2line_count", JS_NewInt64(ctx, s.js_func_pc2line_count));
|
|
JS_SetPropertyStr(ctx, result, "js_func_pc2line_size", JS_NewInt64(ctx, s.js_func_pc2line_size));
|
|
JS_SetPropertyStr(ctx, result, "c_func_count", JS_NewInt64(ctx, s.c_func_count));
|
|
JS_SetPropertyStr(ctx, result, "array_count", JS_NewInt64(ctx, s.array_count));
|
|
JS_SetPropertyStr(ctx, result, "fast_array_count", JS_NewInt64(ctx, s.fast_array_count));
|
|
JS_SetPropertyStr(ctx, result, "fast_array_elements", JS_NewInt64(ctx, s.fast_array_elements));
|
|
JS_SetPropertyStr(ctx, result, "binary_object_count", JS_NewInt64(ctx, s.binary_object_count));
|
|
JS_SetPropertyStr(ctx, result, "binary_object_size", JS_NewInt64(ctx, s.binary_object_size));
|
|
|
|
return jsvalue_to_heap(result);
|
|
}
|
|
|
|
OwnedHeapChar *QTS_RuntimeDumpMemoryUsage(JSRuntime *rt) {
|
|
char *result = malloc(sizeof(char) * 1024);
|
|
FILE *memfile = fmemopen(result, 1024, "w");
|
|
JSMemoryUsage s;
|
|
JS_ComputeMemoryUsage(rt, &s);
|
|
JS_DumpMemoryUsage(memfile, &s, rt);
|
|
fclose(memfile);
|
|
return result;
|
|
}
|
|
|
|
int QTS_RecoverableLeakCheck() {
|
|
#ifdef QTS_SANITIZE_LEAK
|
|
return __lsan_do_recoverable_leak_check();
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
int QTS_BuildIsSanitizeLeak() {
|
|
#ifdef QTS_SANITIZE_LEAK
|
|
return 1;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
#ifdef QTS_ASYNCIFY
|
|
EM_JS(void, set_asyncify_stack_size, (size_t size), {
|
|
Asyncify.StackSize = size || 81920;
|
|
});
|
|
#endif
|
|
|
|
/**
|
|
* Set the stack size limit, in bytes. Set to 0 to disable.
|
|
*/
|
|
void QTS_RuntimeSetMaxStackSize(JSRuntime *rt, size_t stack_size) {
|
|
#ifdef QTS_ASYNCIFY
|
|
set_asyncify_stack_size(stack_size);
|
|
#endif
|
|
JS_SetMaxStackSize(rt, stack_size);
|
|
}
|
|
|
|
/**
|
|
* Constant pointers. Because we always use JSValue* from the host Javascript environment,
|
|
* we need helper fuctions to return pointers to these constants.
|
|
*/
|
|
|
|
JSValueConst QTS_Undefined = JS_UNDEFINED;
|
|
JSValueConst *QTS_GetUndefined() {
|
|
return &QTS_Undefined;
|
|
}
|
|
|
|
JSValueConst QTS_Null = JS_NULL;
|
|
JSValueConst *QTS_GetNull() {
|
|
return &QTS_Null;
|
|
}
|
|
|
|
JSValueConst QTS_False = JS_FALSE;
|
|
JSValueConst *QTS_GetFalse() {
|
|
return &QTS_False;
|
|
}
|
|
|
|
JSValueConst QTS_True = JS_TRUE;
|
|
JSValueConst *QTS_GetTrue() {
|
|
return &QTS_True;
|
|
}
|
|
|
|
/**
|
|
* Standard FFI functions
|
|
*/
|
|
|
|
JSRuntime *QTS_NewRuntime() {
|
|
return JS_NewRuntime();
|
|
}
|
|
|
|
void QTS_FreeRuntime(JSRuntime *rt) {
|
|
JS_FreeRuntime(rt);
|
|
}
|
|
|
|
JSContext *QTS_NewContext(JSRuntime *rt) {
|
|
return JS_NewContext(rt);
|
|
}
|
|
|
|
void QTS_FreeContext(JSContext *ctx) {
|
|
JS_FreeContext(ctx);
|
|
}
|
|
|
|
void QTS_FreeValuePointer(JSContext *ctx, JSValue *value) {
|
|
JS_FreeValue(ctx, *value);
|
|
free(value);
|
|
}
|
|
|
|
void QTS_FreeValuePointerRuntime(JSRuntime *rt, JSValue *value) {
|
|
JS_FreeValueRT(rt, *value);
|
|
free(value);
|
|
}
|
|
|
|
void QTS_FreeVoidPointer(JSContext *ctx, JSVoid *ptr) {
|
|
js_free(ctx, ptr);
|
|
}
|
|
|
|
void QTS_FreeCString(JSContext *ctx, JSBorrowedChar *str) {
|
|
JS_FreeCString(ctx, str);
|
|
}
|
|
|
|
JSValue *QTS_DupValuePointer(JSContext *ctx, JSValueConst *val) {
|
|
return jsvalue_to_heap(JS_DupValue(ctx, *val));
|
|
}
|
|
|
|
JSValue *QTS_NewObject(JSContext *ctx) {
|
|
return jsvalue_to_heap(JS_NewObject(ctx));
|
|
}
|
|
|
|
JSValue *QTS_NewObjectProto(JSContext *ctx, JSValueConst *proto) {
|
|
return jsvalue_to_heap(JS_NewObjectProto(ctx, *proto));
|
|
}
|
|
|
|
JSValue *QTS_NewArray(JSContext *ctx) {
|
|
return jsvalue_to_heap(JS_NewArray(ctx));
|
|
}
|
|
|
|
JSValue *QTS_NewFloat64(JSContext *ctx, double num) {
|
|
return jsvalue_to_heap(JS_NewFloat64(ctx, num));
|
|
}
|
|
|
|
double QTS_GetFloat64(JSContext *ctx, JSValueConst *value) {
|
|
double result = NAN;
|
|
JS_ToFloat64(ctx, &result, *value);
|
|
return result;
|
|
}
|
|
|
|
JSValue *QTS_NewString(JSContext *ctx, BorrowedHeapChar *string) {
|
|
return jsvalue_to_heap(JS_NewString(ctx, string));
|
|
}
|
|
|
|
JSBorrowedChar *QTS_GetString(JSContext *ctx, JSValueConst *value) {
|
|
return JS_ToCString(ctx, *value);
|
|
}
|
|
|
|
JSValue qts_get_symbol_key(JSContext *ctx, JSValueConst *value) {
|
|
JSValue global = JS_GetGlobalObject(ctx);
|
|
JSValue Symbol = JS_GetPropertyStr(ctx, global, "Symbol");
|
|
JS_FreeValue(ctx, global);
|
|
|
|
JSValue Symbol_keyFor = JS_GetPropertyStr(ctx, Symbol, "keyFor");
|
|
JSValue key = JS_Call(ctx, Symbol_keyFor, Symbol, 1, value);
|
|
JS_FreeValue(ctx, Symbol_keyFor);
|
|
JS_FreeValue(ctx, Symbol);
|
|
return key;
|
|
}
|
|
|
|
JSValue *QTS_NewSymbol(JSContext *ctx, BorrowedHeapChar *description, int isGlobal) {
|
|
JSValue global = JS_GetGlobalObject(ctx);
|
|
JSValue Symbol = JS_GetPropertyStr(ctx, global, "Symbol");
|
|
JS_FreeValue(ctx, global);
|
|
JSValue descriptionValue = JS_NewString(ctx, description);
|
|
JSValue symbol;
|
|
|
|
if (isGlobal != 0) {
|
|
JSValue Symbol_for = JS_GetPropertyStr(ctx, Symbol, "for");
|
|
symbol = JS_Call(ctx, Symbol_for, Symbol, 1, &descriptionValue);
|
|
JS_FreeValue(ctx, descriptionValue);
|
|
JS_FreeValue(ctx, Symbol_for);
|
|
JS_FreeValue(ctx, Symbol);
|
|
return jsvalue_to_heap(symbol);
|
|
}
|
|
|
|
symbol = JS_Call(ctx, Symbol, JS_UNDEFINED, 1, &descriptionValue);
|
|
JS_FreeValue(ctx, descriptionValue);
|
|
JS_FreeValue(ctx, Symbol);
|
|
|
|
return jsvalue_to_heap(symbol);
|
|
}
|
|
|
|
MaybeAsync(JSBorrowedChar *) QTS_GetSymbolDescriptionOrKey(JSContext *ctx, JSValueConst *value) {
|
|
JSBorrowedChar *result;
|
|
|
|
JSValue key = qts_get_symbol_key(ctx, value);
|
|
if (!JS_IsUndefined(key)) {
|
|
result = JS_ToCString(ctx, key);
|
|
JS_FreeValue(ctx, key);
|
|
return result;
|
|
}
|
|
|
|
JSValue description = JS_GetPropertyStr(ctx, *value, "description");
|
|
result = JS_ToCString(ctx, description);
|
|
JS_FreeValue(ctx, description);
|
|
return result;
|
|
}
|
|
|
|
int QTS_IsGlobalSymbol(JSContext *ctx, JSValueConst *value) {
|
|
JSValue key = qts_get_symbol_key(ctx, value);
|
|
int undefined = JS_IsUndefined(key);
|
|
JS_FreeValue(ctx, key);
|
|
|
|
if (undefined) {
|
|
return 0;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
int QTS_IsJobPending(JSRuntime *rt) {
|
|
return JS_IsJobPending(rt);
|
|
}
|
|
|
|
/*
|
|
runs pending jobs (Promises/async functions) until it encounters
|
|
an exception or it executed the passed maxJobsToExecute jobs.
|
|
|
|
Passing a negative value will run the loop until there are no more
|
|
pending jobs or an exception happened
|
|
|
|
Returns the executed number of jobs or the exception encountered
|
|
*/
|
|
MaybeAsync(JSValue *) QTS_ExecutePendingJob(JSRuntime *rt, int maxJobsToExecute, JSContext **lastJobContext) {
|
|
JSContext *pctx;
|
|
int status = 1;
|
|
int executed = 0;
|
|
while (executed != maxJobsToExecute && status == 1) {
|
|
status = JS_ExecutePendingJob(rt, &pctx);
|
|
if (status == -1) {
|
|
*lastJobContext = pctx;
|
|
return jsvalue_to_heap(JS_GetException(pctx));
|
|
} else if (status == 1) {
|
|
*lastJobContext = pctx;
|
|
executed++;
|
|
}
|
|
}
|
|
#ifdef QTS_DEBUG_MODE
|
|
char msg[500];
|
|
sprintf(msg, "QTS_ExecutePendingJob(executed: %d, pctx: %p, lastJobExecuted: %p)", executed, pctx, *lastJobContext);
|
|
QTS_DEBUG(msg)
|
|
#endif
|
|
return jsvalue_to_heap(JS_NewFloat64(pctx, executed));
|
|
}
|
|
|
|
MaybeAsync(JSValue *) QTS_GetProp(JSContext *ctx, JSValueConst *this_val, JSValueConst *prop_name) {
|
|
JSAtom prop_atom = JS_ValueToAtom(ctx, *prop_name);
|
|
JSValue prop_val = JS_GetProperty(ctx, *this_val, prop_atom);
|
|
JS_FreeAtom(ctx, prop_atom);
|
|
return jsvalue_to_heap(prop_val);
|
|
}
|
|
|
|
MaybeAsync(void) QTS_SetProp(JSContext *ctx, JSValueConst *this_val, JSValueConst *prop_name, JSValueConst *prop_value) {
|
|
JSAtom prop_atom = JS_ValueToAtom(ctx, *prop_name);
|
|
JSValue extra_prop_value = JS_DupValue(ctx, *prop_value);
|
|
// TODO: should we use DefineProperty internally if this object doesn't have the property yet?
|
|
JS_SetProperty(ctx, *this_val, prop_atom, extra_prop_value); // consumes extra_prop_value
|
|
JS_FreeAtom(ctx, prop_atom);
|
|
}
|
|
|
|
void QTS_DefineProp(JSContext *ctx, JSValueConst *this_val, JSValueConst *prop_name, JSValueConst *prop_value, JSValueConst *get, JSValueConst *set, bool configurable, bool enumerable, bool has_value) {
|
|
JSAtom prop_atom = JS_ValueToAtom(ctx, *prop_name);
|
|
|
|
int flags = 0;
|
|
if (configurable) {
|
|
flags = flags | JS_PROP_CONFIGURABLE;
|
|
if (has_value) {
|
|
flags = flags | JS_PROP_HAS_CONFIGURABLE;
|
|
}
|
|
}
|
|
if (enumerable) {
|
|
flags = flags | JS_PROP_ENUMERABLE;
|
|
if (has_value) {
|
|
flags = flags | JS_PROP_HAS_ENUMERABLE;
|
|
}
|
|
}
|
|
if (!JS_IsUndefined(*get)) {
|
|
flags = flags | JS_PROP_HAS_GET;
|
|
}
|
|
if (!JS_IsUndefined(*set)) {
|
|
flags = flags | JS_PROP_HAS_SET;
|
|
}
|
|
if (has_value) {
|
|
flags = flags | JS_PROP_HAS_VALUE;
|
|
}
|
|
|
|
JS_DefineProperty(ctx, *this_val, prop_atom, *prop_value, *get, *set, flags);
|
|
JS_FreeAtom(ctx, prop_atom);
|
|
}
|
|
|
|
MaybeAsync(JSValue *) QTS_Call(JSContext *ctx, JSValueConst *func_obj, JSValueConst *this_obj, int argc, JSValueConst **argv_ptrs) {
|
|
// convert array of pointers to array of values
|
|
JSValueConst argv[argc];
|
|
int i;
|
|
for (i = 0; i < argc; i++) {
|
|
argv[i] = *(argv_ptrs[i]);
|
|
}
|
|
|
|
return jsvalue_to_heap(JS_Call(ctx, *func_obj, *this_obj, argc, argv));
|
|
}
|
|
|
|
/**
|
|
* If maybe_exception is an exception, get the error.
|
|
* Otherwise, return NULL.
|
|
*/
|
|
JSValue *QTS_ResolveException(JSContext *ctx, JSValue *maybe_exception) {
|
|
if (JS_IsException(*maybe_exception)) {
|
|
return jsvalue_to_heap(JS_GetException(ctx));
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
MaybeAsync(JSBorrowedChar *) QTS_Dump(JSContext *ctx, JSValueConst *obj) {
|
|
JSValue obj_json_value = JS_JSONStringify(ctx, *obj, JS_UNDEFINED, JS_UNDEFINED);
|
|
if (!JS_IsException(obj_json_value)) {
|
|
const char *obj_json_chars = JS_ToCString(ctx, obj_json_value);
|
|
JS_FreeValue(ctx, obj_json_value);
|
|
if (obj_json_chars != NULL) {
|
|
JSValue enumerable_props = JS_ParseJSON(ctx, obj_json_chars, strlen(obj_json_chars), "<dump>");
|
|
JS_FreeCString(ctx, obj_json_chars);
|
|
if (!JS_IsException(enumerable_props)) {
|
|
// Copy common non-enumerable props for different object types.
|
|
// Errors:
|
|
copy_prop_if_needed(ctx, enumerable_props, *obj, "name");
|
|
copy_prop_if_needed(ctx, enumerable_props, *obj, "message");
|
|
copy_prop_if_needed(ctx, enumerable_props, *obj, "stack");
|
|
|
|
// Serialize again.
|
|
JSValue enumerable_json = JS_JSONStringify(ctx, enumerable_props, JS_UNDEFINED, JS_UNDEFINED);
|
|
JS_FreeValue(ctx, enumerable_props);
|
|
|
|
JSBorrowedChar *result = QTS_GetString(ctx, &enumerable_json);
|
|
JS_FreeValue(ctx, enumerable_json);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef QTS_DEBUG_MODE
|
|
qts_log("Error dumping JSON:");
|
|
js_std_dump_error(ctx);
|
|
#endif
|
|
|
|
// Fallback: convert to string
|
|
return QTS_GetString(ctx, obj);
|
|
}
|
|
|
|
MaybeAsync(JSValue *) QTS_Eval(JSContext *ctx, BorrowedHeapChar *js_code, const char *filename, EvalDetectModule detectModule, EvalFlags evalFlags) {
|
|
size_t js_code_len = strlen(js_code);
|
|
|
|
if (detectModule) {
|
|
if (JS_DetectModule((const char *)js_code, js_code_len)) {
|
|
QTS_DEBUG("QTS_Eval: Detected module = true");
|
|
evalFlags |= JS_EVAL_TYPE_MODULE;
|
|
} else {
|
|
QTS_DEBUG("QTS_Eval: Detected module = false");
|
|
}
|
|
} else {
|
|
QTS_DEBUG("QTS_Eval: do not detect module");
|
|
}
|
|
|
|
return jsvalue_to_heap(JS_Eval(ctx, js_code, strlen(js_code), filename, evalFlags));
|
|
}
|
|
|
|
OwnedHeapChar *QTS_Typeof(JSContext *ctx, JSValueConst *value) {
|
|
const char *result = "unknown";
|
|
uint32_t tag = JS_VALUE_GET_TAG(*value);
|
|
|
|
if (JS_IsNumber(*value)) {
|
|
result = "number";
|
|
} else if (JS_IsBigInt(ctx, *value)) {
|
|
result = "bigint";
|
|
} else if (JS_IsBigFloat(*value)) {
|
|
result = "bigfloat";
|
|
} else if (JS_IsBigDecimal(*value)) {
|
|
result = "bigdecimal";
|
|
} else if (JS_IsFunction(ctx, *value)) {
|
|
result = "function";
|
|
} else if (JS_IsBool(*value)) {
|
|
result = "boolean";
|
|
} else if (JS_IsNull(*value)) {
|
|
result = "object";
|
|
} else if (JS_IsUndefined(*value)) {
|
|
result = "undefined";
|
|
} else if (JS_IsUninitialized(*value)) {
|
|
result = "undefined";
|
|
} else if (JS_IsString(*value)) {
|
|
result = "string";
|
|
} else if (JS_IsSymbol(*value)) {
|
|
result = "symbol";
|
|
} else if (JS_IsObject(*value)) {
|
|
result = "object";
|
|
}
|
|
|
|
char *out = strdup(result);
|
|
return out;
|
|
}
|
|
|
|
JSValue *QTS_GetGlobalObject(JSContext *ctx) {
|
|
return jsvalue_to_heap(JS_GetGlobalObject(ctx));
|
|
}
|
|
|
|
JSValue *QTS_NewPromiseCapability(JSContext *ctx, JSValue **resolve_funcs_out) {
|
|
JSValue resolve_funcs[2];
|
|
JSValue promise = JS_NewPromiseCapability(ctx, resolve_funcs);
|
|
resolve_funcs_out[0] = jsvalue_to_heap(resolve_funcs[0]);
|
|
resolve_funcs_out[1] = jsvalue_to_heap(resolve_funcs[1]);
|
|
return jsvalue_to_heap(promise);
|
|
}
|
|
|
|
void QTS_TestStringArg(const char *string) {
|
|
// pass
|
|
}
|
|
|
|
int QTS_BuildIsDebug() {
|
|
#ifdef QTS_DEBUG_MODE
|
|
return 1;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
int QTS_BuildIsAsyncify() {
|
|
#ifdef QTS_ASYNCIFY
|
|
return 1;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Module loading helpers
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// C -> Host Callbacks
|
|
// Note: inside EM_JS, we need to use ['...'] subscript syntax for accessing JS
|
|
// objects, because in optimized builds, Closure compiler will mangle all the
|
|
// names.
|
|
|
|
// -------------------
|
|
// function: C -> Host
|
|
#ifdef __EMSCRIPTEN__
|
|
EM_JS(MaybeAsync(JSValue *), qts_host_call_function, (JSContext * ctx, JSValueConst *this_ptr, int argc, JSValueConst *argv, uint32_t magic_func_id), {
|
|
#ifdef QTS_ASYNCIFY
|
|
const asyncify = {['handleSleep'] : Asyncify.handleSleep};
|
|
#else
|
|
const asyncify = undefined;
|
|
#endif
|
|
return Module['callbacks']['callFunction'](asyncify, ctx, this_ptr, argc, argv, magic_func_id);
|
|
});
|
|
#endif
|
|
|
|
// Function: QuickJS -> C
|
|
JSValue qts_call_function(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) {
|
|
JSValue *result_ptr = qts_host_call_function(ctx, &this_val, argc, argv, magic);
|
|
if (result_ptr == NULL) {
|
|
return JS_UNDEFINED;
|
|
}
|
|
JSValue result = *result_ptr;
|
|
free(result_ptr);
|
|
return result;
|
|
}
|
|
|
|
// Function: Host -> QuickJS
|
|
JSValue *QTS_NewFunction(JSContext *ctx, uint32_t func_id, const char *name) {
|
|
#ifdef QTS_DEBUG_MODE
|
|
char msg[500];
|
|
sprintf(msg, "new_function(name: %s, magic: %d)", name, func_id);
|
|
QTS_DEBUG(msg)
|
|
#endif
|
|
JSValue func_obj = JS_NewCFunctionMagic(
|
|
/* context */ ctx,
|
|
/* JSCFunctionMagic* */ &qts_call_function,
|
|
/* name */ name,
|
|
/* min argc */ 0,
|
|
/* function type */ JS_CFUNC_generic_magic,
|
|
/* magic: fn id */ func_id);
|
|
return jsvalue_to_heap(func_obj);
|
|
}
|
|
|
|
JSValueConst *QTS_ArgvGetJSValueConstPointer(JSValueConst *argv, int index) {
|
|
return &argv[index];
|
|
}
|
|
|
|
// --------------------
|
|
// interrupt: C -> Host
|
|
#ifdef __EMSCRIPTEN__
|
|
EM_JS(int, qts_host_interrupt_handler, (JSRuntime * rt), {
|
|
// Async not supported here.
|
|
// #ifdef QTS_ASYNCIFY
|
|
// const asyncify = Asyncify;
|
|
// #else
|
|
const asyncify = undefined;
|
|
// #endif
|
|
return Module['callbacks']['shouldInterrupt'](asyncify, rt);
|
|
});
|
|
#endif
|
|
|
|
// interrupt: QuickJS -> C
|
|
int qts_interrupt_handler(JSRuntime *rt, void *_unused) {
|
|
return qts_host_interrupt_handler(rt);
|
|
}
|
|
|
|
// interrupt: Host -> QuickJS
|
|
void QTS_RuntimeEnableInterruptHandler(JSRuntime *rt) {
|
|
JS_SetInterruptHandler(rt, &qts_interrupt_handler, NULL);
|
|
}
|
|
|
|
void QTS_RuntimeDisableInterruptHandler(JSRuntime *rt) {
|
|
JS_SetInterruptHandler(rt, NULL, NULL);
|
|
}
|
|
|
|
// --------------------
|
|
// load module: C -> Host
|
|
// TODO: a future version can support host returning JSModuleDef* directly;
|
|
// for now we only support loading module source code.
|
|
|
|
/*
|
|
The module loading model under ASYNCIFY is convoluted. We need to make sure we
|
|
never have an async request running concurrently for loading modules.
|
|
|
|
The first implemenation looked like this:
|
|
|
|
C HOST SUSPENDED
|
|
qts_host_load_module(name) ------> false
|
|
call rt.loadModule(name) false
|
|
Start async load module false
|
|
Suspend C true
|
|
Async load complete true
|
|
< --------------- QTS_CompileModule(source) true
|
|
QTS_Eval(source, COMPILE_ONLY) true
|
|
Loaded module has import true
|
|
qts_host_load_module(dep) -------> true
|
|
call rt.loadModule(dep) true
|
|
Start async load module true
|
|
ALREADY SUSPENDED, CRASH
|
|
|
|
We can solve this in two different ways:
|
|
|
|
1. Return to C as soon as we async load the module source.
|
|
That way, we unsuspend before calling QTS_CompileModule.
|
|
2. Once we load the module, use a new API to detect and async
|
|
load the module's downstream dependencies. This way
|
|
they're loaded synchronously so we don't need to suspend "again".
|
|
|
|
Probably we could optimize (2) to make it more performant, eg with parallel
|
|
loading, but (1) seems much easier to implement in the sort run.
|
|
*/
|
|
|
|
JSModuleDef *qts_compile_module(JSContext *ctx, const char *module_name, BorrowedHeapChar *module_body) {
|
|
#ifdef QTS_DEBUG_MODE
|
|
char msg[500];
|
|
sprintf(msg, "QTS_CompileModule(ctx: %p, name: %s, bodyLength: %lu)", ctx, module_name, strlen(module_body));
|
|
QTS_DEBUG(msg)
|
|
#endif
|
|
JSValue func_val = JS_Eval(ctx, module_body, strlen(module_body), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
if (JS_IsException(func_val)) {
|
|
return NULL;
|
|
}
|
|
// TODO: Is exception ok?
|
|
// TODO: set import.meta?
|
|
JSModuleDef *module = JS_VALUE_GET_PTR(func_val);
|
|
JS_FreeValue(ctx, func_val);
|
|
return module;
|
|
}
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
EM_JS(MaybeAsync(char *), qts_host_load_module_source, (JSRuntime * rt, JSContext *ctx, const char *module_name), {
|
|
#ifdef QTS_ASYNCIFY
|
|
const asyncify = {['handleSleep'] : Asyncify.handleSleep};
|
|
#else
|
|
const asyncify = undefined;
|
|
#endif
|
|
// https://emscripten.org/docs/api_reference/preamble.js.html#UTF8ToString
|
|
const moduleNameString = UTF8ToString(module_name);
|
|
return Module['callbacks']['loadModuleSource'](asyncify, rt, ctx, moduleNameString);
|
|
});
|
|
|
|
EM_JS(MaybeAsync(char *), qts_host_normalize_module, (JSRuntime * rt, JSContext *ctx, const char *module_base_name, const char *module_name), {
|
|
#ifdef QTS_ASYNCIFY
|
|
const asyncify = {['handleSleep'] : Asyncify.handleSleep};
|
|
#else
|
|
const asyncify = undefined;
|
|
#endif
|
|
// https://emscripten.org/docs/api_reference/preamble.js.html#UTF8ToString
|
|
const moduleBaseNameString = UTF8ToString(module_base_name);
|
|
const moduleNameString = UTF8ToString(module_name);
|
|
return Module['callbacks']['normalizeModule'](asyncify, rt, ctx, moduleBaseNameString, moduleNameString);
|
|
});
|
|
#endif
|
|
|
|
// load module: QuickJS -> C
|
|
// See js_module_loader in quickjs/quickjs-libc.c:567
|
|
JSModuleDef *qts_load_module(JSContext *ctx, const char *module_name, void *_unused) {
|
|
JSRuntime *rt = JS_GetRuntime(ctx);
|
|
#ifdef QTS_DEBUG_MODE
|
|
char msg[500];
|
|
sprintf(msg, "qts_load_module(rt: %p, ctx: %p, name: %s)", rt, ctx, module_name);
|
|
QTS_DEBUG(msg)
|
|
#endif
|
|
char *module_source = qts_host_load_module_source(rt, ctx, module_name);
|
|
if (module_source == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
JSModuleDef *module = qts_compile_module(ctx, module_name, module_source);
|
|
free(module_source);
|
|
return module;
|
|
}
|
|
|
|
char *qts_normalize_module(JSContext *ctx, const char *module_base_name, const char *module_name, void *_unused) {
|
|
JSRuntime *rt = JS_GetRuntime(ctx);
|
|
#ifdef QTS_DEBUG_MODE
|
|
char msg[500];
|
|
sprintf(msg, "qts_normalize_module(rt: %p, ctx: %p, base_name: %s, name: %s)", rt, ctx, module_base_name, module_name);
|
|
QTS_DEBUG(msg)
|
|
#endif
|
|
char *em_module_name = qts_host_normalize_module(rt, ctx, module_base_name, module_name);
|
|
char *js_module_name = js_strdup(ctx, em_module_name);
|
|
free(em_module_name);
|
|
return js_module_name;
|
|
}
|
|
|
|
// Load module: Host -> QuickJS
|
|
void QTS_RuntimeEnableModuleLoader(JSRuntime *rt, int use_custom_normalize) {
|
|
JSModuleNormalizeFunc *module_normalize = NULL; /* use default name normalizer */
|
|
if (use_custom_normalize) {
|
|
module_normalize = &qts_normalize_module;
|
|
}
|
|
JS_SetModuleLoaderFunc(rt, module_normalize, &qts_load_module, NULL);
|
|
}
|
|
|
|
void QTS_RuntimeDisableModuleLoader(JSRuntime *rt) {
|
|
JS_SetModuleLoaderFunc(rt, NULL, NULL, NULL);
|
|
} |