Current section
Files
Jump to
Current section
Files
c_src/mquickjs_ex.c
/*
* MquickjsEx NIF Implementation
*
* Embeds MQuickJS JavaScript engine into Elixir via NIFs.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <time.h>
#include "erl_nif.h"
#include "vendor/mquickjs.h"
#include "vendor/mquickjs_priv.h"
/* ============================================================================
* C functions required by the stdlib (print, Date.now, performance.now)
* These MUST be defined before including mquickjs_ex_stdlib.h
* ============================================================================ */
static JSValue js_print(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv)
{
int i;
JSValue v;
for (i = 0; i < argc; i++) {
if (i != 0)
putchar(' ');
v = argv[i];
if (JS_IsString(ctx, v)) {
JSCStringBuf buf;
const char *str;
size_t len;
str = JS_ToCStringLen(ctx, &len, v, &buf);
fwrite(str, 1, len, stdout);
} else {
JS_PrintValueF(ctx, argv[i], JS_DUMP_LONG);
}
}
putchar('\n');
return JS_UNDEFINED;
}
static JSValue js_date_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return JS_NewInt64(ctx, (int64_t)tv.tv_sec * 1000 + (tv.tv_usec / 1000));
}
static JSValue js_performance_now(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return JS_NewInt64(ctx, (int64_t)tv.tv_sec * 1000 + (tv.tv_usec / 1000));
}
/* Now include the generated stdlib header */
#include "mquickjs_ex_stdlib.h"
/* ============================================================================
* NIF Functions
* ============================================================================ */
/* NIF resource type for JS context */
static ErlNifResourceType *JS_CONTEXT_TYPE;
/* Maximum recursion depth for nested structures */
#define MAX_SERIALIZE_DEPTH 100
/* Yield marker prefix for exception messages */
#define YIELD_MARKER "__yield__:"
typedef struct {
uint8_t *mem_buf;
size_t mem_size;
JSContext *ctx;
uint64_t deadline_ms; /* Timeout deadline (0 = no timeout) */
} JsContext;
/* Get current time in milliseconds (monotonic clock) */
static uint64_t get_time_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* Interrupt handler for timeout support */
static int timeout_interrupt_handler(JSContext *ctx, void *opaque) {
JsContext *js = (JsContext *)opaque;
if (js->deadline_ms == 0) return 0;
return (get_time_ms() > js->deadline_ms) ? 1 : 0;
}
static void js_log_func(void *opaque, const void *buf, size_t buf_len)
{
fwrite(buf, 1, buf_len, stdout);
}
/* ============================================================================
* JS → Elixir Type Conversion
* ============================================================================ */
/* Forward declaration for recursion */
static ERL_NIF_TERM js_to_erl(ErlNifEnv *env, JSContext *ctx, JSValue val, int depth);
/* Convert JS array to Elixir list */
static ERL_NIF_TERM js_array_to_erl(ErlNifEnv *env, JSContext *ctx, JSValue arr, int depth)
{
JSGCRef arr_ref, len_ref;
JSValue *arr_val, *len_val;
if (depth > MAX_SERIALIZE_DEPTH) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "max_depth_exceeded"));
}
/* Protect the array value from GC */
arr_val = JS_PushGCRef(ctx, &arr_ref);
*arr_val = arr;
len_val = JS_PushGCRef(ctx, &len_ref);
*len_val = js_array_get_length(ctx, arr_val, 0, NULL);
if (JS_IsException(*len_val)) {
JS_PopGCRef(ctx, &len_ref);
JS_PopGCRef(ctx, &arr_ref);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "array_length_failed"));
}
int len;
if (JS_ToInt32(ctx, &len, *len_val) < 0 || len < 0) {
JS_PopGCRef(ctx, &len_ref);
JS_PopGCRef(ctx, &arr_ref);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "invalid_array_length"));
}
JS_PopGCRef(ctx, &len_ref);
/* Build list in reverse, then reverse at end (standard pattern) */
ERL_NIF_TERM list = enif_make_list(env, 0);
for (int i = len - 1; i >= 0; i--) {
JSGCRef elem_ref;
JSValue *elem_val = JS_PushGCRef(ctx, &elem_ref);
*elem_val = JS_GetPropertyUint32(ctx, *arr_val, (uint32_t)i);
if (JS_IsException(*elem_val)) {
JS_PopGCRef(ctx, &elem_ref);
JS_PopGCRef(ctx, &arr_ref);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "array_element_failed"));
}
ERL_NIF_TERM elem_term = js_to_erl(env, ctx, *elem_val, depth + 1);
JS_PopGCRef(ctx, &elem_ref);
/* Check if nested conversion failed */
if (enif_is_tuple(env, elem_term)) {
int arity;
const ERL_NIF_TERM *tuple;
if (enif_get_tuple(env, elem_term, &arity, &tuple) && arity == 2) {
if (enif_is_identical(tuple[0], enif_make_atom(env, "error"))) {
JS_PopGCRef(ctx, &arr_ref);
return elem_term;
}
}
}
list = enif_make_list_cell(env, elem_term, list);
}
JS_PopGCRef(ctx, &arr_ref);
return list;
}
/* Convert JS object to Elixir map */
static ERL_NIF_TERM js_object_to_erl(ErlNifEnv *env, JSContext *ctx, JSValue obj, int depth)
{
JSGCRef obj_ref, keys_ref;
JSValue *obj_val, *keys_val;
if (depth > MAX_SERIALIZE_DEPTH) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "max_depth_exceeded"));
}
/* Protect the object value from GC */
obj_val = JS_PushGCRef(ctx, &obj_ref);
*obj_val = obj;
/* Get object keys using Object.keys() */
keys_val = JS_PushGCRef(ctx, &keys_ref);
*keys_val = js_object_keys(ctx, NULL, 1, obj_val);
if (JS_IsException(*keys_val)) {
JS_PopGCRef(ctx, &keys_ref);
JS_PopGCRef(ctx, &obj_ref);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "object_keys_failed"));
}
/* Get keys array length */
JSGCRef len_ref;
JSValue *len_val = JS_PushGCRef(ctx, &len_ref);
*len_val = js_array_get_length(ctx, keys_val, 0, NULL);
if (JS_IsException(*len_val)) {
JS_PopGCRef(ctx, &len_ref);
JS_PopGCRef(ctx, &keys_ref);
JS_PopGCRef(ctx, &obj_ref);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "keys_length_failed"));
}
int len;
if (JS_ToInt32(ctx, &len, *len_val) < 0 || len < 0) {
JS_PopGCRef(ctx, &len_ref);
JS_PopGCRef(ctx, &keys_ref);
JS_PopGCRef(ctx, &obj_ref);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "invalid_keys_length"));
}
JS_PopGCRef(ctx, &len_ref);
/* Build map from keys */
ERL_NIF_TERM map = enif_make_new_map(env);
for (int i = 0; i < len; i++) {
JSGCRef key_ref, val_ref;
JSValue *key_val = JS_PushGCRef(ctx, &key_ref);
JSValue *prop_val = JS_PushGCRef(ctx, &val_ref);
*key_val = JS_GetPropertyUint32(ctx, *keys_val, (uint32_t)i);
if (JS_IsException(*key_val)) {
JS_PopGCRef(ctx, &val_ref);
JS_PopGCRef(ctx, &key_ref);
JS_PopGCRef(ctx, &keys_ref);
JS_PopGCRef(ctx, &obj_ref);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "key_get_failed"));
}
/* Get string value of key */
JSCStringBuf key_buf;
size_t key_len;
const char *key_str = JS_ToCStringLen(ctx, &key_len, *key_val, &key_buf);
/* Get property value */
*prop_val = JS_GetPropertyStr(ctx, *obj_val, key_str);
if (JS_IsException(*prop_val)) {
JS_PopGCRef(ctx, &val_ref);
JS_PopGCRef(ctx, &key_ref);
JS_PopGCRef(ctx, &keys_ref);
JS_PopGCRef(ctx, &obj_ref);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "property_get_failed"));
}
/* Convert key to Elixir binary */
ERL_NIF_TERM key_term;
unsigned char *key_data = enif_make_new_binary(env, key_len, &key_term);
memcpy(key_data, key_str, key_len);
/* Recursively convert value */
ERL_NIF_TERM val_term = js_to_erl(env, ctx, *prop_val, depth + 1);
JS_PopGCRef(ctx, &val_ref);
JS_PopGCRef(ctx, &key_ref);
/* Check if nested conversion failed */
if (enif_is_tuple(env, val_term)) {
int arity;
const ERL_NIF_TERM *tuple;
if (enif_get_tuple(env, val_term, &arity, &tuple) && arity == 2) {
if (enif_is_identical(tuple[0], enif_make_atom(env, "error"))) {
JS_PopGCRef(ctx, &keys_ref);
JS_PopGCRef(ctx, &obj_ref);
return val_term;
}
}
}
enif_make_map_put(env, map, key_term, val_term, &map);
}
JS_PopGCRef(ctx, &keys_ref);
JS_PopGCRef(ctx, &obj_ref);
return map;
}
/* Main JS → Elixir conversion function */
static ERL_NIF_TERM js_to_erl(ErlNifEnv *env, JSContext *ctx, JSValue val, int depth)
{
/* Check null and undefined first */
if (JS_IsNull(val) || JS_IsUndefined(val)) {
return enif_make_atom(env, "nil");
}
/* Check booleans */
if (JS_IsBool(val)) {
return JS_VALUE_GET_SPECIAL_VALUE(val) ?
enif_make_atom(env, "true") :
enif_make_atom(env, "false");
}
/* Check integers (31-bit signed in MQuickJS) */
if (JS_IsInt(val)) {
int32_t i = JS_VALUE_GET_INT(val);
return enif_make_int(env, i);
}
#ifdef JS_USE_SHORT_FLOAT
/* Check short floats (64-bit platforms) */
if (JS_IsShortFloat(val)) {
/* Short floats are encoded in the value itself */
/* Need to decode - this is platform specific */
/* For now, convert via JS_ToNumber */
double d;
if (JS_ToNumber(ctx, &d, val) == 0) {
/* Check if it's actually an integer */
if (d == (int64_t)d && d >= INT32_MIN && d <= INT32_MAX) {
return enif_make_int(env, (int)d);
}
return enif_make_double(env, d);
}
return enif_make_atom(env, "nil");
}
#endif
/* Check if it's a pointer type (object, string, float64, etc.) */
if (JS_IsPtr(val)) {
/* Check for float64 */
if (JS_IsNumber(ctx, val) && !JS_IsInt(val)) {
double d;
if (JS_ToNumber(ctx, &d, val) == 0) {
/* Check if it's actually an integer */
if (d == (int64_t)d && d >= INT32_MIN && d <= INT32_MAX) {
return enif_make_int(env, (int)d);
}
return enif_make_double(env, d);
}
}
/* Check strings */
if (JS_IsString(ctx, val)) {
JSCStringBuf buf;
size_t len;
const char *str = JS_ToCStringLen(ctx, &len, val, &buf);
ERL_NIF_TERM binary;
unsigned char *data = enif_make_new_binary(env, len, &binary);
memcpy(data, str, len);
return binary;
}
/* Check for array (must be before generic object check) */
int class_id = JS_GetClassID(ctx, val);
if (class_id == JS_CLASS_ARRAY) {
return js_array_to_erl(env, ctx, val, depth);
}
/* Check for functions - not serializable */
if (JS_IsFunction(ctx, val)) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "function_not_serializable"));
}
/* Check for generic objects */
if (class_id == JS_CLASS_OBJECT) {
return js_object_to_erl(env, ctx, val, depth);
}
/* Typed arrays, ArrayBuffer, etc. - TODO: could add support */
if (class_id >= JS_CLASS_ARRAY_BUFFER && class_id <= JS_CLASS_FLOAT64_ARRAY) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "typed_array_not_supported"));
}
}
/* Unknown type */
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "unknown_js_type"));
}
/* ============================================================================
* Elixir → JS Type Conversion
* ============================================================================ */
/* Forward declaration for recursion */
static JSValue erl_to_js(ErlNifEnv *env, JSContext *ctx, ERL_NIF_TERM term, int depth);
/* Convert Elixir list to JS array */
static JSValue erl_list_to_js(ErlNifEnv *env, JSContext *ctx, ERL_NIF_TERM list, int depth)
{
if (depth > MAX_SERIALIZE_DEPTH) {
return JS_ThrowInternalError(ctx, "max depth exceeded");
}
/* Get list length */
unsigned int len;
if (!enif_get_list_length(env, list, &len)) {
return JS_ThrowTypeError(ctx, "expected list");
}
JSGCRef arr_ref;
JSValue *arr = JS_PushGCRef(ctx, &arr_ref);
*arr = JS_NewArray(ctx, (int)len);
if (JS_IsException(*arr)) {
JS_PopGCRef(ctx, &arr_ref);
return JS_EXCEPTION;
}
ERL_NIF_TERM head, tail;
int i = 0;
tail = list;
while (enif_get_list_cell(env, tail, &head, &tail)) {
JSGCRef elem_ref;
JSValue *elem = JS_PushGCRef(ctx, &elem_ref);
*elem = erl_to_js(env, ctx, head, depth + 1);
if (JS_IsException(*elem)) {
JS_PopGCRef(ctx, &elem_ref);
JS_PopGCRef(ctx, &arr_ref);
return JS_EXCEPTION;
}
JSValue set_result = JS_SetPropertyUint32(ctx, *arr, (uint32_t)i, *elem);
JS_PopGCRef(ctx, &elem_ref);
if (JS_IsException(set_result)) {
JS_PopGCRef(ctx, &arr_ref);
return JS_EXCEPTION;
}
i++;
}
JSValue result = *arr;
JS_PopGCRef(ctx, &arr_ref);
return result;
}
/* Convert Elixir map to JS object */
static JSValue erl_map_to_js(ErlNifEnv *env, JSContext *ctx, ERL_NIF_TERM map, int depth)
{
if (depth > MAX_SERIALIZE_DEPTH) {
return JS_ThrowInternalError(ctx, "max depth exceeded");
}
JSGCRef obj_ref;
JSValue *obj = JS_PushGCRef(ctx, &obj_ref);
*obj = JS_NewObject(ctx);
if (JS_IsException(*obj)) {
JS_PopGCRef(ctx, &obj_ref);
return JS_EXCEPTION;
}
ErlNifMapIterator iter;
if (!enif_map_iterator_create(env, map, &iter, ERL_NIF_MAP_ITERATOR_FIRST)) {
JS_PopGCRef(ctx, &obj_ref);
return JS_ThrowTypeError(ctx, "expected map");
}
ERL_NIF_TERM key, value;
while (enif_map_iterator_get_pair(env, &iter, &key, &value)) {
/* Convert key to string */
char key_buf[1024];
int key_len = 0;
/* Try as binary first */
ErlNifBinary key_bin;
if (enif_inspect_binary(env, key, &key_bin)) {
if (key_bin.size < sizeof(key_buf)) {
memcpy(key_buf, key_bin.data, key_bin.size);
key_len = (int)key_bin.size;
} else {
enif_map_iterator_destroy(env, &iter);
JS_PopGCRef(ctx, &obj_ref);
return JS_ThrowTypeError(ctx, "map key too long");
}
}
/* Try as atom */
else if (enif_get_atom(env, key, key_buf, sizeof(key_buf), ERL_NIF_LATIN1)) {
key_len = (int)strlen(key_buf);
}
/* Try as integer (convert to string) */
else {
long i;
if (enif_get_long(env, key, &i)) {
key_len = snprintf(key_buf, sizeof(key_buf), "%ld", i);
} else {
enif_map_iterator_destroy(env, &iter);
JS_PopGCRef(ctx, &obj_ref);
return JS_ThrowTypeError(ctx, "unsupported map key type");
}
}
key_buf[key_len] = '\0';
/* Convert value recursively */
JSGCRef val_ref;
JSValue *val = JS_PushGCRef(ctx, &val_ref);
*val = erl_to_js(env, ctx, value, depth + 1);
if (JS_IsException(*val)) {
JS_PopGCRef(ctx, &val_ref);
enif_map_iterator_destroy(env, &iter);
JS_PopGCRef(ctx, &obj_ref);
return JS_EXCEPTION;
}
JSValue set_result = JS_SetPropertyStr(ctx, *obj, key_buf, *val);
JS_PopGCRef(ctx, &val_ref);
if (JS_IsException(set_result)) {
enif_map_iterator_destroy(env, &iter);
JS_PopGCRef(ctx, &obj_ref);
return JS_EXCEPTION;
}
enif_map_iterator_next(env, &iter);
}
enif_map_iterator_destroy(env, &iter);
JSValue result = *obj;
JS_PopGCRef(ctx, &obj_ref);
return result;
}
/* Main Elixir → JS conversion function */
static JSValue erl_to_js(ErlNifEnv *env, JSContext *ctx, ERL_NIF_TERM term, int depth)
{
/* Check for nil/null */
if (enif_is_identical(term, enif_make_atom(env, "nil"))) {
return JS_NULL;
}
/* Check for booleans */
if (enif_is_identical(term, enif_make_atom(env, "true"))) {
return JS_TRUE;
}
if (enif_is_identical(term, enif_make_atom(env, "false"))) {
return JS_FALSE;
}
/* Check for integers */
ErlNifSInt64 i64;
if (enif_get_int64(env, term, &i64)) {
/* MQuickJS uses 31-bit signed integers */
if (i64 >= INT32_MIN && i64 <= INT32_MAX) {
return JS_NewInt32(ctx, (int32_t)i64);
}
/* Large integers become floats (loses precision beyond 2^53) */
return JS_NewFloat64(ctx, (double)i64);
}
/* Check for floats */
double d;
if (enif_get_double(env, term, &d)) {
return JS_NewFloat64(ctx, d);
}
/* Check for binaries (strings) */
ErlNifBinary bin;
if (enif_inspect_binary(env, term, &bin)) {
return JS_NewStringLen(ctx, (char *)bin.data, bin.size);
}
/* Check for atoms (convert to string, except nil/true/false handled above) */
char atom_buf[256];
if (enif_get_atom(env, term, atom_buf, sizeof(atom_buf), ERL_NIF_LATIN1)) {
return JS_NewString(ctx, atom_buf);
}
/* Check for lists → JS Array */
if (enif_is_list(env, term)) {
return erl_list_to_js(env, ctx, term, depth);
}
/* Check for maps → JS Object */
if (enif_is_map(env, term)) {
return erl_map_to_js(env, ctx, term, depth);
}
/* Unsupported type */
return JS_ThrowTypeError(ctx, "unsupported Elixir type");
}
/* ============================================================================
* NIF Functions
* ============================================================================ */
/* Create a new JS context */
static ERL_NIF_TERM nif_new(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
unsigned long mem_size = 65536; /* Default 64KB */
/* Parse memory option if provided */
if (argc > 0) {
if (!enif_get_ulong(env, argv[0], &mem_size)) {
return enif_make_badarg(env);
}
}
/* Allocate context resource */
JsContext *js = enif_alloc_resource(JS_CONTEXT_TYPE, sizeof(JsContext));
if (!js) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "alloc_failed"));
}
js->mem_size = mem_size;
/* Use simple malloc - same as working standalone test */
js->mem_buf = malloc(mem_size);
if (!js->mem_buf) {
enif_release_resource(js);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "alloc_failed"));
}
/* Initialize MQuickJS */
js->ctx = JS_NewContext(js->mem_buf, js->mem_size, &js_stdlib);
if (!js->ctx) {
free(js->mem_buf);
enif_release_resource(js);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "js_context_failed"));
}
/* Initialize timeout to disabled */
js->deadline_ms = 0;
/* Set up context opaque for interrupt handler */
JS_SetContextOpaque(js->ctx, js);
JS_SetInterruptHandler(js->ctx, timeout_interrupt_handler);
/* Set up logging */
JS_SetLogFunc(js->ctx, js_log_func);
ERL_NIF_TERM result = enif_make_resource(env, js);
enif_release_resource(js);
return enif_make_tuple2(env, enif_make_atom(env, "ok"), result);
}
/* Evaluate JavaScript code and return the result */
static ERL_NIF_TERM nif_eval(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
JsContext *js;
if (!enif_get_resource(env, argv[0], JS_CONTEXT_TYPE, (void **)&js)) {
return enif_make_badarg(env);
}
ErlNifBinary code_bin;
if (!enif_inspect_binary(env, argv[1], &code_bin)) {
return enif_make_badarg(env);
}
/* Get timeout parameter (0 = no timeout) */
unsigned long timeout_ms = 0;
if (argc > 2) {
if (!enif_get_ulong(env, argv[2], &timeout_ms)) {
return enif_make_badarg(env);
}
}
/* Set deadline for timeout */
if (timeout_ms > 0) {
js->deadline_ms = get_time_ms() + timeout_ms;
} else {
js->deadline_ms = 0;
}
/*
* IMPORTANT: Copy the code to a local buffer before passing to JS_Eval.
*
* Passing the Erlang binary data pointer directly to JS_Eval causes
* intermittent parsing failures ("unexpected character" or undefined
* variable errors). The exact cause is unclear but may be related to
* how the BEAM manages binary memory. Copying to a malloc'd buffer
* eliminates this issue.
*
* See docs/known-issues.md for the investigation history.
*/
char *code_copy = malloc(code_bin.size + 1);
if (!code_copy) {
js->deadline_ms = 0; /* Reset deadline on error */
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "alloc_failed"));
}
memcpy(code_copy, code_bin.data, code_bin.size);
code_copy[code_bin.size] = '\0';
/* Use JS_EVAL_RETVAL to get the expression result */
JSValue result = JS_Eval(js->ctx, code_copy, code_bin.size, "eval", JS_EVAL_RETVAL);
free(code_copy);
/* Reset deadline after eval */
uint64_t deadline_was = js->deadline_ms;
js->deadline_ms = 0;
if (JS_IsException(result)) {
/* Check if this was a timeout */
if (deadline_was > 0 && get_time_ms() > deadline_was) {
/* Clear the exception */
JS_GetException(js->ctx);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "timeout"));
}
JSValue err = JS_GetException(js->ctx);
/* Try to get error message */
JSValue msg = JS_GetPropertyStr(js->ctx, err, "message");
if (JS_IsString(js->ctx, msg)) {
JSCStringBuf buf;
const char *str = JS_ToCString(js->ctx, msg, &buf);
/* Check for "interrupted" message which indicates timeout */
if (strcmp(str, "interrupted") == 0) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "timeout"));
}
size_t len = strlen(str);
ERL_NIF_TERM binary;
unsigned char *data = enif_make_new_binary(env, len, &binary);
memcpy(data, str, len);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
binary);
}
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "js_exception"));
}
/* Convert JS result to Elixir term */
ERL_NIF_TERM erl_result = js_to_erl(env, js->ctx, result, 0);
/* Check if conversion failed */
if (enif_is_tuple(env, erl_result)) {
int arity;
const ERL_NIF_TERM *tuple;
if (enif_get_tuple(env, erl_result, &arity, &tuple) && arity == 2) {
if (enif_is_identical(tuple[0], enif_make_atom(env, "error"))) {
return erl_result;
}
}
}
return enif_make_tuple2(env, enif_make_atom(env, "ok"), erl_result);
}
/* Get a global variable */
static ERL_NIF_TERM nif_get(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
JsContext *js;
if (!enif_get_resource(env, argv[0], JS_CONTEXT_TYPE, (void **)&js)) {
return enif_make_badarg(env);
}
/* Get the variable name */
char name_buf[256];
ErlNifBinary name_bin;
if (enif_inspect_binary(env, argv[1], &name_bin)) {
if (name_bin.size >= sizeof(name_buf)) {
return enif_make_badarg(env);
}
memcpy(name_buf, name_bin.data, name_bin.size);
name_buf[name_bin.size] = '\0';
} else if (enif_get_atom(env, argv[1], name_buf, sizeof(name_buf), ERL_NIF_LATIN1)) {
/* Already null-terminated */
} else {
return enif_make_badarg(env);
}
/* Get global object and then the property */
JSValue global = JS_GetGlobalObject(js->ctx);
JSValue val = JS_GetPropertyStr(js->ctx, global, name_buf);
if (JS_IsException(val)) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "get_failed"));
}
ERL_NIF_TERM result = js_to_erl(env, js->ctx, val, 0);
/* Check if conversion failed */
if (enif_is_tuple(env, result)) {
int arity;
const ERL_NIF_TERM *tuple;
if (enif_get_tuple(env, result, &arity, &tuple) && arity == 2) {
if (enif_is_identical(tuple[0], enif_make_atom(env, "error"))) {
return result;
}
}
}
return enif_make_tuple2(env, enif_make_atom(env, "ok"), result);
}
/* Set a value at a path in the global object */
static ERL_NIF_TERM nif_set_path(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
JsContext *js;
if (!enif_get_resource(env, argv[0], JS_CONTEXT_TYPE, (void **)&js)) {
return enif_make_badarg(env);
}
/* argv[1] = list of keys (atoms or binaries) */
/* argv[2] = value to set */
unsigned path_len;
if (!enif_get_list_length(env, argv[1], &path_len) || path_len == 0) {
return enif_make_badarg(env);
}
/* Extract keys into array - max 32 levels, 256 chars per key */
char keys[32][256];
if (path_len > 32) {
return enif_make_badarg(env);
}
ERL_NIF_TERM list = argv[1];
ERL_NIF_TERM head;
for (unsigned i = 0; i < path_len; i++) {
if (!enif_get_list_cell(env, list, &head, &list)) {
return enif_make_badarg(env);
}
/* Get key as string (atom or binary) */
ErlNifBinary bin;
if (enif_inspect_binary(env, head, &bin)) {
if (bin.size >= 256) return enif_make_badarg(env);
memcpy(keys[i], bin.data, bin.size);
keys[i][bin.size] = '\0';
} else if (!enif_get_atom(env, head, keys[i], 256, ERL_NIF_LATIN1)) {
return enif_make_badarg(env);
}
}
/* Walk path, creating objects as needed */
JSValue current = JS_GetGlobalObject(js->ctx);
for (unsigned i = 0; i < path_len - 1; i++) {
JSValue next = JS_GetPropertyStr(js->ctx, current, keys[i]);
if (JS_IsUndefined(next) || JS_IsNull(next)) {
/* Create new object and set it on current */
next = JS_NewObject(js->ctx);
JS_SetPropertyStr(js->ctx, current, keys[i], next);
} else {
/* Check if it's a plain object (not array, function, etc.) */
int class_id = JS_GetClassID(js->ctx, next);
if (class_id != JS_CLASS_OBJECT) {
const char *msg = "cannot set nested path: intermediate is not an object";
ERL_NIF_TERM binary;
unsigned char *data = enif_make_new_binary(env, strlen(msg), &binary);
memcpy(data, msg, strlen(msg));
return enif_make_tuple2(env, enif_make_atom(env, "error"), binary);
}
}
current = next;
}
/* Set final value */
JSValue js_val = erl_to_js(env, js->ctx, argv[2], 0);
if (JS_IsException(js_val)) {
JSValue err = JS_GetException(js->ctx);
JSValue msg = JS_GetPropertyStr(js->ctx, err, "message");
if (JS_IsString(js->ctx, msg)) {
JSCStringBuf buf;
const char *str = JS_ToCString(js->ctx, msg, &buf);
size_t len = strlen(str);
ERL_NIF_TERM binary;
unsigned char *data = enif_make_new_binary(env, len, &binary);
memcpy(data, str, len);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
binary);
}
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "conversion_failed"));
}
JSValue set_result = JS_SetPropertyStr(js->ctx, current, keys[path_len - 1], js_val);
if (JS_IsException(set_result)) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "set_failed"));
}
return enif_make_atom(env, "ok");
}
/* Trigger garbage collection */
static ERL_NIF_TERM nif_gc(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
JsContext *js;
if (!enif_get_resource(env, argv[0], JS_CONTEXT_TYPE, (void **)&js)) {
return enif_make_badarg(env);
}
JS_GC(js->ctx);
return enif_make_atom(env, "ok");
}
/* ============================================================================
* nif_run: Execute JS code with trampoline support
* ============================================================================ */
/*
* Helper: Check if an exception message starts with the yield marker.
* Returns 1 if it's a yield, 0 otherwise.
* If it's a yield, extracts func_name and args_json.
*/
static int is_yield_exception(const char *msg, const char **func_name, size_t *func_name_len,
const char **args_json, size_t *args_json_len)
{
size_t marker_len = strlen(YIELD_MARKER);
if (strncmp(msg, YIELD_MARKER, marker_len) != 0) {
return 0;
}
/* Parse: "__yield__:<func_name>:<args_json>" */
const char *rest = msg + marker_len;
const char *colon = strchr(rest, ':');
if (!colon) {
return 0;
}
*func_name = rest;
*func_name_len = (size_t)(colon - rest);
*args_json = colon + 1;
*args_json_len = strlen(*args_json);
return 1;
}
/*
* nif_run(ctx, code, cached_results_json) - Execute JS code with callback support.
*
* This NIF:
* 1. Sets up __yield as a global function
* 2. Sets __call_results from cached_results_json (list of JSON strings)
* 3. Injects the trampoline wrapper code
* 4. Evaluates the user code
* 5. If yield exception: returns {:yield, func_name, args_json}
* 6. If real exception: returns {:error, message}
* 7. If success: returns {:ok, result}
*
* The cached_results_json is a list of JSON strings from previous callback results.
*/
static ERL_NIF_TERM nif_run(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
JsContext *js;
if (!enif_get_resource(env, argv[0], JS_CONTEXT_TYPE, (void **)&js)) {
return enif_make_badarg(env);
}
ErlNifBinary code_bin;
if (!enif_inspect_binary(env, argv[1], &code_bin)) {
return enif_make_badarg(env);
}
/* argv[2] is the list of cached results (as JSON strings) */
if (!enif_is_list(env, argv[2])) {
return enif_make_badarg(env);
}
/* Get timeout parameter (0 = no timeout) - argv[3] */
unsigned long timeout_ms = 0;
if (argc > 3) {
if (!enif_get_ulong(env, argv[3], &timeout_ms)) {
return enif_make_badarg(env);
}
}
/* Set deadline for timeout */
if (timeout_ms > 0) {
js->deadline_ms = get_time_ms() + timeout_ms;
} else {
js->deadline_ms = 0;
}
/*
* Strategy: Use pure JavaScript for the yield mechanism.
*
* We inject a __call function that either returns a cached result
* or throws a special exception to yield control back to Elixir.
*
* JavaScript code structure:
* 1. var __call_results = [<cached_result_1>, <cached_result_2>, ...];
* 2. var __call_index = 0;
* 3. function __call(name, args) {
* if (__call_index < __call_results.length) {
* return __call_results[__call_index++];
* }
* throw new Error("__yield__:" + name + ":" + JSON.stringify(args));
* }
* 4. <user code>
*/
/* Build the setup code with cached results */
/* First, count the cached results and build the JSON array literal */
unsigned int cached_len;
if (!enif_get_list_length(env, argv[2], &cached_len)) {
return enif_make_badarg(env);
}
/* Build __call_results array literal: [result1, result2, ...] */
/* Each result is already a JSON string, so we just need to join them */
size_t results_buf_size = 2 + cached_len * 2; /* "[]" + commas */
ERL_NIF_TERM head, tail = argv[2];
while (enif_get_list_cell(env, tail, &head, &tail)) {
ErlNifBinary item_bin;
if (enif_inspect_binary(env, head, &item_bin)) {
results_buf_size += item_bin.size;
}
}
char *results_array = malloc(results_buf_size + 1);
if (!results_array) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "alloc_failed"));
}
char *p = results_array;
*p++ = '[';
tail = argv[2];
int first = 1;
while (enif_get_list_cell(env, tail, &head, &tail)) {
ErlNifBinary item_bin;
if (enif_inspect_binary(env, head, &item_bin)) {
if (!first) *p++ = ',';
first = 0;
memcpy(p, item_bin.data, item_bin.size);
p += item_bin.size;
}
}
*p++ = ']';
*p = '\0';
/* Build the full setup + user code */
const char *setup_template =
"var __call_results = %s;\n"
"var __call_index = 0;\n"
"function __call(name, args) {\n"
" if (__call_index < __call_results.length) {\n"
" return __call_results[__call_index++];\n"
" }\n"
" throw new Error(\"__yield__:\" + name + \":\" + JSON.stringify(args));\n"
"}\n";
size_t setup_len = strlen(setup_template) + strlen(results_array);
size_t full_len = setup_len + code_bin.size + 10;
char *full_code = malloc(full_len);
if (!full_code) {
free(results_array);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "alloc_failed"));
}
int written = snprintf(full_code, full_len, setup_template, results_array);
free(results_array);
memcpy(full_code + written, code_bin.data, code_bin.size);
full_code[written + code_bin.size] = '\0';
/* Evaluate the full code */
JSValue result = JS_Eval(js->ctx, full_code, written + code_bin.size, "run", JS_EVAL_RETVAL);
free(full_code);
/* Reset deadline after eval */
uint64_t deadline_was = js->deadline_ms;
js->deadline_ms = 0;
if (JS_IsException(result)) {
/* Check if this was a timeout */
if (deadline_was > 0 && get_time_ms() > deadline_was) {
/* Clear the exception */
JS_GetException(js->ctx);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "timeout"));
}
JSValue err = JS_GetException(js->ctx);
/* Try to get error message */
JSValue msg_val = JS_GetPropertyStr(js->ctx, err, "message");
if (JS_IsString(js->ctx, msg_val)) {
JSCStringBuf buf;
const char *msg = JS_ToCString(js->ctx, msg_val, &buf);
/* Check for "interrupted" message which indicates timeout */
if (strcmp(msg, "interrupted") == 0) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "timeout"));
}
/* Check if it's a yield exception */
const char *func_name;
size_t func_name_len;
const char *args_json;
size_t args_json_len;
if (is_yield_exception(msg, &func_name, &func_name_len,
&args_json, &args_json_len)) {
/* Return {:yield, func_name, args_json} */
ERL_NIF_TERM func_name_bin;
unsigned char *fn_data = enif_make_new_binary(env, func_name_len, &func_name_bin);
memcpy(fn_data, func_name, func_name_len);
ERL_NIF_TERM args_bin;
unsigned char *args_data = enif_make_new_binary(env, args_json_len, &args_bin);
memcpy(args_data, args_json, args_json_len);
return enif_make_tuple3(env,
enif_make_atom(env, "yield"),
func_name_bin,
args_bin);
}
/* Real exception - return error */
size_t msg_len = strlen(msg);
ERL_NIF_TERM msg_bin;
unsigned char *data = enif_make_new_binary(env, msg_len, &msg_bin);
memcpy(data, msg, msg_len);
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
msg_bin);
}
return enif_make_tuple2(env,
enif_make_atom(env, "error"),
enif_make_atom(env, "js_exception"));
}
/* Convert JS result to Elixir term */
ERL_NIF_TERM erl_result = js_to_erl(env, js->ctx, result, 0);
/* Check if conversion failed */
if (enif_is_tuple(env, erl_result)) {
int arity;
const ERL_NIF_TERM *tuple;
if (enif_get_tuple(env, erl_result, &arity, &tuple) && arity == 2) {
if (enif_is_identical(tuple[0], enif_make_atom(env, "error"))) {
return erl_result;
}
}
}
return enif_make_tuple2(env, enif_make_atom(env, "ok"), erl_result);
}
/* Resource destructor */
static void js_context_destructor(ErlNifEnv *env, void *obj)
{
JsContext *js = (JsContext *)obj;
if (js->ctx) {
JS_FreeContext(js->ctx);
js->ctx = NULL;
}
if (js->mem_buf) {
free(js->mem_buf);
js->mem_buf = NULL;
}
}
/* NIF initialization */
static int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info)
{
JS_CONTEXT_TYPE = enif_open_resource_type(env, NULL, "JsContext",
js_context_destructor, ERL_NIF_RT_CREATE, NULL);
if (!JS_CONTEXT_TYPE) {
return 1;
}
return 0;
}
static ErlNifFunc nif_funcs[] = {
{"nif_new", 1, nif_new, 0},
{"nif_eval", 3, nif_eval, 0},
{"nif_get", 2, nif_get, 0},
{"nif_set_path", 3, nif_set_path, 0},
{"nif_gc", 1, nif_gc, 0},
{"nif_run", 4, nif_run, 0},
};
ERL_NIF_INIT(Elixir.MquickjsEx.NIF, nif_funcs, load, NULL, NULL, NULL)