diff --git a/CMakeLists.txt b/CMakeLists.txt index 32484ee851..5231f057c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ option(AVM_RELEASE "Build an AtomVM release" OFF) option(AVM_CREATE_STACKTRACES "Create stacktraces" ON) option(AVM_BUILD_RUNTIME_ONLY "Only build the AtomVM runtime" OFF) option(COVERAGE "Build for code coverage" OFF) +option(AVM_USE_WASM_MJS "Use ES modules for Emscripten platform" OFF) option(AVM_PRINT_PROCESS_CRASH_DUMPS "Print crash reports when processes die with non-standard reasons" ON) # JIT & execution of precompiled code diff --git a/src/platforms/emscripten/src/CMakeLists.txt b/src/platforms/emscripten/src/CMakeLists.txt index 38da49320f..eb0452a309 100644 --- a/src/platforms/emscripten/src/CMakeLists.txt +++ b/src/platforms/emscripten/src/CMakeLists.txt @@ -28,8 +28,12 @@ add_subdirectory(../../../libAtomVM libAtomVM) target_link_libraries(AtomVM PUBLIC libAtomVM) target_compile_options(libAtomVM PUBLIC -O3 -fno-exceptions -fno-rtti -pthread -sINLINING_LIMIT -sUSE_ZLIB=1) target_compile_definitions(libAtomVM PRIVATE WITH_ZLIB) -target_link_options(AtomVM PRIVATE -sEXPORTED_RUNTIME_METHODS=ccall -sUSE_ZLIB=1 -O3 -pthread -sFETCH -lwebsocket.js --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/atomvm.pre.js) - +if (AVM_USE_WASM_MJS) + target_link_options(AtomVM PRIVATE -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,stringToNewUTF8 -sEMULATE_FUNCTION_POINTER_CASTS=1 -sEXPORTED_FUNCTIONS=_malloc,_cast,_call,_next_tracked_object_key,_main -sEXPORT_ES6=1 -sUSE_ZLIB=1 -O3 -pthread -sFETCH -lwebsocket.js --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/atomvm.pre.js) + set(CMAKE_EXECUTABLE_SUFFIX ".mjs") +else() + target_link_options(AtomVM PRIVATE -sEXPORTED_RUNTIME_METHODS=ccall -sUSE_ZLIB=1 -O3 -pthread -sFETCH -lwebsocket.js --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/atomvm.pre.js) +endif() if (CMAKE_BUILD_TYPE STREQUAL "Debug") target_link_options(AtomVM PRIVATE -sASSERTIONS=2 -sSAFE_HEAP -sSTACK_OVERFLOW_CHECK) endif() diff --git a/src/platforms/emscripten/src/atomvm.pre.js b/src/platforms/emscripten/src/atomvm.pre.js index 73ac9f647a..91f0f7484d 100644 --- a/src/platforms/emscripten/src/atomvm.pre.js +++ b/src/platforms/emscripten/src/atomvm.pre.js @@ -17,10 +17,60 @@ * * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later */ -Module['cast'] = function(name, message) { - ccall("cast", 'void', ['string', 'string'], [name, message]); +Module["cast"] = function (name, message) { + ccall("cast", "void", ["string", "string"], [name, message]); }; -Module['call'] = async function(name, message) { - const promiseId = ccall("call", 'integer', ['string', 'string'], [name, message]); - return promiseMap.get(promiseId).promise; +Module["call"] = async function (name, message) { + const promiseId = ccall( + "call", + "integer", + ["string", "string"], + [name, message], + ); + return promiseMap.get(promiseId).promise; }; + +// experimental API +Module["nextTrackedObjectKey"] = function () { + return ccall("next_tracked_object_key", "integer", [], []); +}; +Module["trackedObjectsMap"] = new Map(); +Module["onTrackedObjectDelete"] = (key) => { + Module["trackedObjectsMap"].delete(key); +}; +Module["onGetTrackedObjects"] = (keys) => { + const getTrackedObject = (key) => Module["trackedObjectsMap"].get(key); + return keys.map(getTrackedObject); +}; +Module["onRunTrackedJs"] = (scriptString, isDebug) => { + const trackValue = (value) => { + const key = Module["nextTrackedObjectKey"](); + Module["trackedObjectsMap"].set(key, value); + return key; + }; + + let result; + try { + const indirectEval = eval; + result = indirectEval(scriptString); + } catch (_e) { + return null; + } + isDebug && ensureValidResult(result); + return result?.map(trackValue) ?? []; +}; + +function ensureValidResult(result) { + const isIndex = (k) => typeof k === "number"; + + if (result === null) { + return; + } + if (Array.isArray(result) && keys.every(isIndex)) { + return; + } + + const message = + "Evaluated script returned invalid value. Expected number array or null"; + throw new Error(message); +} diff --git a/src/platforms/emscripten/src/lib/emscripten_sys.h b/src/platforms/emscripten/src/lib/emscripten_sys.h index 1889b2ab97..bbbab60656 100644 --- a/src/platforms/emscripten/src/lib/emscripten_sys.h +++ b/src/platforms/emscripten/src/lib/emscripten_sys.h @@ -113,6 +113,7 @@ struct EmscriptenPlatformData pthread_mutex_t poll_mutex; pthread_cond_t poll_cond; struct ListHead messages; + atomic_size_t next_tracked_object_key; ErlNifResourceType *promise_resource_type; ErlNifResourceType *htmlevent_user_data_resource_type; ErlNifResourceType *websocket_resource_type; @@ -134,6 +135,7 @@ void sys_enqueue_emscripten_cast_message(GlobalContext *glb, const char *target, em_promise_t sys_enqueue_emscripten_call_message(GlobalContext *glb, const char *target, const char *message); void sys_enqueue_emscripten_htmlevent_message(GlobalContext *glb, int32_t target_pid, term message, term user_data, HeapFragment *heap); void sys_enqueue_emscripten_unregister_htmlevent_message(GlobalContext *glb, struct HTMLEventUserDataResource *rsrc); +size_t sys_get_next_tracked_object_key(GlobalContext *glb); void sys_promise_resolve_int_and_destroy(em_promise_t promise, em_promise_result_t result, int value); void sys_promise_resolve_str_and_destroy(em_promise_t promise, em_promise_result_t result, int value); diff --git a/src/platforms/emscripten/src/lib/platform_defaultatoms.def b/src/platforms/emscripten/src/lib/platform_defaultatoms.def index 5ffa6a5bcb..9c4c0cb904 100644 --- a/src/platforms/emscripten/src/lib/platform_defaultatoms.def +++ b/src/platforms/emscripten/src/lib/platform_defaultatoms.def @@ -24,3 +24,4 @@ X(WEBSOCKET_ATOM, "\x9", "websocket") X(WEBSOCKET_OPEN_ATOM, "\xE", "websocket_open") X(WEBSOCKET_CLOSE_ATOM, "\xF", "websocket_close") X(WEBSOCKET_ERROR_ATOM, "\xF", "websocket_error") +X(BADVALUE_ATOM, "\x8", "badvalue") diff --git a/src/platforms/emscripten/src/lib/platform_nifs.c b/src/platforms/emscripten/src/lib/platform_nifs.c index ed0a75f871..20443df72d 100644 --- a/src/platforms/emscripten/src/lib/platform_nifs.c +++ b/src/platforms/emscripten/src/lib/platform_nifs.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -157,6 +158,289 @@ static term nif_emscripten_promise_reject(Context *ctx, int argc, term argv[]) return nif_emscripten_promise_resolve_reject(ctx, argc, argv, EM_PROMISE_REJECT); } +static term term_tracked_object_from_key(Context *ctx, atomic_size_t key) +{ + struct EmscriptenPlatformData *platform = ctx->global->platform_data; + struct TrackedObjectResource *rsrc_obj = enif_alloc_resource(platform->tracked_object_resource_type, sizeof(struct TrackedObjectResource)); + if (IS_NULL_PTR(rsrc_obj)) { + return term_invalid_term(); + } + rsrc_obj->key = key; + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + return obj; +} + +// clang-format off +EM_JS(uint32_t *, js_tracked_eval, (const char *code, uint32_t *size, bool debug), { + const keys = Module['onRunTrackedJs'](UTF8ToString(code), debug); + const error = keys === null; + if (error) { + HEAPU32[size / HEAPU32.BYTES_PER_ELEMENT] = 0; + return 0; + } + + const ptr = Module['_malloc'](keys.length * HEAPU32.BYTES_PER_ELEMENT); + HEAPU32[size / HEAPU32.BYTES_PER_ELEMENT] = keys.length; + HEAPU32.set(keys, ptr / HEAPU32.BYTES_PER_ELEMENT); + return ptr; +}); +// clang-format on + +static void do_run_script_tracked(const char *script, int32_t sync_caller_pid, GlobalContext *global) +{ +#ifdef NDEBUG + bool debug = false; +#else + bool debug = true; +#endif + uint32_t keys_n; + uint32_t *keys = js_tracked_eval(script, &keys_n, debug); + Context *target_ctx = globalcontext_get_process_lock(global, sync_caller_pid); + if (target_ctx) { + term result = term_invalid_term(); + term refs = term_nil(); + if (UNLIKELY(memory_ensure_free_opt(target_ctx, TUPLE_SIZE(2) + LIST_SIZE(keys_n, TERM_BOXED_REFC_BINARY_SIZE), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + // TODO: how to raise? + result = OUT_OF_MEMORY_ATOM; + goto send_result; + } + result = term_alloc_tuple(2, &target_ctx->heap); + + if (IS_NULL_PTR(keys)) { + term_put_tuple_element(result, 0, ERROR_ATOM); + term_put_tuple_element(result, 1, BADARG_ATOM); + goto send_result; + } + + if (keys_n == 0) { + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, term_nil()); + goto send_result; + } + + for (long i = keys_n - 1; i >= 0; --i) { + term tracked_object = term_tracked_object_from_key(target_ctx, keys[i]); + // we can't easily recover from OOM here + assert(!term_is_invalid_term(tracked_object)); + refs = term_list_prepend(tracked_object, refs, &target_ctx->heap); + } + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, refs); + + send_result: + free(keys); + mailbox_send_term_signal(target_ctx, TrapAnswerSignal, result); + globalcontext_get_process_unlock(global, target_ctx); + } else { + // sender died + free(keys); + } +} + +static term nif_emscripten_run_script_tracked(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term script_term = argv[0]; + + int ok; + char *script = interop_term_to_string(script_term, &ok); + if (UNLIKELY(!ok)) { + RAISE_ERROR(BADARG_ATOM); + } + // Trap caller waiting for completion + context_update_flags(ctx, ~NoFlags, Trap); + // script will be freed as it's passed as satellite + emscripten_dispatch_to_thread(emscripten_main_runtime_thread_id(), EM_FUNC_SIG_VIII, do_run_script_tracked, script, script, ctx->process_id, ctx->global); + return term_invalid_term(); +} + +// clang-format off +EM_JS(char *, js_get_tracked_objects, (uint32_t *keys_ptr, uint32_t keys_n, uint32_t **sizes, uint8_t **statuses, uint32_t *objects_n, uint32_t *strings_n, uint32_t *all_byte_size), { + const OK = 0; + const BAD_KEY = 1; + const NOT_STRING = 2; + + const keysOffset = keys_ptr / HEAPU32.BYTES_PER_ELEMENT; + const keys = [...HEAPU32.subarray(keysOffset, keysOffset + keys_n)]; + + const objects = Module['onGetTrackedObjects'](keys); + if (!Array.isArray(objects)) { + return 0; + } + const n = objects.length; + + if (n === 0) { + return 0; + } + + const sizesPtr = Module['_malloc'](n * HEAPU32.BYTES_PER_ELEMENT); + const statusPtr = Module['_malloc'](n * HEAPU8.BYTES_PER_ELEMENT); + let allByteSize = 0; + let stringsN = 0; + + for (let i = 0; i < n; ++i) { + let status = OK; + let byteSize = 0; + const object = objects[i]; + if (object === undefined) { + status = BAD_KEY; + } else if (typeof object !== "string") { + status = NOT_STRING; + } else { + byteSize = lengthBytesUTF8(object); + stringsN += 1; + } + HEAPU8[statusPtr / HEAPU8.BYTES_PER_ELEMENT + i] = status; + HEAPU32[sizesPtr / HEAPU32.BYTES_PER_ELEMENT + i] = byteSize; + allByteSize += byteSize; + } + + const stringsPtr = Module['_malloc'](allByteSize * HEAPU8.BYTES_PER_ELEMENT); + let currentStringsPtr = stringsPtr; + for (let i = 0; i < n; ++i) { + const status = HEAPU8[statusPtr / HEAPU8.BYTES_PER_ELEMENT + i]; + if (status !== OK) { + continue; + } + const string = objects[i]; + const size = HEAPU32[sizesPtr / HEAPU32.BYTES_PER_ELEMENT + i]; + // stringToUTF8 includes null byte which we don't need + stringToUTF8(string, currentStringsPtr, size+1); + currentStringsPtr += size; + } + + HEAPU32[statuses / HEAPU32.BYTES_PER_ELEMENT] = statusPtr; + HEAPU32[sizes / HEAPU32.BYTES_PER_ELEMENT] = sizesPtr; + HEAPU32[objects_n / HEAPU32.BYTES_PER_ELEMENT] = n; + HEAPU32[strings_n / HEAPU32.BYTES_PER_ELEMENT] = stringsN; + HEAPU32[all_byte_size / HEAPU32.BYTES_PER_ELEMENT] = allByteSize; + + return stringsPtr; +}); +// clang-format on + +static void do_get_tracked_objects(uint32_t *ref_keys, size_t keys_n, int32_t sync_caller_pid, GlobalContext *global) +{ + static const uint8_t OK = 0; + static const uint8_t BAD_KEY = 1; + static const uint8_t NOT_STRING = 2; + UNUSED(BAD_KEY); + + uint32_t objects_n = 0; + uint32_t strings_n = 0; + uint32_t all_byte_size = 0; + uint32_t *sizes = NULL; + uint8_t *statuses = NULL; + + char *strings = js_get_tracked_objects(ref_keys, keys_n, &sizes, &statuses, &objects_n, &strings_n, &all_byte_size); + assert(strings_n <= objects_n); + Context *target_ctx = globalcontext_get_process_lock(global, sync_caller_pid); + if (target_ctx) { + term result = term_invalid_term(); + if (IS_NULL_PTR(strings)) { + result = BADVALUE_ATOM; + goto send_result; + } + size_t size = LIST_SIZE(objects_n, TUPLE_SIZE(2)) + LIST_SIZE(strings_n, BINARY_HEADER_SIZE) + term_binary_data_size_in_terms(all_byte_size); + if (UNLIKELY(memory_ensure_free_opt(target_ctx, size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto send_result; + } + + // move pointer to one byte past buffer + const char *current_string = strings + all_byte_size; + result = term_nil(); + for (long i = objects_n - 1; i >= 0; --i) { + uint8_t status = statuses[i]; + + term tuple = term_alloc_tuple(2, &target_ctx->heap); + if (status == OK) { + size_t size = sizes[i]; + current_string -= size; + + term binary = term_create_uninitialized_binary(size, &target_ctx->heap, global); + char *data = (char *) term_binary_data(binary); + memcpy(data, current_string, size); + + term_put_tuple_element(tuple, 0, OK_ATOM); + term_put_tuple_element(tuple, 1, binary); + } else if (status == NOT_STRING) { + term_put_tuple_element(tuple, 0, ERROR_ATOM); + term_put_tuple_element(tuple, 1, BADVALUE_ATOM); + } else /* BAD_KEY */ { + term_put_tuple_element(tuple, 0, ERROR_ATOM); + term_put_tuple_element(tuple, 1, BADKEY_ATOM); + } + result = term_list_prepend(tuple, result, &target_ctx->heap); + } + + send_result: + free(sizes); + free(statuses); + free(strings); + mailbox_send_term_signal(target_ctx, TrapAnswerSignal, result); + globalcontext_get_process_unlock(global, target_ctx); + } // else: sender died +} + +static term nif_emscripten_get_tracked(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term refs = argv[0]; + term type = argv[1]; + struct EmscriptenPlatformData *platform = ctx->global->platform_data; + ErlNifEnv *env = erl_nif_env_from_context(ctx); + + VALIDATE_VALUE(refs, term_is_list); + if (UNLIKELY(type != KEY_ATOM && type != VALUE_ATOM)) { + RAISE_ERROR(BADARG_ATOM); + } + int proper; + size_t n = term_list_length(refs, &proper); + if (UNLIKELY(!proper)) { + RAISE_ERROR(BADARG_ATOM); + } + + if (n == 0) { + RAISE_ERROR(BADARG_ATOM); + } + + int32_t *ref_keys = malloc(n * sizeof(int32_t)); + if (IS_NULL_PTR(ref_keys)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + for (size_t i = 0; i < n; ++i) { + term ref = term_get_list_head(refs); + void *obj; + if (UNLIKELY(!enif_get_resource(env, ref, platform->tracked_object_resource_type, &obj))) { + free(ref_keys); + RAISE_ERROR(BADARG_ATOM); + } + struct TrackedObjectResource *tracked_object_rsrc = (struct TrackedObjectResource *) obj; + ref_keys[i] = tracked_object_rsrc->key; + refs = term_get_list_tail(refs); + } + + if (type == KEY_ATOM) { + if (UNLIKELY(memory_ensure_free_opt(ctx, LIST_SIZE(n, 1), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term keys = term_nil(); + for (long i = n - 1; i >= 0; --i) { + keys = term_list_prepend(term_from_int32(ref_keys[i]), keys, &ctx->heap); + } + return keys; + } + assert(type == VALUE_ATOM); + // Trap caller waiting for completion + context_update_flags(ctx, ~NoFlags, Trap); + emscripten_dispatch_to_thread(emscripten_main_runtime_thread_id(), EM_FUNC_SIG_VIIII, do_get_tracked_objects, NULL, ref_keys, n, ctx->process_id, ctx->global); + return term_invalid_term(); +} + static const struct Nif atomvm_platform_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_atomvm_platform @@ -177,6 +461,14 @@ static const struct Nif emscripten_promise_reject_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_emscripten_promise_reject }; +static const struct Nif emscripten_run_script_tracked = { + .base.type = NIFFunctionType, + .nif_ptr = nif_emscripten_run_script_tracked +}; +static const struct Nif emscripten_get_tracked = { + .base.type = NIFFunctionType, + .nif_ptr = nif_emscripten_get_tracked, +}; static bool get_callback_target(Context *ctx, term t, const char **target, char **str) { @@ -788,6 +1080,12 @@ const struct Nif *platform_nifs_get_nif(const char *nifname) if (strcmp("run_script/2", nifname) == 0) { return &emscripten_run_script_nif; } + if (strcmp("run_script_tracked/1", nifname) == 0) { + return &emscripten_run_script_tracked; + } + if (strcmp("get_tracked/2", nifname) == 0) { + return &emscripten_get_tracked; + } if (strcmp("promise_resolve/1", nifname) == 0) { return &emscripten_promise_resolve_nif; } diff --git a/src/platforms/emscripten/src/lib/sys.c b/src/platforms/emscripten/src/lib/sys.c index ab8c0404b7..6c513f347c 100644 --- a/src/platforms/emscripten/src/lib/sys.c +++ b/src/platforms/emscripten/src/lib/sys.c @@ -48,6 +48,12 @@ #include "platform_defaultatoms.h" #include "websocket_nifs.h" +size_t sys_get_next_tracked_object_key(GlobalContext *glb) +{ + struct EmscriptenPlatformData *platform = glb->platform_data; + return platform->next_tracked_object_key++; +} + /** * @brief resolve a promise with an int value and destroy it * @details called on the main thread using `emscripten_dispatch_to_thread` @@ -158,6 +164,7 @@ void sys_init_platform(GlobalContext *glb) AVM_ABORT(); } list_init(&platform->messages); + platform->next_tracked_object_key = 0; ErlNifEnv env; erl_nif_env_partial_init_from_globalcontext(&env, glb); platform->promise_resource_type = enif_init_resource_type(&env, "promise", &promise_resource_type_init, ERL_NIF_RT_CREATE, NULL); diff --git a/src/platforms/emscripten/src/main.c b/src/platforms/emscripten/src/main.c index c8c00dc07a..0f17372690 100644 --- a/src/platforms/emscripten/src/main.c +++ b/src/platforms/emscripten/src/main.c @@ -138,6 +138,16 @@ em_promise_t call(const char *name, const char *message) return sys_enqueue_emscripten_call_message(global, name, message); } +/** + * @brief Gets a number representing TrackedObject identity. + * @return a TrackedObject id. + */ +EMSCRIPTEN_KEEPALIVE +size_t next_tracked_object_key() +{ + return sys_get_next_tracked_object_key(global); +} + /** * @brief Emscripten entry point * @details For node builds, this function is run in the main thread. For web