diff --git a/MODULE.bazel b/MODULE.bazel index 03062e606fa..20b6d03e3f0 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -57,6 +57,28 @@ filegroup( ] * 5, ) +http_file( + name = "mozjs_wasm", + downloaded_file_path = "mozjs_wasm_api.wasm", + sha256 = "3d5a780b657fdfe66cc2f45f141e9e89e9bcb5ab7a1c90f0fb4778b5c0080b15", + urls = [ + # Implements retry by relisting each url multiple times to be used as a failover. + # TODO(SERVER-86719): Re-implement http_archive to allow sleeping between retries + "https://mdb-build-public.s3.us-east-1.amazonaws.com/wasm_scratch/mozjs_wasm_api.wasm", + ] * 5, +) + +http_file( + name = "spidermonkey_wasip2_dist", + downloaded_file_path = "spidermonkey-wasip2-release.tar.gz", + sha256 = "f528e5730721559781aef47d355079da63185588bb14ebb6bf5266d4aa5e3eb0", + urls = [ + # Implements retry by relisting each url multiple times to be used as a failover. + # TODO(SERVER-86719): Re-implement http_archive to allow sleeping between retries + "https://mdb-build-public.s3.us-east-1.amazonaws.com/wasm_scratch/spidermonkey-wasip2-release.tar.gz", + ] * 5, +) + # SourceGraph indexer http_file( name = "scip-clang", @@ -362,3 +384,14 @@ wasi_deps = use_repo_rule( ) wasi_deps(name = "wasi_sdk") + +spidermonkey_repo = use_repo_rule( + "//src/mongo/scripting/mozjs/wasm:spidermonkey_repo.bzl", + "spidermonkey_repository", +) + +spidermonkey_repo( + name = "spidermonkey", + repository_file = "//src/mongo/scripting/mozjs/wasm/spider-monkey:spider-monkey-repository", + version_file = "//src/mongo/scripting/mozjs/wasm/spider-monkey:spider-monkey-version", +) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7a46b04e10e..64241304efa 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1306,7 +1306,7 @@ "usagesDigest": "9e6XGcxkYrSsu0+gftPsVGZrX02VFK68P4HmulrwVAE=", "recordedFileInputs": { "@@//src/third_party/wasmtime/BUILD.bazel.wasmtime": "f8f59cc6f0f55ead667c8b438e3d056250807cbb565b0cfe1c4b7bce196ca22f", - "@@//MODULE.bazel": "8fc6eef7f959c870cba70c101a56f6e7148dfde24ce476d7f02f0e4002c929ea", + "@@//MODULE.bazel": "dc17238883ec055ae6309e1489ddfec05de2c74dd5fd3671ce4153b9316d4a46", "@@rules_rust~~rust_host_tools~rust_host_tools//rust_host_tools": "ENOENT" }, "recordedDirentsInputs": {}, diff --git a/bazel/toolchains/cc/mongo_defines.bzl b/bazel/toolchains/cc/mongo_defines.bzl index 57a6486932a..683632322a3 100644 --- a/bazel/toolchains/cc/mongo_defines.bzl +++ b/bazel/toolchains/cc/mongo_defines.bzl @@ -18,7 +18,6 @@ BOOST_DEFINES = [ # our current Xcode 12 doesn't offer std::atomic_ref, so we cannot. "BOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF", "BOOST_LOG_NO_SHORTHAND_NAMES", - "BOOST_LOG_USE_NATIVE_SYSLOG", "BOOST_LOG_WITHOUT_THREAD_ATTR", "BOOST_MATH_NO_LONG_DOUBLE_MATH_FUNCTIONS", "BOOST_SYSTEM_NO_DEPRECATED", @@ -27,6 +26,10 @@ BOOST_DEFINES = [ ] + select({ "@//bazel/config:linkdynamic_not_shared_archive": ["BOOST_LOG_DYN_LINK"], "@//conditions:default": [], +}) + select({ + "@platforms//os:windows": ["BOOST_LOG_WITHOUT_SYSLOG"], + "@platforms//os:wasi": ["BOOST_LOG_WITHOUT_SYSLOG"], + "@//conditions:default": ["BOOST_LOG_USE_NATIVE_SYSLOG"], }) ENTERPRISE_DEFINES = select({ diff --git a/bazel/toolchains/cc/mongo_wasm/toolchain/cc_toolchain_config_wasip2.bzl b/bazel/toolchains/cc/mongo_wasm/toolchain/cc_toolchain_config_wasip2.bzl index 3afbfaeef0f..323247a5b92 100644 --- a/bazel/toolchains/cc/mongo_wasm/toolchain/cc_toolchain_config_wasip2.bzl +++ b/bazel/toolchains/cc/mongo_wasm/toolchain/cc_toolchain_config_wasip2.bzl @@ -83,13 +83,21 @@ def _wasi_cc_toolchain_config_wasip2_impl(ctx): # can be used to find it if we use a different repository rule later. "--sysroot={}".format("external/_main~_repo_rules~wasi_sdk/share/wasi-sysroot"), "-fno-common", - "-fno-omit-frame-pointer", + "-Oz", + "-ffunction-sections", + "-fdata-sections", + "-fvisibility=hidden", "-U_FORTIFY_SOURCE", "-D_FORTIFY_SOURCE=0", + + # WASI emulation shims - enable syscall emulation for POSIX APIs "-D_WASI_EMULATED_SIGNAL", "-D_WASI_EMULATED_MMAN", "-D_WASI_EMULATED_PROCESS_CLOCKS", "-D_WASI_EMULATED_GETPID", + + # WASI platform lacks syslog support + "-DBOOST_LOG_WITHOUT_SYSLOG", ])], )], ) @@ -103,6 +111,12 @@ def _wasi_cc_toolchain_config_wasip2_impl(ctx): flag_groups = [flag_group(flags = [ "-std=c++20", "-fexceptions", + + # Include headers for declarations needed by third-party code + "-include", + "wasm32-wasip2/assert.h", # assert() for Abseil + "-include", + "stdlib.h", # malloc/free for fmt ])], )], ) @@ -115,16 +129,77 @@ def _wasi_cc_toolchain_config_wasip2_impl(ctx): actions = all_link_actions, flag_groups = [flag_group(flags = [ "-Wl,--gc-sections", + "-Wl,--strip-all", "-lc++", "-lc++abi", + "-lwasi-emulated-signal", + "-lwasi-emulated-mman", + "-lwasi-emulated-process-clocks", + "-lwasi-emulated-getpid", ])], )], ) + # Include path features (mirrors the Linux toolchain). + # Without these, external-repo headers (like Abseil) are only added via + # -iquote (quoted includes) and includes fail. + all_compile = [ + ACTION_NAMES.preprocess_assemble, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.c_compile, + ACTION_NAMES.cpp_compile, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.clif_match, + ACTION_NAMES.objc_compile, + ACTION_NAMES.objcpp_compile, + ] + + include_paths_feature = feature( + name = "include_paths", + enabled = True, + flag_sets = [flag_set( + actions = all_compile, + flag_groups = [ + flag_group( + flags = ["-iquote", "%{quote_include_paths}"], + iterate_over = "quote_include_paths", + ), + flag_group( + flags = ["-I%{include_paths}"], + iterate_over = "include_paths", + ), + flag_group( + flags = ["-isystem", "%{system_include_paths}"], + iterate_over = "system_include_paths", + ), + ], + )], + ) + + external_include_paths_feature = feature( + name = "external_include_paths", + enabled = True, + flag_sets = [flag_set( + actions = all_compile, + flag_groups = [ + flag_group( + flags = ["-isystem", "%{external_include_paths}"], + iterate_over = "external_include_paths", + expand_if_available = "external_include_paths", + ), + ], + )], + ) + + # WASI SDK sysroot path (relative to execroot) + wasi_sysroot = "external/_main~_repo_rules~wasi_sdk/share/wasi-sysroot" + # Construct the toolchain. return [cc_common.create_cc_toolchain_config_info( ctx = ctx, action_configs = action_configs, + tool_paths = tool_paths, toolchain_identifier = "wasi_sdk_cc_wasip2", host_system_name = "local", target_system_name = "wasi", @@ -133,11 +208,18 @@ def _wasi_cc_toolchain_config_wasip2_impl(ctx): compiler = "clang", abi_version = "none", abi_libc_version = "wasi", + builtin_sysroot = wasi_sysroot, + cxx_builtin_include_directories = [ + wasi_sysroot + "/include", + wasi_sysroot + "/include/wasm32-wasip2", + ], features = [ compile_flags, cxx20_flags, link_flags, - feature(name = "supports_dynamic_linker", enabled = True), + include_paths_feature, + external_include_paths_feature, + feature(name = "supports_dynamic_linker", enabled = False), ], )] diff --git a/buildscripts/linter/mongolint.py b/buildscripts/linter/mongolint.py index 94474c1464c..fd8d98c52e4 100644 --- a/buildscripts/linter/mongolint.py +++ b/buildscripts/linter/mongolint.py @@ -232,6 +232,7 @@ class Linter: files_to_ignore = set( [ "src/mongo/scripting/mozjs/shell/PosixNSPR.cpp", + "src/mongo/scripting/mozjs/wasm/wit_gen/generated/", "src/mongo/shell/linenoise.cpp", "src/mongo/shell/linenoise.h", "src/mongo/shell/mk_wcwidth.cpp", diff --git a/etc/tsan.suppressions b/etc/tsan.suppressions index f67ba3a9fcf..bb5707ba8c3 100644 --- a/etc/tsan.suppressions +++ b/etc/tsan.suppressions @@ -65,3 +65,14 @@ deadlock:mongo::(anonymous namespace)::SSLManagerOpenSSL::SSLManagerOpenSSL race:S2Loop::FindVertex race:S2EdgeIndex::IncrementQueryCount race:S2RegionCoverer::NewCandidate + +# SERVER-115422: wasmtime's parallel compilation (via rayon) triggers TSan false positives. +# TSan cannot unwind Rust stacks fully, so reported frames vary across runs: sometimes +# wasmtime/rayon/cranelift-specific, sometimes bare Rust stdlib functions (core::ptr, +# core::mem, alloc::, __rustc allocator). These patterns cover all observed variants. +race:rayon_core +race:wasmtime +race:cranelift +race:__rustc +race:core::ptr +race:core::mem diff --git a/src/mongo/logv2/log_domain_global.cpp b/src/mongo/logv2/log_domain_global.cpp index fea9c060275..16d8360faef 100644 --- a/src/mongo/logv2/log_domain_global.cpp +++ b/src/mongo/logv2/log_domain_global.cpp @@ -27,7 +27,6 @@ * it in the license file. */ - #include "mongo/logv2/log_domain_global.h" #include @@ -92,7 +91,14 @@ struct LogDomainGlobal::Impl { RamLogSink, UserAssertSink> ConsoleBackend; -#ifndef _WIN32 +// Syslog support is conditionally compiled using Boost's BOOST_LOG_USE_NATIVE_SYSLOG macro. +// This macro is defined by Boost.Log only when the platform provides a native syslog +// implementation (via ). Platforms that lack syslog, notably WASI and Windows +// will not have this macro defined, causing all syslog-related +// types, members, and logic to be excluded from compilation. This avoids link errors and +// unavailable-API references on those platforms without requiring us to maintain our own +// platform-detection logic. +#ifdef BOOST_LOG_USE_NATIVE_SYSLOG typedef CompositeBackend> _consoleSink; boost::shared_ptr> _rotatableFileSink; boost::shared_ptr> _backtraceSink; -#ifndef _WIN32 +#ifdef BOOST_LOG_USE_NATIVE_SYSLOG boost::shared_ptr> _syslogSink; #endif AtomicWord activeSourceThreadLocals{0}; @@ -152,7 +158,7 @@ LogDomainGlobal::Impl::Impl(LogDomainGlobal& parent) : _parent(parent) { } Status LogDomainGlobal::Impl::configure(LogDomainGlobal::ConfigurationOptions const& options) { -#ifndef _WIN32 +#ifdef BOOST_LOG_USE_NATIVE_SYSLOG if (options.syslogEnabled) { // Create a backend auto backend = boost::make_shared( @@ -244,7 +250,7 @@ Status LogDomainGlobal::Impl::configure(LogDomainGlobal::ConfigurationOptions co _consoleSink->set_formatter(mkFmt()); if (_rotatableFileSink) _rotatableFileSink->set_formatter(mkFmt()); -#ifndef _WIN32 +#ifdef BOOST_LOG_USE_NATIVE_SYSLOG if (_syslogSink) _syslogSink->set_formatter(mkFmt()); #endif diff --git a/src/mongo/platform/process_id.cpp b/src/mongo/platform/process_id.cpp index f76ed062d62..c8c52726dbd 100644 --- a/src/mongo/platform/process_id.cpp +++ b/src/mongo/platform/process_id.cpp @@ -80,6 +80,13 @@ inline NativeProcessId getCurrentNativeThreadId() { inline NativeProcessId getCurrentNativeThreadId() { return pthread_getthreadid_np(); } +#elif defined(__wasi__) +inline NativeProcessId getCurrentNativeThreadId() { + // WASI doesn't support threads or thread IDs, so return a constant value + // Using the SERVER-115422 ticket number as a return number for better debugging + // and searchability in logs. + return 115422; +} #else inline NativeProcessId getCurrentNativeThreadId() { return ::syscall(SYS_gettid); diff --git a/src/mongo/platform/random.cpp b/src/mongo/platform/random.cpp index 8ec032a11cd..d8712a1d1c3 100644 --- a/src/mongo/platform/random.cpp +++ b/src/mongo/platform/random.cpp @@ -57,7 +57,8 @@ #ifdef _WIN32 #define SECURE_RANDOM_BCRYPT -#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__EMSCRIPTEN__) +#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || \ + defined(__EMSCRIPTEN__) || defined(__wasi__) #define SECURE_RANDOM_URANDOM #elif defined(__OpenBSD__) #define SECURE_RANDOM_ARCFOUR diff --git a/src/mongo/platform/waitable_atomic.cpp b/src/mongo/platform/waitable_atomic.cpp index 36e85fe6dfa..06d9d23e89c 100644 --- a/src/mongo/platform/waitable_atomic.cpp +++ b/src/mongo/platform/waitable_atomic.cpp @@ -235,6 +235,39 @@ bool waitUntil(const void* uaddr, return timeoutOverflow || errno != ETIMEDOUT; } +#elif defined(__wasi__) + +// WASI is single-threaded and has no futex or atomic-wait primitives. +// notify* are no-ops; waitUntil returns false (timeout) when the value +// hasn't changed because a single-threaded environment can never receive +// an external wake-up. Returning true would cause an infinite busy-spin. + +void notifyOne(const void* uaddr) { + (void)uaddr; +} + +void notifyMany(const void* uaddr, int nToWake) { + (void)uaddr; + (void)nToWake; +} + +void notifyAll(const void* uaddr) { + (void)uaddr; +} + +bool waitUntil(const void* uaddr, + uint32_t old, + boost::optional deadline) { + // Value already changed before we started waiting. + if (*static_cast(uaddr) != old) { + return true; + } + + // Single-threaded: the value cannot change during this call. + // Return false (timeout) to avoid infinite busy-spin. + return false; +} + #else #error "Need an implementation of waitUntil(), notifyOne(), notifyMany(), notifyAll() for this OS" #endif diff --git a/src/mongo/scripting/BUILD.bazel b/src/mongo/scripting/BUILD.bazel index 48a4fd68473..4f726754b59 100644 --- a/src/mongo/scripting/BUILD.bazel +++ b/src/mongo/scripting/BUILD.bazel @@ -1,5 +1,4 @@ -load("@bazel_skylib//lib:selects.bzl", "selects") -load("//bazel:mongo_src_rules.bzl", "idl_generator", "mongo_cc_binary", "mongo_cc_library", "mongo_cc_test", "mongo_cc_unit_test") +load("//bazel:mongo_src_rules.bzl", "idl_generator", "mongo_cc_library", "mongo_cc_unit_test") package(default_visibility = ["//visibility:public"]) diff --git a/src/mongo/scripting/engine.cpp b/src/mongo/scripting/engine.cpp index 245a827d9e9..4f4ccd5d8c8 100644 --- a/src/mongo/scripting/engine.cpp +++ b/src/mongo/scripting/engine.cpp @@ -261,14 +261,6 @@ void Scope::storedFuncMod(OperationContext* opCtx) { [](OperationContext*, boost::optional) { _lastVersion.fetchAndAdd(1); }); } -void Scope::validateObjectIdString(const string& str) { - uassert(10448, "invalid object id: length", str.size() == 24); - auto isAllHex = [](StringData s) { - return std::all_of(s.begin(), s.end(), [](char c) { return ctype::isXdigit(c); }); - }; - uassert(10430, "invalid object id: not hex", isAllHex(str)); -} - void Scope::loadStored(OperationContext* opCtx, bool ignoreNotConnected) { if (_localDBName.isEmpty()) { if (ignoreNotConnected) diff --git a/src/mongo/scripting/engine.h b/src/mongo/scripting/engine.h index c3d8f80fd68..a8a2b3f3adf 100644 --- a/src/mongo/scripting/engine.h +++ b/src/mongo/scripting/engine.h @@ -39,6 +39,7 @@ #include "mongo/db/service_context.h" #include "mongo/platform/atomic_word.h" #include "mongo/platform/decimal128.h" +#include "mongo/scripting/js_regex.h" #include "mongo/util/assert_util.h" #include "mongo/util/modules.h" #include "mongo/util/time_support.h" @@ -67,15 +68,6 @@ struct MONGO_MOD_NEEDS_REPLACEMENT JSFile { const StringData source; }; -struct MONGO_MOD_PUBLIC JSRegEx { - std::string pattern; - std::string flags; - - JSRegEx() = default; - JSRegEx(std::string pattern, std::string flags) - : pattern(std::move(pattern)), flags(std::move(flags)) {} -}; - class MONGO_MOD_OPEN Scope { Scope(const Scope&) = delete; Scope& operator=(const Scope&) = delete; @@ -209,8 +201,6 @@ public: */ static void storedFuncMod(OperationContext* opCtx); - static void validateObjectIdString(const std::string& str); - /** gets the time at which the scope was created */ Date_t getCreateTime() const { return _createTime; diff --git a/src/mongo/scripting/mozjs/common/scope_access.cpp b/src/mongo/scripting/js_regex.h similarity index 83% rename from src/mongo/scripting/mozjs/common/scope_access.cpp rename to src/mongo/scripting/js_regex.h index fbec22947db..e2813d51ca9 100644 --- a/src/mongo/scripting/mozjs/common/scope_access.cpp +++ b/src/mongo/scripting/js_regex.h @@ -27,14 +27,20 @@ * it in the license file. */ -#include "mongo/scripting/mozjs/common/scope_base.h" +#pragma once -#include +#include +#include -namespace mongo::mozjs { +namespace mongo { -MozJSScopeBase* getMozJSScope(JSContext* cx) { - return static_cast(JS_GetContextPrivate(cx)); -} +struct JSRegEx { + std::string pattern; + std::string flags; -} // namespace mongo::mozjs + JSRegEx() = default; + JSRegEx(std::string pattern, std::string flags) + : pattern(std::move(pattern)), flags(std::move(flags)) {} +}; + +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/common/BUILD.bazel b/src/mongo/scripting/mozjs/common/BUILD.bazel index 46df713bfc6..b2d62e85aaa 100644 --- a/src/mongo/scripting/mozjs/common/BUILD.bazel +++ b/src/mongo/scripting/mozjs/common/BUILD.bazel @@ -10,26 +10,40 @@ exports_files( ]), ) +# Type sources shared between WASI and non-WASI builds. +# status.cpp is excluded because WASI and non-WASI builds use different +# implementations (wasm stub vs common/types). +TYPES_SRCS = [ + "//src/mongo/scripting/mozjs/common/types:bindata.cpp", + "//src/mongo/scripting/mozjs/common/types:bson.cpp", + "//src/mongo/scripting/mozjs/common/types:code.cpp", + "//src/mongo/scripting/mozjs/common/types:dbpointer.cpp", + "//src/mongo/scripting/mozjs/common/types:dbref.cpp", + "//src/mongo/scripting/mozjs/common/types:maxkey.cpp", + "//src/mongo/scripting/mozjs/common/types:minkey.cpp", + "//src/mongo/scripting/mozjs/common/types:nativefunction.cpp", + "//src/mongo/scripting/mozjs/common/types:numberdecimal.cpp", + "//src/mongo/scripting/mozjs/common/types:numberint.cpp", + "//src/mongo/scripting/mozjs/common/types:numberlong.cpp", + "//src/mongo/scripting/mozjs/common/types:object.cpp", + "//src/mongo/scripting/mozjs/common/types:oid.cpp", + "//src/mongo/scripting/mozjs/common/types:regexp.cpp", + "//src/mongo/scripting/mozjs/common/types:timestamp.cpp", +] + +# Filegroup for WASI builds - exports source files for compilation in genrules. +# WASI builds use a stub status.cpp from the wasm package. +filegroup( + name = "wasi_sources", + srcs = glob(["*.cpp"]) + TYPES_SRCS + [ + "//src/mongo/scripting/mozjs/wasm:status.cpp", + ], + visibility = ["//src/mongo/scripting/mozjs/wasm:__pkg__"], +) + mongo_cc_library( name = "mozjs_common", - srcs = glob(["*.cpp"]) + [ - "//src/mongo/scripting/mozjs/common/types:bindata.cpp", - "//src/mongo/scripting/mozjs/common/types:bson.cpp", - "//src/mongo/scripting/mozjs/common/types:code.cpp", - "//src/mongo/scripting/mozjs/common/types:dbpointer.cpp", - "//src/mongo/scripting/mozjs/common/types:dbref.cpp", - "//src/mongo/scripting/mozjs/common/types:maxkey.cpp", - "//src/mongo/scripting/mozjs/common/types:minkey.cpp", - "//src/mongo/scripting/mozjs/common/types:nativefunction.cpp", - "//src/mongo/scripting/mozjs/common/types:numberdecimal.cpp", - "//src/mongo/scripting/mozjs/common/types:numberint.cpp", - "//src/mongo/scripting/mozjs/common/types:numberlong.cpp", - "//src/mongo/scripting/mozjs/common/types:object.cpp", - "//src/mongo/scripting/mozjs/common/types:oid.cpp", - "//src/mongo/scripting/mozjs/common/types:regexp.cpp", - "//src/mongo/scripting/mozjs/common/types:status.cpp", - "//src/mongo/scripting/mozjs/common/types:timestamp.cpp", - ], + srcs = glob(["*.cpp"]) + TYPES_SRCS, copts = select({ "@platforms//os:windows": [ # The default MSVC preprocessor elides commas in some cases as a @@ -41,10 +55,23 @@ mongo_cc_library( ], "//conditions:default": [], }), - textual_hdrs = glob(["*.defs"]), - deps = [ - "//src/mongo/scripting:scripting_common", - "//src/mongo/util:buildinfo", - "//src/third_party/mozjs", + srcs_select = [ + { + "//bazel/config:wasi": ["//src/mongo/scripting/mozjs/wasm:status.cpp"], + "//conditions:default": ["//src/mongo/scripting/mozjs/common/types:status.cpp"], + }, ], + textual_hdrs = glob(["*.defs"]), + deps = select({ + "//bazel/config:wasi": [ + # WASI builds don't need scripting_common (avoids networking/SSL dependencies) + # and use SpiderMonkey headers from @spidermonkey, not //src/third_party/mozjs + "//src/mongo/util:buildinfo", + ], + "//conditions:default": [ + "//src/mongo/scripting:scripting_common", + "//src/mongo/util:buildinfo", + "//src/third_party/mozjs", + ], + }), ) diff --git a/src/mongo/scripting/mozjs/common/exception.cpp b/src/mongo/scripting/mozjs/common/exception.cpp index 99a1311c0e6..d7f6dc39ea9 100644 --- a/src/mongo/scripting/mozjs/common/exception.cpp +++ b/src/mongo/scripting/mozjs/common/exception.cpp @@ -32,7 +32,7 @@ #include "mongo/scripting/mozjs/common/error.h" #include "mongo/scripting/mozjs/common/jsstringwrapper.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/types/status.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/scripting/mozjs/common/wraptype.h" @@ -45,7 +45,11 @@ #include #include #include +#ifdef MONGO_MOZJS_WASI_BUILD +#include "mongo/scripting/mozjs/wasm/engine/error.h" +#else #include +#endif namespace mongo { namespace mozjs { @@ -64,16 +68,16 @@ void mongoToJSException(JSContext* cx) { JS::ReportUncatchableException(cx); // If a JSAPI callback returns false without setting a pending exception, SpiderMonkey will // treat it as an uncatchable error. - auto* scope = getMozJSScope(cx); - scope->setStatus(std::move(status)); + auto* runtime = getCommonRuntime(cx); + runtime->setStatus(std::move(status)); } } std::string currentJSStackToString(JSContext* cx) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); JS::RootedValue error(cx); - getProto(scope).newInstance(&error); + getProto(runtime).newInstance(&error); return ObjectWrapper(cx, error).getString("stack"); } @@ -127,21 +131,26 @@ Status jsExceptionToStatus(JSContext* cx, JS::HandleValue excn, ErrorCodes::Error altCode, StringData altReason) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); // It's possible that we have an uncaught exception for OOM, which is reported on the // exception status of the JSContext. We must check for this OOM exception first to ensure // we return the correct error code and message (i.e JSInterpreterFailure). This is consistent // with MozJSImplScope::_checkForPendingException(). + // JS_IsThrowingOutOfMemoryException is a MongoDB-specific modification to + // SpiderMonkey (see src/third_party/mozjs). The WASI build uses upstream + // Firefox SpiderMonkey which doesn't have this. +#ifndef MONGO_MOZJS_WASI_BUILD if (JS_IsThrowingOutOfMemoryException(cx, excn)) { return Status(ErrorCodes::JSInterpreterFailure, "Out of memory"); } +#endif if (!excn.isObject()) { return Status(altCode, ValueWriter(cx, excn).toString()); } - if (getProto(scope).instanceOf(excn)) { + if (getProto(runtime).instanceOf(excn)) { return MongoStatusInfo::toStatus(cx, excn); } diff --git a/src/mongo/scripting/mozjs/common/global.cpp b/src/mongo/scripting/mozjs/common/global.cpp index a6618302c8d..e3c32cb9d9c 100644 --- a/src/mongo/scripting/mozjs/common/global.cpp +++ b/src/mongo/scripting/mozjs/common/global.cpp @@ -32,13 +32,19 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/logv2/log.h" +#ifndef MONGO_MOZJS_WASI_BUILD #include "mongo/scripting/engine.h" +#else +#include "mongo/scripting/js_regex.h" +#endif #include "mongo/scripting/mozjs/common/jsstringwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/util/assert_util.h" +#ifndef MONGO_MOZJS_WASI_BUILD #include "mongo/util/buildinfo.h" +#endif #include "mongo/util/version.h" #include @@ -55,6 +61,12 @@ namespace mongo { namespace mozjs { +#ifdef MONGO_MOZJS_WASI_BUILD +namespace wasm { +extern uint32_t g_wasmJsHeapLimitMB; +} // namespace wasm +#endif + const JSFunctionSpec GlobalInfo::freeFunctions[7] = { MONGO_ATTACH_JS_FUNCTION(sleep), MONGO_ATTACH_JS_FUNCTION(gc), @@ -102,18 +114,27 @@ void GlobalInfo::Functions::version::call(JSContext* cx, JS::CallArgs args) { } void GlobalInfo::Functions::buildInfo::call(JSContext* cx, JS::CallArgs args) { +#ifdef MONGO_MOZJS_WASI_BUILD + // WASI builds don't have getBuildInfo() - return empty object + BSONObjBuilder b; + ValueReader(cx, args.rval()).fromBSON(b.obj(), nullptr, false); +#else BSONObjBuilder b; getBuildInfo().serialize(&b); ValueReader(cx, args.rval()).fromBSON(b.obj(), nullptr, false); +#endif } void GlobalInfo::Functions::getJSHeapLimitMB::call(JSContext* cx, JS::CallArgs args) { - ValueReader(cx, args.rval()).fromDouble(mongo::getGlobalScriptEngine()->getJSHeapLimitMB()); +#ifdef MONGO_MOZJS_WASI_BUILD + ValueReader(cx, args.rval()).fromDouble(static_cast(wasm::g_wasmJsHeapLimitMB)); +#else + ValueReader(cx, args.rval()).fromDouble(getGlobalScriptEngine()->getJSHeapLimitMB()); +#endif } void GlobalInfo::Functions::gc::call(JSContext* cx, JS::CallArgs args) { - getMozJSScope(cx)->gc(); - + getCommonRuntime(cx)->gc(); args.rval().setUndefined(); } @@ -123,7 +144,7 @@ void GlobalInfo::Functions::sleep::call(JSContext* cx, JS::CallArgs args) { args.length() == 1 && args.get(0).isNumber()); int64_t duration = ValueWriter(cx, args.get(0)).toInt64(); - getMozJSScope(cx)->sleep(Milliseconds(duration)); + getCommonRuntime(cx)->sleep(Milliseconds(duration)); args.rval().setUndefined(); } diff --git a/src/mongo/scripting/mozjs/common/internedstring.cpp b/src/mongo/scripting/mozjs/common/internedstring.cpp index 795486e7c3e..68d509715b4 100644 --- a/src/mongo/scripting/mozjs/common/internedstring.cpp +++ b/src/mongo/scripting/mozjs/common/internedstring.cpp @@ -29,7 +29,7 @@ #include "mongo/scripting/mozjs/common/internedstring.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/util/assert_util.h" #include @@ -63,7 +63,7 @@ InternedStringTable::~InternedStringTable() { } InternedStringId::InternedStringId(JSContext* cx, InternedString id) - : _id(cx, getMozJSScope(cx)->getInternedStringId(id)) {} + : _id(cx, getCommonRuntime(cx)->getInternedStringId(id)) {} } // namespace mozjs } // namespace mongo diff --git a/src/mongo/scripting/mozjs/common/objectwrapper.cpp b/src/mongo/scripting/mozjs/common/objectwrapper.cpp index 6594de73cc7..7df1a69d595 100644 --- a/src/mongo/scripting/mozjs/common/objectwrapper.cpp +++ b/src/mongo/scripting/mozjs/common/objectwrapper.cpp @@ -33,8 +33,9 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/bson/util/builder.h" #include "mongo/platform/decimal128.h" +#include "mongo/scripting/js_regex.h" #include "mongo/scripting/mozjs/common/idwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/types/bson.h" #include "mongo/scripting/mozjs/common/types/dbref.h" #include "mongo/scripting/mozjs/common/valuereader.h" @@ -599,9 +600,9 @@ void ObjectWrapper::callMethod(JS::HandleValue fun, JS::MutableHandleValue out) } BSONObj ObjectWrapper::toBSON() { - auto* scope = getMozJSScope(_context); - if (getProto(scope).instanceOf(_object) || - getProto(scope).instanceOf(_object)) { + auto* runtime = getCommonRuntime(_context); + if (getProto(runtime).instanceOf(_object) || + getProto(runtime).instanceOf(_object)) { BSONObj* originalBSON = nullptr; bool altered; @@ -734,9 +735,9 @@ ObjectWrapper::WriteFieldRecursionFrame::WriteFieldRecursionFrame(JSContext* cx, } } - auto* scope = getMozJSScope(cx); - if (getProto(scope).instanceOf(thisv) || - getProto(scope).instanceOf(thisv)) { + auto* runtime = getCommonRuntime(cx); + if (getProto(runtime).instanceOf(thisv) || + getProto(runtime).instanceOf(thisv)) { std::tie(originalBSON, altered) = BSONInfo::originalBSON(cx, thisv); } } diff --git a/src/mongo/scripting/mozjs/common/objectwrapper.h b/src/mongo/scripting/mozjs/common/objectwrapper.h index 3f8d0ef2323..f007039cc62 100644 --- a/src/mongo/scripting/mozjs/common/objectwrapper.h +++ b/src/mongo/scripting/mozjs/common/objectwrapper.h @@ -38,13 +38,16 @@ #include "mongo/bson/oid.h" #include "mongo/bson/timestamp.h" #include "mongo/platform/decimal128.h" -#include "mongo/scripting/engine.h" #include "mongo/scripting/mozjs/common/exception.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/jsstringwrapper.h" #include "mongo/scripting/mozjs/common/lifetimestack.h" #include "mongo/util/modules.h" +namespace mongo { +struct JSRegEx; +} + #include #include #include diff --git a/src/mongo/scripting/mozjs/common/oid_validation.h b/src/mongo/scripting/mozjs/common/oid_validation.h new file mode 100644 index 00000000000..11eb879d75d --- /dev/null +++ b/src/mongo/scripting/mozjs/common/oid_validation.h @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/base/string_data.h" +#include "mongo/util/assert_util.h" + +namespace mongo { +namespace mozjs { + +/** + * Validates that a string is a valid ObjectId string (24 hex characters). + * Extracted from Scope::validateObjectIdString for WASI builds. + */ +inline void validateObjectIdString(StringData str) { + uassert(11542200, "invalid object id: length", str.size() == 24); + auto isAllHex = [](StringData s) { + return std::all_of(s.begin(), s.end(), [](char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + }); + }; + uassert(11542201, "invalid object id: not hex", isAllHex(str)); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/common/scope_base.h b/src/mongo/scripting/mozjs/common/runtime.h similarity index 50% rename from src/mongo/scripting/mozjs/common/scope_base.h rename to src/mongo/scripting/mozjs/common/runtime.h index b421df7c622..4afc0c927d7 100644 --- a/src/mongo/scripting/mozjs/common/scope_base.h +++ b/src/mongo/scripting/mozjs/common/runtime.h @@ -29,8 +29,6 @@ #pragma once -#include "mongo/base/string_data.h" -#include "mongo/scripting/engine.h" #include "mongo/util/duration.h" #include "mongo/util/modules.h" @@ -38,9 +36,14 @@ #include #include +#include + #include -struct JSContext; +namespace mongo { +class Status; +class StringData; +} // namespace mongo namespace mongo::mozjs { @@ -52,14 +55,11 @@ enum class InternedString; struct BinDataInfo; struct BSONInfo; struct CodeInfo; -struct CursorHandleInfo; -struct CursorInfo; struct DBPointerInfo; struct DBRefInfo; struct ErrorInfo; struct MaxKeyInfo; struct MinKeyInfo; -struct MongoExternalInfo; struct MongoStatusInfo; struct NativeFunctionInfo; struct NumberDecimalInfo; @@ -67,194 +67,171 @@ struct NumberIntInfo; struct NumberLongInfo; struct OIDInfo; struct RegExpInfo; -struct SessionInfo; struct TimestampInfo; -struct URIInfo; /** - * MozJS-specific scope base class. + * Abstract interface for the JS runtime services shared by both the shell + * (MozJSImplScope) and the WASM sandbox (wasm::MozJSScriptEngine). * - * This class extends the engine-agnostic Scope class with MozJS-specific - * functionality needed by code in `scripting/mozjs/common/`. + * Provides access to common BSON type prototypes, interned strings, GC, + * status propagation, and ASAN pointer tracking. * - * Intended inheritance hierarchy: - * Scope (scripting/engine.h) - * └── MozJSScopeBase - * └── MozJSImplScope (shell/implscope.h) - * └── MozJSWasmScope (wasm/implscope.h) + * The concrete shell scope composes + * the hierarchy as: + * + * MozJSImplScope : public Scope, + * public MozJSShellRuntimeInterface, + * private MozJSCommonRuntimeInterface + * + * A pointer to this interface is stored in JSContext private data and + * retrieved via getCommonRuntime(cx). */ -class MONGO_MOD_PUB MozJSScopeBase : public Scope { -public: - ~MozJSScopeBase() override = default; +struct MONGO_MOD_PUB MozJSCommonRuntimeInterface { + virtual ~MozJSCommonRuntimeInterface() = default; + + virtual void gc() = 0; + + virtual void sleep(Milliseconds ms) = 0; + + virtual std::size_t getGeneration() const = 0; virtual JS::HandleId getInternedStringId(InternedString name) = 0; - // Helpers used by `common/valuereader.cpp`. - virtual bool isJavaScriptProtectionEnabled() const = 0; - virtual void newFunction(StringData code, JS::MutableHandleValue out) = 0; virtual std::int64_t* trackedNewInt64(std::int64_t value) = 0; - // Scope generation tracking (used by `common/types/bson.cpp`). - virtual std::size_t getGeneration() const = 0; - - // Whether this scope requires all bound BSON objects to be owned. - virtual bool requiresOwnedObjects() const = 0; - - // Store a "current status" on the scope (used by `common/exception.cpp`). - virtual void setStatus(Status status) = 0; - - // Sleep for the given duration. Used by the JS `sleep()` function. - virtual void sleep(Milliseconds ms) = 0; - - // Pointer tracking hooks for ASANHandles. Implementations may no-op these when not needed. - virtual void trackNewPointer(void* ptr) = 0; - virtual void trackDeletePointer(void* ptr) = 0; - - // --- Prototype accessors --- + virtual WrapType& numberLongProto() = 0; + virtual WrapType& numberIntProto() = 0; + virtual WrapType& numberDecimalProto() = 0; + virtual WrapType& oidProto() = 0; virtual WrapType& binDataProto() = 0; - virtual WrapType& bsonProto() = 0; - virtual WrapType& codeProto() = 0; - virtual WrapType& cursorHandleProto() = 0; - virtual WrapType& cursorProto() = 0; - virtual WrapType& dbPointerProto() = 0; - virtual WrapType& dbRefProto() = 0; - virtual WrapType& errorProto() = 0; + virtual WrapType& timestampProto() = 0; virtual WrapType& maxKeyProto() = 0; virtual WrapType& minKeyProto() = 0; - virtual WrapType& mongoExternalProto() = 0; - virtual WrapType& mongoStatusProto() = 0; + virtual WrapType& codeProto() = 0; + virtual WrapType& dbPointerProto() = 0; virtual WrapType& nativeFunctionProto() = 0; - virtual WrapType& numberDecimalProto() = 0; - virtual WrapType& numberIntProto() = 0; - virtual WrapType& numberLongProto() = 0; - virtual WrapType& oidProto() = 0; + virtual WrapType& errorProto() = 0; + virtual WrapType& mongoStatusProto() = 0; + virtual WrapType& bsonProto() = 0; + virtual WrapType& dbRefProto() = 0; virtual WrapType& regExpProto() = 0; - virtual WrapType& sessionProto() = 0; - virtual WrapType& timestampProto() = 0; - virtual WrapType& uriProto() = 0; + + virtual void setStatus(Status status) = 0; + + virtual bool isJavaScriptProtectionEnabled() const = 0; + + virtual bool requiresOwnedObjects() const = 0; + + virtual void newFunction(StringData code, JS::MutableHandleValue out) = 0; + + // Pointer tracking for ASAN + virtual void trackNewPointer(void* ptr) = 0; + virtual void trackDeletePointer(void* ptr) = 0; }; -MONGO_MOD_PUB MozJSScopeBase* getMozJSScope(JSContext* cx); - template -WrapType& getProto(MozJSScopeBase* scope); +WrapType& getProto(MozJSCommonRuntimeInterface* runtime); template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->binDataProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->numberLongProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->bsonProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->numberIntProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->codeProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->numberDecimalProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->cursorHandleProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->oidProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->cursorProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->binDataProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->dbPointerProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->timestampProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->dbRefProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->maxKeyProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->errorProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->minKeyProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->maxKeyProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->codeProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->minKeyProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->dbPointerProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->mongoExternalProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->nativeFunctionProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->mongoStatusProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->errorProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->nativeFunctionProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->mongoStatusProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->numberDecimalProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->bsonProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->numberIntProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->dbRefProto(); } template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->numberLongProto(); +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return runtime->regExpProto(); } -template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->oidProto(); -} - -template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->regExpProto(); -} - -template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->sessionProto(); -} - -template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->timestampProto(); -} - -template <> -inline WrapType& getProto(MozJSScopeBase* scope) { - return scope->uriProto(); -} +// +// Tracked memory allocation helpers +// These ensure proper ASAN tracking of dynamically allocated objects. +// template -T* trackedNew(MozJSScopeBase* scope, Args&&... args) { +T* trackedNew(MozJSCommonRuntimeInterface* runtime, Args&&... args) { T* t = new T(std::forward(args)...); - scope->trackNewPointer(t); + runtime->trackNewPointer(t); return t; } template -void trackedDelete(MozJSScopeBase* scope, T* t) { - scope->trackDeletePointer(t); - delete (t); +void trackedDelete(MozJSCommonRuntimeInterface* runtime, T* t) { + runtime->trackDeletePointer(t); + delete t; +} + +MONGO_MOD_PUB inline MozJSCommonRuntimeInterface* getCommonRuntime(JSContext* cx) { + return static_cast(JS_GetContextPrivate(cx)); } } // namespace mongo::mozjs diff --git a/src/mongo/scripting/mozjs/common/types/bindata.cpp b/src/mongo/scripting/mozjs/common/types/bindata.cpp index 629dda2f856..4a3ddca0f90 100644 --- a/src/mongo/scripting/mozjs/common/types/bindata.cpp +++ b/src/mongo/scripting/mozjs/common/types/bindata.cpp @@ -38,7 +38,7 @@ #include "mongo/scripting/mozjs/common/freeOpToJSContext.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep @@ -97,7 +97,7 @@ void hexToBinData(JSContext* cx, int type, const JS::Handle hexdata, JS::MutableHandleValue out) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); uassert(ErrorCodes::BadValue, "BinData data must be a String", hexdata.isString()); auto hexstr = ValueWriter(cx, hexdata).toString(); @@ -110,7 +110,7 @@ void hexToBinData(JSContext* cx, args[0].setInt32(type); ValueReader(cx, args[1]).fromStringData(encoded); - return getProto(scope).newInstance(args, out); + return getProto(runtime).newInstance(args, out); } std::string* getEncoded(JS::HandleValue thisv) { @@ -128,7 +128,7 @@ void BinDataInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { auto str = getEncoded(obj); if (str) { - trackedDelete(getMozJSScope(freeOpToJSContext(gcCtx)), str); + trackedDelete(getCommonRuntime(freeOpToJSContext(gcCtx)), str); } } @@ -156,7 +156,7 @@ void BinDataInfo::Functions::UUID::call(JSContext* cx, JS::CallArgs args) { JS::RootedValueArray<2> newArgs(cx); newArgs[0].setInt32(newUUID); ValueReader(cx, newArgs[1]).fromStringData(encoded); - getProto(getMozJSScope(cx)).newInstance(newArgs, args.rval()); + getCommonRuntime(cx)->binDataProto().newInstance(newArgs, args.rval()); } void BinDataInfo::Functions::MD5::call(JSContext* cx, JS::CallArgs args) { @@ -247,7 +247,7 @@ void BinDataInfo::Functions::hex::call(JSContext* cx, JS::CallArgs args) { } void BinDataInfo::construct(JSContext* cx, JS::CallArgs args) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); if (args.length() != 2) { uasserted(ErrorCodes::BadValue, "BinData takes 2 arguments -- BinData(subtype,data)"); @@ -271,7 +271,7 @@ void BinDataInfo::construct(JSContext* cx, JS::CallArgs args) { auto tmpBase64 = mongo::base64::decode(str); JS::RootedObject thisv(cx); - getProto(scope).newObject(&thisv); + getProto(runtime).newObject(&thisv); ObjectWrapper o(cx, thisv); JS::RootedValue len(cx); @@ -280,8 +280,9 @@ void BinDataInfo::construct(JSContext* cx, JS::CallArgs args) { o.defineProperty(InternedString::len, len, JSPROP_READONLY); o.defineProperty(InternedString::type, type, JSPROP_READONLY); - JS::SetReservedSlot( - thisv, BinDataStringSlot, JS::PrivateValue(trackedNew(scope, std::move(str)))); + JS::SetReservedSlot(thisv, + BinDataStringSlot, + JS::PrivateValue(trackedNew(runtime, std::move(str)))); args.rval().setObjectOrNull(thisv); } diff --git a/src/mongo/scripting/mozjs/common/types/bson.cpp b/src/mongo/scripting/mozjs/common/types/bson.cpp index 9204123953b..7ed015fdeb9 100644 --- a/src/mongo/scripting/mozjs/common/types/bson.cpp +++ b/src/mongo/scripting/mozjs/common/types/bson.cpp @@ -26,6 +26,7 @@ * exception statement from all source files in the program, then also delete * it in the license file. */ + #include "mongo/scripting/mozjs/common/types/bson.h" #include "mongo/base/error_codes.h" @@ -34,13 +35,15 @@ #include "mongo/bson/bsonelement.h" #include "mongo/bson/bsonobj_comparator_interface.h" #include "mongo/bson/bsontypes.h" +#ifndef MONGO_MOZJS_WASI_BUILD #include "mongo/db/pipeline/resume_token.h" +#endif #include "mongo/scripting/mozjs/common/freeOpToJSContext.h" #include "mongo/scripting/mozjs/common/idwrapper.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/jsstringwrapper.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/util/assert_util.h" @@ -49,7 +52,9 @@ #include #include +#if !defined(MONGO_MOZJS_WASI_BUILD) #include +#endif #include #include @@ -96,17 +101,21 @@ BSONObj getBSONFromArg(JSContext* cx, JS::HandleValue arg, bool isBSON) { * the appearance of mutable state on the read/write versions. */ struct BSONHolder { - BSONHolder(const BSONObj& obj, const BSONObj* parent, const MozJSScopeBase* scope, bool ro) + BSONHolder(const BSONObj& obj, + const BSONObj* parent, + const MozJSCommonRuntimeInterface* runtime, + bool ro) : _obj(obj), - _generation(scope->getGeneration()), + _generation(runtime->getGeneration()), _isOwned(obj.isOwned() || (parent && parent->isOwned())), _resolved(false), _readOnly(ro), _altered(false) { + bool requiresOwned = runtime->requiresOwnedObjects(); uassert( ErrorCodes::BadValue, "Attempt to bind an unowned BSON Object to a JS scope marked as requiring ownership", - _isOwned || (!scope->requiresOwnedObjects())); + _isOwned || (!requiresOwned)); if (parent) { _parent.emplace(*parent); } @@ -117,7 +126,7 @@ struct BSONHolder { } void uassertValid(JSContext* cx) const { - if (!_isOwned && getMozJSScope(cx)->getGeneration() != _generation) + if (!_isOwned && getCommonRuntime(cx)->getGeneration() != _generation) uasserted(ErrorCodes::BadValue, "Attempt to access an invalidated BSON Object in JS scope"); } @@ -162,12 +171,13 @@ void definePropertyFromBSONElement(JSContext* cx, void BSONInfo::make( JSContext* cx, JS::MutableHandleObject obj, BSONObj bson, const BSONObj* parent, bool ro) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); - getProto(scope).newObject(obj); - JS::SetReservedSlot(obj, - BSONHolderSlot, - JS::PrivateValue(trackedNew(scope, bson, parent, scope, ro))); + getProto(runtime).newObject(obj); + JS::SetReservedSlot( + obj, + BSONHolderSlot, + JS::PrivateValue(trackedNew(runtime, bson, parent, runtime, ro))); } void BSONInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { @@ -176,7 +186,7 @@ void BSONInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { if (!holder) return; - trackedDelete(getMozJSScope(freeOpToJSContext(gcCtx)), holder); + trackedDelete(getCommonRuntime(freeOpToJSContext(gcCtx)), holder); } /* @@ -342,9 +352,9 @@ void bsonCompareCommon(JSContext* cx, uasserted(ErrorCodes::BadValue, fmt::format("{} needs 2 arguments", funcName)); // If either argument is not proper BSON, then we wrap both objects. - auto* scope = getMozJSScope(cx); - bool isBSON = getProto(scope).instanceOf(args.get(0)) && - getProto(scope).instanceOf(args.get(1)); + auto* runtime = getCommonRuntime(cx); + bool isBSON = getProto(runtime).instanceOf(args.get(0)) && + getProto(runtime).instanceOf(args.get(1)); BSONObj bsonObject1 = getBSONFromArg(cx, args.get(0), isBSON); BSONObj bsonObject2 = getBSONFromArg(cx, args.get(1), isBSON); @@ -370,9 +380,9 @@ void BSONInfo::Functions::bsonBinaryEqual::call(JSContext* cx, JS::CallArgs args uasserted(ErrorCodes::BadValue, "bsonBinaryEqual needs 2 arguments"); // If either argument is not a proper BSON, then we wrap both objects. - auto* scope = getMozJSScope(cx); - bool isBSON = getProto(scope).instanceOf(args.get(0)) && - getProto(scope).instanceOf(args.get(1)); + auto* runtime = getCommonRuntime(cx); + bool isBSON = getProto(runtime).instanceOf(args.get(0)) && + getProto(runtime).instanceOf(args.get(1)); BSONObj bsonObject1 = getBSONFromArg(cx, args.get(0), isBSON); BSONObj bsonObject2 = getBSONFromArg(cx, args.get(1), isBSON); diff --git a/src/mongo/scripting/mozjs/common/types/code.cpp b/src/mongo/scripting/mozjs/common/types/code.cpp index c2d38d396b9..742fca03ab6 100644 --- a/src/mongo/scripting/mozjs/common/types/code.cpp +++ b/src/mongo/scripting/mozjs/common/types/code.cpp @@ -33,7 +33,7 @@ #include "mongo/bson/bsonobj.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep #include "mongo/util/assert_util.h" @@ -70,11 +70,11 @@ void CodeInfo::construct(JSContext* cx, JS::CallArgs args) { uassert(ErrorCodes::BadValue, "Code needs 0, 1 or 2 arguments", args.length() == 0 || args.length() == 1 || args.length() == 2); - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); JS::RootedObject thisv(cx); - getProto(scope).newObject(&thisv); + getProto(runtime).newObject(&thisv); ObjectWrapper o(cx, thisv); if (args.length() == 0) { diff --git a/src/mongo/scripting/mozjs/common/types/dbpointer.cpp b/src/mongo/scripting/mozjs/common/types/dbpointer.cpp index 7dbf733ee03..5218ae275d8 100644 --- a/src/mongo/scripting/mozjs/common/types/dbpointer.cpp +++ b/src/mongo/scripting/mozjs/common/types/dbpointer.cpp @@ -32,7 +32,7 @@ #include "mongo/base/error_codes.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/types/oid.h" #include "mongo/scripting/mozjs/common/wraptype.h" #include "mongo/util/assert_util.h" @@ -47,7 +47,7 @@ namespace mozjs { const char* const DBPointerInfo::className = "DBPointer"; void DBPointerInfo::construct(JSContext* cx, JS::CallArgs args) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); if (args.length() != 2) uasserted(ErrorCodes::BadValue, "DBPointer needs 2 arguments"); @@ -55,11 +55,11 @@ void DBPointerInfo::construct(JSContext* cx, JS::CallArgs args) { if (!args.get(0).isString()) uasserted(ErrorCodes::BadValue, "DBPointer 1st parameter must be a string"); - if (!getProto(scope).instanceOf(args.get(1))) + if (!getProto(runtime).instanceOf(args.get(1))) uasserted(ErrorCodes::BadValue, "DBPointer 2nd parameter must be an ObjectId"); JS::RootedObject thisv(cx); - getProto(scope).newObject(&thisv); + getProto(runtime).newObject(&thisv); ObjectWrapper o(cx, thisv); o.setValue(InternedString::ns, args.get(0)); diff --git a/src/mongo/scripting/mozjs/common/types/dbref.cpp b/src/mongo/scripting/mozjs/common/types/dbref.cpp index 220d3d7e4ba..6df860a169a 100644 --- a/src/mongo/scripting/mozjs/common/types/dbref.cpp +++ b/src/mongo/scripting/mozjs/common/types/dbref.cpp @@ -32,7 +32,7 @@ #include "mongo/base/error_codes.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/types/bson.h" #include "mongo/scripting/mozjs/common/wraptype.h" #include "mongo/util/assert_util.h" @@ -115,9 +115,9 @@ void DBRefInfo::make( JS::RootedObject local(cx); BSONInfo::make(cx, &local, std::move(bson), parent, ro); - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); - getProto(scope).newObject(obj); + getProto(runtime).newObject(obj); JS::SetReservedSlot( obj, diff --git a/src/mongo/scripting/mozjs/common/types/maxkey.cpp b/src/mongo/scripting/mozjs/common/types/maxkey.cpp index a0b67e43138..9967eb74f2e 100644 --- a/src/mongo/scripting/mozjs/common/types/maxkey.cpp +++ b/src/mongo/scripting/mozjs/common/types/maxkey.cpp @@ -35,7 +35,7 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep #include "mongo/util/assert_util.h" @@ -68,22 +68,22 @@ void MaxKeyInfo::construct(JSContext* cx, JS::CallArgs args) { * == and === to MinKey even if created by "new MinKey()" in JS. */ void MaxKeyInfo::call(JSContext* cx, JS::CallArgs args) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); - ObjectWrapper o(cx, getProto(scope).getProto()); + ObjectWrapper o(cx, getProto(runtime).getProto()); JS::RootedValue val(cx); if (!o.hasField(InternedString::singleton)) { JS::RootedObject thisv(cx); - getProto(scope).newObject(&thisv); + getProto(runtime).newObject(&thisv); val.setObjectOrNull(thisv); o.setValue(InternedString::singleton, val); } else { o.getValue(InternedString::singleton, &val); - if (!getProto(scope).instanceOf(val)) + if (!getProto(runtime).instanceOf(val)) uasserted(ErrorCodes::BadValue, "MaxKey singleton not of type MaxKey"); } @@ -102,15 +102,15 @@ void MaxKeyInfo::Functions::hasInstance::call(JSContext* cx, JS::CallArgs args) uassert(ErrorCodes::BadValue, "hasInstance needs 1 argument", args.length() == 1); uassert(ErrorCodes::BadValue, "argument must be an object", args.get(0).isObject()); - auto* scope = getMozJSScope(cx); - args.rval().setBoolean(getProto(scope).instanceOf(args.get(0))); + auto* runtime = getCommonRuntime(cx); + args.rval().setBoolean(getProto(runtime).instanceOf(args.get(0))); } void MaxKeyInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto) { ObjectWrapper protoWrapper(cx, proto); JS::RootedValue value(cx); - getProto(getMozJSScope(cx)).newObject(&value); + getCommonRuntime(cx)->maxKeyProto().newObject(&value); ObjectWrapper(cx, global).setValue(InternedString::MaxKey, value); protoWrapper.setValue(InternedString::singleton, value); diff --git a/src/mongo/scripting/mozjs/common/types/minkey.cpp b/src/mongo/scripting/mozjs/common/types/minkey.cpp index 5265ec200d2..b0f44dbf6de 100644 --- a/src/mongo/scripting/mozjs/common/types/minkey.cpp +++ b/src/mongo/scripting/mozjs/common/types/minkey.cpp @@ -35,7 +35,7 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep #include "mongo/util/assert_util.h" @@ -68,22 +68,22 @@ void MinKeyInfo::construct(JSContext* cx, JS::CallArgs args) { * == and === to MinKey even if created by "new MinKey()" in JS. */ void MinKeyInfo::call(JSContext* cx, JS::CallArgs args) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); - ObjectWrapper o(cx, getProto(scope).getProto()); + ObjectWrapper o(cx, getProto(runtime).getProto()); JS::RootedValue val(cx); if (!o.hasField(InternedString::singleton)) { JS::RootedObject thisv(cx); - getProto(scope).newObject(&thisv); + getProto(runtime).newObject(&thisv); val.setObjectOrNull(thisv); o.setValue(InternedString::singleton, val); } else { o.getValue(InternedString::singleton, &val); - if (!getProto(scope).instanceOf(val)) + if (!getProto(runtime).instanceOf(val)) uasserted(ErrorCodes::BadValue, "MinKey singleton not of type MinKey"); } @@ -103,15 +103,15 @@ void MinKeyInfo::Functions::hasInstance::call(JSContext* cx, JS::CallArgs args) uassert(ErrorCodes::BadValue, "hasInstance needs 1 argument", args.length() == 1); uassert(ErrorCodes::BadValue, "argument must be an object", args.get(0).isObject()); - auto* scope = getMozJSScope(cx); - args.rval().setBoolean(getProto(scope).instanceOf(args.get(0))); + auto* runtime = getCommonRuntime(cx); + args.rval().setBoolean(getProto(runtime).instanceOf(args.get(0))); } void MinKeyInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto) { ObjectWrapper protoWrapper(cx, proto); JS::RootedValue value(cx); - getProto(getMozJSScope(cx)).newObject(&value); + getCommonRuntime(cx)->minKeyProto().newObject(&value); ObjectWrapper(cx, global).setValue(InternedString::MinKey, value); protoWrapper.setValue(InternedString::singleton, value); diff --git a/src/mongo/scripting/mozjs/common/types/nativefunction.cpp b/src/mongo/scripting/mozjs/common/types/nativefunction.cpp index e0f163b3d84..024ddd9ed5b 100644 --- a/src/mongo/scripting/mozjs/common/types/nativefunction.cpp +++ b/src/mongo/scripting/mozjs/common/types/nativefunction.cpp @@ -33,7 +33,7 @@ #include "mongo/bson/bsonobj.h" #include "mongo/scripting/mozjs/common/freeOpToJSContext.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep #include "mongo/util/assert_util.h" @@ -103,7 +103,7 @@ void NativeFunctionInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { auto holder = JS::GetMaybePtrFromReservedSlot(obj, NativeHolderSlot); if (holder) - trackedDelete(getMozJSScope(freeOpToJSContext(gcCtx)), holder); + trackedDelete(getCommonRuntime(freeOpToJSContext(gcCtx)), holder); } void NativeFunctionInfo::Functions::toString::call(JSContext* cx, JS::CallArgs args) { @@ -120,10 +120,10 @@ void NativeFunctionInfo::make(JSContext* cx, JS::MutableHandleObject obj, NativeFunction function, void* data) { - auto scope = getMozJSScope(cx); - getProto(scope).newObject(obj); + auto runtime = getCommonRuntime(cx); + getProto(runtime).newObject(obj); JS::SetReservedSlot( - obj, NativeHolderSlot, JS::PrivateValue(trackedNew(scope, function, data))); + obj, NativeHolderSlot, JS::PrivateValue(trackedNew(runtime, function, data))); } } // namespace mozjs diff --git a/src/mongo/scripting/mozjs/common/types/numberdecimal.cpp b/src/mongo/scripting/mozjs/common/types/numberdecimal.cpp index f99f870fb89..e4604445d82 100644 --- a/src/mongo/scripting/mozjs/common/types/numberdecimal.cpp +++ b/src/mongo/scripting/mozjs/common/types/numberdecimal.cpp @@ -35,7 +35,7 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/platform/decimal128.h" #include "mongo/scripting/mozjs/common/freeOpToJSContext.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep @@ -66,7 +66,7 @@ void NumberDecimalInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { auto x = JS::GetMaybePtrFromReservedSlot(obj, Decimal128Slot); if (x) - trackedDelete(getMozJSScope(freeOpToJSContext(gcCtx)), x); + trackedDelete(getCommonRuntime(freeOpToJSContext(gcCtx)), x); } Decimal128 NumberDecimalInfo::ToNumberDecimal(JSContext* cx, JS::HandleValue thisv) { @@ -97,11 +97,11 @@ void NumberDecimalInfo::Functions::toJSON::call(JSContext* cx, JS::CallArgs args } void NumberDecimalInfo::construct(JSContext* cx, JS::CallArgs args) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); JS::RootedObject thisv(cx); - getProto(scope).newObject(&thisv); + getProto(runtime).newObject(&thisv); Decimal128 x(0); @@ -112,18 +112,19 @@ void NumberDecimalInfo::construct(JSContext* cx, JS::CallArgs args) { } else { uasserted(ErrorCodes::BadValue, "NumberDecimal takes 0 or 1 arguments"); } - JS::SetReservedSlot(thisv, Decimal128Slot, JS::PrivateValue(trackedNew(scope, x))); + JS::SetReservedSlot( + thisv, Decimal128Slot, JS::PrivateValue(trackedNew(runtime, x))); args.rval().setObjectOrNull(thisv); } void NumberDecimalInfo::make(JSContext* cx, JS::MutableHandleValue thisv, Decimal128 decimal) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); - getProto(scope).newObject(thisv); + getProto(runtime).newObject(thisv); JS::SetReservedSlot(thisv.toObjectOrNull(), Decimal128Slot, - JS::PrivateValue(trackedNew(scope, decimal))); + JS::PrivateValue(trackedNew(runtime, decimal))); } } // namespace mozjs diff --git a/src/mongo/scripting/mozjs/common/types/numberint.cpp b/src/mongo/scripting/mozjs/common/types/numberint.cpp index b0be656560b..21a9351e23a 100644 --- a/src/mongo/scripting/mozjs/common/types/numberint.cpp +++ b/src/mongo/scripting/mozjs/common/types/numberint.cpp @@ -31,7 +31,7 @@ #include "mongo/base/error_codes.h" #include "mongo/scripting/mozjs/common/freeOpToJSContext.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep @@ -64,7 +64,7 @@ void NumberIntInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { auto x = JS::GetMaybePtrFromReservedSlot(obj, IntSlot); if (x) - trackedDelete(getMozJSScope(freeOpToJSContext(gcCtx)), x); + trackedDelete(getCommonRuntime(freeOpToJSContext(gcCtx)), x); } int NumberIntInfo::ToNumberInt(JSContext* cx, JS::HandleValue thisv) { @@ -107,7 +107,7 @@ void NumberIntInfo::Functions::toJSON::call(JSContext* cx, JS::CallArgs args) { void NumberIntInfo::construct(JSContext* cx, JS::CallArgs args) { JS::RootedObject thisv(cx); - getProto(getMozJSScope(cx)).newObject(&thisv); + getCommonRuntime(cx)->numberIntProto().newObject(&thisv); int32_t x = 0; @@ -118,7 +118,7 @@ void NumberIntInfo::construct(JSContext* cx, JS::CallArgs args) { } else { uasserted(ErrorCodes::BadValue, "NumberInt takes 0 or 1 arguments"); } - JS::SetReservedSlot(thisv, IntSlot, JS::PrivateValue(trackedNew(getMozJSScope(cx), x))); + JS::SetReservedSlot(thisv, IntSlot, JS::PrivateValue(trackedNew(getCommonRuntime(cx), x))); args.rval().setObjectOrNull(thisv); } diff --git a/src/mongo/scripting/mozjs/common/types/numberlong.cpp b/src/mongo/scripting/mozjs/common/types/numberlong.cpp index a9730d930d6..e12583cc3d8 100644 --- a/src/mongo/scripting/mozjs/common/types/numberlong.cpp +++ b/src/mongo/scripting/mozjs/common/types/numberlong.cpp @@ -38,7 +38,7 @@ #include "mongo/scripting/mozjs/common/freeOpToJSContext.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep @@ -80,7 +80,7 @@ void NumberLongInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { auto numLong = JS::GetMaybePtrFromReservedSlot(obj, Int64Slot); if (numLong) - trackedDelete(getMozJSScope(freeOpToJSContext(gcCtx)), numLong); + trackedDelete(getCommonRuntime(freeOpToJSContext(gcCtx)), numLong); } int64_t NumberLongInfo::ToNumberLong(JSContext* cx, JS::HandleValue thisv) { @@ -129,7 +129,7 @@ void NumberLongInfo::Functions::compare::call(JSContext* cx, JS::CallArgs args) uassert(ErrorCodes::BadValue, "NumberLong.compare() needs 1 argument", args.length() == 1); uassert(ErrorCodes::BadValue, "NumberLong.compare() argument must be a NumberLong", - getProto(getMozJSScope(cx)).instanceOf(args.get(0))); + getCommonRuntime(cx)->numberLongProto().instanceOf(args.get(0))); int64_t thisVal = NumberLongInfo::ToNumberLong(cx, args.thisv()); int64_t otherVal = NumberLongInfo::ToNumberLong(cx, args.get(0)); @@ -171,11 +171,11 @@ void NumberLongInfo::construct(JSContext* cx, JS::CallArgs args) { "NumberLong needs 0, 1 or 3 arguments", args.length() == 0 || args.length() == 1 || args.length() == 3); - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); JS::RootedObject thisv(cx); - getProto(scope).newObject(&thisv); + getProto(runtime).newObject(&thisv); int64_t numLong; @@ -221,7 +221,7 @@ void NumberLongInfo::construct(JSContext* cx, JS::CallArgs args) { numLong = (top << 32) + bot; } JS::SetReservedSlot( - thisv, Int64Slot, JS::PrivateValue(getMozJSScope(cx)->trackedNewInt64(numLong))); + thisv, Int64Slot, JS::PrivateValue(getCommonRuntime(cx)->trackedNewInt64(numLong))); args.rval().setObjectOrNull(thisv); } @@ -234,7 +234,7 @@ void NumberLongInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::Han if (!JS_DefinePropertyById( cx, proto, - getMozJSScope(cx)->getInternedStringId(InternedString::floatApprox), + getCommonRuntime(cx)->getInternedStringId(InternedString::floatApprox), smUtils::wrapConstrainedMethod, nullptr, JSPROP_ENUMERATE)) { @@ -245,7 +245,7 @@ void NumberLongInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::Han if (!JS_DefinePropertyById( cx, proto, - getMozJSScope(cx)->getInternedStringId(InternedString::top), + getCommonRuntime(cx)->getInternedStringId(InternedString::top), smUtils::wrapConstrainedMethod, nullptr, JSPROP_ENUMERATE)) { @@ -256,7 +256,7 @@ void NumberLongInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::Han if (!JS_DefinePropertyById( cx, proto, - getMozJSScope(cx)->getInternedStringId(InternedString::bottom), + getCommonRuntime(cx)->getInternedStringId(InternedString::bottom), smUtils::wrapConstrainedMethod, nullptr, JSPROP_ENUMERATE)) { @@ -267,7 +267,7 @@ void NumberLongInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::Han if (!JS_DefinePropertyById( cx, proto, - getMozJSScope(cx)->getInternedStringId(InternedString::exactValueString), + getCommonRuntime(cx)->getInternedStringId(InternedString::exactValueString), smUtils::wrapConstrainedMethod, nullptr, JSPROP_ENUMERATE)) { diff --git a/src/mongo/scripting/mozjs/common/types/oid.cpp b/src/mongo/scripting/mozjs/common/types/oid.cpp index 835eb6a594c..e57ccd36a61 100644 --- a/src/mongo/scripting/mozjs/common/types/oid.cpp +++ b/src/mongo/scripting/mozjs/common/types/oid.cpp @@ -33,13 +33,13 @@ #include "mongo/base/string_data.h" #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobjbuilder.h" -#include "mongo/scripting/engine.h" #include "mongo/scripting/mozjs/common/freeOpToJSContext.h" #include "mongo/scripting/mozjs/common/internedstring.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep +#include "mongo/scripting/oid_validation.h" #include "mongo/util/assert_util.h" #include "mongo/util/str.h" @@ -70,7 +70,7 @@ void OIDInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { auto oid = JS::GetMaybePtrFromReservedSlot(obj, OIDSlot); if (oid) { - trackedDelete(getMozJSScope(freeOpToJSContext(gcCtx)), oid); + trackedDelete(getCommonRuntime(freeOpToJSContext(gcCtx)), oid); } } @@ -101,7 +101,7 @@ void OIDInfo::construct(JSContext* cx, JS::CallArgs args) { } else { auto str = ValueWriter(cx, args.get(0)).toString(); - Scope::validateObjectIdString(str); + validateObjectIdString(str); oid.init(str); } @@ -109,11 +109,11 @@ void OIDInfo::construct(JSContext* cx, JS::CallArgs args) { } void OIDInfo::make(JSContext* cx, const OID& oid, JS::MutableHandleValue out) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); JS::RootedObject thisv(cx); - getProto(scope).newObject(&thisv); - JS::SetReservedSlot(thisv, OIDSlot, JS::PrivateValue(trackedNew(scope, oid))); + getProto(runtime).newObject(&thisv); + JS::SetReservedSlot(thisv, OIDSlot, JS::PrivateValue(trackedNew(runtime, oid))); out.setObjectOrNull(thisv); } @@ -139,7 +139,7 @@ void OIDInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObje if (!JS_DefinePropertyById(cx, proto, - getMozJSScope(cx)->getInternedStringId(InternedString::str), + getCommonRuntime(cx)->getInternedStringId(InternedString::str), smUtils::wrapConstrainedMethod, nullptr, JSPROP_ENUMERATE)) { diff --git a/src/mongo/scripting/mozjs/common/types/status.cpp b/src/mongo/scripting/mozjs/common/types/status.cpp index ddb551fda41..f29727eef6b 100644 --- a/src/mongo/scripting/mozjs/common/types/status.cpp +++ b/src/mongo/scripting/mozjs/common/types/status.cpp @@ -35,7 +35,7 @@ #include "mongo/scripting/mozjs/common/freeOpToJSContext.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep #include "mongo/util/assert_util.h" @@ -70,7 +70,7 @@ Status MongoStatusInfo::toStatus(JSContext* cx, JS::HandleValue value) { void MongoStatusInfo::fromStatus(JSContext* cx, Status status, JS::MutableHandleValue value) { invariant(status != Status::OK()); - auto scope = getMozJSScope(cx); + auto runtime = getCommonRuntime(cx); JS::RootedValue undef(cx); undef.setUndefined(); @@ -78,10 +78,10 @@ void MongoStatusInfo::fromStatus(JSContext* cx, Status status, JS::MutableHandle JS::RootedValueArray<1> args(cx); ValueReader(cx, args[0]).fromStringData(status.reason()); JS::RootedObject error(cx); - getProto(scope).newInstance(args, &error); + getProto(runtime).newInstance(args, &error); JS::RootedObject thisv(cx); - getProto(scope).newObjectWithProto(&thisv, error); + getProto(runtime).newObjectWithProto(&thisv, error); ObjectWrapper thisvObj(cx, thisv); thisvObj.defineProperty(InternedString::code, JSPROP_ENUMERATE, @@ -103,7 +103,7 @@ void MongoStatusInfo::fromStatus(JSContext* cx, Status status, JS::MutableHandle nullptr); JS::SetReservedSlot( - thisv, StatusSlot, JS::PrivateValue(trackedNew(scope, std::move(status)))); + thisv, StatusSlot, JS::PrivateValue(trackedNew(runtime, std::move(status)))); value.setObjectOrNull(thisv); } @@ -112,7 +112,7 @@ void MongoStatusInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { auto status = JS::GetMaybePtrFromReservedSlot(obj, StatusSlot); if (status) - trackedDelete(getMozJSScope(freeOpToJSContext(gcCtx)), status); + trackedDelete(getCommonRuntime(freeOpToJSContext(gcCtx)), status); } void MongoStatusInfo::Functions::code::call(JSContext* cx, JS::CallArgs args) { @@ -160,11 +160,11 @@ void MongoStatusInfo::Functions::stack::call(JSContext* cx, JS::CallArgs args) { } void MongoStatusInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto) { - auto scope = getMozJSScope(cx); + auto runtime = getCommonRuntime(cx); JS::SetReservedSlot(proto, StatusSlot, JS::PrivateValue(trackedNew( - scope, Status(ErrorCodes::UnknownError, "Mongo Status Prototype")))); + runtime, Status(ErrorCodes::UnknownError, "Mongo Status Prototype")))); } } // namespace mozjs diff --git a/src/mongo/scripting/mozjs/common/types/timestamp.cpp b/src/mongo/scripting/mozjs/common/types/timestamp.cpp index 7941fa2e2c0..b02f89795d7 100644 --- a/src/mongo/scripting/mozjs/common/types/timestamp.cpp +++ b/src/mongo/scripting/mozjs/common/types/timestamp.cpp @@ -35,7 +35,7 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuereader.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep @@ -79,10 +79,10 @@ double getTimestampComponent(JSContext* cx, JS::HandleValue component, std::stri } // namespace void TimestampInfo::construct(JSContext* cx, JS::CallArgs args) { - auto* scope = getMozJSScope(cx); + auto* runtime = getCommonRuntime(cx); JS::RootedObject thisv(cx); - getProto(scope).newObject(&thisv); + getProto(runtime).newObject(&thisv); ObjectWrapper o(cx, thisv); if (args.length() == 0) { diff --git a/src/mongo/scripting/mozjs/common/valuereader.cpp b/src/mongo/scripting/mozjs/common/valuereader.cpp index 146ae795871..7472a0cf4de 100644 --- a/src/mongo/scripting/mozjs/common/valuereader.cpp +++ b/src/mongo/scripting/mozjs/common/valuereader.cpp @@ -35,7 +35,7 @@ #include "mongo/bson/oid.h" #include "mongo/logv2/log.h" #include "mongo/platform/decimal128.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/types/bindata.h" #include "mongo/scripting/mozjs/common/types/bson.h" #include "mongo/scripting/mozjs/common/types/code.h" @@ -58,7 +58,9 @@ #include #include +#if !defined(MONGO_MOZJS_WASI_BUILD) #include +#endif #include #include @@ -83,36 +85,34 @@ ValueReader::ValueReader(JSContext* cx, JS::MutableHandleValue value) : _context(cx), _value(value) {} void ValueReader::fromBSONElement(const BSONElement& elem, const BSONObj& parent, bool readOnly) { - auto scope = getMozJSScope(_context); + auto runtime = getCommonRuntime(_context); switch (elem.type()) { case BSONType::code: // javascriptProtection prevents Code and CodeWScope BSON types from // being automatically marshalled into executable functions. - if (scope->isJavaScriptProtectionEnabled()) { + if (runtime->isJavaScriptProtectionEnabled()) { JS::RootedValueArray<1> args(_context); ValueReader(_context, args[0]).fromStringData(elem.valueStringData()); - - JS::RootedObject obj(_context); - getProto(scope).newInstance(args, _value); + getProto(runtime).newInstance(args, _value); } else { - scope->newFunction(elem.valueStringData(), _value); + runtime->newFunction(elem.valueStringData(), _value); } return; case BSONType::codeWScope: - if (scope->isJavaScriptProtectionEnabled()) { + if (runtime->isJavaScriptProtectionEnabled()) { JS::RootedValueArray<2> args(_context); - ValueReader(_context, args[0]).fromStringData(elem.codeWScopeCode()); ValueReader(_context, args[1]) .fromBSON(elem.codeWScopeObject().getOwned(), nullptr, readOnly); - - getProto(scope).newInstance(args, _value); + getProto(runtime).newInstance(args, _value); } else { +#ifndef MONGO_MOZJS_WASI_BUILD if (!elem.codeWScopeObject().isEmpty()) LOGV2_WARNING(23826, "CodeWScope doesn't transfer to db.eval"); - scope->newFunction(StringData(elem.codeWScopeCode(), elem.codeWScopeCodeLen() - 1), - _value); +#endif + runtime->newFunction( + StringData(elem.codeWScopeCode(), elem.codeWScopeCodeLen() - 1), _value); } return; case BSONType::symbol: @@ -157,7 +157,7 @@ void ValueReader::fromBSONElement(const BSONElement& elem, const BSONObj& parent ValueReader(_context, args[1]).fromStringData(elem.regexFlags()); JS::RootedObject obj(_context); - getProto(scope).newInstance(args, &obj); + getProto(runtime).newInstance(args, &obj); _value.setObjectOrNull(obj); @@ -175,7 +175,7 @@ void ValueReader::fromBSONElement(const BSONElement& elem, const BSONObj& parent ValueReader(_context, args[1]).fromStringData(ss.str()); - getProto(scope).newInstance(args, _value); + getProto(runtime).newInstance(args, _value); return; } case BSONType::timestamp: { @@ -185,16 +185,16 @@ void ValueReader::fromBSONElement(const BSONElement& elem, const BSONObj& parent .fromDouble(static_cast(elem.timestampTime().toMillisSinceEpoch()) / 1000); ValueReader(_context, args[1]).fromDouble(elem.timestampInc()); - getProto(scope).newInstance(args, _value); + getProto(runtime).newInstance(args, _value); return; } case BSONType::numberLong: { JS::RootedObject thisv(_context); - getProto(scope).newObject(&thisv); + getProto(runtime).newObject(&thisv); JS::SetReservedSlot(thisv, NumberLongInfo::Int64Slot, - JS::PrivateValue(scope->trackedNewInt64(elem.numberLong()))); + JS::PrivateValue(runtime->trackedNewInt64(elem.numberLong()))); _value.setObjectOrNull(thisv); return; } @@ -204,16 +204,16 @@ void ValueReader::fromBSONElement(const BSONElement& elem, const BSONObj& parent ValueReader(_context, args[0]).fromDecimal128(decimal); JS::RootedObject obj(_context); - getProto(scope).newInstance(args, &obj); + getProto(runtime).newInstance(args, &obj); _value.setObjectOrNull(obj); return; } case BSONType::minKey: - getProto(scope).newInstance(_value); + getProto(runtime).newInstance(_value); return; case BSONType::maxKey: - getProto(scope).newInstance(_value); + getProto(runtime).newInstance(_value); return; case BSONType::dbRef: { JS::RootedValueArray<1> oidArgs(_context); @@ -221,9 +221,9 @@ void ValueReader::fromBSONElement(const BSONElement& elem, const BSONObj& parent JS::RootedValueArray<2> dbPointerArgs(_context); ValueReader(_context, dbPointerArgs[0]).fromStringData(elem.dbrefNS()); - getProto(scope).newInstance(oidArgs, dbPointerArgs[1]); + getProto(runtime).newInstance(oidArgs, dbPointerArgs[1]); - getProto(scope).newInstance(dbPointerArgs, _value); + getProto(runtime).newInstance(dbPointerArgs, _value); return; } default: @@ -337,11 +337,11 @@ void ValueReader::fromDouble(double d) { } void ValueReader::fromInt64(int64_t i) { - auto scope = getMozJSScope(_context); + auto runtime = getCommonRuntime(_context); JS::RootedObject num(_context); - getProto(scope).newObject(&num); + getProto(runtime).newObject(&num); JS::SetReservedSlot( - num, NumberLongInfo::Int64Slot, JS::PrivateValue(scope->trackedNewInt64(i))); + num, NumberLongInfo::Int64Slot, JS::PrivateValue(runtime->trackedNewInt64(i))); _value.setObjectOrNull(num); } diff --git a/src/mongo/scripting/mozjs/common/valuewriter.cpp b/src/mongo/scripting/mozjs/common/valuewriter.cpp index fd512704a93..aed9d791d5f 100644 --- a/src/mongo/scripting/mozjs/common/valuewriter.cpp +++ b/src/mongo/scripting/mozjs/common/valuewriter.cpp @@ -33,11 +33,12 @@ #include "mongo/bson/bsonelement.h" #include "mongo/bson/bsontypes.h" #include "mongo/platform/decimal128.h" +#include "mongo/scripting/js_regex.h" #include "mongo/scripting/mozjs/common/exception.h" #include "mongo/scripting/mozjs/common/internedstring.h" #include "mongo/scripting/mozjs/common/jsstringwrapper.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/types/bindata.h" #include "mongo/scripting/mozjs/common/types/code.h" #include "mongo/scripting/mozjs/common/types/dbpointer.h" @@ -137,22 +138,22 @@ int ValueWriter::type() { } if (auto jsClass = JS::GetClass(obj)) { - auto scope = getMozJSScope(_context); - if (scope->numberIntProto().getJSClass() == jsClass) { + auto runtime = getCommonRuntime(_context); + if (runtime->numberIntProto().getJSClass() == jsClass) { return stdx::to_underlying(BSONType::numberInt); - } else if (scope->numberLongProto().getJSClass() == jsClass) { + } else if (runtime->numberLongProto().getJSClass() == jsClass) { return stdx::to_underlying(BSONType::numberLong); - } else if (scope->numberDecimalProto().getJSClass() == jsClass) { + } else if (runtime->numberDecimalProto().getJSClass() == jsClass) { return stdx::to_underlying(BSONType::numberDecimal); - } else if (scope->oidProto().getJSClass() == jsClass) { + } else if (runtime->oidProto().getJSClass() == jsClass) { return stdx::to_underlying(BSONType::oid); - } else if (scope->binDataProto().getJSClass() == jsClass) { + } else if (runtime->binDataProto().getJSClass() == jsClass) { return stdx::to_underlying(BSONType::binData); - } else if (scope->timestampProto().getJSClass() == jsClass) { + } else if (runtime->timestampProto().getJSClass() == jsClass) { return stdx::to_underlying(BSONType::timestamp); - } else if (scope->minKeyProto().getJSClass() == jsClass) { + } else if (runtime->minKeyProto().getJSClass() == jsClass) { return stdx::to_underlying(BSONType::minKey); - } else if (scope->maxKeyProto().getJSClass() == jsClass) { + } else if (runtime->maxKeyProto().getJSClass() == jsClass) { return stdx::to_underlying(BSONType::maxKey); } } @@ -251,7 +252,7 @@ int32_t ValueWriter::toInt32() { int64_t ValueWriter::toInt64() { int64_t out; - if (getMozJSScope(_context)->numberLongProto().instanceOf(_value)) + if (getCommonRuntime(_context)->numberLongProto().instanceOf(_value)) return NumberLongInfo::ToNumberLong(_context, _value); if (JS::ToInt64(_context, _value, &out)) @@ -265,13 +266,13 @@ Decimal128 ValueWriter::toDecimal128() { if (_value.isNumber()) { return Decimal128(toNumber(), Decimal128::kRoundTo15Digits); } - if (getMozJSScope(_context)->numberIntProto().instanceOf(_value)) + if (getCommonRuntime(_context)->numberIntProto().instanceOf(_value)) return Decimal128(NumberIntInfo::ToNumberInt(_context, _value)); - if (getMozJSScope(_context)->numberLongProto().instanceOf(_value)) + if (getCommonRuntime(_context)->numberLongProto().instanceOf(_value)) return Decimal128(static_cast(NumberLongInfo::ToNumberLong(_context, _value))); - if (getMozJSScope(_context)->numberDecimalProto().instanceOf(_value)) + if (getCommonRuntime(_context)->numberDecimalProto().instanceOf(_value)) return NumberDecimalInfo::ToNumberDecimal(_context, _value); if (_value.isString()) { @@ -296,7 +297,7 @@ Decimal128 ValueWriter::toDecimal128() { } OID ValueWriter::toOID() { - if (getMozJSScope(_context)->oidProto().instanceOf(_value)) { + if (getCommonRuntime(_context)->oidProto().instanceOf(_value)) { return OIDInfo::getOID(_context, _value); } @@ -304,7 +305,7 @@ OID ValueWriter::toOID() { } void ValueWriter::toBinData(std::function withBinData) { - if (!getMozJSScope(_context)->binDataProto().instanceOf(_value)) { + if (!getCommonRuntime(_context)->binDataProto().instanceOf(_value)) { throwCurrentJSException(_context, ErrorCodes::BadValue, "Unable to write BinData value."); } @@ -318,9 +319,8 @@ void ValueWriter::toBinData(std::function withBinData) uassert(ErrorCodes::BadValue, "Cannot call getter on BinData prototype", binDataStr); auto binData = base64::decode(*binDataStr); - withBinData(BSONBinData(binData.c_str(), - binData.size(), - static_cast(static_cast(subType)))); + withBinData(BSONBinData( + binData.c_str(), binData.size(), static_cast(static_cast(subType)))); } Timestamp ValueWriter::toTimestamp() { @@ -328,7 +328,7 @@ Timestamp ValueWriter::toTimestamp() { uassert(ErrorCodes::BadValue, "Unable to write Timestamp value.", - getMozJSScope(_context)->timestampProto().getJSClass() == JS::GetClass(obj)); + getCommonRuntime(_context)->timestampProto().getJSClass() == JS::GetClass(obj)); return TimestampInfo::getValidatedValue(_context, obj); } @@ -393,7 +393,7 @@ void ValueWriter::writeThis(BSONObjBuilder* b, void ValueWriter::_writeObject(BSONObjBuilder* b, StringData sd, ObjectWrapper::WriteFieldRecursionFrames* frames) { - auto scope = getMozJSScope(_context); + auto runtime = getCommonRuntime(_context); // We open a block here because it's important that the two rooting types // we need (obj and o) go out of scope before we actually open a @@ -407,26 +407,26 @@ void ValueWriter::_writeObject(BSONObjBuilder* b, auto jsclass = JS::GetClass(obj); if (jsclass) { - if (scope->oidProto().getJSClass() == jsclass) { + if (runtime->oidProto().getJSClass() == jsclass) { b->append(sd, OIDInfo::getOID(_context, obj)); return; } - if (scope->numberLongProto().getJSClass() == jsclass) { + if (runtime->numberLongProto().getJSClass() == jsclass) { long long out = NumberLongInfo::ToNumberLong(_context, obj); b->append(sd, out); return; } - if (scope->numberIntProto().getJSClass() == jsclass) { + if (runtime->numberIntProto().getJSClass() == jsclass) { b->append(sd, NumberIntInfo::ToNumberInt(_context, obj)); return; } - if (scope->codeProto().getJSClass() == jsclass) { + if (runtime->codeProto().getJSClass() == jsclass) { if (o.hasOwnField(InternedString::scope) // CodeWScope && o.type(InternedString::scope) == stdx::to_underlying(BSONType::object)) { if (o.type(InternedString::code) != stdx::to_underlying(BSONType::string)) { @@ -445,16 +445,16 @@ void ValueWriter::_writeObject(BSONObjBuilder* b, return; } - if (scope->numberDecimalProto().getJSClass() == jsclass) { + if (runtime->numberDecimalProto().getJSClass() == jsclass) { b->append(sd, NumberDecimalInfo::ToNumberDecimal(_context, obj)); return; } - if (scope->dbPointerProto().getJSClass() == jsclass) { + if (runtime->dbPointerProto().getJSClass() == jsclass) { uassert(ErrorCodes::BadValue, "can't serialize DBPointer prototype", - scope->dbPointerProto().getProto() != obj); + runtime->dbPointerProto().getProto() != obj); JS::RootedValue id(_context); o.getValue("id", &id); @@ -464,7 +464,7 @@ void ValueWriter::_writeObject(BSONObjBuilder* b, return; } - if (scope->binDataProto().getJSClass() == jsclass) { + if (runtime->binDataProto().getJSClass() == jsclass) { auto str = JS::GetMaybePtrFromReservedSlot( obj, BinDataInfo::BinDataStringSlot); @@ -479,26 +479,26 @@ void ValueWriter::_writeObject(BSONObjBuilder* b, b->appendBinData(sd, binData.size(), - static_cast(static_cast(subType)), + static_cast(static_cast(subType)), binData.c_str()); return; } - if (scope->timestampProto().getJSClass() == jsclass) { + if (runtime->timestampProto().getJSClass() == jsclass) { Timestamp ot = TimestampInfo::getValidatedValue(_context, obj); b->append(sd, ot); return; } - if (scope->minKeyProto().getJSClass() == jsclass) { + if (runtime->minKeyProto().getJSClass() == jsclass) { b->appendMinKey(sd); return; } - if (scope->maxKeyProto().getJSClass() == jsclass) { + if (runtime->maxKeyProto().getJSClass() == jsclass) { b->appendMaxKey(sd); return; @@ -511,7 +511,7 @@ void ValueWriter::_writeObject(BSONObjBuilder* b, case JSProto_Function: { uassert(16716, "cannot convert native function to BSON", - !scope->nativeFunctionProto().instanceOf(obj)); + !runtime->nativeFunctionProto().instanceOf(obj)); JSStringWrapper jsstr; b->appendCode(sd, ValueWriter(_context, _value).toStringData(&jsstr)); return; diff --git a/src/mongo/scripting/mozjs/common/valuewriter.h b/src/mongo/scripting/mozjs/common/valuewriter.h index 8164cb6c9a2..3f00fd0aafb 100644 --- a/src/mongo/scripting/mozjs/common/valuewriter.h +++ b/src/mongo/scripting/mozjs/common/valuewriter.h @@ -36,11 +36,14 @@ #include "mongo/bson/oid.h" #include "mongo/bson/timestamp.h" #include "mongo/platform/decimal128.h" -#include "mongo/scripting/engine.h" #include "mongo/scripting/mozjs/common/jsstringwrapper.h" #include "mongo/scripting/mozjs/common/objectwrapper.h" #include "mongo/util/modules.h" +namespace mongo { +struct JSRegEx; +} + #include #include #include diff --git a/src/mongo/scripting/mozjs/common/wrapconstrainedmethod.h b/src/mongo/scripting/mozjs/common/wrapconstrainedmethod.h index ed1c48a3663..2d25a8289e2 100644 --- a/src/mongo/scripting/mozjs/common/wrapconstrainedmethod.h +++ b/src/mongo/scripting/mozjs/common/wrapconstrainedmethod.h @@ -30,7 +30,7 @@ #pragma once #include "mongo/scripting/mozjs/common/objectwrapper.h" -#include "mongo/scripting/mozjs/common/scope_base.h" +#include "mongo/scripting/mozjs/common/runtime.h" #include "mongo/scripting/mozjs/common/valuewriter.h" #include "mongo/scripting/mozjs/common/wraptype.h" #include "mongo/util/assert_util.h" @@ -52,8 +52,8 @@ namespace smUtils { * void as the last type in wrapConstrainedMethod. */ template -bool instanceOf(MozJSScopeBase* scope, bool* isProto, JS::HandleValue value) { - auto& proto = getProto(scope); +bool instanceOf(MozJSCommonRuntimeInterface* runtime, bool* isProto, JS::HandleValue value) { + auto& proto = getProto(runtime); if (proto.instanceOf(value)) { if (value.toObjectOrNull() == proto.getProto()) { @@ -63,7 +63,7 @@ bool instanceOf(MozJSScopeBase* scope, bool* isProto, JS::HandleValue value) { return true; } - return instanceOf(scope, isProto, value); + return instanceOf(runtime, isProto, value); } /** @@ -72,7 +72,9 @@ bool instanceOf(MozJSScopeBase* scope, bool* isProto, JS::HandleValue value) { * We use this to identify the end of the template list in the general case. */ template <> -inline bool instanceOf(MozJSScopeBase* scope, bool* isProto, JS::HandleValue value) { +inline bool instanceOf(MozJSCommonRuntimeInterface* runtime, + bool* isProto, + JS::HandleValue value) { return false; } @@ -85,7 +87,7 @@ inline bool instanceOf(MozJSScopeBase* scope, bool* isProto, JS::HandleVal * ::call - a static function of type void (JSContext* cx, JS::CallArgs args) * ::name - a static function which returns a const char* with the type name * noProto - whether the method can be invoked on the prototype - * Args - The list of types to check against scope->getProto().instanceOf + * Args - The list of types to check against runtime->getProto().instanceOf * for the thisv the method has been invoked against */ template @@ -101,7 +103,7 @@ bool wrapConstrainedMethod(JSContext* cx, unsigned argc, JS::Value* vp) { << ValueWriter(cx, args.thisv()).typeAsString() << "\""); } - if (!instanceOf(getMozJSScope(cx), &isProto, args.thisv())) { + if (!instanceOf(getCommonRuntime(cx), &isProto, args.thisv())) { uasserted(ErrorCodes::BadValue, str::stream() << "Cannot call \"" << T::name() << "\" on object of type \"" << ObjectWrapper(cx, args.thisv()).getClassName() << "\""); diff --git a/src/mongo/scripting/mozjs/shared/BUILD.bazel b/src/mongo/scripting/mozjs/shared/BUILD.bazel new file mode 100644 index 00000000000..392038d0e05 --- /dev/null +++ b/src/mongo/scripting/mozjs/shared/BUILD.bazel @@ -0,0 +1,19 @@ +load("//bazel:mongo_src_rules.bzl", "mongo_cc_library") + +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "mozjs_error_types.h", + "mozjs_wasm_startup_options.h", +]) + +# Shared types and definitions for MozJS that can be used by both +# the WASM engine and the mongod host side. +mongo_cc_library( + name = "mozjs_shared_types", + hdrs = [ + "mozjs_error_types.h", + "mozjs_wasm_startup_options.h", + ], + deps = [], +) diff --git a/src/mongo/scripting/mozjs/shared/mozjs_error_types.h b/src/mongo/scripting/mozjs/shared/mozjs_error_types.h new file mode 100644 index 00000000000..3e96508f6aa --- /dev/null +++ b/src/mongo/scripting/mozjs/shared/mozjs_error_types.h @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include +#include + +namespace mongo { +namespace mozjs { +namespace wasm { +/** + * Shared error codes for MozJS operations. + * Used by both the WASM engine and the mongo host side. + */ +typedef enum { + SM_OK = 0, + + // Wrapper / host-side errors + SM_E_INVALID_ARG, + SM_E_BAD_STATE, + SM_E_NOMEM, + SM_E_IO, + SM_E_TIMEOUT, + SM_E_NOT_SUPPORTED, + SM_E_INTERNAL, + + // MozJS API usage + SM_E_JSAPI_FAIL, + SM_E_PENDING_EXCEPTION, + SM_E_NO_EXCEPTION, + SM_E_TERMINATED, + SM_E_OOM, + + // Script failures + SM_E_COMPILE, + SM_E_RUNTIME, + SM_E_MODULE, + SM_E_PROMISE_REJECTION, + SM_E_STACK_OVERFLOW, + + // Data marshalling / boundary errors + SM_E_TYPE, + SM_E_ENCODING, +} err_code_t; + +typedef struct { + char* msg; // Error message + size_t msg_len; // Length of msg (excluding null terminator) + + char* filename; // Source filename + size_t filename_len; // Length of filename (excluding null terminator) + + char* stack; // Stack trace + size_t stack_len; // Length of stack (excluding null terminator) + + uint32_t line; + uint32_t column; + err_code_t code; +} wasm_mozjs_error_t; + +} // namespace wasm +} // namespace mozjs +} // namespace mongo + diff --git a/src/mongo/scripting/mozjs/shared/mozjs_wasm_startup_options.h b/src/mongo/scripting/mozjs/shared/mozjs_wasm_startup_options.h new file mode 100644 index 00000000000..2863de05095 --- /dev/null +++ b/src/mongo/scripting/mozjs/shared/mozjs_wasm_startup_options.h @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include +#include + +namespace mongo { +namespace mozjs { +namespace wasm { +typedef struct { + size_t opTimeout; // microseconds (0 = no timeout) + size_t heapSize; // in MB +} wasm_mozjs_startup_options_t; + +} // namespace wasm +} // namespace mozjs +} // namespace mongo + diff --git a/src/mongo/scripting/mozjs/shell/engine.cpp b/src/mongo/scripting/mozjs/shell/engine.cpp index 8292bdce838..414a24bef0d 100644 --- a/src/mongo/scripting/mozjs/shell/engine.cpp +++ b/src/mongo/scripting/mozjs/shell/engine.cpp @@ -68,7 +68,7 @@ bool isExternalScriptingEnabled() { } namespace { -auto operationMozJSScopeBaseDecoration = +auto operationMozJSShellRuntimeInterfaceDecoration = OperationContext::declareDecoration(); } @@ -117,8 +117,8 @@ mongo::Scope* MozJSScriptEngine::createScopeForCurrentThread(boost::optionalkill(); + if (opCtx && (*opCtx)[operationMozJSShellRuntimeInterfaceDecoration]) { + (*opCtx)[operationMozJSShellRuntimeInterfaceDecoration]->kill(); LOGV2_DEBUG(22808, 2, "Interrupting op", "opId"_attr = opCtx->getOpID()); } else if (opCtx) { LOGV2_DEBUG( @@ -133,8 +133,8 @@ void MozJSScriptEngine::interruptAll(ServiceContextLock& svcCtxLock) { while (auto client = cursor.next()) { stdx::lock_guard lk(*client); if (auto opCtx = client->getOperationContext(); - opCtx && (*opCtx)[operationMozJSScopeBaseDecoration]) { - (*opCtx)[operationMozJSScopeBaseDecoration]->kill(); + opCtx && (*opCtx)[operationMozJSShellRuntimeInterfaceDecoration]) { + (*opCtx)[operationMozJSShellRuntimeInterfaceDecoration]->kill(); } } } @@ -183,7 +183,7 @@ void MozJSScriptEngine::registerOperation(OperationContext* opCtx, MozJSImplScop "opId"_attr = opCtx->getOpID()); stdx::lock_guard lk(*opCtx->getClient()); - (*opCtx)[operationMozJSScopeBaseDecoration] = scope; + (*opCtx)[operationMozJSShellRuntimeInterfaceDecoration] = scope; if (auto status = opCtx->checkForInterruptNoAssert(); !status.isOK()) { scope->kill(); @@ -198,7 +198,7 @@ void MozJSScriptEngine::unregisterOperation(OperationContext* opCtx) { "opId"_attr = opCtx->getOpID()); stdx::lock_guard lk(*opCtx->getClient()); - (*opCtx)[operationMozJSScopeBaseDecoration] = nullptr; + (*opCtx)[operationMozJSShellRuntimeInterfaceDecoration] = nullptr; } } // namespace mozjs diff --git a/src/mongo/scripting/mozjs/shell/implscope.cpp b/src/mongo/scripting/mozjs/shell/implscope.cpp index f9eae8b12bc..c23244b1bfe 100644 --- a/src/mongo/scripting/mozjs/shell/implscope.cpp +++ b/src/mongo/scripting/mozjs/shell/implscope.cpp @@ -582,7 +582,7 @@ MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine, boost::optional j { JS_AddInterruptCallback(_context, _interruptCallback); JS_SetGCCallback(_context, _gcCallback, this); - JS_SetContextPrivate(_context, this); + JS_SetContextPrivate(_context, static_cast(this)); JSAutoRealm ac(_context, _global); _environmentPreparer = std::make_unique(_context); @@ -631,6 +631,10 @@ MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine, boost::optional j currentJSScope = this; } +MozJSShellRuntimeInterface* getShellRuntime(JSContext* cx) { + return static_cast(getCommonRuntime(cx)); +} + MozJSImplScope::~MozJSImplScope() { invariant(!_promiseResult.initialized()); currentJSScope = nullptr; diff --git a/src/mongo/scripting/mozjs/shell/implscope.h b/src/mongo/scripting/mozjs/shell/implscope.h index dfbd79f9147..b147cf032d5 100644 --- a/src/mongo/scripting/mozjs/shell/implscope.h +++ b/src/mongo/scripting/mozjs/shell/implscope.h @@ -44,7 +44,6 @@ #include "mongo/scripting/mozjs/common/freeOpToJSContext.h" #include "mongo/scripting/mozjs/common/global.h" #include "mongo/scripting/mozjs/common/internedstring.h" -#include "mongo/scripting/mozjs/common/scope_base.h" #include "mongo/scripting/mozjs/common/types/bindata.h" #include "mongo/scripting/mozjs/common/types/bson.h" #include "mongo/scripting/mozjs/common/types/code.h" @@ -74,6 +73,7 @@ #include "mongo/scripting/mozjs/shell/mongo.h" #include "mongo/scripting/mozjs/shell/mongohelpers.h" #include "mongo/scripting/mozjs/shell/resumetoken.h" +#include "mongo/scripting/mozjs/shell/runtime.h" #include "mongo/scripting/mozjs/shell/session.h" #include "mongo/scripting/mozjs/shell/uri.h" #include "mongo/stdx/condition_variable.h" @@ -131,15 +131,20 @@ const StringData kUnknownError = "Unknown Failure from JSInterpreter"; * * For more information about overriden fields, see mongo::Scope */ -class MONGO_MOD_PUB MozJSImplScope final : public MozJSScopeBase { +class MONGO_MOD_PUB MozJSImplScope final : public Scope, + public MozJSShellRuntimeInterface, + public MozJSCommonRuntimeInterface { MozJSImplScope(const MozJSImplScope&) = delete; MozJSImplScope& operator=(const MozJSImplScope&) = delete; + template + friend WrapType& getProto(MozJSCommonRuntimeInterface*); + public: explicit MozJSImplScope(MozJSScriptEngine* engine, boost::optional jsHeapLimitMB); ~MozJSImplScope() override; - // ---- MozJSScopeBase (common) surface ---- + // ---- Proto accessors ---- WrapType& binDataProto() override { return _binDataProto; } @@ -668,13 +673,37 @@ private: }; MONGO_MOD_PUB inline MozJSImplScope* getScope(JSContext* cx) { - return static_cast(JS_GetContextPrivate(cx)); + return static_cast(getCommonRuntime(cx)); } inline MozJSImplScope* getScope(class JS::GCContext* gcCtx) { return getScope(freeOpToJSContext(gcCtx)); } +template <> +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return static_cast(runtime)->cursorHandleProto(); +} + +template <> +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return static_cast(runtime)->cursorProto(); +} + +template <> +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return static_cast(runtime)->mongoExternalProto(); +} + +template <> +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return static_cast(runtime)->sessionProto(); +} + +template <> +inline WrapType& getProto(MozJSCommonRuntimeInterface* runtime) { + return static_cast(runtime)->uriProto(); +} } // namespace mozjs } // namespace mongo diff --git a/src/mongo/scripting/mozjs/shell/resumetoken.cpp b/src/mongo/scripting/mozjs/shell/resumetoken.cpp index bad2727d0bb..d6e71cd6640 100644 --- a/src/mongo/scripting/mozjs/shell/resumetoken.cpp +++ b/src/mongo/scripting/mozjs/shell/resumetoken.cpp @@ -30,9 +30,9 @@ #include "mongo/scripting/mozjs/shell/resumetoken.h" #include "mongo/db/pipeline/resume_token.h" -#include "mongo/scripting/mozjs/common/scope_base.h" #include "mongo/scripting/mozjs/common/types/bson.h" #include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" // IWYU pragma: keep +#include "mongo/scripting/mozjs/shell/runtime.h" #include @@ -60,8 +60,8 @@ void ResumeTokenDataUtility::Functions::decodeResumeToken::call(JSContext* cx, J const auto resumeTokenDataBson = ResumeToken::parse(encodedResumeTokenBson).getData().toBSON(); JS::RootedObject thisValue(cx); - auto* scope = getMozJSScope(cx); - getProto(scope).newObject(&thisValue); + auto* runtime = getCommonRuntime(cx); + getProto(runtime).newObject(&thisValue); BSONInfo::make(cx, &thisValue, std::move(resumeTokenDataBson), nullptr, true); args.rval().setObjectOrNull(thisValue); diff --git a/src/mongo/scripting/mozjs/shell/runtime.h b/src/mongo/scripting/mozjs/shell/runtime.h new file mode 100644 index 00000000000..9ca315dc9e1 --- /dev/null +++ b/src/mongo/scripting/mozjs/shell/runtime.h @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/scripting/mozjs/common/runtime.h" +#include "mongo/util/modules.h" + +struct JSContext; + +namespace mongo::mozjs { + +struct CursorHandleInfo; +struct CursorInfo; +struct MongoExternalInfo; +struct SessionInfo; +struct URIInfo; + +/** + * Shell-specific prototype accessors for JS types that only exist in the + * full mongo shell (cursors, connections, sessions, URIs). + */ +class MONGO_MOD_PUB MozJSShellRuntimeInterface { +public: + virtual ~MozJSShellRuntimeInterface() = default; + + virtual WrapType& cursorHandleProto() = 0; + virtual WrapType& cursorProto() = 0; + virtual WrapType& mongoExternalProto() = 0; + virtual WrapType& sessionProto() = 0; + virtual WrapType& uriProto() = 0; +}; + +/** + * Get the MozJSShellRuntimeInterface from a JSContext. + * Use this only when shell-specific features are needed. + * For common code, use getCommonRuntime() from scope.h instead. + */ +MozJSShellRuntimeInterface* getShellRuntime(JSContext* cx); + +} // namespace mongo::mozjs diff --git a/src/mongo/scripting/mozjs/wasm/BUILD.bazel b/src/mongo/scripting/mozjs/wasm/BUILD.bazel new file mode 100644 index 00000000000..e490e7273e5 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/BUILD.bazel @@ -0,0 +1,195 @@ +load("//bazel:mongo_src_rules.bzl", "mongo_cc_benchmark", "mongo_cc_binary", "mongo_cc_library", "mongo_cc_unit_test") # @unused mongo_cc_binary mongo_cc_library +load("//bazel/toolchains/cc/mongo_wasm/toolchain:with_wasi_config.bzl", "with_wasi_config") +load("//src/mongo/scripting/mozjs/wasm/engine:linkset.bzl", "cc_linkset") + +# Gate WASM module compilation behind --define=build_mozjs_wasm=true. +# Without this flag the test compiles and runs but skips (no .wasm module found). +config_setting( + name = "build_mozjs_wasm", + define_values = {"build_mozjs_wasm": "true"}, +) + +exports_files([ + "status.cpp", +]) + +cc_linkset( + name = "mongo_base_linkset", + extra_flags = [ + "-lc++", + "-lc++abi", + "-lwasi-emulated-signal", + "-lwasi-emulated-mman", + "-lwasi-emulated-process-clocks", + "-lwasi-emulated-getpid", + "-Wl,--gc-sections", + "-Wl,--no-entry", + "-Wl,--export-memory", + "-Wl,--stack-first", + "-mexec-model=reactor", + ], + deps = ["//src/mongo:base"], +) + +genrule( + name = "spidermonkey_wasip2_dist_release_from_source", + srcs = [ + "@spidermonkey//:mach", + "@spidermonkey//:srcs", + "support/rust_shims/src/lib.rs", + "support/rust_shims/Cargo.toml.template", + "scripts/build_spidermonkey_wasip2.sh", + ], + outs = ["spidermonkey-wasip2-release.tar.gz"], + cmd = """ + export WASI_SDK_BIN_FILES="$(locations @wasi_sdk//:bin)" + export SPIDER_MACH_PATH="$(location @spidermonkey//:mach)" + export RUST_SHIMS_LIB_RS="$(location support/rust_shims/src/lib.rs)" + export CARGO_TEMPLATE_PATH="$(location support/rust_shims/Cargo.toml.template)" + export OUTPUT="$@" + bash $(location scripts/build_spidermonkey_wasip2.sh) + """, + tags = [ + "local", + "manual", + "no-sandbox", + ], + tools = [ + "@wasi_sdk//:bin", + ], +) + +alias( + name = "spidermonkey_wasip2_dist_release", + actual = select({ + "@platforms//os:wasi": "@spidermonkey_wasip2_dist//file", + "//conditions:default": ":spidermonkey_wasip2_dist_release_from_source", + }), +) + +# Source files for WASM compilation (excludes test files and stubs) +filegroup( + name = "wasm_sources", + srcs = glob( + ["*.cpp"], + exclude = [ + "status.cpp", # Included in wasi_sources filegroup + ], + ) + [ + # engine/ is its own package so glob cannot reach it; list explicitly. + "//src/mongo/scripting/mozjs/wasm/engine:engine.cpp", + "//src/mongo/scripting/mozjs/wasm/engine:error.cpp", + ], +) + +# Rust shims for WASI linkage (provides encoding_* symbols) +# These are built by spidermonkey_wasip2_dist_release and included in the tarball. +# This genrule extracts them from the tarball. +genrule( + name = "rust_shims", + srcs = [ + ":spidermonkey_wasip2_dist_release", + "scripts/extract_rust_shims.sh", + ], + outs = ["rust_shims.a"], + cmd = """ + export TARBALL="$(location :spidermonkey_wasip2_dist_release)" + export OUTPUT="$@" + bash $(location scripts/extract_rust_shims.sh) + """, + tags = ["manual"], + tools = [], +) + +genrule( + name = "mozjs_wasm_api", + srcs = [ + ":wasm_sources", + "//src/mongo/scripting/mozjs/common:wasi_sources", + "//src/mongo/scripting/mozjs/wasm/engine:exception_stubs.cpp", + "//src/mongo/scripting/mozjs/wasm/engine:api.cpp", + ":spidermonkey_wasip2_dist_release", + ":rust_shims", + ":mongo_base_linkset", + "//src/mongo/base:error_codes_header", + "//src/mongo:mongo_config_header", + "//src/mongo:base", + + # WIT + generated bindings + "//src/mongo/scripting/mozjs/wasm/wit:mozjs.wit", + "//src/mongo/scripting/mozjs/wasm/wit_gen/generated:api.c", + "//src/mongo/scripting/mozjs/wasm/wit_gen/generated:api.h", + "//src/mongo/scripting/mozjs/wasm/wit_gen/generated:api_component_type.o", + + # Build scripts + "scripts/compile_mozjs_wasm_api.sh", + "scripts/compile_wasi_source.sh", + ], + outs = ["mozjs_wasm_api.wasm"], + cmd = """ + export WASI_SDK_BIN_FILES="$(locations @wasi_sdk//:bin)" + export SM_TARBALL="$(location :spidermonkey_wasip2_dist_release)" + export RUST_SHIMS_PATH="$(location :rust_shims)" + export WASM_SOURCES="$(locations :wasm_sources)" + export COMMON_WASI_SOURCES="$(locations //src/mongo/scripting/mozjs/common:wasi_sources)" + export EXCEPTION_STUBS_SRC="$(location //src/mongo/scripting/mozjs/wasm/engine:exception_stubs.cpp)" + export MOZJS_API_SRC="$(location //src/mongo/scripting/mozjs/wasm/engine:api.cpp)" + export WIT_API_C="$(location //src/mongo/scripting/mozjs/wasm/wit_gen/generated:api.c)" + export WIT_COMPONENT_TYPE_OBJ="$(location //src/mongo/scripting/mozjs/wasm/wit_gen/generated:api_component_type.o)" + export ERROR_CODES_HEADER_FILES="$(locations //src/mongo/base:error_codes_header)" + export CONFIG_HEADER_FILES="$(locations //src/mongo:mongo_config_header)" + export LINKSET_FILES="$(locations :mongo_base_linkset)" + export COMPILE_HELPER="$(location scripts/compile_wasi_source.sh)" + export OUTPUT="$@" + bash $(location scripts/compile_mozjs_wasm_api.sh) + """, + tags = [ + "local", + "manual", + "no-sandbox", + ], + target_compatible_with = select({ + "@platforms//os:wasi": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + tools = [ + "@wasi_sdk//:bin", + ], + visibility = ["//visibility:public"], +) + +with_wasi_config( + name = "mozjs_wasm_api_data_from_source", + srcs = [":mozjs_wasm_api"], + tags = ["manual"], + target_compatible_with = ["@platforms//os:linux"], +) + +alias( + name = "mozjs_wasm_api_data", + actual = select({ + ":build_mozjs_wasm": ":mozjs_wasm_api_data_from_source", + "//conditions:default": "@mozjs_wasm//file", + }), +) + +mongo_cc_unit_test( + name = "wasm_mozjs_test", + size = "large", + timeout = "long", + srcs = ["//src/mongo/scripting/mozjs/wasm/engine:engine_test.cpp"], + auto_header = False, + data = [":mozjs_wasm_api_data"], + tags = [ + "incompatible_with_bazel_remote_test", + "local", + "mongo_unittest_eighth_group", + "mozjs_wasm_tests", + "no-sandbox", + ], + target_compatible_with = ["@platforms//os:linux"], + deps = [ + "//src/mongo:base", + "@crates//:wasmtime_c", + ], +) diff --git a/src/mongo/scripting/mozjs/wasm/cabi_realloc.cpp b/src/mongo/scripting/mozjs/wasm/cabi_realloc.cpp new file mode 100644 index 00000000000..598528aba32 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/cabi_realloc.cpp @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include + +// mozglue stub for rust hooks +extern "C" void install_rust_hooks() {} + +// Canonical ABI allocator hook used by the component model. +// Required for wasm-component-ld to produce a valid component. +extern "C" void* cabi_realloc(void* ptr, size_t old_size, size_t align, size_t new_size) { + (void)old_size; + (void)align; + if (new_size == 0) { + std::free(ptr); + return nullptr; + } + void* result = std::realloc(ptr, new_size); + if (!result) { + // realloc failed -- original ptr is still valid but we cannot grow. + // In WASM component model this is typically fatal. + std::abort(); + } + return result; +} diff --git a/src/mongo/scripting/mozjs/wasm/engine/BUILD.bazel b/src/mongo/scripting/mozjs/wasm/engine/BUILD.bazel new file mode 100644 index 00000000000..af49d1ebbf8 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/BUILD.bazel @@ -0,0 +1 @@ +exports_files(glob(["*"])) diff --git a/src/mongo/scripting/mozjs/wasm/engine/README.md b/src/mongo/scripting/mozjs/wasm/engine/README.md new file mode 100644 index 00000000000..7b11e73e363 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/README.md @@ -0,0 +1,81 @@ +# MozJS WASM Engine + +The WASM engine wraps SpiderMonkey into a WASI Preview 2 component +(`mozjs_wasm_api.wasm`) that the host (mongod/mongos) loads via Wasmtime. The +host calls exported WIT functions to create JS contexts, compile functions, +invoke them with BSON arguments, and read back BSON results — all inside a +sandboxed WebAssembly instance. + +## Architecture + +``` +Host (Wasmtime) + │ + ▼ +WIT exports ── api.cpp extern "C" functions generated by wit-bindgen + │ + ▼ +MozJSScriptEngine ── engine.cpp/.h manages JSContext, function slots, BSON ↔ JS + │ + ▼ +SpiderMonkey (libjs_static.a, compiled for wasm32-wasip2) +``` + +Key files in this directory: + +| File | Purpose | +| ----------------------- | ----------------------------------------------------------------------------------------------- | +| `engine.h / engine.cpp` | `MozJSScriptEngine` — owns the SpiderMonkey runtime, prototype installer, function handle table | +| `api.cpp` | Implements each WIT export; bridges WIT types to `MozJSScriptEngine` | +| `error.h / error.cpp` | `ExecutionCheck` — wraps JSAPI calls and captures exceptions into `wasm_mozjs_error_t` | +| `exception_stubs.cpp` | C++ exception / RTTI stubs (WASM has no native exception support) | +| `utils.h` | `cabi_realloc` helper and string utilities | +| `engine_test.cpp` | Unit tests — loads the `.wasm` module via Wasmtime's component model | +| `linkset.bzl` | `cc_linkset` Starlark rule for collecting linker inputs into response files | + +## WIT Interface + +The public API is defined in `../wit/mozjs.wit` (package `mongo:mozjs`, world +`api`). The world exports a single `mozjs` interface: + +- `initialize-engine` / `shutdown-engine` / `interrupt-current-op` — lifecycle +- `create-function(source)` → `function-handle` — compile JS source +- `invoke-function(handle, bson)` — call a compiled function with BSON args +- `get-return-value-bson` — retrieve the last return value as BSON +- `set-global` / `get-global` — read/write named JS globals as BSON + +C bindings live in `../wit_gen/generated/` and are produced by: + +```bash +wit-bindgen c ../wit --out-dir ../wit_gen/generated +``` + +The generated `api.h` declares the `exports_*` symbols that `api.cpp` +implements. The generated `api_component_type.o` is linked into the final +`.wasm` to embed the component type section. + +## Building + +Everything is driven by Bazel from `../BUILD.bazel`. + +Without `--define=build_mozjs_wasm=true` a prebuilt `.wasm` is fetched from S3 +instead. + +**Build WASM module and run unit tests:** + +```bash +bazel test //src/mongo/scripting/mozjs/wasm:wasm_mozjs_test --define=build_mozjs_wasm=true --spawn_strategy=local +``` + +Tests load `mozjs_wasm_api.wasm` into Wasmtime, instantiate it as a WASI +Preview 2 component, and exercise every WIT export. + +## Dependencies + +| Dependency | How it's pulled in | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **SpiderMonkey** | `@spidermonkey//` Bazel repo (version in `../spider-monkey/`). Built for `wasm32-wasip2` by `scripts/build_spidermonkey_wasip2.sh`. | +| **WASI SDK** | `@wasi_sdk//` Bazel repo (`bazel/toolchains/cc/mongo_wasm/`). Provides `clang` cross-compiler and WASI sysroot. | +| **Rust shims** | `../support/rust_shims/` — small Rust crate providing `encoding_c` / `encoding_c_mem` symbols needed by SpiderMonkey's ICU layer. Built during the SpiderMonkey build, extracted by `scripts/extract_rust_shims.sh`. | +| **Wasmtime** | `@crates//:wasmtime_c`. Used by the host side and tests, not compiled into the `.wasm`. | +| **MongoDB base** | `//src/mongo:base` — linked into the WASM module via the `cc_linkset` rule so that code like `BSONObj`, `Status`, etc. is available inside the sandbox. | diff --git a/src/mongo/scripting/mozjs/wasm/engine/api.cpp b/src/mongo/scripting/mozjs/wasm/engine/api.cpp new file mode 100644 index 00000000000..149148c77d8 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/api.cpp @@ -0,0 +1,478 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/scripting/mozjs/wasm/wit_gen/generated/api.h" + +#include "mongo/bson/bsonelement.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/scripting/mozjs/shared/mozjs_error_types.h" +#include "mongo/scripting/mozjs/shared/mozjs_wasm_startup_options.h" + +#include +#include +#include + +#include "engine.h" +#include "error.h" + +namespace { + +// Maximum JavaScript source code size (1 MB) +constexpr size_t kMaxJsSourceSize = 1 * 1024 * 1024; + +// Maximum BSON document size (16mb) +constexpr size_t kMaxBsonSize = 16 * 1024 * 1024; + +// Maximum number of functions that can be created +constexpr size_t kMaxFunctions = 10000; + +// Default operation timeout in microseconds (30 seconds) +// This prevents infinite loops in JavaScript code. +constexpr size_t kDefaultOpTimeoutMicros = 30 * 1000 * 1000; + +// Default JS heap size in MB for the WASM engine. +constexpr uint32_t kDefaultHeapSizeMB = 100; + +} // namespace +namespace mongo { +namespace mozjs { +namespace wasm { + +// Thread-safety: This module is designed for single-threaded use within a +// single WASM instance. Each WASM instance gets its own linear memory and +// thus its own copy of g_engine. Do not share a WASM instance across threads. +static MozJSScriptEngine g_engine; + +} // namespace wasm +} // namespace mozjs +} // namespace mongo + +static void set_opt_string(api_option_string_t* out, const char* ptr, size_t len) { + if (ptr && len > 0) { + out->is_some = true; + api_string_dup_n(&out->val, ptr, len); + } else { + out->is_some = false; + out->val.ptr = nullptr; + out->val.len = 0; + } +} + +static void fill_wit_error(exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* out, + const mongo::mozjs::wasm::wasm_mozjs_error_t& in) { + out->code = static_cast(in.code); + + set_opt_string(&out->msg, in.msg, in.msg_len); + set_opt_string(&out->filename, in.filename, in.filename_len); + set_opt_string(&out->stack, in.stack, in.stack_len); + + out->line = in.line; + out->column = in.column; +} + +static bool return_err(exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err, + mongo::mozjs::wasm::wasm_mozjs_error_t* native_err) { + if (!native_err) { + return false; + } + fill_wit_error(err, *native_err); + mongo::mozjs::wasm::clear_error(native_err); + // wit-bindgen C convention: return false means error (adapter does: is_err = !retval) + return false; +} + +// Returns false on allocation failure so callers can propagate OOM errors. +static bool list_u8_dup(api_list_u8_t* out, const uint8_t* data, size_t len) { + if (len == 0) { + out->ptr = nullptr; + out->len = 0; + return true; + } + auto* p = static_cast(std::malloc(len)); + if (!p) { + out->ptr = nullptr; + out->len = 0; + return false; // OOM + } + std::memcpy(p, data, len); + out->ptr = p; + out->len = len; + return true; +} + +// Validates BSON data before constructing BSONObj to prevent crashes from +// malformed input. This performs basic structural validation. + +static bool validate_bson(const uint8_t* data, + size_t len, + mongo::mozjs::wasm::wasm_mozjs_error_t* err) { + if (len < 5 || len > kMaxBsonSize) { + if (err) { + err->code = mongo::mozjs::wasm::SM_E_INVALID_ARG; + mongo::mozjs::wasm::set_string(&err->msg, + &err->msg_len, + len < 5 ? "BSON too small (minimum 5 bytes)" + : "BSON exceeds 16 MB maximum"); + } + return false; + } + + int32_t declared_size; + std::memcpy(&declared_size, data, sizeof(declared_size)); + + if (declared_size < 5 || static_cast(declared_size) > len || + data[declared_size - 1] != 0) { + if (err) { + err->code = mongo::mozjs::wasm::SM_E_INVALID_ARG; + mongo::mozjs::wasm::set_string( + &err->msg, &err->msg_len, "BSON header invalid (size mismatch or no terminator)"); + } + return false; + } + + return true; +} + +// Track function count for limiting. +static size_t g_function_count = 0; + +// Exported Functions from `mongo:mozjs/mozjs` + +extern "C" bool exports_mongo_mozjs_mozjs_initialize_engine( + exports_mongo_mozjs_mozjs_ok_t* ret, exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + // TODO SERVER-116051: Make wasm_mozjs_startup_options_t an argument. + mongo::mozjs::wasm::wasm_mozjs_startup_options_t opt{}; + opt.opTimeout = opt.opTimeout > 0 ? opt.opTimeout : kDefaultOpTimeoutMicros; + opt.heapSize = opt.heapSize > 0 ? opt.heapSize : kDefaultHeapSizeMB; + int64_t rc = mongo::mozjs::wasm::g_engine.init(&opt, &e); + + if (rc == mongo::mozjs::wasm::SM_OK) { + if (ret) + *ret = 0; // Set the ok value + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_shutdown_engine( + exports_mongo_mozjs_mozjs_ok_t* ret, exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + int64_t rc = mongo::mozjs::wasm::g_engine.shutdown(&e); + if (rc == mongo::mozjs::wasm::SM_OK) { + g_function_count = 0; + if (ret) + *ret = 0; + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_interrupt_current_op( + exports_mongo_mozjs_mozjs_ok_t* ret, exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + int64_t rc = mongo::mozjs::wasm::g_engine.interrupt(&e); + if (rc == mongo::mozjs::wasm::SM_OK) { + if (ret) + *ret = 0; + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_create_function( + api_list_u8_t* source, + exports_mongo_mozjs_mozjs_function_handle_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + + const uint8_t* bytes = source ? source->ptr : nullptr; + size_t len = source ? source->len : 0; + + if (len > kMaxJsSourceSize) { + e.code = mongo::mozjs::wasm::SM_E_INVALID_ARG; + mongo::mozjs::wasm::set_string(&e.msg, &e.msg_len, "JS source exceeds maximum size (1 MB)"); + return return_err(err, &e); + } + + if (g_function_count >= kMaxFunctions) { + e.code = mongo::mozjs::wasm::SM_E_NOMEM; + mongo::mozjs::wasm::set_string( + &e.msg, &e.msg_len, "Maximum function count reached (10000)"); + return return_err(err, &e); + } + + uint64_t handle = 0; + int64_t rc = mongo::mozjs::wasm::g_engine.createFunction(bytes, len, &handle, &e); + if (rc == mongo::mozjs::wasm::SM_OK) { + g_function_count++; + *ret = static_cast(handle); + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_invoke_function( + exports_mongo_mozjs_mozjs_function_handle_t handle, + api_list_u8_t* bson, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + + if (handle == 0) { + e.code = mongo::mozjs::wasm::SM_E_INVALID_ARG; + return return_err(err, &e); + } + + mongo::BSONObj argsObj; + if (bson && bson->ptr && bson->len > 0) { + if (!validate_bson(bson->ptr, bson->len, &e)) { + return return_err(err, &e); + } + argsObj = mongo::BSONObj(reinterpret_cast(bson->ptr)); + } + + mongo::BSONObj outBson; + auto rc = static_cast(mongo::mozjs::wasm::g_engine.invokeFunction( + static_cast(handle), std::move(argsObj), &outBson, &e)); + + if (rc == mongo::mozjs::wasm::SM_OK) { + if (ret) + *ret = 0; + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_get_return_value_bson( + api_list_u8_t* ret, exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + mongo::BSONObj out; + + auto rc = static_cast(mongo::mozjs::wasm::g_engine.getReturnValueBson(&out, &e)); + if (rc != mongo::mozjs::wasm::SM_OK) { + return return_err(err, &e); + } + + if (!list_u8_dup(ret, reinterpret_cast(out.objdata()), out.objsize())) { + e.code = mongo::mozjs::wasm::SM_E_NOMEM; + mongo::mozjs::wasm::set_string( + &e.msg, &e.msg_len, "OOM: failed to allocate BSON return buffer"); + return return_err(err, &e); + } + return true; +} + +extern "C" bool exports_mongo_mozjs_mozjs_set_global( + api_string_t* name, + api_list_u8_t* bson_value, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + + if (!name || name->len == 0) { + e.code = mongo::mozjs::wasm::SM_E_INVALID_ARG; + return return_err(err, &e); + } + + mongo::BSONObj valueObj; + if (bson_value && bson_value->ptr && bson_value->len > 0) { + if (!validate_bson(bson_value->ptr, bson_value->len, &e)) { + return return_err(err, &e); + } + valueObj = mongo::BSONObj(reinterpret_cast(bson_value->ptr)); + } + + auto rc = static_cast(mongo::mozjs::wasm::g_engine.setGlobal( + reinterpret_cast(name->ptr), name->len, valueObj, &e)); + + if (rc == mongo::mozjs::wasm::SM_OK) { + if (ret) + *ret = 0; + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_get_global( + api_string_t* name, api_list_u8_t* ret, exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + + if (!name || name->len == 0) { + e.code = mongo::mozjs::wasm::SM_E_INVALID_ARG; + return return_err(err, &e); + } + + mongo::BSONObj out; + auto rc = static_cast(mongo::mozjs::wasm::g_engine.getGlobal( + reinterpret_cast(name->ptr), name->len, &out, &e)); + + if (rc != mongo::mozjs::wasm::SM_OK) { + return return_err(err, &e); + } + + if (!list_u8_dup(ret, reinterpret_cast(out.objdata()), out.objsize())) { + e.code = mongo::mozjs::wasm::SM_E_NOMEM; + mongo::mozjs::wasm::set_string( + &e.msg, &e.msg_len, "OOM: failed to allocate global return buffer"); + return return_err(err, &e); + } + return true; +} + +extern "C" bool exports_mongo_mozjs_mozjs_set_global_value( + api_string_t* name, + api_list_u8_t* bson_element, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + + if (!name || name->len == 0) { + e.code = mongo::mozjs::wasm::SM_E_INVALID_ARG; + return return_err(err, &e); + } + + mongo::BSONObj valueObj; + if (bson_element && bson_element->ptr && bson_element->len > 0) { + if (!validate_bson(bson_element->ptr, bson_element->len, &e)) { + return return_err(err, &e); + } + valueObj = mongo::BSONObj(reinterpret_cast(bson_element->ptr)); + } + + auto rc = static_cast(mongo::mozjs::wasm::g_engine.setGlobalValue( + reinterpret_cast(name->ptr), name->len, valueObj, &e)); + + if (rc == mongo::mozjs::wasm::SM_OK) { + if (ret) + *ret = 0; + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_setup_emit( + int64_t* maybe_byte_limit, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + + bool hasLimit = (maybe_byte_limit != nullptr); + int64_t limit = hasLimit ? *maybe_byte_limit : 0; + + auto rc = static_cast(mongo::mozjs::wasm::g_engine.setupEmit(limit, hasLimit, &e)); + + if (rc == mongo::mozjs::wasm::SM_OK) { + if (ret) + *ret = 0; + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_invoke_predicate( + exports_mongo_mozjs_mozjs_function_handle_t handle, + api_list_u8_t* document, + bool* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + + if (handle == 0) { + e.code = mongo::mozjs::wasm::SM_E_INVALID_ARG; + return return_err(err, &e); + } + + mongo::BSONObj docObj; + if (document && document->ptr && document->len > 0) { + if (!validate_bson(document->ptr, document->len, &e)) { + return return_err(err, &e); + } + docObj = mongo::BSONObj(reinterpret_cast(document->ptr)); + } + + bool result = false; + auto rc = static_cast(mongo::mozjs::wasm::g_engine.invokePredicate( + static_cast(handle), std::move(docObj), &result, &e)); + + if (rc == mongo::mozjs::wasm::SM_OK) { + if (ret) + *ret = result; + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_invoke_map( + exports_mongo_mozjs_mozjs_function_handle_t handle, + api_list_u8_t* document, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + + if (handle == 0) { + e.code = mongo::mozjs::wasm::SM_E_INVALID_ARG; + return return_err(err, &e); + } + + mongo::BSONObj docObj; + if (document && document->ptr && document->len > 0) { + if (!validate_bson(document->ptr, document->len, &e)) { + return return_err(err, &e); + } + docObj = mongo::BSONObj(reinterpret_cast(document->ptr)); + } + + auto rc = static_cast(mongo::mozjs::wasm::g_engine.invokeMap( + static_cast(handle), std::move(docObj), &e)); + + if (rc == mongo::mozjs::wasm::SM_OK) { + if (ret) + *ret = 0; + return true; + } + return return_err(err, &e); +} + +extern "C" bool exports_mongo_mozjs_mozjs_drain_emit_buffer( + api_list_u8_t* ret, exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err) { + mongo::mozjs::wasm::wasm_mozjs_error_t e{}; + mongo::BSONObj out; + + auto rc = static_cast(mongo::mozjs::wasm::g_engine.drainEmitBuffer(&out, &e)); + if (rc != mongo::mozjs::wasm::SM_OK) { + return return_err(err, &e); + } + + if (!list_u8_dup(ret, reinterpret_cast(out.objdata()), out.objsize())) { + e.code = mongo::mozjs::wasm::SM_E_NOMEM; + mongo::mozjs::wasm::set_string(&e.msg, &e.msg_len, "OOM: failed to allocate emit buffer"); + return return_err(err, &e); + } + return true; +} diff --git a/src/mongo/scripting/mozjs/wasm/engine/engine.cpp b/src/mongo/scripting/mozjs/wasm/engine/engine.cpp new file mode 100644 index 00000000000..7faf3224225 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/engine.cpp @@ -0,0 +1,942 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "engine.h" + +#include "mongo/base/status.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/scripting/mozjs/common/error.h" +#include "mongo/scripting/mozjs/common/runtime.h" +#include "mongo/scripting/mozjs/common/types/bindata.h" +#include "mongo/scripting/mozjs/common/types/bson.h" +#include "mongo/scripting/mozjs/common/types/code.h" +#include "mongo/scripting/mozjs/common/types/dbpointer.h" +#include "mongo/scripting/mozjs/common/types/dbref.h" +#include "mongo/scripting/mozjs/common/types/maxkey.h" +#include "mongo/scripting/mozjs/common/types/minkey.h" +#include "mongo/scripting/mozjs/common/types/nativefunction.h" +#include "mongo/scripting/mozjs/common/types/numberdecimal.h" +#include "mongo/scripting/mozjs/common/types/numberint.h" +#include "mongo/scripting/mozjs/common/types/numberlong.h" +#include "mongo/scripting/mozjs/common/types/oid.h" +#include "mongo/scripting/mozjs/common/types/regexp.h" +#include "mongo/scripting/mozjs/common/types/status.h" +#include "mongo/scripting/mozjs/common/types/timestamp.h" +#include "mongo/scripting/mozjs/common/valuereader.h" +#include "mongo/scripting/mozjs/common/valuewriter.h" +#include "mongo/util/assert_util.h" + +#include +#include +#include +#include +#include + +#include "error.h" +#include "jsapi.h" +#include "jsfriendapi.h" + +#include "js/CompilationAndEvaluation.h" +#include "js/Conversions.h" +#include "js/ErrorReport.h" +#include "js/Exception.h" +#include "js/GlobalObject.h" +#include "js/Initialization.h" +#include "js/Interrupt.h" +#include "js/Realm.h" +#include "js/SourceText.h" +#include "js/String.h" +#include "js/Value.h" +#include "js/ValueArray.h" +#include + +namespace mongo { +namespace mozjs { +namespace wasm { + +uint32_t g_wasmJsHeapLimitMB = 0; + +const char* const kInvokeResult = "__returnValue"; + +FunctionSlot* MozJSScriptEngine::resolveHandle(uint64_t handle, wasm_mozjs_error_t* err) { + if (handle == 0 || handle > _slots.size()) { + if (err) { + err->code = SM_E_INVALID_ARG; + set_string(&err->msg, &err->msg_len, "invalid function handle"); + } + return nullptr; + } + FunctionSlot& slot = _slots[handle - 1]; + if (!slot.fn) { + if (err) { + err->code = SM_E_INVALID_ARG; + set_string(&err->msg, &err->msg_len, "function handle has no function"); + } + return nullptr; + } + return &slot; +} + +MozJSScriptEngine::~MozJSScriptEngine() { + if (_initialized) { + shutdown(nullptr); + } +} + +err_code_t MozJSScriptEngine::init(const wasm_mozjs_startup_options_t* opt, + wasm_mozjs_error_t* err) { + clear_error(err); + + if (_initialized) + return SM_OK; + + if (!JS_Init()) { + if (err) { + err->code = SM_E_INTERNAL; + set_string(&err->msg, &err->msg_len, "JS_Init failed"); + } + return SM_E_INTERNAL; + } + + // Create context with reasonable heap size for WASM builds + constexpr size_t kMaxHeapSizeMB = 2048; // 2 GB upper bound + if (opt->heapSize == 0 || opt->heapSize > kMaxHeapSizeMB) { + if (err) { + err->code = SM_E_INVALID_ARG; + set_string(&err->msg, &err->msg_len, "heapSize out of valid range (1-2048 MB)"); + } + JS_ShutDown(); + return SM_E_INVALID_ARG; + } + _cx = JS_NewContext(static_cast(opt->heapSize) * 1024 * 1024); + if (!_cx) { + if (err) { + err->code = SM_E_OOM; + set_string(&err->msg, &err->msg_len, "JS_NewContext failed"); + } + JS_ShutDown(); + return SM_E_OOM; + } + + // Store configured heap limit for GlobalInfo::getJSHeapLimitMB. + g_wasmJsHeapLimitMB = opt->heapSize; + + _rt = JS_GetRuntime(_cx); + if (!_rt) { + if (err) { + err->code = SM_E_INTERNAL; + set_string(&err->msg, &err->msg_len, "JS_GetRuntime returned null"); + } + JS_DestroyContext(_cx); + _cx = nullptr; + JS_ShutDown(); + return SM_E_INTERNAL; + } + + // Initialize self-hosted code (required before creating global) + ExecutionCheck chk(_cx, err); + if (!chk.ok(JS::InitSelfHostedCode(_cx), SM_E_INTERNAL)) { + JS_DestroyContext(_cx); + _cx = nullptr; + JS_ShutDown(); + return err ? err->code : SM_E_INTERNAL; + } + + // Create global object (must be done before setting interrupt callback) + static const JSClass _globalclass = { + "global", JSCLASS_GLOBAL_FLAGS, &JS::DefaultGlobalClassOps}; + + JS::RealmOptions ro; + _global.init(_cx); + { + JS::RootedObject g(_cx, + chk.okPtr(JS_NewGlobalObject( + _cx, &_globalclass, nullptr, JS::DontFireOnNewGlobalHook, ro))); + if (!g) { + shutdown(nullptr); + return err ? err->code : SM_E_INTERNAL; + } + _global = g; + } + + // Enter realm and initialize standard classes + { + JSAutoRealm ar(_cx, _global); + if (!chk.ok(JS::InitRealmStandardClasses(_cx), SM_E_INTERNAL)) { + shutdown(nullptr); + return err ? err->code : SM_E_INTERNAL; + } + } + + // Fire the new global hook after initialization (as per SpiderMonkey docs) + JS_FireOnNewGlobalObject(_cx, _global); + // JS_FireOnNewGlobalObject doesn't return a value, but we check for exceptions + if (!chk.ok(!JS_IsExceptionPending(_cx), SM_E_INTERNAL)) { + shutdown(nullptr); + return err ? err->code : SM_E_INTERNAL; + } + + // Stash pointer for interrupt callback (after global is set up) + JS_SetContextPrivate(_cx, static_cast(this)); + + // Set up interrupt callback for timeouts/cancel/interrupt + if (!JS_AddInterruptCallback(_cx, &MozJSScriptEngine::interruptCallback)) { + if (err) { + err->code = SM_E_INTERNAL; + set_string(&err->msg, &err->msg_len, "JS_AddInterruptCallback failed"); + } + shutdown(nullptr); + return SM_E_INTERNAL; + } + + _prototypeInstaller = std::make_unique(_cx); + + { + JSAutoRealm ar(_cx, _global); + _internedStrings = std::make_unique(_cx); + _prototypeInstaller->installTypes(_global); + } + + _initialized = true; + return SM_OK; +} + +err_code_t MozJSScriptEngine::shutdown(wasm_mozjs_error_t* err) { + clear_error(err); + + if (!_initialized && !_cx) + return SM_OK; + + // Drop all PersistentRooted objects while the context is alive. + _slots.clear(); + _internedStrings.reset(); + _prototypeInstaller.reset(); + + _global.reset(); + if (_cx) { + // Do NOT null context-private before DestroyContext: + // GC finalizers need getCommonRuntime() during teardown. + JS_DestroyContext(_cx); + _cx = nullptr; + } + + JS_ShutDown(); + + _rt = nullptr; + _initialized = false; + + return SM_OK; +} + +err_code_t MozJSScriptEngine::interrupt(wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx) + return SM_E_BAD_STATE; + + ExecutionCheck chk(_cx, err); + // Request interrupt callback - host side manages interrupt state + JS_RequestInterruptCallback(_cx); + // JS_RequestInterruptCallback doesn't return a value, but we check for exceptions + if (!chk.ok(!JS_IsExceptionPending(_cx), SM_E_INTERNAL)) { + return err ? err->code : SM_E_INTERNAL; + } + return SM_OK; +} + +err_code_t MozJSScriptEngine::createFunction(const uint8_t* src, + size_t len, + uint64_t* out_handle, + wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx || !_global) + return SM_E_BAD_STATE; + if (!src || len == 0 || !out_handle) + return SM_E_INVALID_ARG; + + JSAutoRealm ar(_cx, _global); + + ExecutionCheck chk(_cx, err); + + std::string code_str; + code_str.reserve(len + 2); + code_str += '('; + code_str.append(reinterpret_cast(src), len); + code_str += ')'; + + JS::CompileOptions opts(_cx); + opts.setFileAndLine("wasm:function", 1); + + JS::SourceText text; + // Use .data() and .size() like in implscope.cpp + // The CharT overload accepts const char* for UTF-8 + if (!chk.ok(text.init(_cx, code_str.data(), code_str.size(), JS::SourceOwnership::Borrowed), + SM_E_ENCODING)) { + return err ? err->code : SM_E_ENCODING; + } + + JS::RootedValue v(_cx); + if (!chk.ok(JS::Evaluate(_cx, opts, text, &v), SM_E_COMPILE)) { + // classify common path + if (err && err->code == SM_E_PENDING_EXCEPTION) + err->code = SM_E_COMPILE; + return err ? err->code : SM_E_COMPILE; + } + + if (!v.isObject() || !js::IsFunctionObject(v.toObjectOrNull())) { + if (err) { + err->code = SM_E_TYPE; + set_string( + &err->msg, &err->msg_len, "createFunction: evaluated value is not a function"); + } + return SM_E_TYPE; + } + + _slots.emplace_back(_cx); + FunctionSlot& slot = _slots.back(); + slot.fn = &v.toObject(); + + *out_handle = static_cast(_slots.size()); // 1-based + + return SM_OK; +} + +err_code_t MozJSScriptEngine::invokeFunction(uint64_t handle, + mongo::BSONObj&& argsObject, + mongo::BSONObj* outBson, + wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx || !_global) + return SM_E_BAD_STATE; + + auto* slot = resolveHandle(handle, err); + if (!slot) + return SM_E_INVALID_ARG; + + JSAutoRealm ar(_cx, _global); + + ExecutionCheck chk(_cx, err); + + const int nargs = argsObject.nFields(); + + JS::RootedValueVector args(_cx); + + if (nargs) { + BSONObjIterator it(argsObject); + for (int i = 0; i < nargs; i++) { + BSONElement next = it.next(); + + JS::RootedValue value(_cx); + ValueReader(_cx, &value).fromBSONElement(next, argsObject, false /*readOnlyArgs*/); + + if (!args.append(value)) { + if (err) { + err->code = SM_E_INVALID_ARG; + set_string(&err->msg, &err->msg_len, "Failed to append property"); + } + return SM_E_INVALID_ARG; + } + } + } + + JS::RootedValue out(_cx); + JS::RootedObject funcObj(_cx, slot->fn); + JS::RootedValue funVal(_cx, JS::ObjectValue(*funcObj)); + bool success = JS::Call(_cx, _global, funVal, args, &out); + + if (!chk.ok(success, SM_E_RUNTIME)) { + if (err && err->code == SM_E_PENDING_EXCEPTION) + err->code = SM_E_RUNTIME; + return err ? err->code : SM_E_RUNTIME; + } + + if (_emitByteLimit > 0 && _emitBytesUsed > _emitByteLimit) { + if (err) { + err->code = SM_E_RUNTIME; + set_string(&err->msg, &err->msg_len, "emit() exceeded memory limit"); + } + return SM_E_RUNTIME; + } + + // Store return value on global (same key as implscope) so getReturnValueBson can read it. + ObjectWrapper(_cx, _global).setValue(kInvokeResult, out); + + if (outBson) { + // Also write return value as BSON when caller provides a buffer (wrapped like implscope). + JS::RootedObject rout(_cx, JS_NewPlainObject(_cx)); + if (rout) { + ObjectWrapper wout(_cx, rout); + wout.setValue(kInvokeResult, out); + *outBson = wout.toBSON(); + } + } + + return SM_OK; +} + +err_code_t MozJSScriptEngine::getReturnValueBson(mongo::BSONObj* out, wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx || !_global) + return SM_E_BAD_STATE; + if (!out) + return SM_E_INVALID_ARG; + + JSAutoRealm ar(_cx, _global); + ExecutionCheck chk(_cx, err); + + JS::RootedValue rval(_cx); + JS::RootedString key_str(_cx, chk.okPtr(JS_NewStringCopyZ(_cx, kInvokeResult))); + if (!key_str) { + return err ? err->code : SM_E_INTERNAL; + } + + JS::RootedId rid(_cx); + if (!chk.ok(JS_StringToId(_cx, key_str, &rid), SM_E_INTERNAL)) { + return err ? err->code : SM_E_INTERNAL; + } + + if (!chk.ok(JS_GetPropertyById(_cx, _global, rid, &rval), SM_E_INTERNAL)) { + return err ? err->code : SM_E_INTERNAL; + } + + // Wrap value in object for BSON (same pattern as implscope) so primitives serialize correctly. + JS::RootedObject rout(_cx, JS_NewPlainObject(_cx)); + if (!rout) { + if (err) { + err->code = SM_E_INTERNAL; + set_string( + &err->msg, &err->msg_len, "getReturnValueBson: failed to create wrapper object"); + } + return SM_E_INTERNAL; + } + ObjectWrapper wout(_cx, rout); + wout.setValue(kInvokeResult, rval); + *out = wout.toBSON(); + + return SM_OK; +} + +err_code_t MozJSScriptEngine::invokePredicate(uint64_t handle, + mongo::BSONObj&& document, + bool* outResult, + wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx || !_global) + return SM_E_BAD_STATE; + if (!outResult) + return SM_E_INVALID_ARG; + + auto* slot = resolveHandle(handle, err); + if (!slot) + return SM_E_INVALID_ARG; + + JSAutoRealm ar(_cx, _global); + ExecutionCheck chk(_cx, err); + + JS::RootedValue smrecv(_cx); + if (!document.isEmpty()) + ValueReader(_cx, &smrecv).fromBSON(document, nullptr, false); + else + smrecv.setObjectOrNull(_global); + + JS::RootedValueVector args(_cx); + + JS::RootedValue out(_cx); + JS::RootedObject obj(_cx, smrecv.toObjectOrNull()); + JS::RootedObject funcObj(_cx, slot->fn); + JS::RootedValue funVal(_cx, JS::ObjectValue(*funcObj)); + bool success = JS::Call(_cx, obj, funVal, args, &out); + + if (!chk.ok(success, SM_E_RUNTIME)) { + if (err && err->code == SM_E_PENDING_EXCEPTION) + err->code = SM_E_RUNTIME; + return err ? err->code : SM_E_RUNTIME; + } + + *outResult = JS::ToBoolean(out); + return SM_OK; +} + +err_code_t MozJSScriptEngine::invokeMap(uint64_t handle, + mongo::BSONObj&& document, + wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx || !_global) + return SM_E_BAD_STATE; + + auto* slot = resolveHandle(handle, err); + if (!slot) + return SM_E_INVALID_ARG; + + JSAutoRealm ar(_cx, _global); + ExecutionCheck chk(_cx, err); + + JS::RootedValue smrecv(_cx); + if (!document.isEmpty()) + ValueReader(_cx, &smrecv).fromBSON(document, nullptr, false); + else + smrecv.setObjectOrNull(_global); + + JS::RootedValueVector args(_cx); + + JS::RootedValue out(_cx); + JS::RootedObject obj(_cx, smrecv.toObjectOrNull()); + JS::RootedObject funcObj(_cx, slot->fn); + JS::RootedValue funVal(_cx, JS::ObjectValue(*funcObj)); + bool success = JS::Call(_cx, obj, funVal, args, &out); + + if (!chk.ok(success, SM_E_RUNTIME)) { + if (err && err->code == SM_E_PENDING_EXCEPTION) + err->code = SM_E_RUNTIME; + return err ? err->code : SM_E_RUNTIME; + } + + if (_emitByteLimit > 0 && _emitBytesUsed > _emitByteLimit) { + if (err) { + err->code = SM_E_RUNTIME; + set_string(&err->msg, &err->msg_len, "emit() exceeded memory limit"); + } + return SM_E_RUNTIME; + } + + return SM_OK; +} + +err_code_t MozJSScriptEngine::setGlobal(const char* name, + size_t name_len, + const mongo::BSONObj& value, + wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx || !_global) + return SM_E_BAD_STATE; + if (!name || name_len == 0) + return SM_E_INVALID_ARG; + + JSAutoRealm ar(_cx, _global); + ExecutionCheck chk(_cx, err); + + // Create a JS plain object and populate from BSON fields. + JS::RootedObject jsObj(_cx, JS_NewPlainObject(_cx)); + if (!jsObj) { + if (err) { + err->code = SM_E_OOM; + set_string(&err->msg, &err->msg_len, "setGlobal: JS_NewPlainObject failed"); + } + return SM_E_OOM; + } + + for (BSONObjIterator it(value); it.more();) { + BSONElement elem = it.next(); + JS::RootedValue val(_cx); + ValueReader(_cx, &val).fromBSONElement(elem, value, false /* readOnlyArgs */); + + if (!chk.ok(JS_SetProperty(_cx, jsObj, elem.fieldName(), val), SM_E_INTERNAL)) { + return err ? err->code : SM_E_INTERNAL; + } + } + + // Set the constructed object as a property on the global. + JS::RootedValue objVal(_cx, JS::ObjectValue(*jsObj)); + std::string nameStr(name, name_len); + if (!chk.ok(JS_SetProperty(_cx, _global, nameStr.c_str(), objVal), SM_E_INTERNAL)) { + return err ? err->code : SM_E_INTERNAL; + } + + return SM_OK; +} + +err_code_t MozJSScriptEngine::getGlobal(const char* name, + size_t name_len, + mongo::BSONObj* out, + wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx || !_global) + return SM_E_BAD_STATE; + if (!name || name_len == 0 || !out) + return SM_E_INVALID_ARG; + + JSAutoRealm ar(_cx, _global); + ExecutionCheck chk(_cx, err); + + std::string nameStr(name, name_len); + + // Check if the property exists on the global object. + bool found = false; + if (!chk.ok(JS_HasProperty(_cx, _global, nameStr.c_str(), &found), SM_E_INTERNAL)) { + return err ? err->code : SM_E_INTERNAL; + } + if (!found) { + if (err) { + err->code = SM_E_INVALID_ARG; + set_string(&err->msg, &err->msg_len, "getGlobal: property not found"); + } + return SM_E_INVALID_ARG; + } + + JS::RootedValue rval(_cx); + if (!chk.ok(JS_GetProperty(_cx, _global, nameStr.c_str(), &rval), SM_E_INTERNAL)) { + return err ? err->code : SM_E_INTERNAL; + } + + if (rval.isUndefined()) { + if (err) { + err->code = SM_E_INVALID_ARG; + set_string(&err->msg, &err->msg_len, "getGlobal: property is undefined"); + } + return SM_E_INVALID_ARG; + } + + // Convert the JS value to BSON. + if (rval.isObject()) { + JS::RootedObject robj(_cx, &rval.toObject()); + ObjectWrapper wrapper(_cx, robj); + *out = wrapper.toBSON(); + } else { + // Wrap a non-object (primitive) value in a BSON document. + JS::RootedObject wrapObj(_cx, JS_NewPlainObject(_cx)); + if (!wrapObj) { + if (err) { + err->code = SM_E_OOM; + } + return SM_E_OOM; + } + ObjectWrapper wout(_cx, wrapObj); + wout.setValue("__value", rval); + *out = wout.toBSON(); + } + + return SM_OK; +} + +err_code_t MozJSScriptEngine::setGlobalValue(const char* name, + size_t name_len, + const mongo::BSONObj& singleElementDoc, + wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx || !_global) + return SM_E_BAD_STATE; + if (!name || name_len == 0) + return SM_E_INVALID_ARG; + + BSONObjIterator it(singleElementDoc); + if (!it.more()) { + if (err) { + err->code = SM_E_INVALID_ARG; + set_string(&err->msg, &err->msg_len, "setGlobalValue: empty BSON document"); + } + return SM_E_INVALID_ARG; + } + + BSONElement elem = it.next(); + + JSAutoRealm ar(_cx, _global); + ExecutionCheck chk(_cx, err); + + JS::RootedValue val(_cx); + ValueReader(_cx, &val).fromBSONElement(elem, singleElementDoc, false); + + std::string nameStr(name, name_len); + if (!chk.ok(JS_SetProperty(_cx, _global, nameStr.c_str(), val), SM_E_INTERNAL)) { + return err ? err->code : SM_E_INTERNAL; + } + + return SM_OK; +} + +BSONObj MozJSScriptEngine::_emitCallback(const BSONObj& args, void* data) { + auto* engine = static_cast(data); + + BSONObjIterator it(args); + BSONElement keyElem = it.more() ? it.next() : BSONElement(); + BSONElement valElem = it.more() ? it.next() : BSONElement(); + + BSONObjBuilder b; + if (keyElem.type() == BSONType::undefined || keyElem.eoo()) + b.appendNull("k"); + else + b.appendAs(keyElem, "k"); + + if (valElem.eoo()) + b.appendNull("v"); + else + b.appendAs(valElem, "v"); + + BSONObj doc = b.obj(); + engine->_emitBytesUsed += doc.objsize(); + if (engine->_emitBytesUsed <= engine->_emitByteLimit) + engine->_emitBuffer.push_back(std::move(doc)); + + return BSONObj(); +} + +err_code_t MozJSScriptEngine::setupEmit(int64_t byteLimit, + bool hasByteLimit, + wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized || !_cx || !_global) + return SM_E_BAD_STATE; + + _emitByteLimit = hasByteLimit ? byteLimit : (16 * 1024 * 1024); + _emitBytesUsed = 0; + _emitBuffer.clear(); + + JSAutoRealm ar(_cx, _global); + + JS::RootedObject obj(_cx); + NativeFunctionInfo::make(_cx, &obj, _emitCallback, this); + + JS::RootedValue value(_cx); + value.setObjectOrNull(obj); + ObjectWrapper(_cx, _global).setValue("emit", value); + + return SM_OK; +} + +err_code_t MozJSScriptEngine::drainEmitBuffer(mongo::BSONObj* out, wasm_mozjs_error_t* err) { + clear_error(err); + if (!_initialized) + return SM_E_BAD_STATE; + if (!out) + return SM_E_INVALID_ARG; + + BSONObjBuilder builder; + BSONArrayBuilder arr(builder.subarrayStart("emits")); + for (const auto& doc : _emitBuffer) { + arr.append(doc); + } + arr.done(); + builder.append("bytesUsed", static_cast(_emitBytesUsed)); + + *out = builder.obj(); + + _emitBuffer.clear(); + _emitBytesUsed = 0; + + return SM_OK; +} + +bool MozJSScriptEngine::interruptCallback(JSContext* cx) { + // Interrupt callback is called by MozJS when JS_RequestInterruptCallback is invoked. + // The host side manages interrupt/cancel state and termination logic. + // This callback just allows the interrupt to be processed - the host side + // should check return codes and exceptions to determine if execution was terminated. + (void)cx; + return true; +} + +void MozJSScriptEngine::gc() { + if (!_initialized || !_cx) { + return; + } + JS_GC(_cx); +} + +void MozJSScriptEngine::sleep(Milliseconds ms) { + auto count = ms.count(); + if (count <= 0) + return; + struct timespec ts; + ts.tv_sec = static_cast(count / 1000); + ts.tv_nsec = static_cast((count % 1000) * 1000000L); + nanosleep(&ts, nullptr); +} + +std::size_t MozJSScriptEngine::getGeneration() const { + // Return a constant generation for WASM + return 1; +} + +JS::HandleId MozJSScriptEngine::getInternedStringId(InternedString name) { + return _internedStrings->getInternedString(name); +} + +std::int64_t* MozJSScriptEngine::trackedNewInt64(std::int64_t value) { + auto* p = new std::int64_t(value); + trackNewPointer(p); + return p; +} + +WrapType& MozJSScriptEngine::numberLongProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->numberLongProto(); +} + +WrapType& MozJSScriptEngine::numberIntProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->numberIntProto(); +} + +WrapType& MozJSScriptEngine::numberDecimalProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->numberDecimalProto(); +} + +WrapType& MozJSScriptEngine::oidProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->oidProto(); +} + +WrapType& MozJSScriptEngine::binDataProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->binDataProto(); +} + +WrapType& MozJSScriptEngine::timestampProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->timestampProto(); +} + +WrapType& MozJSScriptEngine::maxKeyProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->maxKeyProto(); +} + +WrapType& MozJSScriptEngine::minKeyProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->minKeyProto(); +} + +void MozJSScriptEngine::trackNewPointer(void* ptr) { + (void)ptr; +} + +void MozJSScriptEngine::trackDeletePointer(void* ptr) { + (void)ptr; +} + +WrapType& MozJSScriptEngine::codeProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->codeProto(); +} + +WrapType& MozJSScriptEngine::dbPointerProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->dbPointerProto(); +} + +WrapType& MozJSScriptEngine::nativeFunctionProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->nativeFunctionProto(); +} + +WrapType& MozJSScriptEngine::errorProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->errorProto(); +} + +WrapType& MozJSScriptEngine::mongoStatusProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->statusProto(); +} + +void MozJSScriptEngine::setStatus(Status status) { + _status = std::move(status); +} + +bool MozJSScriptEngine::isJavaScriptProtectionEnabled() const { + // JavaScript protection (--enableJavaScriptProtection) is not applicable + // in WASM builds where the engine runs in a sandboxed component. + return false; +} + +bool MozJSScriptEngine::requiresOwnedObjects() const { + // WASM engine does not require BSON objects to be owned. + return false; +} + +void MozJSScriptEngine::newFunction(StringData raw, JS::MutableHandleValue out) { + // Compile the code string as a function expression, same as createFunction(). + std::string wrapped; + wrapped.reserve(raw.size() + 2); + wrapped += '('; + wrapped.append(raw.data(), raw.size()); + wrapped += ')'; + + JS::CompileOptions opts(_cx); + opts.setFileAndLine("wasm:newFunction", 1); + + JS::SourceText text; + if (!text.init(_cx, wrapped.data(), wrapped.size(), JS::SourceOwnership::Borrowed)) { + return; // JS exception is pending + } + + JS::Evaluate(_cx, opts, text, out); + // If Evaluate fails, JS exception is pending — caller handles it. +} + +WrapType& MozJSScriptEngine::bsonProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->bsonProto(); +} + +WrapType& MozJSScriptEngine::dbRefProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->dbRefProto(); +} + +WrapType& MozJSScriptEngine::regExpProto() { + if (!_prototypeInstaller) { + __builtin_trap(); // WASM trap: prototypeInstaller not initialized + } + return _prototypeInstaller->regExpProto(); +} + +} // namespace wasm + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/wasm/engine/engine.h b/src/mongo/scripting/mozjs/wasm/engine/engine.h new file mode 100644 index 00000000000..b496c4c2c24 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/engine.h @@ -0,0 +1,312 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/base/status.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/scripting/mozjs/common/error.h" +#include "mongo/scripting/mozjs/common/global.h" +#include "mongo/scripting/mozjs/common/internedstring.h" +#include "mongo/scripting/mozjs/common/runtime.h" +#include "mongo/scripting/mozjs/common/types/bindata.h" +#include "mongo/scripting/mozjs/common/types/bson.h" +#include "mongo/scripting/mozjs/common/types/code.h" +#include "mongo/scripting/mozjs/common/types/dbpointer.h" +#include "mongo/scripting/mozjs/common/types/dbref.h" +#include "mongo/scripting/mozjs/common/types/maxkey.h" +#include "mongo/scripting/mozjs/common/types/minkey.h" +#include "mongo/scripting/mozjs/common/types/nativefunction.h" +#include "mongo/scripting/mozjs/common/types/numberdecimal.h" +#include "mongo/scripting/mozjs/common/types/numberint.h" +#include "mongo/scripting/mozjs/common/types/numberlong.h" +#include "mongo/scripting/mozjs/common/types/oid.h" +#include "mongo/scripting/mozjs/common/types/regexp.h" +#include "mongo/scripting/mozjs/common/types/status.h" +#include "mongo/scripting/mozjs/common/types/timestamp.h" +#include "mongo/scripting/mozjs/common/wraptype.h" +#include "mongo/scripting/mozjs/shared/mozjs_wasm_startup_options.h" +#include "mongo/util/assert_util.h" + +#include +#include +#include + +#include "error.h" + +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" + +struct JSContext; +struct JSRuntime; + +namespace mongo { +namespace mozjs { +namespace wasm { + +extern uint32_t g_wasmJsHeapLimitMB; + +class ExecutionCheck; + +struct FunctionSlot { + JS::PersistentRootedObject fn; + explicit FunctionSlot(JSContext* cx) : fn(cx) {} +}; + +class MozJSPrototypeInstaller { +public: + explicit MozJSPrototypeInstaller(JSContext* context) + : _cx(context), + _globalProto(_cx), + _binDataProto(_cx), + _bsonProto(_cx), + _codeProto(_cx), + _dbPointerProto(_cx), + _dbRefProto(_cx), + _errorProto(_cx), + _maxKeyProto(_cx), + _minKeyProto(_cx), + _nativeFunctionProto(_cx), + _numberDecimalProto(_cx), + _numberIntProto(_cx), + _numberLongProto(_cx), + _oidProto(_cx), + _regExpProto(_cx), + _statusProto(_cx), + _timestampProto(_cx) { + invariant(_cx); + } + + WrapType& globalProto() { + return _globalProto; + } + WrapType& binDataProto() { + return _binDataProto; + } + WrapType& bsonProto() { + return _bsonProto; + } + WrapType& codeProto() { + return _codeProto; + } + WrapType& dbPointerProto() { + return _dbPointerProto; + } + WrapType& dbRefProto() { + return _dbRefProto; + } + WrapType& errorProto() { + return _errorProto; + } + WrapType& maxKeyProto() { + return _maxKeyProto; + } + WrapType& minKeyProto() { + return _minKeyProto; + } + WrapType& nativeFunctionProto() { + return _nativeFunctionProto; + } + WrapType& numberDecimalProto() { + return _numberDecimalProto; + } + WrapType& numberIntProto() { + return _numberIntProto; + } + WrapType& numberLongProto() { + return _numberLongProto; + } + WrapType& oidProto() { + return _oidProto; + } + WrapType& regExpProto() { + return _regExpProto; + } + WrapType& statusProto() { + return _statusProto; + } + WrapType& timestampProto() { + return _timestampProto; + } + + + void installTypes(JS::HandleObject global) { + // GlobalInfo cannot use install() because JSCLASS_GLOBAL_FLAGS + // prevents JS_InitClass on an existing global. Install its + // freeFunctions (print, gc, sleep, etc.) directly. + JS_DefineFunctions(_cx, global, GlobalInfo::freeFunctions); + _binDataProto.install(global); + _bsonProto.install(global); + _codeProto.install(global); + _dbPointerProto.install(global); + _dbRefProto.install(global); + _errorProto.install(global); + _maxKeyProto.install(global); + _minKeyProto.install(global); + _nativeFunctionProto.install(global); + _numberDecimalProto.install(global); + _numberIntProto.install(global); + _numberLongProto.install(global); + _oidProto.install(global); + _regExpProto.install(global); + _timestampProto.install(global); + _statusProto.install(global); + } + +private: + JSContext* _cx = nullptr; + WrapType _globalProto; + WrapType _binDataProto; + WrapType _bsonProto; + WrapType _codeProto; + WrapType _dbPointerProto; + WrapType _dbRefProto; + WrapType _errorProto; + WrapType _maxKeyProto; + WrapType _minKeyProto; + WrapType _nativeFunctionProto; + WrapType _numberDecimalProto; + WrapType _numberIntProto; + WrapType _numberLongProto; + WrapType _oidProto; + WrapType _regExpProto; + WrapType _statusProto; + WrapType _timestampProto; +}; + +class MozJSScriptEngine : private MozJSCommonRuntimeInterface { +public: + MozJSScriptEngine() = default; + ~MozJSScriptEngine(); + + MozJSScriptEngine(const MozJSScriptEngine&) = delete; + MozJSScriptEngine& operator=(const MozJSScriptEngine&) = delete; + + err_code_t init(const wasm_mozjs_startup_options_t* opt, wasm_mozjs_error_t* err); + err_code_t shutdown(wasm_mozjs_error_t* err); + err_code_t interrupt(wasm_mozjs_error_t* err); + err_code_t createFunction(const uint8_t* src, + size_t len, + uint64_t* out_handle, + wasm_mozjs_error_t* err); + err_code_t invokeFunction(uint64_t handle, + mongo::BSONObj&& bsonArgs, + mongo::BSONObj* outBson, + wasm_mozjs_error_t* err); + + /// Invoke a predicate: document becomes `this` e.g `this.someVar`, returns bool. + err_code_t invokePredicate(uint64_t handle, + mongo::BSONObj&& document, + bool* outResult, + wasm_mozjs_error_t* err); + + /// Invoke a map function: document becomes `this`, emits buffered. + err_code_t invokeMap(uint64_t handle, mongo::BSONObj&& document, wasm_mozjs_error_t* err); + err_code_t getReturnValueBson(mongo::BSONObj* out, wasm_mozjs_error_t* err); + + /// Set a named global variable from a BSON-encoded value. + err_code_t setGlobal(const char* name, + size_t name_len, + const mongo::BSONObj& value, + wasm_mozjs_error_t* err); + + /// Get a named global variable as BSON-encoded bytes. + err_code_t getGlobal(const char* name, + size_t name_len, + mongo::BSONObj* out, + wasm_mozjs_error_t* err); + + /// Set a named global to a single BSON element's JS value directly. + err_code_t setGlobalValue(const char* name, + size_t name_len, + const mongo::BSONObj& singleElementDoc, + wasm_mozjs_error_t* err); + + /// Set up the emit() built-in for mapReduce. Resets the emit buffer. + err_code_t setupEmit(int64_t byteLimit, bool hasByteLimit, wasm_mozjs_error_t* err); + + /// Drain the emit buffer: returns accumulated {k,v} pairs, then clears. + err_code_t drainEmitBuffer(mongo::BSONObj* out, wasm_mozjs_error_t* err); + + // MozJSCommonRuntimeInterface implementation + void gc() override; + void sleep(Milliseconds ms) override; + std::size_t getGeneration() const override; + JS::HandleId getInternedStringId(InternedString name) override; + std::int64_t* trackedNewInt64(std::int64_t value) override; + WrapType& numberLongProto() override; + WrapType& numberIntProto() override; + WrapType& numberDecimalProto() override; + WrapType& oidProto() override; + WrapType& binDataProto() override; + WrapType& timestampProto() override; + WrapType& maxKeyProto() override; + WrapType& minKeyProto() override; + WrapType& codeProto() override; + WrapType& dbPointerProto() override; + WrapType& nativeFunctionProto() override; + WrapType& errorProto() override; + WrapType& mongoStatusProto() override; + WrapType& bsonProto() override; + WrapType& dbRefProto() override; + WrapType& regExpProto() override; + + void setStatus(Status status) override; + bool isJavaScriptProtectionEnabled() const override; + void newFunction(StringData code, JS::MutableHandleValue out) override; + bool requiresOwnedObjects() const override; + void trackNewPointer(void* ptr) override; + void trackDeletePointer(void* ptr) override; + +private: + FunctionSlot* resolveHandle(uint64_t handle, wasm_mozjs_error_t* err); + static bool interruptCallback(JSContext* cx); + + bool _initialized = false; + + JSContext* _cx = nullptr; + JSRuntime* _rt = nullptr; + JS::PersistentRootedObject _global; + + std::vector _slots; + std::unique_ptr _prototypeInstaller; + std::unique_ptr _internedStrings; + + Status _status = Status::OK(); + + std::vector _emitBuffer; + int64_t _emitBytesUsed = 0; + int64_t _emitByteLimit = 16 * 1024 * 1024; // default 16 MB + + static mongo::BSONObj _emitCallback(const mongo::BSONObj& args, void* data); +}; + +} // namespace wasm +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/wasm/engine/engine_test.cpp b/src/mongo/scripting/mozjs/wasm/engine/engine_test.cpp new file mode 100644 index 00000000000..d3c1da0c29a --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/engine_test.cpp @@ -0,0 +1,3353 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +/** + * Unit tests for the MozJS WASM component API via wasmtime. + * + * These tests load the compiled mozjs_wasm_api.wasm component, + * instantiate it in a wasmtime runtime, and exercise the WIT + * interface functions (initialize-engine, create-function, + * invoke-function, invoke-predicate, invoke-map, + * get-return-value-bson, shutdown-engine). + * + */ + +#include "mongo/bson/bsonmisc.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/bsontypes.h" +#include "mongo/bson/oid.h" +#include "mongo/bson/timestamp.h" +#include "mongo/platform/decimal128.h" +#include "mongo/unittest/unittest.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace wt = wasmtime; +namespace wc = wasmtime::component; + +namespace mongo { +namespace mozjs { +namespace wasm { +namespace { + +// TODO SERVER-115423: Replace raw usages of the Wasmtime API with the +// MozJS Wasm Bridge implementation. +static std::vector readWasmFile(const std::string& path) { + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f) { + return {}; + } + auto pos = f.tellg(); + if (pos < 0) { + return {}; + } + auto size = static_cast(pos); + f.seekg(0); + std::vector buf(size); + if (!f.read(reinterpret_cast(buf.data()), static_cast(size))) { + return {}; + } + return buf; +} + +static wc::List makeListU8(const uint8_t* data, size_t len) { + wasmtime_component_vallist_t raw; + wasmtime_component_vallist_new_uninit(&raw, len); + for (size_t i = 0; i < len; i++) { + raw.data[i].kind = WASMTIME_COMPONENT_U8; + raw.data[i].of.u8 = data[i]; + } + return wc::List(std::move(raw)); +} + +static wc::List makeListU8(std::string_view s) { + return makeListU8(reinterpret_cast(s.data()), s.size()); +} + +// Create a component Val for WIT `string` type (distinct from list). +static wc::Val makeString(std::string_view s) { + wasmtime_component_val_t raw; + raw.kind = WASMTIME_COMPONENT_STRING; + wasm_byte_vec_new(&raw.of.string, s.size(), s.data()); + return wc::Val(std::move(raw)); +} + +static wc::List makeListU8(const BSONObj& obj) { + return makeListU8(reinterpret_cast(obj.objdata()), + static_cast(obj.objsize())); +} + +static std::vector extractListU8(const wc::Val& v) { + std::vector out; + if (!v.is_list()) + return out; + const wc::List& list = v.get_list(); + out.reserve(list.size()); + for (const wc::Val& elem : list) { + if (elem.is_u8()) { + out.push_back(elem.get_u8()); + } + } + return out; +} + +static std::optional getMozjsFunc(wc::Instance& instance, + wt::Store::Context ctx, + std::string_view ifaceName, + std::string_view funcName) { + auto ifaceIdx = instance.get_export_index(ctx, nullptr, ifaceName); + if (!ifaceIdx) + return std::nullopt; + auto funcIdx = instance.get_export_index(ctx, &*ifaceIdx, funcName); + if (!funcIdx) + return std::nullopt; + return instance.get_func(ctx, *funcIdx); +} + +template +static bool callFunc( + wc::Func& func, wt::Store::Context ctx, wc::Val* results, size_t numResults, Args&&... args) { + std::array argsArr = {std::forward(args)...}; + wt::Span argsSpan(argsArr.data(), argsArr.size()); + wt::Span resultsSpan(results, numResults); + auto callResult = func.call(ctx, argsSpan, resultsSpan); + if (!callResult) + return false; + auto postResult = func.post_return(ctx); + return static_cast(postResult); +} + +static bool callFuncNoArgs(wc::Func& func, + wt::Store::Context ctx, + wc::Val* results, + size_t numResults) { + const wc::Val* emptyPtr = nullptr; + wt::Span empty(emptyPtr, size_t{0}); + wt::Span resultsSpan(results, numResults); + auto callResult = func.call(ctx, empty, resultsSpan); + if (!callResult) + return false; + auto postResult = func.post_return(ctx); + return static_cast(postResult); +} + +static bool isResultOk(const wc::Val& result) { + return result.is_result() && result.get_result().is_ok(); +} + +struct WasmError { + std::string code; + std::string msg; + std::string filename; + std::string stack; + uint32_t line = 0; + uint32_t column = 0; +}; + +static std::string extractOptString(const wc::Val& optionVal) { + const wc::Val* inner = optionVal.get_option().value(); + if (inner && inner->is_string()) + return std::string(inner->get_string()); + return {}; +} + +// WIT record fields are always in declaration order (mozjs.wit): +// 0: code (err-code enum) +// 1: msg (option) +// 2: filename (option) +// 3: stack (option) +// 4: line (u32) +// 5: column (u32) +static std::optional extractError(const wc::Val& result) { + if (!result.is_result()) + return std::nullopt; + const auto& witResult = result.get_result(); + if (witResult.is_ok()) + return std::nullopt; + const wc::Val* payload = witResult.payload(); + if (!payload || !payload->is_record()) + return std::nullopt; + + const wc::Record& rec = payload->get_record(); + auto* f = rec.begin(); + + WasmError error; + error.code = std::string(f[0].value().get_enum()); + error.msg = extractOptString(f[1].value()); + error.filename = extractOptString(f[2].value()); + error.stack = extractOptString(f[3].value()); + error.line = f[4].value().get_u32(); + error.column = f[5].value().get_u32(); + return error; +} + +static std::string resolveWasmPath() { + // 1. Explicit env var + if (const char* envPath = std::getenv("WASM_MODULE_PATH")) { + return envPath; + } + // 2. Bazel TEST_SRCDIR runfiles + if (const char* srcdir = std::getenv("TEST_SRCDIR")) { + for (const char* candidate : { + "/_main/src/mongo/scripting/mozjs/wasm/mozjs_wasm_api.wasm", + "/_main~_repo_rules~mozjs_wasm/file/mozjs_wasm_api.wasm", + }) { + std::string p = std::string(srcdir) + candidate; + std::ifstream check(p); + if (check.good()) + return p; + } + } + // 3. Bazel-bin output directory (when built separately with --config=wasi) + { + // Try relative path that works when run from repo root via bazel test + for (const char* candidate : { + "bazel-bin/src/mongo/scripting/mozjs/wasm/mozjs_wasm_api.wasm", + "src/mongo/scripting/mozjs/wasm/mozjs_wasm_api.wasm", + }) { + std::ifstream check(candidate); + if (check.good()) + return candidate; + } + } + // 4. Last attempt + return "src/mongo/scripting/mozjs/wasm/mozjs_wasm_api.wasm"; +} + +// Shares wasmtime engine + compiled component across all tests. +// Only the store (and thus instance) are recreated per test to provide isolation. +class WasmMozJSTest : public unittest::Test { +public: + // One-time setup: compile the WASM component (expensive, ~40s). + static void SetUpTestSuite() { + s_wasmPath = resolveWasmPath(); + auto wasmBytes = readWasmFile(s_wasmPath); + if (wasmBytes.empty()) { + return; + } + + wt::Config config; + config.wasm_component_model(true); + + s_engine.emplace(std::move(config)); + + wt::Span wasmSpan(wasmBytes.data(), wasmBytes.size()); + auto componentResult = wc::Component::compile(*s_engine, wasmSpan); + if (!componentResult) { + return; + } + s_component.emplace(componentResult.ok()); + s_suiteReady = true; + } + + static void TearDownTestSuite() { + s_component.reset(); + s_engine.reset(); + s_suiteReady = false; + } + +protected: + // Per-test setup: create a fresh store and instance. + void setUp() override { + ASSERT_TRUE(s_suiteReady); + + _store.emplace(*s_engine); + auto ctx = _store->context(); + + wt::WasiConfig wasiConfig; + wasiConfig.inherit_stdout(); + wasiConfig.inherit_stderr(); + auto wasiResult = ctx.set_wasi(std::move(wasiConfig)); + // WASI config must be set successfully. + ASSERT_TRUE(!!wasiResult); + + wc::Linker linker(*s_engine); + auto wasip2Result = linker.add_wasip2(); + // wasip2 must be added to the linker. + ASSERT_TRUE(!!wasip2Result); + + auto instanceResult = linker.instantiate(ctx, *s_component); + // Component instantiation must succeed. + ASSERT_TRUE(!!instanceResult); + _instance.emplace(instanceResult.ok()); + _ready = true; + } + + void tearDown() override { + if (_ready && _engineInitialized) { + auto shutdownFunc = getFunc("shutdown-engine"); + if (shutdownFunc) { + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFuncNoArgs(*shutdownFunc, ctx(), &result, 1); + } + } + } + + wt::Store::Context ctx() { + return _store->context(); + } + + std::optional getFunc(std::string_view funcName) { + return getMozjsFunc(*_instance, ctx(), "mongo:mozjs/mozjs", funcName); + } + + // Initialize the MozJS engine inside WASM. Returns true on success. + bool initEngine() { + auto initFunc = getFunc("initialize-engine"); + ASSERT_TRUE(initFunc.has_value()); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFuncNoArgs(*initFunc, ctx(), &result, 1)); + bool ok = isResultOk(result); + if (ok) + _engineInitialized = true; + return ok; + } + + // Create a JS function. Returns function handle on success, 0 on failure. + uint64_t createFunction(std::string_view source) { + auto createFunc = getFunc("create-function"); + ASSERT_TRUE(createFunc.has_value()); + wc::Val srcArg(makeListU8(source)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + if (!callFunc(*createFunc, ctx(), &result, 1, std::move(srcArg))) + return 0; + if (!isResultOk(result)) + return 0; + const wc::Val* payload = result.get_result().payload(); + if (!payload || !payload->is_u64()) + return 0; + return payload->get_u64(); + } + + // Invoke a function with BSON args. Returns true on success. + bool invokeFunction(uint64_t handle, const BSONObj& args) { + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + wc::Val arg0(handle); + wc::Val arg1(makeListU8(args)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + if (!callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1))) + return false; + return isResultOk(result); + } + + // Invoke a function expecting failure. Returns the WasmError. + WasmError invokeFunctionError(uint64_t handle, const BSONObj& args) { + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + wc::Val arg0(handle); + wc::Val arg1(makeListU8(args)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + auto err = extractError(result); + ASSERT_TRUE(err.has_value()) << "expected error but call succeeded"; + return *err; + } + + // Invoke a map function expecting failure. Returns the WasmError. + WasmError invokeMapError(uint64_t handle, const BSONObj& document) { + auto func = getFunc("invoke-map"); + ASSERT_TRUE(func.has_value()); + wc::Val arg0(handle); + wc::Val arg1(makeListU8(document)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*func, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + auto err = extractError(result); + ASSERT_TRUE(err.has_value()) << "expected error but call succeeded"; + return *err; + } + + // Invoke a predicate: document becomes `this`, returns the bool result. + // Asserts that the call succeeds. + bool invokePredicate(uint64_t handle, const BSONObj& document) { + auto func = getFunc("invoke-predicate"); + ASSERT_TRUE(func.has_value()); + wc::Val arg0(handle); + wc::Val arg1(makeListU8(document)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFunc(*func, ctx(), &result, 1, std::move(arg0), std::move(arg1))); + ASSERT_TRUE(isResultOk(result)); + const wc::Val* payload = result.get_result().payload(); + ASSERT_TRUE(payload != nullptr && payload->is_bool()); + return payload->get_bool(); + } + + // Invoke a map function: document becomes `this`, emits buffered internally. + bool invokeMap(uint64_t handle, const BSONObj& document) { + auto func = getFunc("invoke-map"); + ASSERT_TRUE(func.has_value()); + wc::Val arg0(handle); + wc::Val arg1(makeListU8(document)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + if (!callFunc(*func, ctx(), &result, 1, std::move(arg0), std::move(arg1))) + return false; + return isResultOk(result); + } + + // Set up the emit() built-in for mapReduce. Resets the emit buffer. + bool setupEmit() { + auto func = getFunc("setup-emit"); + ASSERT_TRUE(func.has_value()); + wc::Val arg0{wc::WitOption(std::nullopt)}; + wc::Val result(wc::WitResult::ok(std::nullopt)); + if (!callFunc(*func, ctx(), &result, 1, std::move(arg0))) + return false; + return isResultOk(result); + } + + // Set up emit() with an explicit byte limit. + bool setupEmitWithLimit(int64_t byteLimit) { + auto func = getFunc("setup-emit"); + ASSERT_TRUE(func.has_value()); + wc::Val arg0{wc::WitOption(wc::Val(byteLimit))}; + wc::Val result(wc::WitResult::ok(std::nullopt)); + if (!callFunc(*func, ctx(), &result, 1, std::move(arg0))) + return false; + return isResultOk(result); + } + + // Drain the emit buffer: returns accumulated {k,v} pairs as BSON. + BSONObj drainEmitBuffer() { + auto func = getFunc("drain-emit-buffer"); + ASSERT_TRUE(func.has_value()); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFuncNoArgs(*func, ctx(), &result, 1)); + ASSERT_TRUE(isResultOk(result)); + auto bytes = extractListU8(*result.get_result().payload()); + return BSONObj(reinterpret_cast(bytes.data())).getOwned(); + } + + // Get the last return value as BSON. + BSONObj getReturnValueBson() { + auto getFunc = this->getFunc("get-return-value-bson"); + ASSERT_TRUE(getFunc.has_value()); + wc::Val result(wc::WitResult::ok(std::nullopt)); + if (!callFuncNoArgs(*getFunc, ctx(), &result, 1)) + return BSONObj(); + if (!isResultOk(result)) + return BSONObj(); + const wc::Val* payload = result.get_result().payload(); + if (!payload) + return BSONObj(); + auto bytes = extractListU8(*payload); + if (bytes.size() < 5) + return BSONObj(); + // Copy into owned buffer for BSONObj + auto buf = SharedBuffer::allocate(bytes.size()); + std::memcpy(buf.get(), bytes.data(), bytes.size()); + return BSONObj(std::move(buf)); + } + + // Set a named global variable from a BSON-encoded value. Returns true on success. + bool setGlobal(std::string_view name, const BSONObj& value) { + auto setFunc = getFunc("set-global"); + ASSERT_TRUE(setFunc.has_value()); + wc::Val nameArg = makeString(name); + wc::Val valueArg(makeListU8(value)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + if (!callFunc(*setFunc, ctx(), &result, 1, std::move(nameArg), std::move(valueArg))) + return false; + return isResultOk(result); + } + + // Set a named global to a single BSON element's JS value (like Scope::setFunction). + bool setGlobalValue(std::string_view name, const BSONObj& singleElementDoc) { + auto func = getFunc("set-global-value"); + ASSERT_TRUE(func.has_value()); + wc::Val nameArg = makeString(name); + wc::Val valueArg(makeListU8(singleElementDoc)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + if (!callFunc(*func, ctx(), &result, 1, std::move(nameArg), std::move(valueArg))) + return false; + return isResultOk(result); + } + + // Convenience: set a named global to a JS function from source code. + bool setFunction(std::string_view name, std::string_view code) { + BSONObjBuilder b; + b.appendCode("val", std::string(code)); + return setGlobalValue(name, b.obj()); + } + + // Get a named global variable as BSON. Returns empty BSONObj on failure. + BSONObj getGlobal(std::string_view name) { + auto getGlobalFunc = getFunc("get-global"); + ASSERT_TRUE(getGlobalFunc.has_value()); + wc::Val nameArg = makeString(name); + wc::Val result(wc::WitResult::ok(std::nullopt)); + if (!callFunc(*getGlobalFunc, ctx(), &result, 1, std::move(nameArg))) + return BSONObj(); + if (!isResultOk(result)) + return BSONObj(); + const wc::Val* payload = result.get_result().payload(); + if (!payload) + return BSONObj(); + auto bytes = extractListU8(*payload); + if (bytes.size() < 5) + return BSONObj(); + auto buf = SharedBuffer::allocate(bytes.size()); + std::memcpy(buf.get(), bytes.data(), bytes.size()); + return BSONObj(std::move(buf)); + } + +private: + // Per-test state + bool _ready = false; + bool _engineInitialized = false; + + std::optional _store; + std::optional _instance; + + // Shared across all tests (expensive to create) + static std::string s_wasmPath; + static bool s_suiteReady; + static std::optional s_engine; + static std::optional s_component; +}; + +// Static member definitions +std::string WasmMozJSTest::s_wasmPath; +bool WasmMozJSTest::s_suiteReady = false; +std::optional WasmMozJSTest::s_engine; +std::optional WasmMozJSTest::s_component; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, EngineInitializeAndShutdown) { + ASSERT_TRUE(initEngine()); + + // Shutdown + auto shutdownFunc = getFunc("shutdown-engine"); + ASSERT_TRUE(shutdownFunc.has_value()); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFuncNoArgs(*shutdownFunc, ctx(), &result, 1)); + ASSERT_TRUE(isResultOk(result)); +} + +TEST_F(WasmMozJSTest, CreateFunctionReturnsHandle) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction("function() { return 42; }"); + ASSERT_NE(handle, 0u); +} + +TEST_F(WasmMozJSTest, InvokeFunctionNoArgs) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction("function() { return {\"answer\": 42}; }"); + ASSERT_NE(handle, 0u); + + // Invoke with empty BSON args + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + // Retrieve result + BSONObj result = getReturnValueBson(); + ASSERT_FALSE(result.isEmpty()); + + // The result is wrapped: { __returnValue: { answer: 42 } } + BSONElement retVal = result.getField("__returnValue"); + ASSERT_FALSE(retVal.eoo()); + ASSERT_TRUE(retVal.isABSONObj()); + + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getIntField("answer"), 42); +} + +// --------------------------------------------------------------------------- +// BSON argument passing tests +// +// invokeFunction passes each BSON field as a POSITIONAL JS argument: +// BSONObj {a: 21, b: "hello"} → JS call: func(21, "hello") +// The field names determine insertion order but are not visible to JS. +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, BsonArgsInt32) { + ASSERT_TRUE(initEngine()); + + // Each BSON field becomes a positional JS arg. + uint64_t handle = createFunction( + "function(value, multiplier) {" + " return { result: value * multiplier };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.append("value", 21); + bob.append("multiplier", 2); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_FALSE(retVal.eoo()); + ASSERT_TRUE(retVal.isABSONObj()); + ASSERT_EQ(retVal.Obj().getIntField("result"), 42); +} + +TEST_F(WasmMozJSTest, BsonArgsDouble) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(a, b) {" + " return { sum: a + b };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.append("a", 3.14); + bob.append("b", 2.86); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + double sum = retVal.Obj().getField("sum").Number(); + ASSERT_APPROX_EQUAL(sum, 6.0, 1e-10); +} + +TEST_F(WasmMozJSTest, BsonArgsString) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(greeting, name) {" + " return { message: greeting + ', ' + name + '!' };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.append("greeting", "Hello"); + bob.append("name", "MongoDB"); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + ASSERT_EQ(retVal.Obj().getStringField("message"), "Hello, MongoDB!"); +} + +TEST_F(WasmMozJSTest, BsonArgsBoolean) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(flag) {" + " return { negated: !flag, type: typeof flag };" + "}"); + ASSERT_NE(handle, 0u); + + { + BSONObjBuilder bob; + bob.appendBool("flag", true); + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getBoolField("negated"), false); + ASSERT_EQ(inner.getStringField("type"), "boolean"); + } + { + BSONObjBuilder bob; + bob.appendBool("flag", false); + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getBoolField("negated"), true); + } +} + +TEST_F(WasmMozJSTest, BsonArgsNull) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(val) {" + " return { isNull: val === null, type: typeof val };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.appendNull("val"); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getBoolField("isNull"), true); + ASSERT_EQ(inner.getStringField("type"), "object"); // typeof null === "object" in JS +} + +TEST_F(WasmMozJSTest, BsonArgsObject) { + ASSERT_TRUE(initEngine()); + + // Nested BSON object becomes a JS object positional arg + uint64_t handle = createFunction( + "function(doc) {" + " return {" + " name: doc.name," + " age: doc.age," + " hasCity: doc.address !== undefined && doc.address.city !== undefined" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + { + BSONObjBuilder nested(bob.subobjStart("doc")); + nested.append("name", "Alice"); + nested.append("age", 30); + { + BSONObjBuilder addr(nested.subobjStart("address")); + addr.append("city", "NYC"); + addr.append("zip", "10001"); + } + } + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getStringField("name"), "Alice"); + ASSERT_EQ(inner.getIntField("age"), 30); + ASSERT_EQ(inner.getBoolField("hasCity"), true); +} + +TEST_F(WasmMozJSTest, BsonArgsArray) { + ASSERT_TRUE(initEngine()); + + // BSON array becomes a JS Array positional arg + uint64_t handle = createFunction( + "function(arr) {" + " return {" + " length: arr.length," + " sum: arr.reduce(function(a, b) { return a + b; }, 0)," + " first: arr[0]," + " last: arr[arr.length - 1]" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + { + BSONArrayBuilder arr(bob.subarrayStart("arr")); + arr.append(10); + arr.append(20); + arr.append(30); + arr.append(40); + } + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getIntField("length"), 4); + ASSERT_EQ(inner.getIntField("sum"), 100); + ASSERT_EQ(inner.getIntField("first"), 10); + ASSERT_EQ(inner.getIntField("last"), 40); +} + +TEST_F(WasmMozJSTest, BsonArgsMixedTypes) { + ASSERT_TRUE(initEngine()); + + // Multiple BSON fields of different types as positional args + uint64_t handle = createFunction( + "function(num, str, flag, obj) {" + " return {" + " numType: typeof num," + " strType: typeof str," + " flagType: typeof flag," + " objType: typeof obj," + " computed: flag ? (num + ' ' + str) : obj.key" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.append("num", 42); + bob.append("str", "hello"); + bob.appendBool("flag", true); + { + BSONObjBuilder nested(bob.subobjStart("obj")); + nested.append("key", "fallback"); + } + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getStringField("numType"), "number"); + ASSERT_EQ(inner.getStringField("strType"), "string"); + ASSERT_EQ(inner.getStringField("flagType"), "boolean"); + ASSERT_EQ(inner.getStringField("objType"), "object"); + ASSERT_EQ(inner.getStringField("computed"), "42 hello"); +} + +TEST_F(WasmMozJSTest, BsonArgsEmptyObject) { + ASSERT_TRUE(initEngine()); + + // No args (empty BSON) — function receives no arguments + uint64_t handle = createFunction( + "function() {" + " return { argCount: arguments.length };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + ASSERT_EQ(retVal.Obj().getIntField("argCount"), 0); +} + +TEST_F(WasmMozJSTest, BsonArgsReturnArray) { + ASSERT_TRUE(initEngine()); + + // JS function that returns an array + uint64_t handle = createFunction( + "function(n) {" + " var result = [];" + " for (var i = 0; i < n; i++) {" + " result.push(i * i);" + " }" + " return result;" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.append("n", 5); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_FALSE(retVal.eoo()); + // Arrays serialize as BSON arrays (which are BSONType::array) + ASSERT_EQ(retVal.type(), BSONType::array); + BSONObj arr = retVal.Obj(); + // Elements are keyed "0", "1", "2", ... + ASSERT_EQ(arr.getIntField("0"), 0); // 0*0 + ASSERT_EQ(arr.getIntField("1"), 1); // 1*1 + ASSERT_EQ(arr.getIntField("2"), 4); // 2*2 + ASSERT_EQ(arr.getIntField("3"), 9); // 3*3 + ASSERT_EQ(arr.getIntField("4"), 16); // 4*4 +} + +TEST_F(WasmMozJSTest, BsonArgsReturnPrimitives) { + ASSERT_TRUE(initEngine()); + + // Returning various primitive types + BSONObj emptyArgs; + + // Integer + { + uint64_t h = createFunction("function() { return 42; }"); + ASSERT_NE(h, 0u); + ASSERT_TRUE(invokeFunction(h, emptyArgs)); + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_EQ(retVal.numberInt(), 42); + } + + // Double + { + uint64_t h = createFunction("function() { return 3.14159; }"); + ASSERT_NE(h, 0u); + ASSERT_TRUE(invokeFunction(h, emptyArgs)); + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_APPROX_EQUAL(retVal.Number(), 3.14159, 1e-5); + } + + // Boolean true + { + uint64_t h = createFunction("function() { return true; }"); + ASSERT_NE(h, 0u); + ASSERT_TRUE(invokeFunction(h, emptyArgs)); + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_EQ(retVal.type(), BSONType::boolean); + ASSERT_EQ(retVal.Bool(), true); + } + + // Boolean false + { + uint64_t h = createFunction("function() { return false; }"); + ASSERT_NE(h, 0u); + ASSERT_TRUE(invokeFunction(h, emptyArgs)); + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_EQ(retVal.type(), BSONType::boolean); + ASSERT_EQ(retVal.Bool(), false); + } + + // null + { + uint64_t h = createFunction("function() { return null; }"); + ASSERT_NE(h, 0u); + ASSERT_TRUE(invokeFunction(h, emptyArgs)); + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_EQ(retVal.type(), BSONType::null); + } + + // String + { + uint64_t h = createFunction("function() { return 'hello'; }"); + ASSERT_NE(h, 0u); + ASSERT_TRUE(invokeFunction(h, emptyArgs)); + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_EQ(retVal.type(), BSONType::string); + ASSERT_EQ(retVal.str(), "hello"); + } +} + +TEST_F(WasmMozJSTest, BsonArgsNestedArraysAndObjects) { + ASSERT_TRUE(initEngine()); + + // Deeply nested structure: object with arrays containing objects + uint64_t handle = createFunction( + "function(data) {" + " var total = 0;" + " for (var i = 0; i < data.items.length; i++) {" + " total += data.items[i].price * data.items[i].qty;" + " }" + " return { total: total, itemCount: data.items.length };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + { + BSONObjBuilder data(bob.subobjStart("data")); + { + BSONArrayBuilder items(data.subarrayStart("items")); + { + BSONObjBuilder item(items.subobjStart()); + item.append("price", 10); + item.append("qty", 3); + } + { + BSONObjBuilder item(items.subobjStart()); + item.append("price", 25); + item.append("qty", 2); + } + { + BSONObjBuilder item(items.subobjStart()); + item.append("price", 5); + item.append("qty", 10); + } + } + } + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + // 10*3 + 25*2 + 5*10 = 30 + 50 + 50 = 130 + ASSERT_EQ(inner.getIntField("total"), 130); + ASSERT_EQ(inner.getIntField("itemCount"), 3); +} + +TEST_F(WasmMozJSTest, BsonArgsObjectId) { + ASSERT_TRUE(initEngine()); + + // ObjectId round-trips through JS preserving type and value + uint64_t handle = createFunction( + "function(oid) {" + " return { val: oid };" + "}"); + ASSERT_NE(handle, 0u); + + OID testOid = OID::gen(); + BSONObjBuilder bob; + bob.append("oid", testOid); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + BSONElement valElem = inner.getField("val"); + ASSERT_EQ(valElem.type(), BSONType::oid); + ASSERT_EQ(valElem.OID(), testOid); +} + +TEST_F(WasmMozJSTest, BsonArgsDate) { + ASSERT_TRUE(initEngine()); + + // Date round-trips through JS. JS Date supports getUTCFullYear etc. + uint64_t handle = createFunction( + "function(d) {" + " return {" + " val: d," + " year: d.getUTCFullYear()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + // 2025-06-15T00:00:00Z = 1750032000000ms since epoch + Date_t testDate = Date_t::fromMillisSinceEpoch(1750032000000LL); + BSONObjBuilder bob; + bob.appendDate("d", testDate); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + // Date round-trips as BSONType::date + BSONElement valElem = inner.getField("val"); + ASSERT_EQ(valElem.type(), BSONType::date); + ASSERT_EQ(valElem.Date(), testDate); + // JS method access works on native Date + ASSERT_EQ(inner.getIntField("year"), 2025); +} + +// --------------------------------------------------------------------------- +// Extended BSON type tests: NumberLong, Decimal128, Timestamp, Regex, +// MinKey/MaxKey, and BinData. +// +// These types use custom SpiderMonkey prototypes (NumberLongInfo, +// NumberDecimalInfo, TimestampInfo, etc.) which are now installed on the +// global via MozJSPrototypeInstaller::installBSONTypes(). trackedNewInt64 +// is also implemented, so all these types round-trip correctly through JS. +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, BsonArgsNumberLong) { + ASSERT_TRUE(initEngine()); + + // NumberLong (int64) round-trips through JS as a NumberLong object. + uint64_t handle = createFunction( + "function(n) {" + " return { val: n };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.append("n", 1234567890123LL); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + BSONElement valElem = inner.getField("val"); + ASSERT_EQ(valElem.type(), BSONType::numberLong); + ASSERT_EQ(valElem.Long(), 1234567890123LL); +} + +TEST_F(WasmMozJSTest, BsonArgsNumberDecimal128) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(dec) {" + " return { val: dec };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.append("dec", Decimal128("123.456")); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + BSONElement valElem = inner.getField("val"); + ASSERT_EQ(valElem.type(), BSONType::numberDecimal); + ASSERT_TRUE(valElem.numberDecimal() == Decimal128("123.456")); +} + +TEST_F(WasmMozJSTest, BsonArgsTimestamp) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(ts) {" + " return { val: ts };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.append("ts", Timestamp(1700000000, 42)); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + BSONElement tsElem = inner.getField("val"); + ASSERT_EQ(tsElem.type(), BSONType::timestamp); + ASSERT_EQ(tsElem.timestamp(), Timestamp(1700000000, 42)); +} + +TEST_F(WasmMozJSTest, BsonArgsRegex) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(re) {" + " return { val: re };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.appendRegex("re", "Hello", "i"); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + BSONElement reElem = inner.getField("val"); + ASSERT_EQ(reElem.type(), BSONType::regEx); + ASSERT_EQ(std::string(reElem.regex()), "Hello"); + ASSERT_EQ(std::string(reElem.regexFlags()), "i"); +} + +TEST_F(WasmMozJSTest, BsonArgsMinKeyMaxKey) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(mn, mx) {" + " return { minVal: mn, maxVal: mx };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.appendMinKey("mn"); + bob.appendMaxKey("mx"); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_EQ(inner.getField("minVal").type(), BSONType::minKey); + ASSERT_EQ(inner.getField("maxVal").type(), BSONType::maxKey); +} + +TEST_F(WasmMozJSTest, BsonArgsBinData) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(bd) {" + " return { val: bd };" + "}"); + ASSERT_NE(handle, 0u); + + const char binPayload[] = {'\x01', '\x02', '\x03', '\x04', '\x05'}; + BSONObjBuilder bob; + bob.appendBinData("bd", sizeof(binPayload), BinDataGeneral, binPayload); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + BSONElement bdElem = inner.getField("val"); + ASSERT_EQ(bdElem.type(), BSONType::binData); + int len = 0; + const char* data = bdElem.binData(len); + ASSERT_EQ(len, 5); + ASSERT_EQ(std::memcmp(data, binPayload, 5), 0); +} + +TEST_F(WasmMozJSTest, BsonArgsArrayAsArg) { + ASSERT_TRUE(initEngine()); + + // Pass a raw BSON array as a positional arg — JS receives it as an Array + uint64_t handle = createFunction( + "function(arr) {" + " var max = -Infinity;" + " var min = Infinity;" + " for (var i = 0; i < arr.length; i++) {" + " if (arr[i] > max) max = arr[i];" + " if (arr[i] < min) min = arr[i];" + " }" + " return { max: max, min: min, len: arr.length };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + { + BSONArrayBuilder arr(bob.subarrayStart("arr")); + arr.append(5); + arr.append(99); + arr.append(-3); + arr.append(42); + arr.append(0); + } + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getIntField("max"), 99); + ASSERT_EQ(inner.getIntField("min"), -3); + ASSERT_EQ(inner.getIntField("len"), 5); +} + +TEST_F(WasmMozJSTest, BsonArgsArrayOfStrings) { + ASSERT_TRUE(initEngine()); + + // Pass array of strings, join them in JS + uint64_t handle = createFunction( + "function(words) {" + " return { sentence: words.join(' '), count: words.length };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + { + BSONArrayBuilder arr(bob.subarrayStart("words")); + arr.append("the"); + arr.append("quick"); + arr.append("brown"); + arr.append("fox"); + } + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getStringField("sentence"), "the quick brown fox"); + ASSERT_EQ(inner.getIntField("count"), 4); +} + +TEST_F(WasmMozJSTest, BsonArgsArrayOfMixedTypes) { + ASSERT_TRUE(initEngine()); + + // Array containing mixed types: int, string, bool, null, nested object + uint64_t handle = createFunction( + "function(arr) {" + " var types = [];" + " for (var i = 0; i < arr.length; i++) {" + " types.push(arr[i] === null ? 'null' : typeof arr[i]);" + " }" + " return { types: types.join(','), len: arr.length };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + { + BSONArrayBuilder arr(bob.subarrayStart("arr")); + arr.append(42); + arr.append("hello"); + arr.append(true); + arr.appendNull(); + { + BSONObjBuilder nested(arr.subobjStart()); + nested.append("key", "val"); + } + } + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getStringField("types"), "number,string,boolean,null,object"); + ASSERT_EQ(inner.getIntField("len"), 5); +} + +TEST_F(WasmMozJSTest, BsonArgsArrayPassthrough) { + ASSERT_TRUE(initEngine()); + + // Verify that a JS function can return the array it received (round-trip) + uint64_t handle = createFunction( + "function(arr) {" + " return arr;" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + { + BSONArrayBuilder arr(bob.subarrayStart("arr")); + arr.append(10); + arr.append(20); + arr.append(30); + } + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_EQ(retVal.type(), BSONType::array); + BSONObj arr = retVal.Obj(); + ASSERT_EQ(arr.getIntField("0"), 10); + ASSERT_EQ(arr.getIntField("1"), 20); + ASSERT_EQ(arr.getIntField("2"), 30); +} + +TEST_F(WasmMozJSTest, BsonArgsObjectIdRoundTrip) { + ASSERT_TRUE(initEngine()); + + // Pass ObjectId through JS and get it back as-is + uint64_t handle = createFunction( + "function(oid) {" + " return { id: oid };" + "}"); + ASSERT_NE(handle, 0u); + + OID testOid = OID::gen(); + BSONObjBuilder bob; + bob.append("oid", testOid); + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj retObj = retVal.Obj(); + BSONElement idElem = retObj.getField("id"); + ASSERT_EQ(idElem.type(), BSONType::oid); + ASSERT_EQ(idElem.OID(), testOid); +} + +TEST_F(WasmMozJSTest, BsonArgsAllTypesInOneCall) { + ASSERT_TRUE(initEngine()); + + // Pass many different BSON types as positional args in one call + uint64_t handle = createFunction( + "function(i32, dbl, str, flag, nullVal, obj, arr, oid, dt) {" + " return {" + " argCount: arguments.length," + " i32: i32," + " dbl: dbl," + " str: str," + " flag: flag," + " isNull: nullVal === null," + " objKey: obj.k," + " arrLen: arr.length," + " oidStr: oid.str," + " dtYear: dt.getUTCFullYear()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder bob; + bob.append("i32", 7); + bob.append("dbl", 2.718); + bob.append("str", "test"); + bob.appendBool("flag", false); + bob.appendNull("nullVal"); + { + BSONObjBuilder nested(bob.subobjStart("obj")); + nested.append("k", "v"); + } + { + BSONArrayBuilder arr(bob.subarrayStart("arr")); + arr.append(1); + arr.append(2); + arr.append(3); + } + bob.append("oid", OID::gen()); + bob.appendDate("dt", Date_t::fromMillisSinceEpoch(1750032000000LL)); // 2025-06-15 + + ASSERT_TRUE(invokeFunction(handle, bob.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj(); + ASSERT_EQ(inner.getIntField("argCount"), 9); + ASSERT_EQ(inner.getIntField("i32"), 7); + ASSERT_APPROX_EQUAL(inner.getField("dbl").Number(), 2.718, 1e-5); + ASSERT_EQ(inner.getStringField("str"), "test"); + ASSERT_EQ(inner.getBoolField("flag"), false); + ASSERT_EQ(inner.getBoolField("isNull"), true); + ASSERT_EQ(inner.getStringField("objKey"), "v"); + ASSERT_EQ(inner.getIntField("arrLen"), 3); + ASSERT_TRUE(inner.hasField("oidStr")); + ASSERT_EQ(inner.getIntField("dtYear"), 2025); +} + +TEST_F(WasmMozJSTest, CreateMultipleFunctions) { + ASSERT_TRUE(initEngine()); + + uint64_t h1 = createFunction("function() { return 1; }"); + uint64_t h2 = createFunction("function() { return 2; }"); + uint64_t h3 = createFunction("function() { return 3; }"); + + ASSERT_NE(h1, 0u); + ASSERT_NE(h2, 0u); + ASSERT_NE(h3, 0u); + + // Handles should be distinct + ASSERT_NE(h1, h2); + ASSERT_NE(h2, h3); + ASSERT_NE(h1, h3); + + // Invoke each and verify results + BSONObj emptyArgs; + + ASSERT_TRUE(invokeFunction(h1, emptyArgs)); + BSONObj r1 = getReturnValueBson(); + ASSERT_EQ(r1.getField("__returnValue").numberInt(), 1); + + ASSERT_TRUE(invokeFunction(h2, emptyArgs)); + BSONObj r2 = getReturnValueBson(); + ASSERT_EQ(r2.getField("__returnValue").numberInt(), 2); + + ASSERT_TRUE(invokeFunction(h3, emptyArgs)); + BSONObj r3 = getReturnValueBson(); + ASSERT_EQ(r3.getField("__returnValue").numberInt(), 3); +} + +TEST_F(WasmMozJSTest, InvokeWithInvalidHandleFails) { + ASSERT_TRUE(initEngine()); + + // Invoke with handle=0 (invalid) + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + + wc::Val arg0(uint64_t(0)); + wc::Val arg1(makeListU8(BSONObj())); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + + // Should return an error result + ASSERT_TRUE(result.is_result()); + // Invalid handle should produce an error result. + ASSERT_FALSE(result.get_result().is_ok()); +} + +TEST_F(WasmMozJSTest, CreateFunctionWithInvalidSourceFails) { + ASSERT_TRUE(initEngine()); + + // Invalid JavaScript - not a function expression + auto createFunc = getFunc("create-function"); + ASSERT_TRUE(createFunc.has_value()); + + wc::Val srcArg(makeListU8("this is not valid javascript {{{")); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*createFunc, ctx(), &result, 1, std::move(srcArg)); + + ASSERT_TRUE(result.is_result()); + // Invalid JS source should produce an error result. + ASSERT_FALSE(result.get_result().is_ok()); +} + +// --------------------------------------------------------------------------- +// Error extraction and failure scenario tests +// +// These tests exercise failure paths and verify that the wasm-mozjs-error +// record returned by WIT contains the correct error code, message, filename, +// and line/column information for various failure modes. +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, CompileErrorHasCorrectErrorCode) { + ASSERT_TRUE(initEngine()); + + auto createFunc = getFunc("create-function"); + ASSERT_TRUE(createFunc.has_value()); + + wc::Val srcArg(makeListU8("this is not valid javascript {{{")); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*createFunc, ctx(), &result, 1, std::move(srcArg)); + + ASSERT_TRUE(result.is_result()); + ASSERT_FALSE(result.get_result().is_ok()); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-compile"); +} + +TEST_F(WasmMozJSTest, CompileErrorContainsMessage) { + ASSERT_TRUE(initEngine()); + + auto createFunc = getFunc("create-function"); + ASSERT_TRUE(createFunc.has_value()); + + wc::Val srcArg(makeListU8("function( { invalid syntax")); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*createFunc, ctx(), &result, 1, std::move(srcArg)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-compile"); + ASSERT_FALSE(error->msg.empty()); +} + +TEST_F(WasmMozJSTest, CompileErrorHasFileAndLineInfo) { + ASSERT_TRUE(initEngine()); + + auto createFunc = getFunc("create-function"); + ASSERT_TRUE(createFunc.has_value()); + + wc::Val srcArg(makeListU8("function() { var x = ; }")); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*createFunc, ctx(), &result, 1, std::move(srcArg)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-compile"); + ASSERT_FALSE(error->filename.empty()); + ASSERT_GT(error->line, 0u); + ASSERT_GT(error->column, 0u); +} + +TEST_F(WasmMozJSTest, RuntimeErrorHasCorrectErrorCode) { + ASSERT_TRUE(initEngine()); + + auto createFunc = getFunc("create-function"); + ASSERT_TRUE(createFunc.has_value()); + + // Valid function that throws at runtime + uint64_t handle = createFunction("function() { throw new Error('boom'); }"); + ASSERT_NE(handle, 0u); + + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + + wc::Val arg0(handle); + wc::Val arg1(makeListU8(BSONObj())); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + + ASSERT_TRUE(result.is_result()); + ASSERT_FALSE(result.get_result().is_ok()); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-runtime"); +} + +TEST_F(WasmMozJSTest, RuntimeErrorContainsMessage) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction("function() { throw new Error('specific error text'); }"); + ASSERT_NE(handle, 0u); + + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + + wc::Val arg0(handle); + wc::Val arg1(makeListU8(BSONObj())); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-runtime"); + ASSERT_TRUE(error->msg.find("specific error text") != std::string::npos); +} + +TEST_F(WasmMozJSTest, RuntimeErrorFromUndefinedVariable) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction("function() { return nonexistentVariable.property; }"); + ASSERT_NE(handle, 0u); + + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + + wc::Val arg0(handle); + wc::Val arg1(makeListU8(BSONObj())); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-runtime"); + ASSERT_TRUE(error->msg.find("nonexistentVariable") != std::string::npos); +} + +TEST_F(WasmMozJSTest, RuntimeErrorFromTypeError) { + ASSERT_TRUE(initEngine()); + + // Calling a non-function triggers a TypeError at runtime + uint64_t handle = createFunction("function() { var x = 42; return x(); }"); + ASSERT_NE(handle, 0u); + + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + + wc::Val arg0(handle); + wc::Val arg1(makeListU8(BSONObj())); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-runtime"); + ASSERT_FALSE(error->msg.empty()); +} + +TEST_F(WasmMozJSTest, TypeErrorFromNonFunctionSource) { + ASSERT_TRUE(initEngine()); + + // "42" evaluates to a number, not a function + auto createFunc = getFunc("create-function"); + ASSERT_TRUE(createFunc.has_value()); + + wc::Val srcArg(makeListU8("42")); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*createFunc, ctx(), &result, 1, std::move(srcArg)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-type"); +} + +TEST_F(WasmMozJSTest, InvalidUtf8InCreateFunction) { + ASSERT_TRUE(initEngine()); + + auto createFunc = getFunc("create-function"); + ASSERT_TRUE(createFunc.has_value()); + + // 0xFE 0xFF are never valid in UTF-8. + std::string bad = "function() { return '\xFE\xFF'; }"; + wc::Val srcArg(makeListU8(bad)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*createFunc, ctx(), &result, 1, std::move(srcArg)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_FALSE(error->msg.empty()) << "expected an error message for invalid UTF-8"; +} + +TEST_F(WasmMozJSTest, InvalidHandleHasCorrectErrorCode) { + ASSERT_TRUE(initEngine()); + + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + + wc::Val arg0(uint64_t(0)); + wc::Val arg1(makeListU8(BSONObj())); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-invalid-arg"); +} + +TEST_F(WasmMozJSTest, StaleHandleAfterShutdownHasCorrectErrorCode) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction("function() { return 1; }"); + ASSERT_NE(handle, 0u); + + // Shutdown and re-initialize + auto shutdownFunc = getFunc("shutdown-engine"); + ASSERT_TRUE(shutdownFunc.has_value()); + wc::Val shutdownResult(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFuncNoArgs(*shutdownFunc, ctx(), &shutdownResult, 1)); + ASSERT_TRUE(isResultOk(shutdownResult)); + + ASSERT_TRUE(initEngine()); + + // Old handle should be invalid after re-init + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + + wc::Val arg0(handle); + wc::Val arg1(makeListU8(BSONObj())); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-invalid-arg"); +} + +TEST_F(WasmMozJSTest, OutOfRangeHandleHasCorrectErrorCode) { + ASSERT_TRUE(initEngine()); + + auto invokeFunc = getFunc("invoke-function"); + ASSERT_TRUE(invokeFunc.has_value()); + + // Handle with a very large index that's never been allocated + wc::Val arg0(uint64_t(0xFFFFFFFF)); + wc::Val arg1(makeListU8(BSONObj())); + wc::Val result(wc::WitResult::ok(std::nullopt)); + callFunc(*invokeFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-invalid-arg"); +} + +TEST_F(WasmMozJSTest, ShutdownAndReinitialize) { + // First init + ASSERT_TRUE(initEngine()); + + uint64_t h1 = createFunction("function() { return 'first'; }"); + ASSERT_NE(h1, 0u); + + // Shutdown + auto shutdownFunc = getFunc("shutdown-engine"); + ASSERT_TRUE(shutdownFunc.has_value()); + wc::Val shutdownResult(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFuncNoArgs(*shutdownFunc, ctx(), &shutdownResult, 1)); + ASSERT_TRUE(isResultOk(shutdownResult)); + + // Re-initialize + ASSERT_TRUE(initEngine()); + + // Create a new function (old handles are invalid after shutdown) + uint64_t h2 = createFunction("function() { return 'second'; }"); + ASSERT_NE(h2, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(h2, emptyArgs)); + BSONObj result = getReturnValueBson(); + ASSERT_FALSE(result.isEmpty()); +} + +TEST_F(WasmMozJSTest, FunctionReturningString) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction("function() { return \"hello world\"; }"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + ASSERT_FALSE(result.isEmpty()); + + BSONElement retVal = result.getField("__returnValue"); + ASSERT_FALSE(retVal.eoo()); + ASSERT_EQ(retVal.type(), mongo::BSONType::string); + ASSERT_EQ(retVal.str(), "hello world"); +} + +TEST_F(WasmMozJSTest, FunctionReturningNestedObject) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " return {" + " outer: {" + " inner: {" + " value: 99" + " }" + " }" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + ASSERT_FALSE(result.isEmpty()); + + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + BSONObj inner = retVal.Obj().getObjectField("outer").getObjectField("inner"); + ASSERT_EQ(inner.getIntField("value"), 99); +} + +// --------------------------------------------------------------------------- +// set-global / get-global tests +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, SetGlobalGetGlobalRoundTrip) { + ASSERT_TRUE(initEngine()); + + BSONObjBuilder bob; + bob.append("x", 10); + bob.append("y", "hello"); + BSONObj value = bob.obj(); + + ASSERT_TRUE(setGlobal("myVar", value)); + + BSONObj retrieved = getGlobal("myVar"); + ASSERT_FALSE(retrieved.isEmpty()); + ASSERT_EQ(retrieved.getIntField("x"), 10); + ASSERT_EQ(retrieved.getStringField("y"), "hello"); +} + +TEST_F(WasmMozJSTest, GetGlobalNonexistent) { + ASSERT_TRUE(initEngine()); + + // get-global for a name that was never set should return empty (error). + // If get-global is not yet implemented, it also returns empty - either way + // the assertion holds. + BSONObj retrieved = getGlobal("nonexistent_global_12345"); + // get-global for a nonexistent name should return an error (empty). + ASSERT_TRUE(retrieved.isEmpty()); +} + +TEST_F(WasmMozJSTest, SetGlobalThenUseInFunction) { + ASSERT_TRUE(initEngine()); + + BSONObjBuilder bob; + bob.append("factor", 7); + ASSERT_TRUE(setGlobal("config", bob.obj())); + + // JS function that reads the global we set (by name) and uses it. + // set-global should install "config" on the JS global scope. + uint64_t handle = createFunction( + "function(n) {" + " if (typeof config === 'undefined') return { result: -1 };" + " return { result: n * config.factor };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder args; + args.append("n", 6); + ASSERT_TRUE(invokeFunction(handle, args.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + // 6 * config.factor(7) = 42. + ASSERT_EQ(retVal.Obj().getIntField("result"), 42); +} + +TEST_F(WasmMozJSTest, SetGlobalOverwrite) { + ASSERT_TRUE(initEngine()); + + BSONObjBuilder b1; + b1.append("v", 1); + ASSERT_TRUE(setGlobal("g", b1.obj())); + ASSERT_EQ(getGlobal("g").getIntField("v"), 1); + + // Overwrite with a new value + BSONObjBuilder b2; + b2.append("v", 2); + b2.append("extra", "second"); + ASSERT_TRUE(setGlobal("g", b2.obj())); + BSONObj g2 = getGlobal("g"); + ASSERT_EQ(g2.getIntField("v"), 2); + ASSERT_EQ(g2.getStringField("extra"), "second"); +} + +TEST_F(WasmMozJSTest, SetGlobalMultipleNames) { + ASSERT_TRUE(initEngine()); + + BSONObjBuilder b1; + b1.append("val", 1); + ASSERT_TRUE(setGlobal("alpha", b1.obj())); + + BSONObjBuilder b2; + b2.append("val", 2); + ASSERT_TRUE(setGlobal("beta", b2.obj())); + + BSONObjBuilder b3; + b3.append("val", 3); + ASSERT_TRUE(setGlobal("gamma", b3.obj())); + + // Each global should be independently retrievable + ASSERT_EQ(getGlobal("alpha").getIntField("val"), 1); + ASSERT_EQ(getGlobal("beta").getIntField("val"), 2); + ASSERT_EQ(getGlobal("gamma").getIntField("val"), 3); +} + +TEST_F(WasmMozJSTest, SetGlobalVariousTypes) { + ASSERT_TRUE(initEngine()); + + // Set global with various BSON types and verify round-trip via get-global + { + BSONObjBuilder bob; + bob.append("n", 42); + bob.append("d", 3.14); + bob.append("s", "hello"); + bob.appendBool("b", true); + bob.appendNull("nil"); + ASSERT_TRUE(setGlobal("mixed", bob.obj())); + } + + BSONObj retrieved = getGlobal("mixed"); + ASSERT_FALSE(retrieved.isEmpty()); + ASSERT_EQ(retrieved.getIntField("n"), 42); + ASSERT_APPROX_EQUAL(retrieved.getField("d").Number(), 3.14, 1e-10); + ASSERT_EQ(retrieved.getStringField("s"), "hello"); + ASSERT_EQ(retrieved.getBoolField("b"), true); + ASSERT_EQ(retrieved.getField("nil").type(), BSONType::null); +} + +TEST_F(WasmMozJSTest, SetGlobalVisibleAcrossFunctions) { + ASSERT_TRUE(initEngine()); + + BSONObjBuilder bob; + bob.append("counter", 100); + ASSERT_TRUE(setGlobal("state", bob.obj())); + + // Two different functions should both see the same global + uint64_t h1 = createFunction( + "function() {" + " return { val: state.counter + 1 };" + "}"); + ASSERT_NE(h1, 0u); + + uint64_t h2 = createFunction( + "function() {" + " return { val: state.counter + 2 };" + "}"); + ASSERT_NE(h2, 0u); + + BSONObj emptyArgs; + + ASSERT_TRUE(invokeFunction(h1, emptyArgs)); + BSONObj r1 = getReturnValueBson(); + ASSERT_EQ(r1.getField("__returnValue").Obj().getIntField("val"), 101); + + ASSERT_TRUE(invokeFunction(h2, emptyArgs)); + BSONObj r2 = getReturnValueBson(); + ASSERT_EQ(r2.getField("__returnValue").Obj().getIntField("val"), 102); +} + +// --------------------------------------------------------------------------- +// Stored procedure tests +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, StoredProcedureViaSetGlobalCode) { + ASSERT_TRUE(initEngine()); + + // Install a stored procedure as a global via BSON Code type. + BSONObjBuilder bob; + bob.appendCode("multiply", "function(a, b) { return a * b; }"); + ASSERT_TRUE(setGlobal("procs", bob.obj())); + + uint64_t handle = createFunction( + "function(x, y) {" + " return { result: procs.multiply(x, y) };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder args; + args.append("x", 6); + args.append("y", 7); + ASSERT_TRUE(invokeFunction(handle, args.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + ASSERT_EQ(retVal.Obj().getIntField("result"), 42); +} + +TEST_F(WasmMozJSTest, StoredProcedureMultiple) { + ASSERT_TRUE(initEngine()); + + // Install multiple stored procedures at once. + BSONObjBuilder bob; + bob.appendCode("add", "function(a, b) { return a + b; }"); + bob.appendCode("square", "function(x) { return x * x; }"); + bob.appendCode("negate", "function(x) { return -x; }"); + ASSERT_TRUE(setGlobal("math", bob.obj())); + + uint64_t handle = createFunction( + "function(a, b) {" + " var sum = math.add(a, b);" + " var sq = math.square(sum);" + " return { result: math.negate(sq) };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder args; + args.append("a", 3); + args.append("b", 4); + ASSERT_TRUE(invokeFunction(handle, args.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + // negate(square(add(3, 4))) = negate(square(7)) = negate(49) = -49 + ASSERT_EQ(retVal.Obj().getIntField("result"), -49); +} + +TEST_F(WasmMozJSTest, StoredProcedureChaining) { + ASSERT_TRUE(initEngine()); + + // Install procedures that call each other. + BSONObjBuilder bob; + bob.appendCode("double_", "function(x) { return x * 2; }"); + bob.appendCode("doubleAndAdd", "function(a, b) { return utils.double_(a) + b; }"); + ASSERT_TRUE(setGlobal("utils", bob.obj())); + + uint64_t handle = createFunction( + "function(n) {" + " return { result: utils.doubleAndAdd(n, 10) };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder args; + args.append("n", 5); + ASSERT_TRUE(invokeFunction(handle, args.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + // doubleAndAdd(5, 10) = double_(5) + 10 = 10 + 10 = 20 + ASSERT_EQ(retVal.Obj().getIntField("result"), 20); +} + +TEST_F(WasmMozJSTest, StoredProcedureCalledFromMultipleFunctions) { + ASSERT_TRUE(initEngine()); + + BSONObjBuilder bob; + bob.appendCode("transform", "function(x) { return x * x + 1; }"); + ASSERT_TRUE(setGlobal("sp", bob.obj())); + + // Two different functions both use the same stored procedure. + uint64_t h1 = createFunction("function(n) { return { result: sp.transform(n) }; }"); + ASSERT_NE(h1, 0u); + + uint64_t h2 = + createFunction("function(a, b) { return { result: sp.transform(a) + sp.transform(b) }; }"); + ASSERT_NE(h2, 0u); + + { + BSONObjBuilder a; + a.append("n", 3); + ASSERT_TRUE(invokeFunction(h1, a.obj())); + BSONObj r = getReturnValueBson(); + // transform(3) = 3*3 + 1 = 10 + ASSERT_EQ(r.getField("__returnValue").Obj().getIntField("result"), 10); + } + + { + BSONObjBuilder a; + a.append("a", 2); + a.append("b", 4); + ASSERT_TRUE(invokeFunction(h2, a.obj())); + BSONObj r = getReturnValueBson(); + // transform(2) + transform(4) = (4+1) + (16+1) = 5 + 17 = 22 + ASSERT_EQ(r.getField("__returnValue").Obj().getIntField("result"), 22); + } +} + +TEST_F(WasmMozJSTest, StoredProcedureWithCodeWScope) { + ASSERT_TRUE(initEngine()); + + // CodeWScope is also evaluated as a function (scope is ignored per SpiderMonkey behavior). + BSONObjBuilder bob; + bob.appendCodeWScope("greet", "function(name) { return 'Hello, ' + name; }", BSONObj()); + ASSERT_TRUE(setGlobal("funcs", bob.obj())); + + uint64_t handle = createFunction("function() { return { msg: funcs.greet('World') }; }"); + ASSERT_NE(handle, 0u); + + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + ASSERT_EQ(retVal.Obj().getStringField("msg"), "Hello, World"); +} + +TEST_F(WasmMozJSTest, GlobalScopeContainsExpectedVars) { + ASSERT_TRUE(initEngine()); + + // Enumerate all global property names from the JS environment. + uint64_t handle = createFunction( + "function() {" + " var global = (function() { return this; })();" + " return Object.getOwnPropertyNames(global);" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_EQ(retVal.type(), BSONType::array); + + // Collect all property names into a set. + BSONObj arr = retVal.Obj(); + std::set globals; + for (const auto& elem : arr) { + if (elem.type() == BSONType::string) { + globals.insert(elem.str()); + } + } + + // Verify custom globals installed by GlobalInfo::freeFunctions. + for (const auto& name : {"print", "gc", "sleep", "version", "buildInfo", "getJSHeapLimitMB"}) { + ASSERT_TRUE(globals.count(name) > 0); + } + + // Verify BSON type constructors installed via WrapType::install() (InstallType::Global). + for (const auto& name : {"BinData", + "Code", + "DBPointer", + "DBRef", + "NumberDecimal", + "NumberInt", + "NumberLong", + "ObjectId", + "MaxKey", + "MinKey", + "Timestamp"}) { + ASSERT_TRUE(globals.count(name) > 0); + } + + // Verify BinDataInfo::freeFunctions (installed as globals by WrapType). + for (const auto& name : {"HexData", "MD5", "UUID"}) { + ASSERT_TRUE(globals.count(name) > 0); + } + + // Verify BSONInfo::freeFunctions (Private type, but freeFunctions still go on global). + for (const auto& name : + {"bsonWoCompare", "bsonUnorderedFieldsCompare", "bsonBinaryEqual", "bsonObjToArray"}) { + ASSERT_TRUE(globals.count(name) > 0); + } + + // Verify standard JS built-ins are present. + for (const auto& name : {"Object", + "Array", + "Math", + "JSON", + "Date", + "RegExp", + "String", + "Number", + "Boolean", + "Error", + "Map", + "Set", + "Promise"}) { + ASSERT_TRUE(globals.count(name) > 0); + } +} + +// --------------------------------------------------------------------------- +// Prototype method tests +// +// Each BSON type installed by MozJSPrototypeInstaller exposes a constructor +// and/or prototype methods. These tests verify that the constructors create +// valid instances and that the expected methods exist and return sensible +// values. +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, BinDataConstructorAndMethods) { + ASSERT_TRUE(initEngine()); + + // BinData(subtype, base64str) constructor + prototype methods + uint64_t handle = createFunction( + "function() {" + " var bd = BinData(0, 'AQIDBA==');" // bytes [1,2,3,4] + " return {" + " hasBase64: typeof bd.base64 === 'function'," + " hasHex: typeof bd.hex === 'function'," + " hasToStr: typeof bd.toString === 'function'," + " hasToJSON: typeof bd.toJSON === 'function'," + " b64Val: bd.base64()," + " hexVal: bd.hex()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("hasBase64")); + ASSERT_TRUE(inner.getBoolField("hasHex")); + ASSERT_TRUE(inner.getBoolField("hasToStr")); + ASSERT_TRUE(inner.getBoolField("hasToJSON")); + ASSERT_EQ(inner.getStringField("b64Val"), "AQIDBA=="); + ASSERT_EQ(inner.getStringField("hexVal"), "01020304"); +} + +TEST_F(WasmMozJSTest, BinDataFreeFunctions) { + ASSERT_TRUE(initEngine()); + + // HexData, MD5, UUID are global free functions from BinDataInfo. + uint64_t handle = createFunction( + "function() {" + " return {" + " hasHexData: typeof HexData === 'function'," + " hasMD5: typeof MD5 === 'function'," + " hasUUID: typeof UUID === 'function'," + " hexDataOk: HexData(0, '01020304').hex() === '01020304'" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("hasHexData")); + ASSERT_TRUE(inner.getBoolField("hasMD5")); + ASSERT_TRUE(inner.getBoolField("hasUUID")); + ASSERT_TRUE(inner.getBoolField("hexDataOk")); +} + +TEST_F(WasmMozJSTest, NumberIntConstructorAndMethods) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " var ni = NumberInt(42);" + " return {" + " hasToNumber: typeof ni.toNumber === 'function'," + " hasToStr: typeof ni.toString === 'function'," + " hasToJSON: typeof ni.toJSON === 'function'," + " hasValueOf: typeof ni.valueOf === 'function'," + " numVal: ni.toNumber()," + " strVal: ni.toString()," + " valOf: ni.valueOf()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("hasToNumber")); + ASSERT_TRUE(inner.getBoolField("hasToStr")); + ASSERT_TRUE(inner.getBoolField("hasToJSON")); + ASSERT_TRUE(inner.getBoolField("hasValueOf")); + ASSERT_EQ(inner.getIntField("numVal"), 42); + ASSERT_EQ(inner.getStringField("strVal"), "NumberInt(42)"); + ASSERT_EQ(inner.getIntField("valOf"), 42); +} + +TEST_F(WasmMozJSTest, NumberLongConstructorAndMethods) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " var nl = NumberLong('1234567890123');" + " return {" + " hasToNumber: typeof nl.toNumber === 'function'," + " hasToStr: typeof nl.toString === 'function'," + " hasToJSON: typeof nl.toJSON === 'function'," + " hasValueOf: typeof nl.valueOf === 'function'," + " hasCompare: typeof nl.compare === 'function'," + " strVal: nl.toString()," + " floatApprox: nl.floatApprox" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("hasToNumber")); + ASSERT_TRUE(inner.getBoolField("hasToStr")); + ASSERT_TRUE(inner.getBoolField("hasToJSON")); + ASSERT_TRUE(inner.getBoolField("hasValueOf")); + ASSERT_TRUE(inner.getBoolField("hasCompare")); + ASSERT_EQ(inner.getStringField("strVal"), "NumberLong(\"1234567890123\")"); +} + +TEST_F(WasmMozJSTest, NumberDecimalConstructorAndMethods) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " var nd = NumberDecimal('123.456');" + " return {" + " hasToStr: typeof nd.toString === 'function'," + " hasToJSON: typeof nd.toJSON === 'function'," + " strVal: nd.toString()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("hasToStr")); + ASSERT_TRUE(inner.getBoolField("hasToJSON")); + ASSERT_EQ(inner.getStringField("strVal"), "NumberDecimal(\"123.456\")"); +} + +TEST_F(WasmMozJSTest, ObjectIdConstructorAndMethods) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " var oid = ObjectId();" + " return {" + " hasToStr: typeof oid.toString === 'function'," + " hasToJSON: typeof oid.toJSON === 'function'," + " hasStr: typeof oid.str === 'string'," + " strLen: oid.str.length," // ObjectId hex string is 24 chars + " toStrVal: oid.toString()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("hasToStr")); + ASSERT_TRUE(inner.getBoolField("hasToJSON")); + ASSERT_TRUE(inner.getBoolField("hasStr")); + ASSERT_EQ(inner.getIntField("strLen"), 24); + // toString() returns 'ObjectId("...")' format + std::string toStr(inner.getStringField("toStrVal")); + // toString() should return 'ObjectId("...")' format. + ASSERT_TRUE(toStr.find("ObjectId(") == 0); +} + +TEST_F(WasmMozJSTest, TimestampConstructorAndMethods) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " var ts = Timestamp(1700000000, 42);" + " return {" + " hasToJSON: typeof ts.toJSON === 'function'," + " jsonVal: ts.toJSON()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("hasToJSON")); + // toJSON() returns { "$timestamp": { "t": ..., "i": ... } } + BSONObj jsonVal = inner.getObjectField("jsonVal"); + // toJSON() returns { "$timestamp": { "t": ..., "i": ... } }. + ASSERT_FALSE(jsonVal.isEmpty()); +} + +TEST_F(WasmMozJSTest, MinKeyMaxKeyMethods) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " var mn = MinKey();" + " var mx = MaxKey();" + " return {" + " mnHasTojson: typeof mn.tojson === 'function'," + " mnHasToJSON: typeof mn.toJSON === 'function'," + " mxHasTojson: typeof mx.tojson === 'function'," + " mxHasToJSON: typeof mx.toJSON === 'function'," + " mnTojson: mn.tojson()," + " mxTojson: mx.tojson()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("mnHasTojson")); + ASSERT_TRUE(inner.getBoolField("mnHasToJSON")); + ASSERT_TRUE(inner.getBoolField("mxHasTojson")); + ASSERT_TRUE(inner.getBoolField("mxHasToJSON")); + ASSERT_EQ(inner.getStringField("mnTojson"), "{ \"$minKey\" : 1 }"); + ASSERT_EQ(inner.getStringField("mxTojson"), "{ \"$maxKey\" : 1 }"); +} + +TEST_F(WasmMozJSTest, BSONFreeFunctionsWork) { + ASSERT_TRUE(initEngine()); + + // bsonWoCompare, bsonBinaryEqual, bsonObjToArray are global free functions + // from BSONInfo (Private installType, but freeFunctions go on global). + uint64_t handle = createFunction( + "function() {" + " var a = {x: 1, y: 2};" + " var b = {x: 1, y: 2};" + " var c = {x: 2, y: 1};" + " return {" + " hasBsonWoCompare: typeof bsonWoCompare === 'function'," + " hasBsonBinaryEqual: typeof bsonBinaryEqual === 'function'," + " hasBsonObjToArray: typeof bsonObjToArray === 'function'," + " hasBsonUnorderedFieldsCompare: typeof bsonUnorderedFieldsCompare === 'function'," + " eqCompare: bsonWoCompare(a, b) === 0," + " neCompare: bsonWoCompare(a, c) !== 0," + " binaryEq: bsonBinaryEqual(a, b)," + " binaryNeq: !bsonBinaryEqual(a, c)," + " arrLen: bsonObjToArray(a).length" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("hasBsonWoCompare")); + ASSERT_TRUE(inner.getBoolField("hasBsonBinaryEqual")); + ASSERT_TRUE(inner.getBoolField("hasBsonObjToArray")); + ASSERT_TRUE(inner.getBoolField("hasBsonUnorderedFieldsCompare")); + ASSERT_TRUE(inner.getBoolField("eqCompare")); + ASSERT_TRUE(inner.getBoolField("neCompare")); + ASSERT_TRUE(inner.getBoolField("binaryEq")); + ASSERT_TRUE(inner.getBoolField("binaryNeq")); + ASSERT_EQ(inner.getIntField("arrLen"), 2); +} + +TEST_F(WasmMozJSTest, CodeConstructor) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " var c = Code('function() { return 1; }');" + " return {" + " hasToStr: typeof c.toString === 'function'," + " strVal: c.toString()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("hasToStr")); +} + +TEST_F(WasmMozJSTest, NumberLongCompare) { + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " var a = NumberLong('100');" + " var b = NumberLong('200');" + " var c = NumberLong('100');" + " return {" + " aLtB: a.compare(b) < 0," + " bGtA: b.compare(a) > 0," + " aEqC: a.compare(c) === 0" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_TRUE(inner.getBoolField("aLtB")); + ASSERT_TRUE(inner.getBoolField("bGtA")); + ASSERT_TRUE(inner.getBoolField("aEqC")); +} + +TEST_F(WasmMozJSTest, NumberIntValueOfEnablesArithmetic) { + ASSERT_TRUE(initEngine()); + + // valueOf() allows NumberInt to participate in JS arithmetic + uint64_t handle = createFunction( + "function() {" + " var a = NumberInt(10);" + " var b = NumberInt(32);" + " return {" + " sum: a + b," + " product: a * b," + " valueOf: a.valueOf()" + " };" + "}"); + ASSERT_NE(handle, 0u); + + BSONObj emptyArgs; + ASSERT_TRUE(invokeFunction(handle, emptyArgs)); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_EQ(inner.getIntField("sum"), 42); + ASSERT_EQ(inner.getIntField("product"), 320); + ASSERT_EQ(inner.getIntField("valueOf"), 10); +} + +TEST_F(WasmMozJSTest, MQLFunctionPattern) { + // Simulates: { $function: { body: , args: ["$name"], lang: "js" } } + // JsExecution::callFunction(func, params, thisObj={}) + // → Scope::invoke(func, ¶ms, &{}, timeout, false) + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(name, factor) {" + " return { upper: name.toUpperCase(), doubled: factor * 2 };" + "}"); + ASSERT_NE(handle, 0u); + + // Params are built as: { "arg0": , "arg1": , ... } + BSONObjBuilder params; + params.append("arg0", "hello"); + params.append("arg1", 21); + ASSERT_TRUE(invokeFunction(handle, params.obj())); + + BSONObj result = getReturnValueBson(); + BSONElement retVal = result.getField("__returnValue"); + ASSERT_TRUE(retVal.isABSONObj()); + ASSERT_EQ(retVal.Obj().getStringField("upper"), "HELLO"); + ASSERT_EQ(retVal.Obj().getIntField("doubled"), 42); +} + +TEST_F(WasmMozJSTest, MQLFunctionPatternWithDocumentArg) { + // $function receives evaluated expressions as args; a common pattern is + // passing the entire document as an argument. + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(doc) {" + " return doc.price * doc.qty;" + "}"); + ASSERT_NE(handle, 0u); + + BSONObjBuilder params; + { + BSONObjBuilder doc(params.subobjStart("arg0")); + doc.append("price", 15); + doc.append("qty", 3); + } + ASSERT_TRUE(invokeFunction(handle, params.obj())); + + BSONObj result = getReturnValueBson(); + ASSERT_EQ(result.getField("__returnValue").numberInt(), 45); +} + +TEST_F(WasmMozJSTest, MQLAccumulatorFullLifecycle) { + // Simulates the $accumulator lifecycle: + // init() → state + // accumulate(state, val) → state (per document) + // merge(s1, s2) → state (across shards) + // finalize(state) → result + ASSERT_TRUE(initEngine()); + + uint64_t initFn = createFunction("function() { return { count: 0, sum: 0 }; }"); + uint64_t accFn = createFunction( + "function(state, val) {" + " return { count: state.count + 1, sum: state.sum + val };" + "}"); + uint64_t mergeFn = createFunction( + "function(s1, s2) {" + " return { count: s1.count + s2.count, sum: s1.sum + s2.sum };" + "}"); + uint64_t finalizeFn = createFunction( + "function(state) {" + " return state.count > 0 ? state.sum / state.count : 0;" + "}"); + + ASSERT_NE(initFn, 0u); + ASSERT_NE(accFn, 0u); + ASSERT_NE(mergeFn, 0u); + ASSERT_NE(finalizeFn, 0u); + + // Shard 1: init → accumulate [10, 20, 30] + ASSERT_TRUE(invokeFunction(initFn, BSONObj())); + BSONObj state1 = getReturnValueBson().getField("__returnValue").Obj().getOwned(); + for (int val : {10, 20, 30}) { + BSONObjBuilder a; + a.append("state", state1); + a.append("val", val); + ASSERT_TRUE(invokeFunction(accFn, a.obj())); + state1 = getReturnValueBson().getField("__returnValue").Obj().getOwned(); + } + ASSERT_EQ(state1.getIntField("count"), 3); + ASSERT_EQ(state1.getIntField("sum"), 60); + + // Shard 2: init → accumulate [40] + ASSERT_TRUE(invokeFunction(initFn, BSONObj())); + BSONObj state2 = getReturnValueBson().getField("__returnValue").Obj().getOwned(); + { + BSONObjBuilder a; + a.append("state", state2); + a.append("val", 40); + ASSERT_TRUE(invokeFunction(accFn, a.obj())); + state2 = getReturnValueBson().getField("__returnValue").Obj().getOwned(); + } + ASSERT_EQ(state2.getIntField("count"), 1); + ASSERT_EQ(state2.getIntField("sum"), 40); + + // Merge shard states + { + BSONObjBuilder a; + a.append("s1", state1); + a.append("s2", state2); + ASSERT_TRUE(invokeFunction(mergeFn, a.obj())); + state1 = getReturnValueBson().getField("__returnValue").Obj().getOwned(); + } + ASSERT_EQ(state1.getIntField("count"), 4); + ASSERT_EQ(state1.getIntField("sum"), 100); + + // Finalize: average = 100/4 = 25 + { + BSONObjBuilder a; + a.append("state", state1); + ASSERT_TRUE(invokeFunction(finalizeFn, a.obj())); + BSONObj result = getReturnValueBson(); + ASSERT_APPROX_EQUAL(result.getField("__returnValue").Number(), 25.0, 1e-10); + } +} + +TEST_F(WasmMozJSTest, MQLAccumulatorPattern) { + // Mirrors real $accumulator: setFunction("__accumulate", code) installs a JS + // function as a directly-callable global via set-global-value with BSONType::Code. + // A wrapper then calls __accumulate(state, val) in a loop. + ASSERT_TRUE(initEngine()); + + ASSERT_TRUE(setFunction("__accumulate", + "function(state, val) {" + " return { count: state.count + 1, sum: state.sum + val };" + "}")); + ASSERT_TRUE(setFunction("__merge", + "function(s1, s2) {" + " return { count: s1.count + s2.count, sum: s1.sum + s2.sum };" + "}")); + + uint64_t wrapper = createFunction( + "function(state, pendingCalls) {" + " for (var i = 0; i < pendingCalls.length; i++) {" + " state = __accumulate(state, pendingCalls[i]);" + " }" + " return state;" + "}"); + ASSERT_NE(wrapper, 0u); + + BSONObjBuilder args; + { + BSONObjBuilder state(args.subobjStart("state")); + state.append("count", 0); + state.append("sum", 0); + } + { + BSONArrayBuilder pending(args.subarrayStart("pendingCalls")); + pending.append(10); + pending.append(20); + pending.append(30); + } + ASSERT_TRUE(invokeFunction(wrapper, args.obj())); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_EQ(inner.getIntField("count"), 3); + ASSERT_EQ(inner.getIntField("sum"), 60); + + uint64_t mergeWrapper = createFunction( + "function(state, pendingMerges) {" + " for (var i = 0; i < pendingMerges.length; i++) {" + " state = __merge(state, pendingMerges[i]);" + " }" + " return state;" + "}"); + ASSERT_NE(mergeWrapper, 0u); + + BSONObjBuilder mergeArgs; + mergeArgs.append("state", inner); + { + BSONArrayBuilder pending(mergeArgs.subarrayStart("pendingMerges")); + { + BSONObjBuilder s(pending.subobjStart()); + s.append("count", 2); + s.append("sum", 70); + } + } + ASSERT_TRUE(invokeFunction(mergeWrapper, mergeArgs.obj())); + + BSONObj merged = getReturnValueBson().getField("__returnValue").Obj().getOwned(); + ASSERT_EQ(merged.getIntField("count"), 5); + ASSERT_EQ(merged.getIntField("sum"), 130); +} + +TEST_F(WasmMozJSTest, MQLWherePattern) { + // $where calls: invoke(func, nullptr, &document, timeout) + // where document becomes 'this'. invoke-predicate handles this directly. + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function() {" + " return this.x > this.y;" + "}"); + ASSERT_NE(handle, 0u); + + // Document that passes filter + ASSERT_TRUE(invokePredicate(handle, BSON("x" << 10 << "y" << 5))); + + // Document that fails filter + ASSERT_FALSE(invokePredicate(handle, BSON("x" << 3 << "y" << 7))); +} + +TEST_F(WasmMozJSTest, MQLMapReduceReducePattern) { + // mapReduce.reduce calls: invoke(reduceFunc, &{key, values}, &{}, timeout) + // 'this' is an empty object. invoke-function (no this binding) works. + ASSERT_TRUE(initEngine()); + + uint64_t reduceFn = createFunction( + "function(key, values) {" + " var total = 0;" + " for (var i = 0; i < values.length; i++) {" + " total += values[i];" + " }" + " return total;" + "}"); + ASSERT_NE(reduceFn, 0u); + + BSONObjBuilder args; + args.append("key", "electronics"); + { + BSONArrayBuilder values(args.subarrayStart("values")); + values.append(100); + values.append(250); + values.append(75); + values.append(300); + } + ASSERT_TRUE(invokeFunction(reduceFn, args.obj())); + + BSONObj result = getReturnValueBson(); + ASSERT_EQ(result.getField("__returnValue").numberInt(), 725); +} + +TEST_F(WasmMozJSTest, MQLMapReduceFinalizePattern) { + // mapReduce.finalize uses $function pattern: + // invoke(finalizeFunc, &{key, reducedValue}, &{}, timeout) + ASSERT_TRUE(initEngine()); + + uint64_t finalizeFn = createFunction( + "function(key, reducedValue) {" + " return { category: key, total: reducedValue, formatted: key + ': $' + reducedValue };" + "}"); + ASSERT_NE(finalizeFn, 0u); + + BSONObjBuilder args; + args.append("key", "electronics"); + args.append("reducedValue", 725); + ASSERT_TRUE(invokeFunction(finalizeFn, args.obj())); + + BSONObj result = getReturnValueBson(); + BSONObj inner = result.getField("__returnValue").Obj(); + ASSERT_EQ(inner.getStringField("category"), "electronics"); + ASSERT_EQ(inner.getIntField("total"), 725); + ASSERT_EQ(inner.getStringField("formatted"), "electronics: $725"); +} + +TEST_F(WasmMozJSTest, MQLMapReduceMapPattern) { + // mapReduce.map calls: invoke(mapFunc, nullptr, &document, timeout) + // where document becomes 'this' and the function calls emit(key, value). + // invoke-map handles this: document as `this`, emits buffered. + ASSERT_TRUE(initEngine()); + ASSERT_TRUE(setupEmit()); + + uint64_t mapFn = createFunction( + "function() {" + " emit(this.category, this.price);" + "}"); + ASSERT_NE(mapFn, 0u); + + ASSERT_TRUE(invokeMap(mapFn, BSON("category" << "electronics" << "price" << 100))); + ASSERT_TRUE(invokeMap(mapFn, BSON("category" << "books" << "price" << 25))); + ASSERT_TRUE(invokeMap(mapFn, BSON("category" << "electronics" << "price" << 250))); + + BSONObj emitDoc = drainEmitBuffer(); + auto emitsArr = emitDoc["emits"].Array(); + ASSERT_EQ(emitsArr.size(), 3u); + ASSERT_EQ(emitsArr[0].Obj()["k"].str(), "electronics"); + ASSERT_EQ(emitsArr[0].Obj()["v"].numberInt(), 100); + ASSERT_EQ(emitsArr[1].Obj()["k"].str(), "books"); + ASSERT_EQ(emitsArr[1].Obj()["v"].numberInt(), 25); + ASSERT_EQ(emitsArr[2].Obj()["k"].str(), "electronics"); + ASSERT_EQ(emitsArr[2].Obj()["v"].numberInt(), 250); +} + +TEST_F(WasmMozJSTest, MQLFunctionReusedAcrossDocuments) { + // $function/$accumulator create the function once + // and invoke it per-document. This tests handle reuse across many calls. + ASSERT_TRUE(initEngine()); + + uint64_t handle = createFunction( + "function(price, qty) {" + " return price * qty;" + "}"); + ASSERT_NE(handle, 0u); + + struct TestCase { + int price; + int qty; + int expected; + }; + TestCase cases[] = { + {10, 5, 50}, + {25, 2, 50}, + {3, 100, 300}, + {0, 42, 0}, + {7, 7, 49}, + }; + + for (const auto& tc : cases) { + BSONObjBuilder args; + args.append("price", tc.price); + args.append("qty", tc.qty); + ASSERT_TRUE(invokeFunction(handle, args.obj())); + BSONObj result = getReturnValueBson(); + ASSERT_EQ(result.getField("__returnValue").numberInt(), tc.expected); + } +} + + +// --------------------------------------------------------------------------- +// invoke-predicate tests ($where pattern) +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, InvokePredicateReturnsTrueForMatch) { + initEngine(); + auto handle = createFunction("function() { return this.x > this.y; }"); + ASSERT_NE(handle, 0u); + + ASSERT_TRUE(invokePredicate(handle, BSON("x" << 10 << "y" << 5))); +} + +TEST_F(WasmMozJSTest, InvokePredicateReturnsFalseForNonMatch) { + initEngine(); + auto handle = createFunction("function() { return this.x > this.y; }"); + ASSERT_NE(handle, 0u); + + ASSERT_FALSE(invokePredicate(handle, BSON("x" << 3 << "y" << 7))); +} + +TEST_F(WasmMozJSTest, InvokePredicateFieldAccess) { + initEngine(); + auto handle = createFunction("function() { return this.name === 'hello'; }"); + ASSERT_NE(handle, 0u); + + ASSERT_TRUE(invokePredicate(handle, BSON("name" << "hello"))); + ASSERT_FALSE(invokePredicate(handle, BSON("name" << "world"))); +} + +TEST_F(WasmMozJSTest, InvokePredicateMultipleDocuments) { + initEngine(); + auto handle = createFunction("function() { return this.age >= 18; }"); + ASSERT_NE(handle, 0u); + + ASSERT_TRUE(invokePredicate(handle, BSON("age" << 25))); + ASSERT_FALSE(invokePredicate(handle, BSON("age" << 10))); + ASSERT_TRUE(invokePredicate(handle, BSON("age" << 18))); + ASSERT_FALSE(invokePredicate(handle, BSON("age" << 17))); +} + +TEST_F(WasmMozJSTest, InvokePredicateRuntimeError) { + initEngine(); + auto handle = createFunction("function() { throw new Error('pred error'); }"); + ASSERT_NE(handle, 0u); + + auto func = getFunc("invoke-predicate"); + ASSERT_TRUE(func.has_value()); + wc::Val arg0(handle); + wc::Val arg1(makeListU8(BSON("x" << 1))); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFunc(*func, ctx(), &result, 1, std::move(arg0), std::move(arg1))); + ASSERT_FALSE(isResultOk(result)); + + auto error = extractError(result); + ASSERT_TRUE(error.has_value()); + ASSERT_EQ(error->code, "e-runtime"); +} + +// --------------------------------------------------------------------------- +// invoke-map tests +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, InvokeMapEmitsDocuments) { + initEngine(); + ASSERT_TRUE(setupEmit()); + + auto handle = createFunction("function() { emit(this.category, this.price); }"); + ASSERT_NE(handle, 0u); + + ASSERT_TRUE(invokeMap(handle, BSON("category" << "A" << "price" << 10))); + ASSERT_TRUE(invokeMap(handle, BSON("category" << "B" << "price" << 20))); + ASSERT_TRUE(invokeMap(handle, BSON("category" << "A" << "price" << 30))); + + BSONObj emitDoc = drainEmitBuffer(); + auto emitsArr = emitDoc["emits"].Array(); + ASSERT_EQ(emitsArr.size(), 3u); + ASSERT_EQ(emitsArr[0].Obj()["k"].str(), "A"); + ASSERT_EQ(emitsArr[0].Obj()["v"].numberInt(), 10); + ASSERT_EQ(emitsArr[1].Obj()["k"].str(), "B"); + ASSERT_EQ(emitsArr[1].Obj()["v"].numberInt(), 20); + ASSERT_EQ(emitsArr[2].Obj()["k"].str(), "A"); + ASSERT_EQ(emitsArr[2].Obj()["v"].numberInt(), 30); +} + +TEST_F(WasmMozJSTest, InvokeMapRuntimeError) { + initEngine(); + auto handle = createFunction("function() { throw new Error('map error'); }"); + ASSERT_NE(handle, 0u); + + ASSERT_FALSE(invokeMap(handle, BSON("x" << 1))); +} + +TEST_F(WasmMozJSTest, InvokeFunctionNoThisBinding) { + initEngine(); + auto handle = createFunction("function() { return typeof this; }"); + ASSERT_NE(handle, 0u); + + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + BSONObj result = getReturnValueBson(); + ASSERT_EQ(result["__returnValue"].str(), "object"); +} + +// --------------------------------------------------------------------------- +// set-global-value tests +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, SetGlobalValueBoolean) { + initEngine(); + auto setFunc = getFunc("set-global-value"); + ASSERT_TRUE(setFunc.has_value()); + + BSONObj boolDoc = BSON("val" << true); + wc::Val arg0 = makeString("fullObject"); + wc::Val arg1(makeListU8(boolDoc)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFunc(*setFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1))); + ASSERT_TRUE(isResultOk(result)); + + // Verify: fullObject should be boolean true, not an object + auto handle = createFunction("function() { return fullObject === true; }"); + ASSERT_NE(handle, 0u); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + BSONObj ret = getReturnValueBson(); + ASSERT_TRUE(ret["__returnValue"].boolean()); +} + +TEST_F(WasmMozJSTest, SetGlobalValueNumber) { + initEngine(); + auto setFunc = getFunc("set-global-value"); + ASSERT_TRUE(setFunc.has_value()); + + BSONObj numDoc = BSON("val" << 3.14); + wc::Val arg0 = makeString("pi"); + wc::Val arg1(makeListU8(numDoc)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFunc(*setFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1))); + ASSERT_TRUE(isResultOk(result)); + + auto handle = createFunction("function() { return pi; }"); + ASSERT_NE(handle, 0u); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + BSONObj ret = getReturnValueBson(); + ASSERT_APPROX_EQUAL(ret["__returnValue"].numberDouble(), 3.14, 0.001); +} + +TEST_F(WasmMozJSTest, SetGlobalValueString) { + initEngine(); + auto setFunc = getFunc("set-global-value"); + ASSERT_TRUE(setFunc.has_value()); + + BSONObj strDoc = BSON("val" << "world"); + wc::Val arg0 = makeString("greeting"); + wc::Val arg1(makeListU8(strDoc)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFunc(*setFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1))); + ASSERT_TRUE(isResultOk(result)); + + auto handle = createFunction("function() { return greeting; }"); + ASSERT_NE(handle, 0u); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + BSONObj ret = getReturnValueBson(); + ASSERT_EQ(ret["__returnValue"].str(), "world"); +} + +TEST_F(WasmMozJSTest, SetGlobalValueCodeFunction) { + initEngine(); + auto setFunc = getFunc("set-global-value"); + ASSERT_TRUE(setFunc.has_value()); + + // Use BSONType::Code to set a callable function as a global value + BSONObjBuilder b; + b.appendCode("val", "function(x) { return x * 2; }"); + BSONObj codeDoc = b.obj(); + + wc::Val arg0 = makeString("doubler"); + wc::Val arg1(makeListU8(codeDoc)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFunc(*setFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1))); + ASSERT_TRUE(isResultOk(result)); + + // doubler should now be a callable function + auto handle = createFunction("function(n) { return doubler(n); }"); + ASSERT_NE(handle, 0u); + ASSERT_TRUE(invokeFunction(handle, BSON("0" << 21))); + BSONObj ret = getReturnValueBson(); + ASSERT_EQ(ret["__returnValue"].numberInt(), 42); +} + +TEST_F(WasmMozJSTest, SetGlobalValueObject) { + initEngine(); + auto setFunc = getFunc("set-global-value"); + ASSERT_TRUE(setFunc.has_value()); + + // Setting an object via set-global-value should set it as-is + BSONObj objDoc = BSON("val" << BSON("a" << 1 << "b" << 2)); + wc::Val arg0 = makeString("config"); + wc::Val arg1(makeListU8(objDoc)); + wc::Val result(wc::WitResult::ok(std::nullopt)); + ASSERT_TRUE(callFunc(*setFunc, ctx(), &result, 1, std::move(arg0), std::move(arg1))); + ASSERT_TRUE(isResultOk(result)); + + auto handle = createFunction("function() { return config.a + config.b; }"); + ASSERT_NE(handle, 0u); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + BSONObj ret = getReturnValueBson(); + ASSERT_EQ(ret["__returnValue"].numberInt(), 3); +} + +// --------------------------------------------------------------------------- +// `emit` in-WASM boundary +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, SetupEmitAndDrain) { + initEngine(); + ASSERT_TRUE(setupEmit()); + + auto handle = createFunction("function(doc) { emit(doc.key, doc.val); }"); + ASSERT_NE(handle, 0u); + ASSERT_TRUE(invokeFunction(handle, BSON("0" << BSON("key" << "a" << "val" << 1)))); + ASSERT_TRUE(invokeFunction(handle, BSON("0" << BSON("key" << "b" << "val" << 2)))); + + BSONObj emitDoc = drainEmitBuffer(); + auto emitsArr = emitDoc["emits"].Array(); + ASSERT_EQ(emitsArr.size(), 2u); + ASSERT_EQ(emitsArr[0].Obj()["k"].str(), "a"); + ASSERT_EQ(emitsArr[0].Obj()["v"].numberInt(), 1); + ASSERT_EQ(emitsArr[1].Obj()["k"].str(), "b"); + ASSERT_EQ(emitsArr[1].Obj()["v"].numberInt(), 2); +} + +TEST_F(WasmMozJSTest, EmitDrainClearsBetweenCalls) { + initEngine(); + ASSERT_TRUE(setupEmit()); + + auto handle = createFunction("function() { emit('x', 10); }"); + ASSERT_NE(handle, 0u); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + + BSONObj d1 = drainEmitBuffer(); + ASSERT_EQ(d1["emits"].Array().size(), 1u); + + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + + BSONObj d2 = drainEmitBuffer(); + ASSERT_EQ(d2["emits"].Array().size(), 2u); +} + +TEST_F(WasmMozJSTest, EmitDrainEmptyBuffer) { + initEngine(); + ASSERT_TRUE(setupEmit()); + + BSONObj doc = drainEmitBuffer(); + ASSERT_EQ(doc["emits"].Array().size(), 0u); + ASSERT_EQ(doc["bytesUsed"].numberLong(), 0); +} + +TEST_F(WasmMozJSTest, EmitWithUndefinedKey) { + initEngine(); + ASSERT_TRUE(setupEmit()); + + auto handle = createFunction("function() { emit(undefined, 42); }"); + ASSERT_NE(handle, 0u); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + + BSONObj doc = drainEmitBuffer(); + auto emits = doc["emits"].Array(); + ASSERT_EQ(emits.size(), 1u); + ASSERT_TRUE(emits[0].Obj()["k"].isNull()); + ASSERT_EQ(emits[0].Obj()["v"].numberInt(), 42); +} + +// --------------------------------------------------------------------------- +// OOM tests +// --------------------------------------------------------------------------- + +TEST_F(WasmMozJSTest, EmitByteLimitEnforced) { + initEngine(); + ASSERT_TRUE(setupEmitWithLimit(256)); + + auto handle = createFunction( + "function() {" + " for (var i = 0; i < 100; i++) {" + " emit('key_' + i, 'value_' + i);" + " }" + "}"); + ASSERT_NE(handle, 0u); + + auto err = invokeFunctionError(handle, BSONObj()); + ASSERT_EQ(err.code, "e-runtime"); + ASSERT_NE(err.msg.find("emit() exceeded memory limit"), std::string::npos); +} + +TEST_F(WasmMozJSTest, EmitByteLimitEnforcedInMapReduce) { + initEngine(); + // Each emitted {k: , v: } BSON doc is ~30 bytes. + // A 64-byte limit allows ~2 emits before the 3rd exceeds. + ASSERT_TRUE(setupEmitWithLimit(64)); + + auto handle = createFunction( + "function() {" + " emit(this.category, this.price);" + "}"); + ASSERT_NE(handle, 0u); + + ASSERT_TRUE(invokeMap(handle, BSON("category" << "A" << "price" << 10))); + ASSERT_TRUE(invokeMap(handle, BSON("category" << "B" << "price" << 20))); + + auto err = invokeMapError(handle, BSON("category" << "C" << "price" << 30)); + ASSERT_EQ(err.code, "e-runtime"); + ASSERT_NE(err.msg.find("emit() exceeded memory limit"), std::string::npos); +} + +TEST_F(WasmMozJSTest, EmitByteLimitResetOnSetupEmit) { + initEngine(); + ASSERT_TRUE(setupEmitWithLimit(128)); + + auto handle = createFunction("function() { emit('k', 'v'); }"); + ASSERT_NE(handle, 0u); + + // Emit a few times, approaching the limit. + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + + // Re-setup resets the buffer and byte counter. + ASSERT_TRUE(setupEmitWithLimit(128)); + + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + ASSERT_TRUE(invokeFunction(handle, BSONObj())); + + BSONObj doc = drainEmitBuffer(); + ASSERT_EQ(doc["emits"].Array().size(), 3u); +} + +TEST_F(WasmMozJSTest, JsHeapOOMFromLargeArrayAllocation) { + initEngine(); + + // Keep multiple large strings alive simultaneously. Doubling creates a + // series of strings: 1 MB, 2 MB, 4 MB ... all pushed into an array so GC + // cannot collect any of them. Total live ≈ 2^(n+1) - 1 MB. + auto handle = createFunction( + "function() {" + " var arr = [];" + " var s = new Array(1024 * 1024 + 1).join('a');" + " for (var i = 0; i < 10; i++) {" + " arr.push(s);" + " s = s + s;" + " }" + " return arr.length;" + "}"); + ASSERT_NE(handle, 0u); + + auto err = invokeFunctionError(handle, BSONObj()); + ASSERT_EQ(err.code, "e-runtime"); + bool hasOomMsg = err.msg.find("out of memory") != std::string::npos || + err.msg.find("allocation size overflow") != std::string::npos; + ASSERT_TRUE(hasOomMsg) << "msg: " << err.msg; +} + +TEST_F(WasmMozJSTest, JsHeapOOMFromStringConcatenation) { + initEngine(); + + auto handle = createFunction( + "function() {" + " var s = new Array(1024 * 1024 + 1).join('x');" + " for (var i = 0; i < 10; i++) {" + " s = s + s;" + " }" + " return s.length;" + "}"); + ASSERT_NE(handle, 0u); + + auto err = invokeFunctionError(handle, BSONObj()); + ASSERT_EQ(err.code, "e-runtime"); + bool hasOomMsg = err.msg.find("out of memory") != std::string::npos || + err.msg.find("allocation size overflow") != std::string::npos; + ASSERT_TRUE(hasOomMsg) << "msg: " << err.msg; +} + +TEST_F(WasmMozJSTest, JsHeapOomFromDeeplyNestedObject) { + initEngine(); + + // Build a chain of objects where each node holds an exponentially growing + // string, preventing GC from collecting anything in the chain. + auto handle = createFunction( + "function() {" + " var s = new Array(1024 * 1024 + 1).join('b');" + " var obj = {val: s};" + " for (var i = 0; i < 10; i++) {" + " s = s + s;" + " obj = {inner: obj, val: s};" + " }" + " return obj.val.length;" + "}"); + ASSERT_NE(handle, 0u); + + auto err = invokeFunctionError(handle, BSONObj()); + ASSERT_EQ(err.code, "e-runtime"); + bool hasOomMsg = err.msg.find("out of memory") != std::string::npos || + err.msg.find("allocation size overflow") != std::string::npos; + ASSERT_TRUE(hasOomMsg) << "msg: " << err.msg; +} + +TEST_F(WasmMozJSTest, MapReduceHighVolumeEmitHitsLimit) { + initEngine(); + + // Use a moderate byte limit (64 KB) and hammer it with many map invocations. + ASSERT_TRUE(setupEmitWithLimit(64 * 1024)); + + auto mapFn = createFunction( + "function() {" + " emit(this._id, this.payload);" + "}"); + ASSERT_NE(mapFn, 0u); + + int emitted = 0; + for (; emitted < 10000; emitted++) { + std::string payload(100, 'A' + (emitted % 26)); + if (!invokeMap(mapFn, BSON("_id" << emitted << "payload" << payload))) + break; + } + ASSERT_GT(emitted, 0); + ASSERT_LT(emitted, 10000); + + // The limit is still exceeded, so the next call also fails with the same error. + auto err = invokeMapError(mapFn, BSON("_id" << 99999 << "payload" << "x")); + ASSERT_EQ(err.code, "e-runtime"); + ASSERT_NE(err.msg.find("emit() exceeded memory limit"), std::string::npos); +} + +TEST_F(WasmMozJSTest, MapReduceEmitLargeValues) { + initEngine(); + ASSERT_TRUE(setupEmitWithLimit(32 * 1024)); + + // Each emit produces a large value (~10 KB). A few calls should exceed 32 KB. + auto mapFn = createFunction( + "function() {" + " var big = new Array(10001).join('z');" + " emit(this.key, big);" + "}"); + ASSERT_NE(mapFn, 0u); + + ASSERT_TRUE(invokeMap(mapFn, BSON("key" << 1))); + ASSERT_TRUE(invokeMap(mapFn, BSON("key" << 2))); + ASSERT_TRUE(invokeMap(mapFn, BSON("key" << 3))); + + auto err = invokeMapError(mapFn, BSON("key" << 4)); + ASSERT_EQ(err.code, "e-runtime"); + ASSERT_NE(err.msg.find("emit() exceeded memory limit"), std::string::npos); +} + +TEST_F(WasmMozJSTest, JsHeapOOMFromFunctionAllocatingManyObjects) { + initEngine(); + + // Exponential string doubling inside a function (same pattern as + // StringConcatenation but using a separate code path: invoke-function). + auto handle = createFunction( + "function() {" + " var s = new Array(1024 * 1024 + 1).join('z');" + " for (var i = 0; i < 10; i++) {" + " s = s + s;" + " }" + " return s.length;" + "}"); + ASSERT_NE(handle, 0u); + + auto err = invokeFunctionError(handle, BSONObj()); + ASSERT_EQ(err.code, "e-runtime"); + bool hasOomMsg = err.msg.find("out of memory") != std::string::npos || + err.msg.find("allocation size overflow") != std::string::npos; + ASSERT_TRUE(hasOomMsg) << "msg: " << err.msg; +} + +TEST_F(WasmMozJSTest, MapReduceOomRecoveryAfterDrain) { + initEngine(); + + // Use a small limit so we can observe recovery after drain. + ASSERT_TRUE(setupEmitWithLimit(512)); + + auto mapFn = createFunction( + "function() {" + " emit(this.key, this.value);" + "}"); + ASSERT_NE(mapFn, 0u); + + // Emit until limit is hit. + int emitted = 0; + while (invokeMap(mapFn, BSON("key" << emitted << "value" << "data"))) { + emitted++; + if (emitted > 100) + break; + } + ASSERT_LT(emitted, 100); + ASSERT_GT(emitted, 0); + + // Verify the failure is an emit-limit error. + auto err = invokeMapError(mapFn, BSON("key" << 999 << "value" << "x")); + ASSERT_EQ(err.code, "e-runtime"); + ASSERT_NE(err.msg.find("emit() exceeded memory limit"), std::string::npos); + + // Drain and re-setup resets the counter, allowing more emits. + drainEmitBuffer(); + ASSERT_TRUE(setupEmitWithLimit(512)); + + int emitted2 = 0; + while (invokeMap(mapFn, BSON("key" << emitted2 << "value" << "data"))) { + emitted2++; + if (emitted2 > 100) + break; + } + ASSERT_LT(emitted2, 100); + ASSERT_GT(emitted2, 0); + ASSERT_EQ(emitted, emitted2); +} + +} // namespace +} // namespace wasm +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/wasm/engine/error.cpp b/src/mongo/scripting/mozjs/wasm/engine/error.cpp new file mode 100644 index 00000000000..9da73d6ba92 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/error.cpp @@ -0,0 +1,104 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "error.h" + +#include "js/ErrorReport.h" +#include "js/Exception.h" + +namespace mongo { +namespace mozjs { +namespace wasm { + +void ExecutionCheck::capture(err_code_t fallback) { + if (!_out) + return; + + clear_error(_out); + _out->code = fallback; + + if (!JS_IsExceptionPending(_cx)) + return; + + _out->code = SM_E_PENDING_EXCEPTION; + + JS::ExceptionStack exnStack(_cx); + if (!JS::StealPendingExceptionStack(_cx, &exnStack)) { + JS_ClearPendingException(_cx); + return; + } + + JS::ErrorReportBuilder report(_cx); + if (!report.init(_cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { + JS_ClearPendingException(_cx); + return; + } + + JSErrorReport* r = report.report(); + if (!r) + return; + + if (r->message()) + set_string(&_out->msg, &_out->msg_len, r->message().c_str()); + if (r->filename) + set_string(&_out->filename, &_out->filename_len, r->filename.c_str()); + _out->line = r->lineno; + _out->column = r->column.oneOriginValue(); +} + +} // namespace wasm + +JSString* mongoErrorReportToString(JSContext* cx, JSErrorReport* reportp) { + if (!reportp) { + return JS_NewStringCopyZ(cx, "Unknown error"); + } + + std::string msg; + constexpr size_t kErrorMsgReserveSize = 256; + msg.reserve(kErrorMsgReserveSize); + if (reportp->message()) { + msg.append(reportp->message().c_str()); + } else { + msg.append("JavaScript error"); + } + + if (reportp->filename) { + msg.append(" at "); + msg.append(reportp->filename.c_str()); + if (reportp->lineno > 0) { + msg.push_back(':'); + msg.append(std::to_string(reportp->lineno)); + } + } + + return JS_NewStringCopyZ(cx, msg.c_str()); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/wasm/engine/error.h b/src/mongo/scripting/mozjs/wasm/engine/error.h new file mode 100644 index 00000000000..23f08a28f29 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/error.h @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/scripting/mozjs/shared/mozjs_error_types.h" + +#include +#include + +#include "jsapi.h" +#include "utils.h" + + +namespace mongo { +namespace mozjs { +namespace wasm { + +inline void clear_error(wasm_mozjs_error_t* e) { + if (!e) + return; + + if (e->msg) { + cabi_realloc(e->msg, e->msg_len + 1, 1, 0); + e->msg = nullptr; + e->msg_len = 0; + } + if (e->filename) { + cabi_realloc(e->filename, e->filename_len + 1, 1, 0); + e->filename = nullptr; + e->filename_len = 0; + } + if (e->stack) { + cabi_realloc(e->stack, e->stack_len + 1, 1, 0); + e->stack = nullptr; + e->stack_len = 0; + } + + e->code = SM_OK; + e->line = 0; + e->column = 0; +} + +// Execution check helper for error handling +class ExecutionCheck { +public: + ExecutionCheck(JSContext* cx, wasm_mozjs_error_t* out_err) : _cx(cx), _out(out_err) { + clear_error(out_err); + } + + bool ok(bool success, err_code_t onFail = SM_E_JSAPI_FAIL) { + if (success) + return true; + capture(onFail); + return false; + } + + template + T* okPtr(T* p, err_code_t onFail = SM_E_JSAPI_FAIL) { + if (p) + return p; + capture(onFail); + return nullptr; + } + +private: + void capture(err_code_t fallback); + + JSContext* _cx; + wasm_mozjs_error_t* _out; +}; + + +} // namespace wasm + +// WASI-compatible error report to string (declaration). +// Implementation in error.cpp. +JSString* mongoErrorReportToString(JSContext* cx, JSErrorReport* reportp); + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/wasm/engine/exception_stubs.cpp b/src/mongo/scripting/mozjs/wasm/engine/exception_stubs.cpp new file mode 100644 index 00000000000..1795b6942ba --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/exception_stubs.cpp @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include +#include +#include + +// Boost source_location stub +namespace boost { +struct source_location { + const char* file_name() const { + return ""; + } + const char* function_name() const { + return ""; + } + unsigned int line() const { + return 0; + } + unsigned int column() const { + return 0; + } +}; +} // namespace boost + +extern "C" { + +// Exception allocation +void* __cxa_allocate_exception(size_t thrown_size) { + (void)thrown_size; + std::fprintf(stderr, + "FATAL: __cxa_allocate_exception called - exceptions not supported in WASM\n"); + __builtin_trap(); + return nullptr; +} + +void __cxa_free_exception(void* thrown_exception) { + (void)thrown_exception; + std::fprintf(stderr, "FATAL: __cxa_free_exception called - exceptions not supported in WASM\n"); + __builtin_trap(); +} + +// Throwing +void __cxa_throw(void* thrown_exception, void* tinfo, void (*dest)(void*)) { + (void)thrown_exception; + (void)tinfo; + (void)dest; + std::fprintf(stderr, "FATAL: __cxa_throw called - exceptions not supported in WASM\n"); + __builtin_trap(); +} + +void __cxa_rethrow(void) { + std::fprintf(stderr, "FATAL: __cxa_rethrow called - exceptions not supported in WASM\n"); + __builtin_trap(); +} + +// Catching +void* __cxa_begin_catch(void* exception_object) { + (void)exception_object; + std::fprintf(stderr, "FATAL: __cxa_begin_catch called - exceptions not supported in WASM\n"); + __builtin_trap(); + return nullptr; +} + +void __cxa_end_catch(void) { + std::fprintf(stderr, "FATAL: __cxa_end_catch called - exceptions not supported in WASM\n"); + __builtin_trap(); +} + +// WASM-specific exception handling +struct __wasm_lpad_context_t { + int selector; + void* exception; +}; +struct __wasm_lpad_context_t __wasm_lpad_context = {0, nullptr}; + +int _Unwind_CallPersonality(void* exception_object) { + (void)exception_object; + std::fprintf(stderr, + "FATAL: _Unwind_CallPersonality called - exceptions not supported in WASM\n"); + __builtin_trap(); + return 0; +} + +// Additional exception-related functions +void* __cxa_get_exception_ptr(void* exception_object) { + (void)exception_object; + return nullptr; +} + +void __cxa_pure_virtual(void) { + std::fprintf(stderr, "FATAL: pure virtual function called\n"); + __builtin_trap(); +} + +void __cxa_deleted_virtual(void) { + std::fprintf(stderr, "FATAL: deleted virtual function called\n"); + __builtin_trap(); +} + +} // extern "C" + +// Boost exception stubs +namespace boost { + +void throw_exception(std::exception const& e) { + std::fprintf(stderr, "FATAL: boost::throw_exception called: %s\n", e.what()); + __builtin_trap(); +} + +void throw_exception(std::exception const& e, boost::source_location const& loc) { + std::fprintf(stderr, + "FATAL: boost::throw_exception called at %s:%u: %s\n", + loc.file_name(), + loc.line(), + e.what()); + __builtin_trap(); +} + +} // namespace boost diff --git a/src/mongo/scripting/mozjs/wasm/engine/linkset.bzl b/src/mongo/scripting/mozjs/wasm/engine/linkset.bzl new file mode 100644 index 00000000000..5ced72bed05 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/linkset.bzl @@ -0,0 +1,111 @@ +"""Expose C++ link information (objects/libs/flags) via response files for genrules.""" + +def _as_list(x): + if x == None: + return [] + t = type(x) + if t == "depset": + return x.to_list() + if t == "list" or t == "tuple": + return x + + # File or string or other singletons + return [x] + +def _collect_files_from_library(lib): + """Return (objects, libs) from a LibraryToLink in a Bazel-version-tolerant way.""" + objs = [] + + # Object files (may be depsets) + if hasattr(lib, "objects"): + objs += _as_list(lib.objects) + if hasattr(lib, "pic_objects"): + objs += _as_list(lib.pic_objects) + + libs = [] + + # Prefer static archives; include others if present + for attr in ( + "static_library", + "pic_static_library", + "dynamic_library", + "interface_library", + ): + if hasattr(lib, attr): + val = getattr(lib, attr) + libs += _as_list(val) + return objs, libs + +def _cc_linkset_impl(ctx): + linking_ctxs = [d[CcInfo].linking_context for d in ctx.attr.deps] + merged = cc_common.merge_linking_contexts(linking_contexts = linking_ctxs) + + objs_ordered = [] + libs_ordered = [] + flags_ordered = [] + + # Iterate in Bazel's link order + linker_inputs_list = merged.linker_inputs.to_list() + + # Collect from all deps - include both direct and transitive + for li in linker_inputs_list: + if hasattr(li, "user_link_flags"): + flags_ordered += list(li.user_link_flags) + for lib in li.libraries: + o, libs = _collect_files_from_library(lib) + + # Collect all objects and libraries + objs_ordered += o + libs_ordered += libs + + noncode = [] + if hasattr(merged, "non_code_inputs"): + noncode = merged.non_code_inputs.to_list() + + # Emit response files + objs_rsp = ctx.actions.declare_file(ctx.label.name + ".objects.rsp") + libs_rsp = ctx.actions.declare_file(ctx.label.name + ".libs.rsp") + flags_rsp = ctx.actions.declare_file(ctx.label.name + ".flags.rsp") + + def _write_list(out, items): + lines = [] + for it in items: + if hasattr(it, "path"): + lines.append(it.path) + else: + lines.append(str(it)) + content = "\n".join(lines) + if content: + content += "\n" + ctx.actions.write(out, content) + + _write_list(objs_rsp, objs_ordered) + _write_list(libs_rsp, libs_ordered + noncode) + _write_list(flags_rsp, flags_ordered + ctx.attr.extra_flags) + + # Expose everything so genrules can use $(locations :target) + all_files = depset( + direct = [objs_rsp, libs_rsp, flags_rsp] + objs_ordered + libs_ordered + noncode, + ) + + return [ + DefaultInfo(files = all_files), + OutputGroupInfo( + rsp = depset([objs_rsp, libs_rsp, flags_rsp]), + objects = depset(objs_ordered), + libs = depset(libs_ordered), + ), + ] + +cc_linkset = rule( + implementation = _cc_linkset_impl, + attrs = { + "deps": attr.label_list(providers = [CcInfo]), + "extra_flags": attr.string_list(default = []), + "_cc_toolchain": attr.label( + default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), + ), + }, + toolchains = ["@bazel_tools//tools/cpp:toolchain_type"], + fragments = ["cpp"], +) diff --git a/src/mongo/scripting/mozjs/wasm/engine/utils.h b/src/mongo/scripting/mozjs/wasm/engine/utils.h new file mode 100644 index 00000000000..d5b866a7f48 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/engine/utils.h @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include +#include + +extern "C" void* cabi_realloc(void* ptr, size_t old_size, size_t align, size_t new_size); + +namespace mongo { +namespace mozjs { +namespace wasm { + +/** + * Set a string field, allocating memory via cabi_realloc. + * If the field already has a value, it will be freed first. + */ +inline void set_string(char** dst, size_t* dst_len, const char* src) { + if (!dst || !dst_len) + return; + + // Free existing string + if (*dst) { + cabi_realloc(*dst, *dst_len + 1, 1, 0); + *dst = nullptr; + *dst_len = 0; + } + + if (!src || src[0] == '\0') { + return; + } + + size_t src_len = std::strlen(src); + + char* allocated = static_cast(cabi_realloc(nullptr, 0, 1, src_len + 1)); + if (!allocated) { + return; + } + + std::memcpy(allocated, src, src_len); + allocated[src_len] = '\0'; + + *dst = allocated; + *dst_len = src_len; +} + +/** + * Copy a C string into a fixed-size output buffer (always null-terminates). + */ +inline void set_cstr(char* dst, size_t cap, const char* src) { + if (!dst || cap == 0) + return; + if (!src) { + dst[0] = '\0'; + return; + } + std::snprintf(dst, cap, "%s", src); +} + +} // namespace wasm +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/wasm/helpers.cpp b/src/mongo/scripting/mozjs/wasm/helpers.cpp new file mode 100644 index 00000000000..a2b3cba87fa --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/helpers.cpp @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/scripting/mozjs/common/freeOpToJSContext.h" + +#include "gc/GCContext.h" +#include "vm/JSContext.h" +#include "vm/Runtime.h" + +namespace mongo { +namespace mozjs { + +JSContext* freeOpToJSContext(JS::GCContext* gcCtx) { + return gcCtx->runtime()->mainContextFromOwnThread(); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/wasm/scripts/README.md b/src/mongo/scripting/mozjs/wasm/scripts/README.md new file mode 100644 index 00000000000..9882cb28967 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/scripts/README.md @@ -0,0 +1,126 @@ +# WASM Build Scripts + +These scripts implement the WASM build steps that Bazel runs via genrules. You can run them **from Bazel** (as usual) or **standalone** for debugging or CI. All behaviour is controlled by environment variables; each script's header lists every variable and a short example. + +--- + +## Run order + +To build **mozjs_wasm_api.wasm** (the main WASM API module) outside Bazel, run scripts in this order: + +| Step | Script | Produces | +| ---- | ------------------------------ | ------------------------------------- | +| 1 | `build_spidermonkey_wasip2.sh` | SpiderMonkey tarball (libs + headers) | +| 2 | `extract_rust_shims.sh` | `rust_shims.a` (from that tarball) | +| 3 | `compile_mozjs_wasm_api.sh` | `mozjs_wasm_api.wasm` | + +Step 3 needs the tarball, `rust_shims.a`, the MongoDB base **linkset** response files (`.libs.rsp` etc.), and all MozJS/WIT sources. The linkset files normally come from building `//src/mongo/scripting/mozjs/wasm:mongo_base_linkset` in Bazel; for a fully standalone build you'd need to copy those RSP files out of the Bazel output tree. + +**Optional:** `link_mongo_base_wasm.sh` builds **mongo_base.wasm** (MongoDB base as a single WASM). It only needs the same linkset RSP files and the WASI SDK; it does not depend on SpiderMonkey. Run it whenever you have the linkset and want `mongo_base.wasm`. + +**Helper (not run directly):** `compile_wasi_source.sh` — compiles one C/C++ file at a time; called by `compile_mozjs_wasm_api.sh` via `xargs`. + +--- + +## Invocation examples + +Assume you are in the **MongoDB repo root** and have a WASI SDK at `$WASI_SDK` (e.g. `/opt/wasi-sdk` or `$HOME/wasi-sdk-24.0`). Paths below are relative to the repo root unless noted. + +### 1. Build SpiderMonkey (WASI Preview 2) + +Produces a tarball with `libjs_static.a`, headers, and Rust shims. Requires SpiderMonkey source (e.g. from Bazel's external repo or a local gecko checkout) and the Rust shim sources under `support/rust_shims/`. + +```bash +cd src/mongo/scripting/mozjs/wasm + +export WASI_SDK_PATH=/opt/wasi-sdk +export SPIDER_MACH_PATH=/path/to/gecko-dev/mach # or path from Bazel execroot to mach +export RUST_SHIMS_LIB_RS=support/rust_shims/src/lib.rs +export CARGO_TEMPLATE_PATH=support/rust_shims/Cargo.toml.template +export OUTPUT=out/spidermonkey-wasip2-release.tar.gz + +mkdir -p out +bash scripts/build_spidermonkey_wasip2.sh +``` + +### 2. Extract Rust shims from the tarball + +```bash +cd src/mongo/scripting/mozjs/wasm + +export TARBALL=out/spidermonkey-wasip2-release.tar.gz +export OUTPUT=out/rust_shims.a + +bash scripts/extract_rust_shims.sh +``` + +### 3. Build mongo_base.wasm (optional) + +You need the three linkset response files (e.g. from a Bazel build of `:mongo_base_linkset`). If they live in `bazel-bin/.../mongo_base_linkset/` as `*.objects.rsp`, `*.libs.rsp`, `*.flags.rsp`: + +```bash +cd src/mongo/scripting/mozjs/wasm + +export CXX=/opt/wasi-sdk/bin/wasm32-wasi-clang++ +export OBJS_RSP=/path/to/xxx.objects.rsp +export LIBS_RSP=/path/to/xxx.libs.rsp +export FLAGS_RSP=/path/to/xxx.flags.rsp +export OUTPUT=out/mongo_base.wasm + +bash scripts/link_mongo_base_wasm.sh +``` + +Or pass the directory and let the script find the RSPs: + +```bash +export LINKSET_FILES="/path/to/foo.objects.rsp /path/to/foo.libs.rsp /path/to/foo.flags.rsp" +export OUTPUT=out/mongo_base.wasm +bash scripts/link_mongo_base_wasm.sh +``` + +### 4. Build mozjs_wasm_api.wasm + +Requires the SpiderMonkey tarball, `rust_shims.a`, the linkset files, all MozJS/WIT sources, and the WIT component-type object. Easiest is to run from the package dir and point at Bazel's outputs for linkset and WIT object; fill in the source lists from the genrule in `BUILD.bazel` if needed. + +```bash +cd src/mongo/scripting/mozjs/wasm + +export WASI_SDK_PATH=/opt/wasi-sdk +export CXX=$WASI_SDK_PATH/bin/wasm32-wasip2-clang++ +export CC=$WASI_SDK_PATH/bin/wasm32-wasip2-clang + +export SM_TARBALL=out/spidermonkey-wasip2-release.tar.gz +export RUST_SHIMS_PATH=out/rust_shims.a +export WIT_COMPONENT_TYPE_OBJ=wit_gen/generated/api_component_type.o # or path in bazel-bin + +# Paths to linkset RSP files (e.g. from bazel build of :mongo_base_linkset) +export LINKSET_FILES="/path/to/xxx.objects.rsp /path/to/xxx.libs.rsp /path/to/xxx.flags.rsp" + +# Header parents for error_codes and mongo config (e.g. bazel-bin dirs) +export ERROR_CODES_HEADER_FILES=/path/to/error_codes +export CONFIG_HEADER_FILES=/path/to/mongo_config + +# Source file lists (space-separated); see BUILD.bazel genrule "mozjs_wasm_api_genrule" for full list +export WASM_SOURCES="engine/engine.cpp engine/error.cpp helpers.cpp ..." +export COMMON_WASI_SOURCES="/path/to/common/wasi_sources..." +export EXCEPTION_STUBS_SRC=engine/exception_stubs.cpp +export MOZJS_API_SRC=engine/api.cpp +export WIT_API_C=wit_gen/generated/api.c + +export OUTPUT=out/mozjs_wasm_api.wasm +bash scripts/compile_mozjs_wasm_api.sh +``` + +For the exact source lists and paths, run the corresponding genrule once and inspect the `cmd` in `BUILD.bazel`, or run the build via Bazel and use the script only for repros. + +--- + +## What each script does + +| Script | Purpose | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **build_spidermonkey_wasip2.sh** | Builds SpiderMonkey for WASI Preview 2 (mozconfig + `mach build`), builds the Rust encoding shims, and packs static libs, headers, and extra objects into a tarball. | +| **extract_rust_shims.sh** | Unpacks `libmongo_wasip2_rust_shims.a` from the SpiderMonkey tarball into a single `.a` file. | +| **compile_mozjs_wasm_api.sh** | Unpacks the SM tarball, compiles all MozJS wrapper and WIT glue sources in parallel, gathers MongoDB base libs from the linkset, and links `mozjs_wasm_api.wasm`. | +| **link_mongo_base_wasm.sh** | Links the MongoDB base library into a single `mongo_base.wasm` using the WASI linkset RSP files and `wasm32-wasi-clang++`. | +| **compile_wasi_source.sh** | Compiles one C or C++ file for WASI. Used by `compile_mozjs_wasm_api.sh` via `xargs`; not intended to be run directly. | diff --git a/src/mongo/scripting/mozjs/wasm/scripts/build_spidermonkey_wasip2.sh b/src/mongo/scripting/mozjs/wasm/scripts/build_spidermonkey_wasip2.sh new file mode 100755 index 00000000000..e8efdfe1eb5 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/scripts/build_spidermonkey_wasip2.sh @@ -0,0 +1,286 @@ +#!/bin/bash +# Builds SpiderMonkey for WASI Preview 2, producing a tarball with: +# - libjs_static.a (the JS engine) +# - headers (public + internal, .h/.hpp only) +# - libjsrust.a (if available) +# - libmongo_wasip2_rust_shims.a (encoding shims) +# - obj-extra/ (support objects outside libjs_static.a) +# +# Required environment variables: +# OUTPUT - Output tarball path (.tar.gz) +# SPIDER_MACH_PATH - Path to SpiderMonkey's mach file +# RUST_SHIMS_LIB_RS - Path to support/rust_shims/src/lib.rs +# CARGO_TEMPLATE_PATH - Path to support/rust_shims/Cargo.toml.template +# +# Compiler discovery (set one of): +# WASI_SDK_BIN_FILES - Space-separated wasi_sdk bin files (auto-discovers clang) +# WASI_SDK_PATH - Direct path to WASI SDK root +# +# Example standalone usage: +# SPIDER_MACH_PATH=/path/to/gecko-dev/mach \ +# RUST_SHIMS_LIB_RS=support/rust_shims/src/lib.rs \ +# CARGO_TEMPLATE_PATH=support/rust_shims/Cargo.toml.template \ +# WASI_SDK_PATH=/opt/wasi-sdk \ +# OUTPUT=spidermonkey-wasip2-release.tar.gz \ +# bash scripts/build_spidermonkey_wasip2.sh + +set -euo pipefail +# Enable verbose tracing only when VERBOSE is set (reduces CI log noise). +[[ -n "${VERBOSE:-}" ]] && set -x + +EXECROOT="${EXECROOT:-$(pwd -P)}" + +# Resolve output to absolute path. +case "$OUTPUT" in +/*) OUT_TAR_ABS="$OUTPUT" ;; +*) OUT_TAR_ABS="$EXECROOT/$OUTPUT" ;; +esac +OUT_DIR_ABS="$(dirname "$OUT_TAR_ABS")" + +PKG_DIR="$OUT_DIR_ABS/pkg" +WORK_DIR="$OUT_DIR_ABS/work" +rm -rf "$PKG_DIR" +mkdir -p "$PKG_DIR/lib" "$WORK_DIR" + +# Keep a real HOME so rustup doesn't trip over Bazel's sandbox home. +REAL_HOME="$(getent passwd "$(id -u)" | cut -d: -f6 || true)" +if [ -z "$REAL_HOME" ]; then + if [ -n "${HOME:-}" ]; then + REAL_HOME="$HOME" + else + echo "ERROR: Could not determine HOME directory; set HOME or ensure getent is available." >&2 + exit 1 + fi +fi +export HOME="$REAL_HOME" +export MOZ_FETCHES_DIR="$HOME/.mozbuild" + +# --- Locate WASI SDK --- +if [ -z "${WASI_SDK_PATH:-}" ]; then + WASI_CLANG="" + for f in ${WASI_SDK_BIN_FILES:-}; do + case "$f" in */clang) + WASI_CLANG="$f" + break + ;; + esac + done + if [ -z "$WASI_CLANG" ]; then + echo "ERROR: clang not found. Set WASI_SDK_PATH or WASI_SDK_BIN_FILES." >&2 + exit 1 + fi + WASI_SDK_PATH="$(cd "$(dirname "$WASI_CLANG")/.." && pwd)" +fi + +# --- Copy SpiderMonkey source into a writable directory --- +SPIDER_ROOT="$(cd "$(dirname "$SPIDER_MACH_PATH")" && pwd)" +SRC_ROOT="$WORK_DIR/spidermonkey-src" +rm -rf "$SRC_ROOT" +mkdir -p "$SRC_ROOT" +cp -a "$SPIDER_ROOT/." "$SRC_ROOT/" +SRC_ROOT_REAL="$(cd "$SRC_ROOT" && pwd -P)" + +MOZCONFIG="$SRC_ROOT_REAL/mozconfig-release" +OBJDIR="$WORK_DIR/obj-release-wasip2" +mkdir -p "$OBJDIR" +OBJDIR_REAL="$(cd "$OBJDIR" && pwd -P)" + +cat >"$MOZCONFIG" <<'EOF' +ac_add_options --enable-project=js +ac_add_options --disable-js-shell +ac_add_options --target=wasm32-unknown-wasip2 +ac_add_options --without-system-zlib +ac_add_options --without-intl-api +ac_add_options --disable-jit +ac_add_options --disable-shared-js +ac_add_options --disable-shared-memory +ac_add_options --disable-tests +ac_add_options --disable-clang-plugin +ac_add_options --enable-optimize=-Oz +ac_add_options --enable-portable-baseline-interp +mk_add_options AUTOCLOBBER=1 +EOF + +echo "mk_add_options MOZ_OBJDIR=$OBJDIR_REAL" >>"$MOZCONFIG" +echo "ac_add_options --prefix=$OBJDIR_REAL/dist" >>"$MOZCONFIG" +echo "ac_add_options --disable-stdcxx-compat" >>"$MOZCONFIG" +echo "ac_add_options --disable-debug" >>"$MOZCONFIG" +echo "ac_add_options --disable-debug-symbols" >>"$MOZCONFIG" +echo "ac_add_options --with-sysroot=$WASI_SDK_PATH/share/wasi-sysroot" >>"$MOZCONFIG" + +# Use WASI Preview2 compiler wrappers. +export WASI_SDK_PATH="$WASI_SDK_PATH" +export CC="$WASI_SDK_PATH/bin/wasm32-wasip2-clang" +export CXX="$WASI_SDK_PATH/bin/wasm32-wasip2-clang++" +export AR="$WASI_SDK_PATH/bin/llvm-ar" +export RANLIB="$WASI_SDK_PATH/bin/llvm-ranlib" +export HOST_CC="$(command -v clang)" +export HOST_CXX="$(command -v clang++)" + +# Enable per-function/data sections so the linker's --gc-sections can remove +# unused code. Also hide internal symbols to aid dead-code elimination. +WASM_SIZE_FLAGS="-ffunction-sections -fdata-sections -fvisibility=hidden" +export CFLAGS="${CFLAGS:-} $WASM_SIZE_FLAGS" +export CXXFLAGS="${CXXFLAGS:-} $WASM_SIZE_FLAGS" + +# Let cargo/rustc know what target to build for when SpiderMonkey builds Rust support code. +export RUST_TARGET=wasm32-wasip2 + +# Configure expects a Rust stdlib for wasm32-wasip2 to be installed. +# This is a local (non-hermetic) build step by design (tags = local/no-sandbox). +if [ -x "$HOME/.cargo/bin/rustup" ]; then + "$HOME/.cargo/bin/rustup" target add wasm32-wasip2 || true +elif command -v rustup >/dev/null 2>&1; then + rustup target add wasm32-wasip2 || true +fi + +export MOZCONFIG="$MOZCONFIG" +cd "$SRC_ROOT_REAL" +python3 "./mach" -v --no-interactive build + +# Build Rust support library if present/required (best-effort). +python3 "./mach" -v --no-interactive build js/src/rust || true + +LIBJS_A="$OBJDIR_REAL/js/src/build/libjs_static.a" +test -s "$LIBJS_A" +cp "$LIBJS_A" "$PKG_DIR/lib/libjs_static.a" + +# Headers for embedders (needed to compile our wrapper). +if [ -d "$OBJDIR_REAL/dist/include" ]; then + cp -Lr "$OBJDIR_REAL/dist/include" "$PKG_DIR/include" + # Generated defines header is used by some includes. + if [ -f "$OBJDIR_REAL/js/src/js-confdefs.h" ]; then + cp "$OBJDIR_REAL/js/src/js-confdefs.h" "$PKG_DIR/include/js-confdefs.h" + fi +fi + +# Internal SpiderMonkey headers (js/src) for gc/GCContext.h, vm/Runtime.h etc. +# Copy source headers first, then overlay generated headers from objdir. +if [ -d "$SRC_ROOT_REAL/js/src" ]; then + echo "Copying js/src internal headers (*.h, *.hpp only) from source..." >&2 + mkdir -p "$PKG_DIR/include/src" + # Copy only header files -- not .cpp, tests, or build configs which bloat + # the tarball by hundreds of megabytes. + (cd "$SRC_ROOT_REAL/js/src" && find . \( -name '*.h' -o -name '*.hpp' \) -type f) | while IFS= read -r hdr; do + mkdir -p "$PKG_DIR/include/src/$(dirname "$hdr")" + cp "$SRC_ROOT_REAL/js/src/$hdr" "$PKG_DIR/include/src/$hdr" + done +fi +if [ -d "$SRC_ROOT_REAL/mfbt" ]; then + echo "Copying mfbt headers (*.h, *.hpp only) from source..." >&2 + mkdir -p "$PKG_DIR/include/mfbt" + (cd "$SRC_ROOT_REAL/mfbt" && find . \( -name '*.h' -o -name '*.hpp' \) -type f) | while IFS= read -r hdr; do + mkdir -p "$PKG_DIR/include/mfbt/$(dirname "$hdr")" + cp "$SRC_ROOT_REAL/mfbt/$hdr" "$PKG_DIR/include/mfbt/$hdr" + done +fi +# Overlay generated .h files from objdir (e.g., wasm/WasmBuiltinModuleGenerated.h) +if [ -d "$OBJDIR_REAL/js/src" ]; then + echo "Overlaying generated headers from objdir..." >&2 + find "$OBJDIR_REAL/js/src" -name "*.h" -type f 2>/dev/null | while read f; do + rel="${f#$OBJDIR_REAL/js/src/}" + mkdir -p "$PKG_DIR/include/src/$(dirname "$rel")" + cp "$f" "$PKG_DIR/include/src/$rel" 2>/dev/null || true + done +fi + +# If a Rust static lib exists, include it (may satisfy encoding_* symbols). +# Name can vary by toolchain/version, so accept libjsrust*.a. +RUST_LIB="$(find "$OBJDIR_REAL" -type f -name 'libjsrust*.a' | head -n1 || true)" +if [ -n "$RUST_LIB" ] && [ -f "$RUST_LIB" ]; then + cp "$RUST_LIB" "$PKG_DIR/lib/libjsrust.a" +fi + +# Build a single Rust staticlib that bundles the encoding shims + Rust +# runtime, so the final wasm can be fully linked without `env::...` +# imports. +CARGO="$(command -v cargo || true)" +if [ -z "$CARGO" ] && [ -x "$HOME/.cargo/bin/cargo" ]; then + CARGO="$HOME/.cargo/bin/cargo" +fi +if [ -z "$CARGO" ]; then + echo "ERROR: cargo not found; cannot build Rust encoding staticlib" >&2 + exit 1 +fi + +RUST_SHIMS_DIR="$WORK_DIR/rust-shims" +rm -rf "$RUST_SHIMS_DIR" +mkdir -p "$RUST_SHIMS_DIR/src" + +# Copy lib.rs from support files. +# Path may be relative to EXECROOT or absolute. +case "$RUST_SHIMS_LIB_RS" in +/*) cp "$RUST_SHIMS_LIB_RS" "$RUST_SHIMS_DIR/src/lib.rs" ;; +*) cp "$EXECROOT/$RUST_SHIMS_LIB_RS" "$RUST_SHIMS_DIR/src/lib.rs" ;; +esac + +# Generate Cargo.toml from template with SpiderMonkey root path. +case "$CARGO_TEMPLATE_PATH" in +/*) sed "s|{{SPIDERMONKEY_ROOT}}|$SRC_ROOT_REAL|g" "$CARGO_TEMPLATE_PATH" >"$RUST_SHIMS_DIR/Cargo.toml" ;; +*) sed "s|{{SPIDERMONKEY_ROOT}}|$SRC_ROOT_REAL|g" "$EXECROOT/$CARGO_TEMPLATE_PATH" >"$RUST_SHIMS_DIR/Cargo.toml" ;; +esac + +export CARGO_TARGET_DIR="$OBJDIR_REAL/cargo-target" +(cd "$RUST_SHIMS_DIR" && "$CARGO" build --release --target wasm32-wasip2) +SHIMS_A="$CARGO_TARGET_DIR/wasm32-wasip2/release/libmongo_wasip2_rust_shims.a" +if [ ! -f "$SHIMS_A" ]; then + echo "ERROR: rust shims archive not found: $SHIMS_A" >&2 + exit 1 +fi +cp "$SHIMS_A" "$PKG_DIR/lib/libmongo_wasip2_rust_shims.a" + +# Include additional build outputs (outside libjs_static.a) needed to link. +# The downstream linker uses libjs_static.a directly for SpiderMonkey objects, +# so we do NOT extract individual .o files from the archive (which would +# roughly double the tarball size). Only the "extra" objects that live outside +# the archive are needed here. +# +# Paths are relative to MOZ_OBJDIR (OBJDIR_REAL). +EXTRA_ROOT="$PKG_DIR/obj-extra" +for rel in \ + memory/build/Unified_cpp_memory_build0.o \ + memory/mozalloc/Unified_cpp_memory_mozalloc0.o \ + mozglue/misc/AutoProfilerLabel.o \ + mozglue/misc/ConditionVariable_noop.o \ + mozglue/misc/MmapFaultHandler.o \ + mozglue/misc/Mutex_noop.o \ + mozglue/misc/Now.o \ + mozglue/misc/Printf.o \ + mozglue/misc/StackWalk.o \ + mozglue/misc/TimeStamp.o \ + mozglue/misc/TimeStamp_posix.o \ + mozglue/misc/Uptime.o \ + mozglue/misc/Decimal.o \ + mozglue/misc/SIMD.o \ + mfbt/lz4.o \ + mfbt/lz4frame.o \ + mfbt/lz4hc.o \ + mfbt/xxhash.o \ + mfbt/Unified_cpp_mfbt0.o \ + mfbt/Unified_cpp_mfbt1.o; do + if [ -f "$OBJDIR_REAL/$rel" ]; then + mkdir -p "$EXTRA_ROOT/$(dirname "$rel")" + cp "$OBJDIR_REAL/$rel" "$EXTRA_ROOT/$rel" + else + echo "WARN: extra object missing (skipping): $OBJDIR_REAL/$rel" >&2 + fi +done + +# Strip debug info from archives and objects to further reduce tarball size. +# Even with --disable-debug-symbols the toolchain may leave some sections. +STRIP="${WASI_SDK_PATH}/bin/llvm-strip" +if [ -x "$STRIP" ]; then + echo "Stripping debug info from libraries and objects..." >&2 + for lib in "$PKG_DIR"/lib/*.a; do + [ -f "$lib" ] && "$STRIP" --strip-debug "$lib" 2>/dev/null || true + done + find "$PKG_DIR/obj-extra" -name '*.o' -type f 2>/dev/null | while IFS= read -r obj; do + "$STRIP" --strip-debug "$obj" 2>/dev/null || true + done +else + echo "WARN: llvm-strip not found at $STRIP; skipping strip step" >&2 +fi + +mkdir -p "$(dirname "$OUT_TAR_ABS")" +tar -C "$PKG_DIR" -czf "$OUT_TAR_ABS" . +test -s "$OUT_TAR_ABS" diff --git a/src/mongo/scripting/mozjs/wasm/scripts/compile_mozjs_wasm_api.sh b/src/mongo/scripting/mozjs/wasm/scripts/compile_mozjs_wasm_api.sh new file mode 100755 index 00000000000..d0e7cd0f95f --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/scripts/compile_mozjs_wasm_api.sh @@ -0,0 +1,268 @@ +#!/bin/bash +# Compiles and links the MozJS WASM API module (mozjs_wasm_api.wasm). +# +# This script unpacks a SpiderMonkey tarball, compiles MozJS wrapper sources +# against it, collects MongoDB base libraries, and links everything into a +# single WASI Preview 2 WASM component. +# +# Required environment variables: +# OUTPUT - Output .wasm file path +# SM_TARBALL - Path to spidermonkey-wasip2-release.tar.gz +# RUST_SHIMS_PATH - Path to extracted rust_shims.a +# WIT_COMPONENT_TYPE_OBJ - Path to WIT component type object file +# +# Source files (space-separated paths): +# WASM_SOURCES - MozJS WASM wrapper sources +# COMMON_WASI_SOURCES - Common WASI source files +# EXCEPTION_STUBS_SRC - engine/exception_stubs.cpp +# MOZJS_API_SRC - engine/api.cpp +# WIT_API_C - WIT-generated api.c glue +# +# Header discovery: +# ERROR_CODES_HEADER_FILES - Space-separated error_codes header file paths +# CONFIG_HEADER_FILES - Space-separated mongo_config header file paths +# +# Linker inputs: +# LINKSET_FILES - Space-separated linkset output files +# +# Compiler discovery (set one of): +# CXX / CC - Direct paths to wasm32-wasip2-clang++ / clang +# WASI_SDK_BIN_FILES - Space-separated wasi_sdk bin files (auto-discovers) +# +# Compile helper (optional, auto-discovered from script directory): +# COMPILE_HELPER - Path to compile_wasi_source.sh +# +# Example standalone usage: +# CXX=/opt/wasi-sdk/bin/wasm32-wasip2-clang++ \ +# CC=/opt/wasi-sdk/bin/wasm32-wasip2-clang \ +# SM_TARBALL=spidermonkey-wasip2-release.tar.gz \ +# RUST_SHIMS_PATH=rust_shims.a \ +# WASM_SOURCES="engine.cpp error.cpp helpers.cpp" \ +# WIT_COMPONENT_TYPE_OBJ=api_component_type.o \ +# OUTPUT=mozjs_wasm_api.wasm \ +# bash scripts/compile_mozjs_wasm_api.sh + +set -euo pipefail + +EXECROOT="${EXECROOT:-$(pwd -P)}" +START_TIME=$(date +%s) + +# ============================================================ +# STEP 1: Find compilers and setup +# ============================================================ +echo "=== STEP 1: Finding WASI compilers ===" >&2 +if [ -z "${CXX:-}" ] || [ -z "${CC:-}" ]; then + for f in ${WASI_SDK_BIN_FILES:-}; do + case "$f" in + */wasm32-wasip2-clang++) CXX="$f" ;; + */wasm32-wasip2-clang) CC="$f" ;; + esac + done +fi +if [ -z "${CXX:-}" ]; then + echo "ERROR: wasm32-wasip2-clang++ not found. Set CXX or WASI_SDK_BIN_FILES." >&2 + exit 1 +fi +if [ -z "${CC:-}" ]; then + echo "ERROR: wasm32-wasip2-clang not found. Set CC or WASI_SDK_BIN_FILES." >&2 + exit 1 +fi + +WASI_SDK_ROOT="$(cd "$(dirname "$CXX")/.." && pwd -P)" +SYSROOT="${SYSROOT:-$WASI_SDK_ROOT/share/wasi-sysroot}" +echo "Using CXX: $CXX" >&2 +echo "Using CC: $CC" >&2 + +# ============================================================ +# STEP 2: Unpack SpiderMonkey +# ============================================================ +echo "=== STEP 2: Unpacking SpiderMonkey ===" >&2 +OUT_DIR="$(dirname "$OUTPUT")" +STAGE="$OUT_DIR/sm_stage" +rm -rf "$STAGE" +mkdir -p "$STAGE" +tar -xzf "$SM_TARBALL" -C "$STAGE" + +test -f "$STAGE/lib/libjs_static.a" || ( + echo "ERROR: libjs_static.a not found" >&2 + exit 1 +) +test -d "$STAGE/include" || ( + echo "ERROR: include directory not found" >&2 + exit 1 +) + +cp "$RUST_SHIMS_PATH" "$STAGE/lib/libmongo_wasip2_rust_shims.a" +echo "SpiderMonkey unpacked to: $STAGE" >&2 + +# ============================================================ +# STEP 3: Setup compilation environment +# ============================================================ +echo "=== STEP 3: Setting up compilation environment ===" >&2 +OBJ_DIR="$STAGE/objs" +mkdir -p "$OBJ_DIR" + +ERROR_CODES_H_PARENT="" +CONFIG_H_PARENT="" +for f in ${ERROR_CODES_HEADER_FILES:-}; do + ERROR_CODES_H_PARENT="$(dirname "$(dirname "$f")")" + break +done +for f in ${CONFIG_HEADER_FILES:-}; do + CONFIG_H_PARENT="$(dirname "$(dirname "$f")")" + break +done + +BOOST_INCLUDES="-I$EXECROOT/src/third_party/boost" +[ -d "$EXECROOT/src/third_party/boost/libs/numeric/conversion/include" ] && + BOOST_INCLUDES="$BOOST_INCLUDES -I$EXECROOT/src/third_party/boost/libs/numeric/conversion/include" +for lib_include in "$EXECROOT/src/third_party/boost/libs"/*/include; do + [ -d "$lib_include" ] && BOOST_INCLUDES="$BOOST_INCLUDES -I$lib_include" +done + +# Auto-detect Bazel bin/src path for generated headers (e.g., error_codes.h). +# This must work on any host architecture (aarch64, x86_64, etc.). +BAZEL_BIN_SRC="" +for d in "$EXECROOT"/bazel-out/*/bin/src; do + if [ -d "$d" ]; then + BAZEL_BIN_SRC="$d" + break + fi +done + +# ============================================================ +# STEP 4: Compile MozJS library sources (including WIT glue) +# ============================================================ +echo "=== STEP 4: Compiling MozJS library sources ===" >&2 +MOZJS_SRCS="${WASM_SOURCES:-}" +MOZJS_SRCS="$MOZJS_SRCS ${COMMON_WASI_SOURCES:-}" +MOZJS_SRCS="$MOZJS_SRCS ${EXCEPTION_STUBS_SRC:-}" +MOZJS_SRCS="$MOZJS_SRCS ${MOZJS_API_SRC:-}" + +# Add wit-bindgen generated glue (C) to compilation inputs. +MOZJS_SRCS="$MOZJS_SRCS ${WIT_API_C:-}" + +SRC_LIST="$STAGE/src_list.txt" +>"$SRC_LIST" +# Note: intentionally unquoted to split space-separated paths (Bazel paths never contain spaces). +for src in $MOZJS_SRCS; do + [ -n "$src" ] && echo "$src" >>"$SRC_LIST" +done + +# Locate the compile helper script. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPILE_HELPER="${COMPILE_HELPER:-$SCRIPT_DIR/compile_wasi_source.sh}" + +NPROC=$(nproc 2>/dev/null || echo 8) +echo "Compiling $(wc -l <"$SRC_LIST") files with $NPROC parallel jobs" >&2 + +OBJ_LIST="$STAGE/obj_list.txt" +if ! cat "$SRC_LIST" | xargs -P "$NPROC" -I {} bash "$COMPILE_HELPER" {} \ + "$CXX" "$CC" "$SYSROOT" "$STAGE" "$OBJ_DIR" "$EXECROOT" \ + "$ERROR_CODES_H_PARENT" "$CONFIG_H_PARENT" "$BOOST_INCLUDES" \ + "$BAZEL_BIN_SRC" >"$OBJ_LIST" 2>&1; then + echo "ERROR: Parallel compilation failed" >&2 + cat "$OBJ_LIST" >&2 + exit 1 +fi + +OBJ_FILES="" +for obj in "$OBJ_DIR"/*.o; do + [ -f "$obj" ] && OBJ_FILES="$OBJ_FILES $obj" +done +COMPILE_COUNT=$(ls -1 "$OBJ_DIR"/*.o 2>/dev/null | wc -l) +echo "Compiled $COMPILE_COUNT files successfully" >&2 + +# ============================================================ +# STEP 5: Collect MongoDB base libraries +# ============================================================ +echo "=== STEP 5: Collecting MongoDB base libraries ===" >&2 + +MONGO_BASE_LIBS="" +MONGO_BASE_LIBS_RSP="" +for f in ${LINKSET_FILES:-}; do + case "$f" in + *.libs.rsp) MONGO_BASE_LIBS_RSP="$f" ;; + esac +done + +# Use only the .a archives from libs.rsp (they contain all objects). +# We skip objects.rsp to avoid duplicate symbols since the archives +# contain the same objects that would appear there. +if [ -n "$MONGO_BASE_LIBS_RSP" ] && [ -f "$MONGO_BASE_LIBS_RSP" ]; then + LIB_COUNT=0 + while IFS= read -r lib_path || [ -n "$lib_path" ]; do + [ -z "$lib_path" ] && continue + case "$lib_path" in "#"*) continue ;; esac + if [ ! "$lib_path" = "${lib_path#/}" ]; then + LIB_ABS="$lib_path" + else + LIB_ABS="$EXECROOT/$lib_path" + fi + if [ -f "$LIB_ABS" ]; then + MONGO_BASE_LIBS="$MONGO_BASE_LIBS $LIB_ABS" + LIB_COUNT=$((LIB_COUNT + 1)) + fi + done <"$MONGO_BASE_LIBS_RSP" + echo "Found $LIB_COUNT libraries from libs.rsp" >&2 +fi + +# ============================================================ +# STEP 6: Final linking (include wit component type object) +# ============================================================ +echo "=== STEP 6: Linking final WASM binary ===" >&2 +EXTRA_OBJS="$(find "$STAGE/obj-extra" -type f -name '*.o' 2>/dev/null | tr '\n' ' ' || true)" +JSRUST_LIB="" +[ -f "$STAGE/lib/libjsrust.a" ] && JSRUST_LIB="$STAGE/lib/libjsrust.a" + +echo "Linking with:" >&2 +echo " MozJS objects: $(echo $OBJ_FILES | wc -w)" >&2 +echo " Mongo base libs: $(echo $MONGO_BASE_LIBS | wc -w)" >&2 + +if ! "$CXX" --sysroot="$SYSROOT" -std=c++20 -Oz \ + -flto -fexceptions \ + $OBJ_FILES \ + "$WIT_COMPONENT_TYPE_OBJ" \ + "$STAGE/lib/libjs_static.a" \ + "$STAGE/lib/libmongo_wasip2_rust_shims.a" \ + $JSRUST_LIB \ + $EXTRA_OBJS \ + $MONGO_BASE_LIBS \ + -L"$SYSROOT/lib/wasm32-wasip2" \ + -lc++ -lc++abi \ + -lwasi-emulated-getpid \ + -lwasi-emulated-signal \ + -lwasi-emulated-mman \ + -lwasi-emulated-process-clocks \ + -mexec-model=reactor \ + -Wl,--export=cabi_realloc \ + -Wl,--gc-sections \ + -Wl,--strip-all \ + -Wl,--no-entry \ + -o "$OUTPUT" 2>&1; then + echo "ERROR: Linking failed" >&2 + exit 1 +fi + +test -s "$OUTPUT" +ls -lh "$OUTPUT" + +# Post-link size optimization with wasm-opt. +# wasm-opt can typically shrink a WASM binary by another 10-20% through +# WASM-specific optimisations (dead code removal, constant folding, etc.). +WASM_OPT="$(command -v wasm-opt 2>/dev/null || true)" +if [ -z "$WASM_OPT" ] && [ -x "$WASI_SDK_ROOT/bin/wasm-opt" ]; then + WASM_OPT="$WASI_SDK_ROOT/bin/wasm-opt" +fi +if [ -n "$WASM_OPT" ]; then + echo "Running wasm-opt -Oz on $OUTPUT ..." >&2 + "$WASM_OPT" -Oz --enable-bulk-memory --enable-sign-ext \ + -o "$OUTPUT.opt" "$OUTPUT" 2>&1 && mv "$OUTPUT.opt" "$OUTPUT" + ls -lh "$OUTPUT" +else + echo "NOTE: wasm-opt not found; skipping post-link optimisation" >&2 +fi + +END_TIME=$(date +%s) +ELAPSED=$((END_TIME - START_TIME)) +echo "=== SUCCESS: Built mozjs_wasm_api.wasm in ${ELAPSED}s ===" >&2 diff --git a/src/mongo/scripting/mozjs/wasm/scripts/compile_wasi_source.sh b/src/mongo/scripting/mozjs/wasm/scripts/compile_wasi_source.sh new file mode 100755 index 00000000000..24f8eb6086e --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/scripts/compile_wasi_source.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Compiles a single C/C++ source file for WASI. +# Used as a helper by compile_mozjs_wasm_api.sh for parallel compilation via xargs. +# +# Positional arguments: +# $1 - Source file path +# $2 - C++ compiler (wasm32-wasip2-clang++) +# $3 - C compiler (wasm32-wasip2-clang) +# $4 - Sysroot path +# $5 - Stage directory (contains unpacked SpiderMonkey headers) +# $6 - Object output directory +# $7 - Execution root (repo root) +# $8 - Error codes header parent directory +# $9 - Config header parent directory +# $10 - Boost include flags +# $11 - Bazel bin/src path (e.g., bazel-out/k8-fastbuild/bin/src) +# +# Example standalone usage: +# bash scripts/compile_wasi_source.sh myfile.cpp \ +# /opt/wasi-sdk/bin/wasm32-wasip2-clang++ \ +# /opt/wasi-sdk/bin/wasm32-wasip2-clang \ +# /opt/wasi-sdk/share/wasi-sysroot \ +# /tmp/sm_stage /tmp/objs /path/to/repo \ +# /path/to/error_codes_parent /path/to/config_parent \ +# "-I/path/to/boost" \ +# /path/to/bazel-out/k8-fastbuild/bin/src + +set -euo pipefail + +src="$1" +CXX="$2" +CC="$3" +SYSROOT="$4" +STAGE="$5" +OBJ_DIR="$6" +EXECROOT="$7" +ERROR_CODES_H_PARENT="$8" +CONFIG_H_PARENT="$9" +BOOST_INCLUDES="${10}" +BAZEL_BIN_SRC="${11:-}" + +# Auto-detect bazel-out bin/src path if not provided. +# Searches for any bazel-out/*/bin/src directory under EXECROOT. +if [ -z "$BAZEL_BIN_SRC" ]; then + for d in "$EXECROOT"/bazel-out/*/bin/src; do + if [ -d "$d" ]; then + BAZEL_BIN_SRC="$d" + break + fi + done +fi + +# Derive a unique object name by prefixing with the parent directory. +# This avoids collisions when files from different directories share the +# same basename (e.g. common/error.cpp vs engine/error.cpp, or +# generated/api.c vs engine/api.cpp). +parent="$(basename "$(dirname "$src")")" +base="$(basename "$src")" +OBJ_NAME="${parent}_${base%.*}.o" +OBJ_PATH="$OBJ_DIR/$OBJ_NAME" + +ext="${src##*.}" +COMP="$CXX" +STD="-std=c++20" +if [ "$ext" = "c" ]; then + COMP="$CC" + STD="-std=c17" +fi + +# Build include flags for Bazel-generated headers (e.g., error_codes.h, config.h). +BAZEL_BIN_INCLUDE="" +if [ -n "$BAZEL_BIN_SRC" ] && [ -d "$BAZEL_BIN_SRC" ]; then + BAZEL_BIN_INCLUDE="-I$BAZEL_BIN_SRC" +fi + +"$COMP" --sysroot="$SYSROOT" $STD -Oz \ + -flto -fexceptions -ffunction-sections -fdata-sections \ + -fvisibility=hidden \ + -D_WASI_EMULATED_SIGNAL -D_WASI_EMULATED_MMAN \ + -D_WASI_EMULATED_PROCESS_CLOCKS -D_WASI_EMULATED_GETPID \ + -DMOZ_HAS_MOZGLUE \ + -I"$STAGE/include" -I"$STAGE/include/js" -I"$STAGE/include/mozilla" \ + -I"$STAGE/include/src" -I"$STAGE/include/mfbt" \ + -include "$STAGE/include/js-confdefs.h" \ + -I"$EXECROOT/src" $BAZEL_BIN_INCLUDE \ + -I"$EXECROOT/src/mongo/scripting/mozjs/wasm" \ + -I"$EXECROOT/src/mongo/scripting/mozjs/common" \ + -I"$EXECROOT/src/mongo/scripting/mozjs/common/types" \ + -I"$EXECROOT/src/third_party/abseil-cpp/dist" \ + -I"$EXECROOT/src/mongo/scripting/mozjs/wasm/wit_gen/generated" \ + -DMONGO_MOZJS_WASI_BUILD \ + $BOOST_INCLUDES \ + -I"$ERROR_CODES_H_PARENT" -I"$CONFIG_H_PARENT" \ + -c "$src" -o "$OBJ_PATH" 2>&1 + +echo "$OBJ_PATH" diff --git a/src/mongo/scripting/mozjs/wasm/scripts/extract_rust_shims.sh b/src/mongo/scripting/mozjs/wasm/scripts/extract_rust_shims.sh new file mode 100755 index 00000000000..648fc571535 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/scripts/extract_rust_shims.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Extracts the Rust encoding-shims static library from a SpiderMonkey WASI tarball. +# +# Required environment variables: +# TARBALL - Path to spidermonkey-wasip2-release.tar.gz +# OUTPUT - Output path for the extracted .a file +# +# Example standalone usage: +# TARBALL=spidermonkey-wasip2-release.tar.gz \ +# OUTPUT=rust_shims.a \ +# bash scripts/extract_rust_shims.sh + +set -euo pipefail +if [ -z "${TARBALL:-}" ]; then + echo "ERROR: TARBALL not set." >&2 + exit 1 +fi +if [ -z "${OUTPUT:-}" ]; then + echo "ERROR: OUTPUT not set." >&2 + exit 1 +fi + +OUT_DIR="$(dirname "$OUTPUT")" +STAGE="$OUT_DIR/rust_stage" +rm -rf "$STAGE" +mkdir -p "$STAGE" + +# Extract lib directory from tarball (tarball has ./lib/ prefix). +tar -xzf "$TARBALL" -C "$STAGE" --strip-components=0 ./lib/libmongo_wasip2_rust_shims.a 2>/dev/null || { + # Try without ./ prefix if that fails. + if ! tar -xzf "$TARBALL" -C "$STAGE" lib/libmongo_wasip2_rust_shims.a 2>/dev/null; then + echo "WARNING: Could not extract rust shims with either path prefix; will check below." >&2 + fi +} + +RUST_SHIMS="$STAGE/lib/libmongo_wasip2_rust_shims.a" +if [ ! -f "$RUST_SHIMS" ]; then + # Try alternative path. + RUST_SHIMS="$STAGE/./lib/libmongo_wasip2_rust_shims.a" +fi + +if [ -f "$RUST_SHIMS" ]; then + cp "$RUST_SHIMS" "$OUTPUT" + echo "Extracted rust shims to: $OUTPUT" >&2 + exit 0 +fi + +echo "ERROR: Rust shims not found in tarball" >&2 +exit 1 diff --git a/src/mongo/scripting/mozjs/wasm/scripts/link_mongo_base_wasm.sh b/src/mongo/scripting/mozjs/wasm/scripts/link_mongo_base_wasm.sh new file mode 100755 index 00000000000..552f56f4564 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/scripts/link_mongo_base_wasm.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Links mongo_base.wasm from WASI linkset response files. +# +# Required environment variables: +# OUTPUT - Output .wasm file path +# +# Compiler discovery (set one of): +# CXX - Direct path to wasm32-wasip2-clang++ +# WASI_SDK_BIN_FILES - Space-separated wasi_sdk bin files (auto-discovers compiler) +# +# RSP files (set one of): +# OBJS_RSP / LIBS_RSP / FLAGS_RSP - Direct paths to response files +# LINKSET_FILES - Space-separated linkset outputs (auto-discovers RSPs) +# +# Example standalone usage: +# CXX=/path/to/wasm32-wasip2-clang++ \ +# OBJS_RSP=objs.rsp LIBS_RSP=libs.rsp FLAGS_RSP=flags.rsp \ +# OUTPUT=mongo_base.wasm \ +# bash scripts/link_mongo_base_wasm.sh + +set -euo pipefail + +# --- Discover compiler --- +# Prefer wasip2 (WASI Preview 2) compiler to match the rest of the build. +if [ -z "${CXX:-}" ]; then + for f in ${WASI_SDK_BIN_FILES:-}; do + case "$f" in */wasm32-wasip2-clang++) + CXX="$f" + break + ;; + esac + done +fi +# Fallback to WASI Preview 1 compiler name if wasip2 variant not found. +if [ -z "${CXX:-}" ]; then + for f in ${WASI_SDK_BIN_FILES:-}; do + case "$f" in */wasm32-wasi-clang++) + CXX="$f" + break + ;; + esac + done +fi +if [ -z "${CXX:-}" ]; then + echo "ERROR: wasm32-wasip2-clang++ (or wasm32-wasi-clang++) not found. Set CXX or WASI_SDK_BIN_FILES." >&2 + exit 1 +fi + +# --- Discover RSP files --- +if [ -z "${OBJS_RSP:-}" ] || [ -z "${LIBS_RSP:-}" ] || [ -z "${FLAGS_RSP:-}" ]; then + for f in ${LINKSET_FILES:-}; do + case "$f" in + *.objects.rsp) OBJS_RSP="$f" ;; + *.libs.rsp) LIBS_RSP="$f" ;; + *.flags.rsp) FLAGS_RSP="$f" ;; + esac + done +fi +if [ -z "${OBJS_RSP:-}" ] || [ -z "${LIBS_RSP:-}" ] || [ -z "${FLAGS_RSP:-}" ]; then + echo "ERROR: Missing RSP files (.objects/.libs/.flags)." >&2 + echo "Set OBJS_RSP/LIBS_RSP/FLAGS_RSP or LINKSET_FILES." >&2 + exit 1 +fi + +# Select the correct --target based on the compiler name. +# wasip2 compiler requires wasip2 target; wasi compiler requires wasi target. +WASM_TARGET="wasm32-wasip2" + +echo "Linking mongo_base.wasm" >&2 +echo " CXX: $CXX" >&2 +echo " TARGET: $WASM_TARGET" >&2 +echo " OBJS_RSP: $OBJS_RSP" >&2 +echo " LIBS_RSP: $LIBS_RSP" >&2 +echo " FLAGS_RSP: $FLAGS_RSP" >&2 + +"$CXX" --target="$WASM_TARGET" -std=c++20 -Oz -fexceptions \ + @"$OBJS_RSP" @"$LIBS_RSP" @"$FLAGS_RSP" \ + -Wl,--gc-sections \ + -Wl,--strip-all \ + -o "$OUTPUT" + +echo "Output: $OUTPUT" >&2 diff --git a/src/mongo/scripting/mozjs/wasm/spider-monkey/BUILD.bazel b/src/mongo/scripting/mozjs/wasm/spider-monkey/BUILD.bazel new file mode 100644 index 00000000000..e2ab5336b89 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/spider-monkey/BUILD.bazel @@ -0,0 +1,6 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "spider-monkey-repository", + "spider-monkey-version", +]) diff --git a/src/mongo/scripting/mozjs/wasm/spider-monkey/spider-monkey-repository b/src/mongo/scripting/mozjs/wasm/spider-monkey/spider-monkey-repository new file mode 100644 index 00000000000..1668b8f6f6a --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/spider-monkey/spider-monkey-repository @@ -0,0 +1 @@ +https://github.com/mozilla-firefox/firefox \ No newline at end of file diff --git a/src/mongo/scripting/mozjs/wasm/spider-monkey/spider-monkey-version b/src/mongo/scripting/mozjs/wasm/spider-monkey/spider-monkey-version new file mode 100644 index 00000000000..701ec3a860f --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/spider-monkey/spider-monkey-version @@ -0,0 +1 @@ +FIREFOX_140_6_0esr_RELEASE \ No newline at end of file diff --git a/src/mongo/scripting/mozjs/wasm/spidermonkey_repo.bzl b/src/mongo/scripting/mozjs/wasm/spidermonkey_repo.bzl new file mode 100644 index 00000000000..b38f250efa0 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/spidermonkey_repo.bzl @@ -0,0 +1,80 @@ +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") + +def _strip(s): + return s.strip(" \t\r\n") + +def _basename_no_git(url): + u = url + if u.endswith("/"): + u = u[:-1] + if u.endswith(".git"): + u = u[:-4] + + # last path component + parts = u.split("/") + return parts[-1] if parts else "repo" + +def _spidermonkey_repository_impl(rctx): + repo_url = _strip(rctx.read(rctx.attr.repository_file)) + version = _strip(rctx.read(rctx.attr.version_file)) + + if not repo_url: + fail("spidermonkey_repository: repository_file was empty") + if not version: + fail("spidermonkey_repository: version_file was empty") + + # Normalize. + if repo_url.endswith("/"): + repo_url = repo_url[:-1] + if repo_url.endswith(".git"): + repo_url = repo_url[:-4] + + repo_name = _basename_no_git(repo_url) + + # GitHub archive URL for tags. + # Example: https://github.com/mozilla-firefox/firefox/archive/refs/tags/FIREFOX_140_6_0esr_RELEASE.tar.gz + archive_url = "{}/archive/refs/tags/{}.tar.gz".format(repo_url, version) + strip_prefix = "{}-{}".format(repo_name, version) + + if not rctx.attr.sha256: + # buildifier: disable=print + print("WARNING: spidermonkey_repository: sha256 not set for %s; download integrity will not be verified." % archive_url) + + rctx.download_and_extract( + url = archive_url, + sha256 = rctx.attr.sha256, + stripPrefix = strip_prefix, + ) + + # Minimal BUILD file so downstream genrules can depend on `:mach` and `:srcs`. + rctx.file( + "BUILD.bazel", + content = """ +package(default_visibility = ["//visibility:public"]) + +exports_files(["mach"]) + +filegroup( + name = "srcs", + srcs = glob( + ["**"], + exclude = [ + "**/.git/**", + "**/.hg/**", + ], + ), +) +""", + ) + +spidermonkey_repository = repository_rule( + implementation = _spidermonkey_repository_impl, + attrs = { + # Labels in the main workspace that contain the repo URL and tag/version. + "repository_file": attr.label(mandatory = True, allow_single_file = True), + "version_file": attr.label(mandatory = True, allow_single_file = True), + # Optional, but recommended for hermeticity. If empty, Bazel will not verify. + "sha256": attr.string(default = ""), + }, + doc = "Downloads SpiderMonkey/Firefox source from a repo+tag stored in files, and exposes `:mach` and `:srcs`.", +) diff --git a/src/mongo/scripting/mozjs/wasm/status.cpp b/src/mongo/scripting/mozjs/wasm/status.cpp new file mode 100644 index 00000000000..5b1c2381e7a --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/status.cpp @@ -0,0 +1,149 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/scripting/mozjs/common/types/status.h" + +#include "mongo/base/error_codes.h" +#include "mongo/base/status.h" +#include "mongo/scripting/mozjs/common/error.h" +#include "mongo/scripting/mozjs/common/freeOpToJSContext.h" +#include "mongo/scripting/mozjs/common/internedstring.h" +#include "mongo/scripting/mozjs/common/objectwrapper.h" +#include "mongo/scripting/mozjs/common/runtime.h" +#include "mongo/scripting/mozjs/common/valuereader.h" +#include "mongo/scripting/mozjs/common/wrapconstrainedmethod.h" +#include "mongo/scripting/mozjs/common/wraptype.h" +#include "mongo/util/assert_util.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mongo { +namespace mozjs { + +const char* const MongoStatusInfo::className = "MongoStatus"; +const char* const MongoStatusInfo::inheritFrom = "Error"; + +Status MongoStatusInfo::toStatus(JSContext* cx, JS::HandleObject object) { + return *JS::GetMaybePtrFromReservedSlot(object, StatusSlot); +} + +Status MongoStatusInfo::toStatus(JSContext* cx, JS::HandleValue value) { + return *JS::GetMaybePtrFromReservedSlot(value.toObjectOrNull(), StatusSlot); +} + +void MongoStatusInfo::fromStatus(JSContext* cx, Status status, JS::MutableHandleValue value) { + invariant(status != Status::OK()); + auto runtime = getCommonRuntime(cx); + + JS::RootedValue undef(cx); + undef.setUndefined(); + + JS::RootedValueArray<1> args(cx); + ValueReader(cx, args[0]).fromStringData(status.reason()); + JS::RootedObject error(cx); + getProto(runtime).newInstance(args, &error); + + JS::RootedObject thisv(cx); + getProto(runtime).newObjectWithProto(&thisv, error); + ObjectWrapper thisvObj(cx, thisv); + thisvObj.defineProperty(InternedString::code, + JSPROP_ENUMERATE, + smUtils::wrapConstrainedMethod, + nullptr); + + thisvObj.defineProperty( + InternedString::reason, + JSPROP_ENUMERATE, + smUtils::wrapConstrainedMethod, + nullptr); + + // We intentionally omit JSPROP_ENUMERATE to match how Error.prototype.stack is a non-enumerable + // property. + thisvObj.defineProperty( + InternedString::stack, + 0, + smUtils::wrapConstrainedMethod, + nullptr); + + JS::SetReservedSlot( + thisv, StatusSlot, JS::PrivateValue(trackedNew(runtime, std::move(status)))); + + value.setObjectOrNull(thisv); +} + +void MongoStatusInfo::finalize(JS::GCContext* gcCtx, JSObject* obj) { + auto status = JS::GetMaybePtrFromReservedSlot(obj, StatusSlot); + + if (status) + trackedDelete(getCommonRuntime(freeOpToJSContext(gcCtx)), status); +} + +void MongoStatusInfo::Functions::code::call(JSContext* cx, JS::CallArgs args) { + args.rval().setInt32(toStatus(cx, args.thisv()).code()); +} + +void MongoStatusInfo::Functions::reason::call(JSContext* cx, JS::CallArgs args) { + ValueReader(cx, args.rval()).fromStringData(toStatus(cx, args.thisv()).reason()); +} + +void MongoStatusInfo::Functions::stack::call(JSContext* cx, JS::CallArgs args) { + JS::RootedObject thisv(cx, args.thisv().toObjectOrNull()); + JS::RootedObject parent(cx); + + if (!JS_GetPrototype(cx, thisv, &parent)) { + uasserted(ErrorCodes::JSInterpreterFailure, "Couldn't get prototype"); + } + + ObjectWrapper parentWrapper(cx, parent); + + auto status = toStatus(cx, args.thisv()); + // WASI version: Skip JSExceptionInfo handling as its not available in WASI builds + // Just return the parent's stack property + parentWrapper.getValue(InternedString::stack, args.rval()); +} + +void MongoStatusInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto) { + auto runtime = getCommonRuntime(cx); + JS::SetReservedSlot(proto, + StatusSlot, + JS::PrivateValue(trackedNew( + runtime, Status(ErrorCodes::UnknownError, "Mongo Status Prototype")))); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/wasm/support/rust_shims/Cargo.toml.template b/src/mongo/scripting/mozjs/wasm/support/rust_shims/Cargo.toml.template new file mode 100644 index 00000000000..15806ffee9a --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/support/rust_shims/Cargo.toml.template @@ -0,0 +1,11 @@ +[package] +name = "mongo_wasip2_rust_shims" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +encoding_c = { path = "{{SPIDERMONKEY_ROOT}}/third_party/rust/encoding_c" } +encoding_c_mem = { path = "{{SPIDERMONKEY_ROOT}}/third_party/rust/encoding_c_mem" } diff --git a/src/mongo/scripting/mozjs/wasm/support/rust_shims/src/lib.rs b/src/mongo/scripting/mozjs/wasm/support/rust_shims/src/lib.rs new file mode 100644 index 00000000000..ff6b241df66 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/support/rust_shims/src/lib.rs @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +// Rust shims for WASI linkage. +// Force-link the encoding shim crates so their exported C symbols are present. +extern crate encoding_c; +extern crate encoding_c_mem; + +#[no_mangle] +pub extern "C" fn mongo_wasip2_rust_shims_keepalive() { + // Touch at least one symbol from each crate so the linker keeps them. + unsafe { + let p = core::ptr::null::(); + let _ = encoding_c::encoding_utf8_valid_up_to(p, 0); + let q = core::ptr::null::(); + let _ = encoding_c_mem::encoding_mem_is_utf8_latin1(p, 0); + let _ = q; + } +} diff --git a/src/mongo/scripting/mozjs/wasm/wit/BUILD.bazel b/src/mongo/scripting/mozjs/wasm/wit/BUILD.bazel new file mode 100644 index 00000000000..10c8a9ab651 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/wit/BUILD.bazel @@ -0,0 +1,3 @@ +exports_files([ + "mozjs.wit", +]) diff --git a/src/mongo/scripting/mozjs/wasm/wit/README.md b/src/mongo/scripting/mozjs/wasm/wit/README.md new file mode 100644 index 00000000000..82b62036808 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/wit/README.md @@ -0,0 +1,35 @@ +# Add more methods to public API: + +``` +world api { + export mozjstest: func() -> s32; + export mynewfunc: func() -> s32; +} +``` + +# Install wit-gen on your dev VM + +``` +cargo install --git https://github.com/bytecodealliance/wit-bindgen --locked wit-bindgen-cli +``` + +# Run the bindgen + +``` +cd src/mongo/scripting/mozjs/wasm +wit-bindgen c ./wit --out-dir wit_gen/generated +``` + +# Implement the symbol in engine/api.cpp + +``` +extern "C" int32_t exports_api_mozjstest(void) { + return 1234; +} + +extern "C" int32_t exports_api_mynewfunc(void) { + return ...; +} +``` + +# Commit the generated files in wit_gen/generated diff --git a/src/mongo/scripting/mozjs/wasm/wit/mozjs.wit b/src/mongo/scripting/mozjs/wasm/wit/mozjs.wit new file mode 100644 index 00000000000..b560b0ce4b5 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/wit/mozjs.wit @@ -0,0 +1,90 @@ +package mongo:mozjs; + +interface mozjs { + /// Error codes mirrored from `err_code_t`. + enum err-code { + ok, + e-invalid-arg, + e-bad-state, + e-nomem, + e-io, + e-timeout, + e-not-supported, + e-internal, + + e-jsapi-fail, + e-pending-exception, + e-no-exception, + e-terminated, + e-oom, + + e-compile, + e-runtime, + e-module, + e-promise-rejection, + e-stack-overflow, + + e-type, + e-encoding, + } + + /// Mirrors `wasm_mozjs_error_t` (but as owned values). + record wasm-mozjs-error { + code: err-code, + + msg: option, + filename: option, + stack: option, + + line: u32, + column: u32, + } + + type ok = u16; + + /// Opaque function handle. + type function-handle = u64; + + /// Initialize/shutdown/interrupt + initialize-engine: func() -> result; + shutdown-engine: func() -> result; + interrupt-current-op: func() -> result; + + /// Create a JS function from source. + create-function: func(source: list) -> result; + + /// Invoke a function with BSON args (no `this.` binding). + /// Used by $function, $accumulator, mapReduce.reduce, mapReduce.finalize. + invoke-function: func(handle: function-handle, bson: list) -> result; + + /// Invoke a predicate with the document as `this`, returns bool directly. + /// Used by $where: function() { return this.age > 18; } + invoke-predicate: func(handle: function-handle, document: list) -> result; + + /// Invoke a map function with the document as `this`; emits buffered internally. + /// Used by mapReduce.map: function() { emit(this.key, this.value); } + invoke-map: func(handle: function-handle, document: list) -> result; + + /// Get last return value as BSON bytes. + get-return-value-bson: func() -> result, wasm-mozjs-error>; + + /// Set a named global variable from a BSON-encoded value. + set-global: func(name: string, bson-value: list) -> result; + + /// Get a named global variable as BSON-encoded bytes. + get-global: func(name: string) -> result, wasm-mozjs-error>; + + /// Set a named global to a single BSON element's JS value directly. + set-global-value: func(name: string, bson-element: list) -> result; + + /// Set up the emit() built-in for mapReduce. Resets the emit buffer. + /// Optional byte-limit caps total emitted bytes (default 16 MiB). + setup-emit: func(byte-limit: option) -> result; + + /// Drain the emit buffer: returns accumulated {k,v} pairs as BSON, then clears. + drain-emit-buffer: func() -> result, wasm-mozjs-error>; +} + +world api { + export mozjs; +} diff --git a/src/mongo/scripting/mozjs/wasm/wit_gen/BUILD.bazel b/src/mongo/scripting/mozjs/wasm/wit_gen/BUILD.bazel new file mode 100644 index 00000000000..ded79e99786 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/wit_gen/BUILD.bazel @@ -0,0 +1,7 @@ +exports_files( + glob([ + "*.h", + "*.cpp", + "*.defs", + ]), +) diff --git a/src/mongo/scripting/mozjs/wasm/wit_gen/generated/BUILD.bazel b/src/mongo/scripting/mozjs/wasm/wit_gen/generated/BUILD.bazel new file mode 100644 index 00000000000..844e02c0afc --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/wit_gen/generated/BUILD.bazel @@ -0,0 +1,7 @@ +exports_files( + glob([ + "*.h", + "*.c", + "*.o", + ]), +) diff --git a/src/mongo/scripting/mozjs/wasm/wit_gen/generated/api.c b/src/mongo/scripting/mozjs/wasm/wit_gen/generated/api.c new file mode 100644 index 00000000000..80fed32ae80 --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/wit_gen/generated/api.c @@ -0,0 +1,1428 @@ +// Generated by `wit-bindgen` 0.51.0. DO NOT EDIT! +#include "api.h" + +#include +#include + +// Exported Functions from `mongo:mozjs/mozjs` + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#initialize-engine"))) void +__wasm_export_exports_mongo_mozjs_mozjs_initialize_engine_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#shutdown-engine"))) void +__wasm_export_exports_mongo_mozjs_mozjs_shutdown_engine_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#interrupt-current-op"))) void +__wasm_export_exports_mongo_mozjs_mozjs_interrupt_current_op_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#create-function"))) void +__wasm_export_exports_mongo_mozjs_mozjs_create_function_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 + 1 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (8 + 3 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (8 + 2 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 + 4 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (8 + 6 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (8 + 5 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 + 7 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (8 + 9 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (8 + 8 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#invoke-function"))) void +__wasm_export_exports_mongo_mozjs_mozjs_invoke_function_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#invoke-predicate"))) void +__wasm_export_exports_mongo_mozjs_mozjs_invoke_predicate_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#invoke-map"))) void +__wasm_export_exports_mongo_mozjs_mozjs_invoke_map_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#get-return-value-bson"))) void +__wasm_export_exports_mongo_mozjs_mozjs_get_return_value_bson_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + size_t len = *((size_t*)(arg0 + (2 * sizeof(void*)))); + if (len > 0) { + uint8_t* ptr = *((uint8_t**)(arg0 + sizeof(void*))); + for (size_t i = 0; i < len; i++) { + uint8_t* base = ptr + i * 1; + (void)base; + } + free(ptr); + } + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#set-global"))) void +__wasm_export_exports_mongo_mozjs_mozjs_set_global_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#get-global"))) void +__wasm_export_exports_mongo_mozjs_mozjs_get_global_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + size_t len = *((size_t*)(arg0 + (2 * sizeof(void*)))); + if (len > 0) { + uint8_t* ptr = *((uint8_t**)(arg0 + sizeof(void*))); + for (size_t i = 0; i < len; i++) { + uint8_t* base = ptr + i * 1; + (void)base; + } + free(ptr); + } + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#set-global-value"))) void +__wasm_export_exports_mongo_mozjs_mozjs_set_global_value_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#setup-emit"))) void +__wasm_export_exports_mongo_mozjs_mozjs_setup_emit_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +__attribute__((__weak__, __export_name__("cabi_post_mongo:mozjs/mozjs#drain-emit-buffer"))) void +__wasm_export_exports_mongo_mozjs_mozjs_drain_emit_buffer_post_return(uint8_t* arg0) { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + 0))) { + case 0: { + size_t len = *((size_t*)(arg0 + (2 * sizeof(void*)))); + if (len > 0) { + uint8_t* ptr = *((uint8_t**)(arg0 + sizeof(void*))); + for (size_t i = 0; i < len; i++) { + uint8_t* base = ptr + i * 1; + (void)base; + } + free(ptr); + } + break; + } + case 1: { + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (2 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (4 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (3 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (5 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (7 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (6 * sizeof(void*))))); + } + break; + } + } + switch ((int32_t)(int32_t)*((uint8_t*)(arg0 + (8 * sizeof(void*))))) { + case 0: { + break; + } + case 1: { + if ((*((size_t*)(arg0 + (10 * sizeof(void*))))) > 0) { + free(*((uint8_t**)(arg0 + (9 * sizeof(void*))))); + } + break; + } + } + break; + } + } +} + +// Canonical ABI intrinsics + +__attribute__((__weak__, __export_name__("cabi_realloc"))) void* cabi_realloc(void* ptr, + size_t old_size, + size_t align, + size_t new_size) { + (void)old_size; + if (new_size == 0) + return (void*)align; + void* ret = realloc(ptr, new_size); + if (!ret) + abort(); + return ret; +} + +__attribute__((__aligned__(8))) static uint8_t RET_AREA[(16 + 10 * sizeof(void*))]; + +// Helper Functions + +void api_option_string_free(api_option_string_t* ptr) { + if (ptr->is_some) { + api_string_free(&ptr->val); + } +} + +void exports_mongo_mozjs_mozjs_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* ptr) { + api_option_string_free(&ptr->msg); + api_option_string_free(&ptr->filename); + api_option_string_free(&ptr->stack); +} + +void exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t* ptr) { + if (!ptr->is_err) { + } else { + exports_mongo_mozjs_mozjs_wasm_mozjs_error_free(&ptr->val.err); + } +} + +void api_list_u8_free(api_list_u8_t* ptr) { + size_t list_len = ptr->len; + if (list_len > 0) { + uint8_t* list_ptr = ptr->ptr; + for (size_t i = 0; i < list_len; i++) { + } + free(list_ptr); + } +} + +void exports_mongo_mozjs_mozjs_result_function_handle_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_result_function_handle_wasm_mozjs_error_t* ptr) { + if (!ptr->is_err) { + } else { + exports_mongo_mozjs_mozjs_wasm_mozjs_error_free(&ptr->val.err); + } +} + +void exports_mongo_mozjs_mozjs_result_bool_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_result_bool_wasm_mozjs_error_t* ptr) { + if (!ptr->is_err) { + } else { + exports_mongo_mozjs_mozjs_wasm_mozjs_error_free(&ptr->val.err); + } +} + +void exports_mongo_mozjs_mozjs_result_list_u8_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_result_list_u8_wasm_mozjs_error_t* ptr) { + if (!ptr->is_err) { + api_list_u8_free(&ptr->val.ok); + } else { + exports_mongo_mozjs_mozjs_wasm_mozjs_error_free(&ptr->val.err); + } +} + +void api_option_s64_free(api_option_s64_t* ptr) { + if (ptr->is_some) { + } +} + +void api_string_set(api_string_t* ret, const char* s) { + ret->ptr = (uint8_t*)s; + ret->len = strlen(s); +} + +void api_string_dup(api_string_t* ret, const char* s) { + ret->len = strlen(s); + ret->ptr = (uint8_t*)cabi_realloc(NULL, 0, 1, ret->len * 1); + memcpy(ret->ptr, s, ret->len * 1); +} + +void api_string_dup_n(api_string_t* ret, const char* s, size_t len) { + ret->len = len; + ret->ptr = (uint8_t*)cabi_realloc(NULL, 0, 1, ret->len * 1); + memcpy(ret->ptr, s, ret->len * 1); +} + +void api_string_free(api_string_t* ret) { + if (ret->len > 0) { + free(ret->ptr); + } + ret->ptr = NULL; + ret->len = 0; +} + +// Component Adapters + +__attribute__((__export_name__("mongo:mozjs/mozjs#initialize-engine"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_initialize_engine(void) { + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t ret; + exports_mongo_mozjs_mozjs_ok_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_initialize_engine(&ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload0 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload0).code; + if (((*payload0).msg).is_some) { + const api_string_t* payload2 = &((*payload0).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload2).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload2).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload0).filename).is_some) { + const api_string_t* payload4 = &((*payload0).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload4).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload4).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload0).stack).is_some) { + const api_string_t* payload6 = &((*payload0).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload6).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload6).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload0).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload0).column); + } else { + const exports_mongo_mozjs_mozjs_ok_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int16_t*)(ptr + sizeof(void*))) = (int32_t)(*payload); + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#shutdown-engine"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_shutdown_engine(void) { + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t ret; + exports_mongo_mozjs_mozjs_ok_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_shutdown_engine(&ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload0 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload0).code; + if (((*payload0).msg).is_some) { + const api_string_t* payload2 = &((*payload0).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload2).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload2).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload0).filename).is_some) { + const api_string_t* payload4 = &((*payload0).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload4).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload4).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload0).stack).is_some) { + const api_string_t* payload6 = &((*payload0).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload6).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload6).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload0).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload0).column); + } else { + const exports_mongo_mozjs_mozjs_ok_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int16_t*)(ptr + sizeof(void*))) = (int32_t)(*payload); + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#interrupt-current-op"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_interrupt_current_op(void) { + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t ret; + exports_mongo_mozjs_mozjs_ok_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_interrupt_current_op(&ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload0 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload0).code; + if (((*payload0).msg).is_some) { + const api_string_t* payload2 = &((*payload0).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload2).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload2).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload0).filename).is_some) { + const api_string_t* payload4 = &((*payload0).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload4).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload4).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload0).stack).is_some) { + const api_string_t* payload6 = &((*payload0).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload6).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload6).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload0).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload0).column); + } else { + const exports_mongo_mozjs_mozjs_ok_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int16_t*)(ptr + sizeof(void*))) = (int32_t)(*payload); + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#create-function"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_create_function(uint8_t* arg, size_t arg0) { + api_list_u8_t arg1 = (api_list_u8_t){(uint8_t*)(arg), (arg0)}; + exports_mongo_mozjs_mozjs_result_function_handle_wasm_mozjs_error_t ret; + exports_mongo_mozjs_mozjs_function_handle_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_create_function(&arg1, &ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload2 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + 8)) = (int32_t)(*payload2).code; + if (((*payload2).msg).is_some) { + const api_string_t* payload4 = &((*payload2).msg).val; + *((int8_t*)(ptr + (8 + 1 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (8 + 3 * sizeof(void*)))) = (*payload4).len; + *((uint8_t**)(ptr + (8 + 2 * sizeof(void*)))) = (uint8_t*)(*payload4).ptr; + } else { + *((int8_t*)(ptr + (8 + 1 * sizeof(void*)))) = 0; + } + if (((*payload2).filename).is_some) { + const api_string_t* payload6 = &((*payload2).filename).val; + *((int8_t*)(ptr + (8 + 4 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (8 + 6 * sizeof(void*)))) = (*payload6).len; + *((uint8_t**)(ptr + (8 + 5 * sizeof(void*)))) = (uint8_t*)(*payload6).ptr; + } else { + *((int8_t*)(ptr + (8 + 4 * sizeof(void*)))) = 0; + } + if (((*payload2).stack).is_some) { + const api_string_t* payload8 = &((*payload2).stack).val; + *((int8_t*)(ptr + (8 + 7 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (8 + 9 * sizeof(void*)))) = (*payload8).len; + *((uint8_t**)(ptr + (8 + 8 * sizeof(void*)))) = (uint8_t*)(*payload8).ptr; + } else { + *((int8_t*)(ptr + (8 + 7 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (8 + 10 * sizeof(void*)))) = (int32_t)((*payload2).line); + *((int32_t*)(ptr + (12 + 10 * sizeof(void*)))) = (int32_t)((*payload2).column); + } else { + const exports_mongo_mozjs_mozjs_function_handle_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int64_t*)(ptr + 8)) = (int64_t)(*payload); + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#invoke-function"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_invoke_function(int64_t arg, uint8_t* arg0, size_t arg1) { + api_list_u8_t arg2 = (api_list_u8_t){(uint8_t*)(arg0), (arg1)}; + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t ret; + exports_mongo_mozjs_mozjs_ok_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_invoke_function((uint64_t)(arg), &arg2, &ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload3 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload3).code; + if (((*payload3).msg).is_some) { + const api_string_t* payload5 = &((*payload3).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload5).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload5).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload3).filename).is_some) { + const api_string_t* payload7 = &((*payload3).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload7).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload7).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload3).stack).is_some) { + const api_string_t* payload9 = &((*payload3).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload9).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload9).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload3).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload3).column); + } else { + const exports_mongo_mozjs_mozjs_ok_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int16_t*)(ptr + sizeof(void*))) = (int32_t)(*payload); + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#invoke-predicate"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_invoke_predicate(int64_t arg, uint8_t* arg0, size_t arg1) { + api_list_u8_t arg2 = (api_list_u8_t){(uint8_t*)(arg0), (arg1)}; + exports_mongo_mozjs_mozjs_result_bool_wasm_mozjs_error_t ret; + bool ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_invoke_predicate((uint64_t)(arg), &arg2, &ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload3 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload3).code; + if (((*payload3).msg).is_some) { + const api_string_t* payload5 = &((*payload3).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload5).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload5).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload3).filename).is_some) { + const api_string_t* payload7 = &((*payload3).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload7).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload7).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload3).stack).is_some) { + const api_string_t* payload9 = &((*payload3).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload9).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload9).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload3).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload3).column); + } else { + const bool* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int8_t*)(ptr + sizeof(void*))) = *payload; + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#invoke-map"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_invoke_map(int64_t arg, uint8_t* arg0, size_t arg1) { + api_list_u8_t arg2 = (api_list_u8_t){(uint8_t*)(arg0), (arg1)}; + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t ret; + exports_mongo_mozjs_mozjs_ok_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_invoke_map((uint64_t)(arg), &arg2, &ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload3 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload3).code; + if (((*payload3).msg).is_some) { + const api_string_t* payload5 = &((*payload3).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload5).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload5).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload3).filename).is_some) { + const api_string_t* payload7 = &((*payload3).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload7).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload7).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload3).stack).is_some) { + const api_string_t* payload9 = &((*payload3).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload9).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload9).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload3).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload3).column); + } else { + const exports_mongo_mozjs_mozjs_ok_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int16_t*)(ptr + sizeof(void*))) = (int32_t)(*payload); + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#get-return-value-bson"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_get_return_value_bson(void) { + exports_mongo_mozjs_mozjs_result_list_u8_wasm_mozjs_error_t ret; + api_list_u8_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_get_return_value_bson(&ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload0 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload0).code; + if (((*payload0).msg).is_some) { + const api_string_t* payload2 = &((*payload0).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload2).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload2).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload0).filename).is_some) { + const api_string_t* payload4 = &((*payload0).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload4).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload4).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload0).stack).is_some) { + const api_string_t* payload6 = &((*payload0).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload6).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload6).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload0).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload0).column); + } else { + const api_list_u8_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((size_t*)(ptr + (2 * sizeof(void*)))) = (*payload).len; + *((uint8_t**)(ptr + sizeof(void*))) = (uint8_t*)(*payload).ptr; + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#set-global"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_set_global(uint8_t* arg, + size_t arg0, + uint8_t* arg1, + size_t arg2) { + api_string_t arg3 = (api_string_t){(uint8_t*)(arg), (arg0)}; + api_list_u8_t arg4 = (api_list_u8_t){(uint8_t*)(arg1), (arg2)}; + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t ret; + exports_mongo_mozjs_mozjs_ok_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_set_global(&arg3, &arg4, &ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload5 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload5).code; + if (((*payload5).msg).is_some) { + const api_string_t* payload7 = &((*payload5).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload7).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload7).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload5).filename).is_some) { + const api_string_t* payload9 = &((*payload5).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload9).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload9).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload5).stack).is_some) { + const api_string_t* payload11 = &((*payload5).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload11).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload11).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload5).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload5).column); + } else { + const exports_mongo_mozjs_mozjs_ok_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int16_t*)(ptr + sizeof(void*))) = (int32_t)(*payload); + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#get-global"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_get_global(uint8_t* arg, size_t arg0) { + api_string_t arg1 = (api_string_t){(uint8_t*)(arg), (arg0)}; + exports_mongo_mozjs_mozjs_result_list_u8_wasm_mozjs_error_t ret; + api_list_u8_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_get_global(&arg1, &ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload2 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload2).code; + if (((*payload2).msg).is_some) { + const api_string_t* payload4 = &((*payload2).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload4).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload4).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload2).filename).is_some) { + const api_string_t* payload6 = &((*payload2).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload6).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload6).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload2).stack).is_some) { + const api_string_t* payload8 = &((*payload2).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload8).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload8).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload2).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload2).column); + } else { + const api_list_u8_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((size_t*)(ptr + (2 * sizeof(void*)))) = (*payload).len; + *((uint8_t**)(ptr + sizeof(void*))) = (uint8_t*)(*payload).ptr; + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#set-global-value"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_set_global_value(uint8_t* arg, + size_t arg0, + uint8_t* arg1, + size_t arg2) { + api_string_t arg3 = (api_string_t){(uint8_t*)(arg), (arg0)}; + api_list_u8_t arg4 = (api_list_u8_t){(uint8_t*)(arg1), (arg2)}; + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t ret; + exports_mongo_mozjs_mozjs_ok_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_set_global_value(&arg3, &arg4, &ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload5 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload5).code; + if (((*payload5).msg).is_some) { + const api_string_t* payload7 = &((*payload5).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload7).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload7).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload5).filename).is_some) { + const api_string_t* payload9 = &((*payload5).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload9).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload9).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload5).stack).is_some) { + const api_string_t* payload11 = &((*payload5).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload11).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload11).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload5).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload5).column); + } else { + const exports_mongo_mozjs_mozjs_ok_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int16_t*)(ptr + sizeof(void*))) = (int32_t)(*payload); + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#setup-emit"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_setup_emit(int32_t arg, int64_t arg0) { + api_option_s64_t option; + switch (arg) { + case 0: { + option.is_some = false; + break; + } + case 1: { + option.is_some = true; + option.val = arg0; + break; + } + } + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t ret; + exports_mongo_mozjs_mozjs_ok_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = + !exports_mongo_mozjs_mozjs_setup_emit(option.is_some ? &(option.val) : NULL, &ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload1 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload1).code; + if (((*payload1).msg).is_some) { + const api_string_t* payload3 = &((*payload1).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload3).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload3).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload1).filename).is_some) { + const api_string_t* payload5 = &((*payload1).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload5).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload5).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload1).stack).is_some) { + const api_string_t* payload7 = &((*payload1).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload7).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload7).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload1).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload1).column); + } else { + const exports_mongo_mozjs_mozjs_ok_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((int16_t*)(ptr + sizeof(void*))) = (int32_t)(*payload); + } + return ptr; +} + +__attribute__((__export_name__("mongo:mozjs/mozjs#drain-emit-buffer"))) uint8_t* +__wasm_export_exports_mongo_mozjs_mozjs_drain_emit_buffer(void) { + exports_mongo_mozjs_mozjs_result_list_u8_wasm_mozjs_error_t ret; + api_list_u8_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + ret.is_err = !exports_mongo_mozjs_mozjs_drain_emit_buffer(&ok, &err); + if (ret.is_err) { + ret.val.err = err; + } + if (!ret.is_err) { + ret.val.ok = ok; + } + uint8_t* ptr = (uint8_t*)&RET_AREA; + if ((ret).is_err) { + const exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* payload0 = &(ret).val.err; + *((int8_t*)(ptr + 0)) = 1; + *((int8_t*)(ptr + sizeof(void*))) = (int32_t)(*payload0).code; + if (((*payload0).msg).is_some) { + const api_string_t* payload2 = &((*payload0).msg).val; + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (4 * sizeof(void*)))) = (*payload2).len; + *((uint8_t**)(ptr + (3 * sizeof(void*)))) = (uint8_t*)(*payload2).ptr; + } else { + *((int8_t*)(ptr + (2 * sizeof(void*)))) = 0; + } + if (((*payload0).filename).is_some) { + const api_string_t* payload4 = &((*payload0).filename).val; + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (7 * sizeof(void*)))) = (*payload4).len; + *((uint8_t**)(ptr + (6 * sizeof(void*)))) = (uint8_t*)(*payload4).ptr; + } else { + *((int8_t*)(ptr + (5 * sizeof(void*)))) = 0; + } + if (((*payload0).stack).is_some) { + const api_string_t* payload6 = &((*payload0).stack).val; + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 1; + *((size_t*)(ptr + (10 * sizeof(void*)))) = (*payload6).len; + *((uint8_t**)(ptr + (9 * sizeof(void*)))) = (uint8_t*)(*payload6).ptr; + } else { + *((int8_t*)(ptr + (8 * sizeof(void*)))) = 0; + } + *((int32_t*)(ptr + (11 * sizeof(void*)))) = (int32_t)((*payload0).line); + *((int32_t*)(ptr + (4 + 11 * sizeof(void*)))) = (int32_t)((*payload0).column); + } else { + const api_list_u8_t* payload = &(ret).val.ok; + *((int8_t*)(ptr + 0)) = 0; + *((size_t*)(ptr + (2 * sizeof(void*)))) = (*payload).len; + *((uint8_t**)(ptr + sizeof(void*))) = (uint8_t*)(*payload).ptr; + } + return ptr; +} + +// Ensure that the *_component_type.o object is linked in + +extern void __component_type_object_force_link_api(void); +__attribute__((used)) void +__component_type_object_force_link_api_public_use_in_this_compilation_unit(void) { + __component_type_object_force_link_api(); +} diff --git a/src/mongo/scripting/mozjs/wasm/wit_gen/generated/api.h b/src/mongo/scripting/mozjs/wasm/wit_gen/generated/api.h new file mode 100644 index 00000000000..d785040317b --- /dev/null +++ b/src/mongo/scripting/mozjs/wasm/wit_gen/generated/api.h @@ -0,0 +1,186 @@ +// Generated by `wit-bindgen` 0.51.0. DO NOT EDIT! +#ifndef __BINDINGS_API_H +#define __BINDINGS_API_H +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +typedef struct api_string_t { + uint8_t* ptr; + size_t len; +} api_string_t; + +// Error codes mirrored from `err_code_t`. +typedef uint8_t exports_mongo_mozjs_mozjs_err_code_t; + +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_OK 0 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_INVALID_ARG 1 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_BAD_STATE 2 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_NOMEM 3 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_IO 4 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_TIMEOUT 5 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_NOT_SUPPORTED 6 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_INTERNAL 7 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_JSAPI_FAIL 8 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_PENDING_EXCEPTION 9 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_NO_EXCEPTION 10 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_TERMINATED 11 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_OOM 12 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_COMPILE 13 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_RUNTIME 14 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_MODULE 15 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_PROMISE_REJECTION 16 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_STACK_OVERFLOW 17 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_TYPE 18 +#define EXPORTS_MONGO_MOZJS_MOZJS_ERR_CODE_E_ENCODING 19 + +typedef struct { + bool is_some; + api_string_t val; +} api_option_string_t; + +// Mirrors `wasm_mozjs_error_t` (but as owned values). +typedef struct exports_mongo_mozjs_mozjs_wasm_mozjs_error_t { + exports_mongo_mozjs_mozjs_err_code_t code; + api_option_string_t msg; + api_option_string_t filename; + api_option_string_t stack; + uint32_t line; + uint32_t column; +} exports_mongo_mozjs_mozjs_wasm_mozjs_error_t; + +typedef uint16_t exports_mongo_mozjs_mozjs_ok_t; + +// Opaque function handle. +typedef uint64_t exports_mongo_mozjs_mozjs_function_handle_t; + +typedef struct { + bool is_err; + union { + exports_mongo_mozjs_mozjs_ok_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + } val; +} exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t; + +typedef struct { + uint8_t* ptr; + size_t len; +} api_list_u8_t; + +typedef struct { + bool is_err; + union { + exports_mongo_mozjs_mozjs_function_handle_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + } val; +} exports_mongo_mozjs_mozjs_result_function_handle_wasm_mozjs_error_t; + +typedef struct { + bool is_err; + union { + bool ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + } val; +} exports_mongo_mozjs_mozjs_result_bool_wasm_mozjs_error_t; + +typedef struct { + bool is_err; + union { + api_list_u8_t ok; + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t err; + } val; +} exports_mongo_mozjs_mozjs_result_list_u8_wasm_mozjs_error_t; + +typedef struct { + bool is_some; + int64_t val; +} api_option_s64_t; + +// Exported Functions from `mongo:mozjs/mozjs` +bool exports_mongo_mozjs_mozjs_initialize_engine(exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_shutdown_engine(exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_interrupt_current_op( + exports_mongo_mozjs_mozjs_ok_t* ret, exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_create_function(api_list_u8_t* source, + exports_mongo_mozjs_mozjs_function_handle_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_invoke_function(exports_mongo_mozjs_mozjs_function_handle_t handle, + api_list_u8_t* bson, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_invoke_predicate(exports_mongo_mozjs_mozjs_function_handle_t handle, + api_list_u8_t* document, + bool* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_invoke_map(exports_mongo_mozjs_mozjs_function_handle_t handle, + api_list_u8_t* document, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_get_return_value_bson( + api_list_u8_t* ret, exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_set_global(api_string_t* name, + api_list_u8_t* bson_value, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_get_global(api_string_t* name, + api_list_u8_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_set_global_value(api_string_t* name, + api_list_u8_t* bson_element, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_setup_emit(int64_t* maybe_byte_limit, + exports_mongo_mozjs_mozjs_ok_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); +bool exports_mongo_mozjs_mozjs_drain_emit_buffer(api_list_u8_t* ret, + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* err); + +// Helper Functions + +void api_option_string_free(api_option_string_t* ptr); + +void exports_mongo_mozjs_mozjs_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_wasm_mozjs_error_t* ptr); + +void exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_result_ok_wasm_mozjs_error_t* ptr); + +void api_list_u8_free(api_list_u8_t* ptr); + +void exports_mongo_mozjs_mozjs_result_function_handle_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_result_function_handle_wasm_mozjs_error_t* ptr); + +void exports_mongo_mozjs_mozjs_result_bool_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_result_bool_wasm_mozjs_error_t* ptr); + +void exports_mongo_mozjs_mozjs_result_list_u8_wasm_mozjs_error_free( + exports_mongo_mozjs_mozjs_result_list_u8_wasm_mozjs_error_t* ptr); + +void api_option_s64_free(api_option_s64_t* ptr); + +// Sets the string `ret` to reference the input string `s` without copying it +void api_string_set(api_string_t* ret, const char* s); + +// Creates a copy of the input nul-terminated string `s` and +// stores it into the component model string `ret`. +void api_string_dup(api_string_t* ret, const char* s); + +// Creates a copy of the input string `s` with length `len` and +// stores it into the component model string `ret`. +// The length is specified in code units (bytes for UTF-8, 16-bit values for UTF-16). +void api_string_dup_n(api_string_t* ret, const char* s, size_t len); + +// Deallocates the string pointed to by `ret`, deallocating +// the memory behind the string. +void api_string_free(api_string_t* ret); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/mongo/scripting/mozjs/wasm/wit_gen/generated/api_component_type.o b/src/mongo/scripting/mozjs/wasm/wit_gen/generated/api_component_type.o new file mode 100644 index 00000000000..9d649bd5282 Binary files /dev/null and b/src/mongo/scripting/mozjs/wasm/wit_gen/generated/api_component_type.o differ diff --git a/src/mongo/scripting/oid_validation.h b/src/mongo/scripting/oid_validation.h new file mode 100644 index 00000000000..9b9e74ce2af --- /dev/null +++ b/src/mongo/scripting/oid_validation.h @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/base/string_data.h" +#include "mongo/util/assert_util.h" +#include "mongo/util/ctype.h" + +#include + +namespace mongo { + +/** + * Validates that a string is a valid ObjectId string (24 hex characters). + * + * This function was extracted from Scope::validateObjectIdString to decouple + * ObjectId validation from the Scope class and engine.h dependencies. + */ +inline void validateObjectIdString(StringData str) { + uassert(10448, "invalid object id: length", str.size() == 24); + auto isAllHex = [](StringData s) { + return std::all_of(s.begin(), s.end(), [](char c) { return mongo::ctype::isXdigit(c); }); + }; + uassert(10430, "invalid object id: not hex", isAllHex(str)); +} + +} // namespace mongo diff --git a/src/mongo/shell/debugger/BUILD.bazel b/src/mongo/shell/debugger/BUILD.bazel index d0864ad6c94..d9c79ec5889 100644 --- a/src/mongo/shell/debugger/BUILD.bazel +++ b/src/mongo/shell/debugger/BUILD.bazel @@ -71,6 +71,7 @@ mongo_cc_library( private_hdrs = [ "//src/mongo/base:string_data.h", "//src/mongo/scripting:engine.h", + "//src/mongo/scripting:js_regex.h", ], deps = [ "//src/mongo:base", diff --git a/src/mongo/util/debugger.cpp b/src/mongo/util/debugger.cpp index d81a6855ed8..fd1c7810b2e 100644 --- a/src/mongo/util/debugger.cpp +++ b/src/mongo/util/debugger.cpp @@ -43,6 +43,32 @@ #include #endif +// If building for WASI/wasm, noop all debugger functionality to avoid POSIX/signal/fork issues. +// See https://github.com/WebAssembly/WASI/issues/166 +#if defined(__wasi__) || defined(__WASI__) || defined(__wasm__) || defined(__wasm32__) + +namespace mongo { + +void breakpoint() { + // no-op under WASI +} + +void setupSIGTRAPforDebugger() { + // no-op under WASI +} + +void waitForDebugger() { + // no-op under WASI +} + +bool isDebuggerActive() { + return false; +} + +} // namespace mongo + +#else // non-WASI builds + #ifndef _WIN32 namespace { std::once_flag breakpointOnceFlag; @@ -199,3 +225,5 @@ bool isDebuggerActive() { } } // namespace mongo +#endif + diff --git a/src/mongo/util/errno_util.cpp b/src/mongo/util/errno_util.cpp index 9ba6b8418ef..12c9ff6114e 100644 --- a/src/mongo/util/errno_util.cpp +++ b/src/mongo/util/errno_util.cpp @@ -38,7 +38,7 @@ #include #endif -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__wasi__) #include #endif @@ -64,6 +64,8 @@ public: std::string message(int e) const override { #ifdef _WIN32 return systemError(e).message(); +#elif defined(__wasi__) + return "getaddrinfo error " + std::to_string(e) + " (networking not supported in WASI)"; #else return gai_strerror(e); #endif diff --git a/src/mongo/util/shell_exec.cpp b/src/mongo/util/shell_exec.cpp index f50483147a8..e8b73599ede 100644 --- a/src/mongo/util/shell_exec.cpp +++ b/src/mongo/util/shell_exec.cpp @@ -228,7 +228,7 @@ private: PROCESS_INFORMATION _process; DWORD _exitcode = STILL_ACTIVE; }; -#else +#elif !defined(__wasi__) class ProcessStream { public: ProcessStream(const std::string& cmd) { @@ -291,6 +291,41 @@ private: int _fd; int _exitcode = 1; }; +#else +// WASI doesn't support process execution +// See: https://github.com/WebAssembly/WASI/issues/414 +class ProcessStream { +public: + ProcessStream(const std::string& cmd) { + uasserted(ErrorCodes::OperationFailed, + str::stream() << "Shell execution not supported in WASI environment: " << cmd); + } + + int close() { + return 1; + } + + bool eof() { + return true; + } + + Status wait(Milliseconds duration) { + (void)duration; + return {ErrorCodes::OperationFailed, "Shell execution not supported in WASI"}; + } + + void read(StringBuilder& sb, size_t len) { + (void)sb; + (void)len; + } + + ~ProcessStream() = default; + +private: + ProcessStream() = delete; + ProcessStream(const ProcessStream&) = delete; + ProcessStream& operator=(const ProcessStream&) = delete; +}; #endif } // namespace } // namespace mongo diff --git a/src/mongo/util/signal_handlers_synchronous.cpp b/src/mongo/util/signal_handlers_synchronous.cpp index 550328249c8..cd17c4bc98f 100644 --- a/src/mongo/util/signal_handlers_synchronous.cpp +++ b/src/mongo/util/signal_handlers_synchronous.cpp @@ -285,7 +285,7 @@ void myPureCallHandler() { abruptQuit(SIGABRT); } -#else +#elif !defined(__wasi__) extern "C" void abruptQuitAction(int signalNum, siginfo_t*, void*) { if (gSynchronousSignalHandlerCb) { @@ -361,6 +361,37 @@ void setupSignalTestingHandler() { #endif } +#else +// WASI doesn't support signals currently. The effort tracking the support +// for signals is https://github.com/WebAssembly/WASI/issues/414, +// See https://github.com/WebAssembly/WASI/issues/166 +extern "C" void abruptQuitAction(int signalNum, void*, void*) { + abruptQuit(signalNum); +}; + +void printSigInfo(const void* siginfo) { + (void)siginfo; +} + +extern "C" void abruptQuitWithAddrSignal(int signalNum, void* siginfo, void* ucontext_erased) { + (void)siginfo; + (void)ucontext_erased; + abruptQuit(signalNum); +} + +extern "C" void noopSignalHandler(int signalNum, void*, void*) { + (void)signalNum; +} + +extern "C" typedef void(sigAction_t)(int signum, void* info, void* context); + +void installSignalHandler(int signal, sigAction_t handler) { + (void)signal; + (void)handler; +} + +void setupSignalTestingHandler() {} + #endif } // namespace @@ -382,7 +413,7 @@ void endProcessWithSignal(int signalNum) { // The exception filter exits the process quickExit(ExitCode::abrupt); } -#else +#elif !defined(__wasi__) // This works by restoring the system-default handler for the given signal, unblocking the // signal, and re-raising it, in order to get the system default termination behavior (i.e., // dumping core, or just exiting). @@ -398,6 +429,10 @@ void endProcessWithSignal(int signalNum) { invariant(sigprocmask(SIG_UNBLOCK, &unblockSignalMask, nullptr) == 0); raise(signalNum); +#else + // WASI doesn't support signals, just exit with the appropriate code + (void)signalNum; + quickExit(ExitCode::abrupt); #endif } @@ -410,7 +445,7 @@ void setupSynchronousSignalHandlers() { _set_purecall_handler(myPureCallHandler); _set_invalid_parameter_handler(myInvalidParameterHandler); setWindowsUnhandledExceptionFilter(); -#else +#elif !defined(__wasi__) static constexpr struct { int signal; sigAction_t* function; // signal ignored if nullptr @@ -435,6 +470,8 @@ void setupSynchronousSignalHandlers() { #if defined(MONGO_STACKTRACE_CAN_DUMP_ALL_THREADS) setupStackTraceSignalAction(stackTraceSignal()); #endif +#else + // WASI doesn't support signals, so signal setup is a no-op #endif } @@ -451,7 +488,7 @@ void reportOutOfMemoryErrorAndExit() { } void clearSignalMask() { -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__wasi__) // We need to make sure that all signals are unmasked so signals are handled correctly sigset_t unblockSignalMask; invariant(sigemptyset(&unblockSignalMask) == 0); diff --git a/src/mongo/util/stacktrace_posix.cpp b/src/mongo/util/stacktrace_posix.cpp index c92188ae158..bbd11d3581d 100644 --- a/src/mongo/util/stacktrace_posix.cpp +++ b/src/mongo/util/stacktrace_posix.cpp @@ -27,7 +27,9 @@ * it in the license file. */ +#ifndef __wasi__ #include +#endif #include // IWYU pragma: no_include "cxxabi.h" @@ -241,6 +243,7 @@ void printMetadata(StackTraceSink& sink, const StackTraceAddressMetadata& meta) } void mergeDlInfo(StackTraceAddressMetadata& f) { +#ifndef __wasi__ if (f.file() && f.symbol()) return; Dl_info dli; @@ -258,6 +261,10 @@ void mergeDlInfo(StackTraceAddressMetadata& f) { if (!f.symbol() && dli.dli_saddr) { f.symbol().assign(reinterpret_cast(dli.dli_saddr), dli.dli_sname); } +#else + // WASI doesn't support dynamic linking, so we can't get symbol info + (void)f; +#endif } #if MONGO_STACKTRACE_BACKEND == MONGO_STACKTRACE_BACKEND_LIBUNWIND diff --git a/src/mongo/util/time_support.cpp b/src/mongo/util/time_support.cpp index d180922ac70..9643d33cb72 100644 --- a/src/mongo/util/time_support.cpp +++ b/src/mongo/util/time_support.cpp @@ -637,7 +637,8 @@ private: // Find minimum timer resolution of OS Nanoseconds getMinimumTimerResolution() { Nanoseconds minTimerResolution; -#if defined(__linux__) || defined(__FreeBSD__) || defined(__EMSCRIPTEN__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__EMSCRIPTEN__) || defined(__wasi__) || \ + defined(__wasm__) || defined(__wasm32__) || defined(__wasm64__) struct timespec tp; int ret = clock_getres(CLOCK_REALTIME, &tp); if (ret == -1) { diff --git a/src/third_party/IntelRDFPMathLib20U1/BUILD.bazel b/src/third_party/IntelRDFPMathLib20U1/BUILD.bazel index f3a9c359f88..e775149d618 100644 --- a/src/third_party/IntelRDFPMathLib20U1/BUILD.bazel +++ b/src/third_party/IntelRDFPMathLib20U1/BUILD.bazel @@ -22,20 +22,30 @@ INTEL_RDFP_MATHLIB_DEFINES = [ "DECIMAL_CALL_BY_REFERENCE=0", "DECIMAL_GLOBAL_ROUNDING=0", "DECIMAL_GLOBAL_EXCEPTION_FLAGS=0", - "UNCHANGED_BINARY_STATUS_FLAGS=0", "USE_COMPILER_F128_TYPE=0", "USE_COMPILER_F80_TYPE=0", "USE_NATIVE_QUAD_TYPE=0", ] + select({ + # WASI/WASM: treat as Linux-like. + # Do NOT define UNCHANGED_BINARY_STATUS_FLAGS so that + # BID_OPT_RESTORE_BINARY_FLAGS() becomes a no-op (WASI has no fenv.h). + "@platforms//os:wasi": [ + "LINUX=1", + "linux=1", + ], "@platforms//os:linux": [ "LINUX=1", "linux=1", + "UNCHANGED_BINARY_STATUS_FLAGS=0", ], "@platforms//os:macos": [ "LINUX=1", "mach=1", + "UNCHANGED_BINARY_STATUS_FLAGS=0", + ], + "//conditions:default": [ + "UNCHANGED_BINARY_STATUS_FLAGS=0", ], - "//conditions:default": [], }) + select({ "@platforms//cpu:s390x": [ "s390x=1", diff --git a/src/third_party/boost/BUILD.bazel b/src/third_party/boost/BUILD.bazel index 0442a8a2de9..55fdee41ddb 100644 --- a/src/third_party/boost/BUILD.bazel +++ b/src/third_party/boost/BUILD.bazel @@ -217,6 +217,10 @@ mongo_cc_library( "libs/thread/src/pthread/once.cpp", "libs/thread/src/pthread/thread.cpp", ], + "@platforms//os:wasi": [ + "libs/thread/src/pthread/once.cpp", + "libs/thread/src/pthread/thread.cpp", + ], "@platforms//os:windows": [ "libs/thread/src/win32/thread.cpp", "libs/thread/src/win32/thread_primitives.cpp", @@ -231,6 +235,9 @@ mongo_cc_library( "@platforms//os:macos": [ "libs/thread/src/pthread/once_atomic.cpp", ], + "@platforms//os:wasi": [ + "libs/thread/src/pthread/once_atomic.cpp", + ], "//conditions:default": [], }), copts = BOOST_COPTS, @@ -242,6 +249,9 @@ mongo_cc_library( "@platforms//os:macos": [ "BOOST_THREAD_PTHREAD", ], + "@platforms//os:wasi": [ + "BOOST_THREAD_PTHREAD", + ], "@platforms//os:windows": [ "BOOST_THREAD_WIN32", ], @@ -287,9 +297,7 @@ mongo_cc_library( "libs/log/src/trivial.cpp", ] + select({ "@platforms//os:windows": [], - "//conditions:default": [ - "libs/log/src/syslog_backend.cpp", - ], + "//conditions:default": ["libs/log/src/syslog_backend.cpp"], }), hdrs = BOOST_LIB_HEADERS, copts = BOOST_COPTS + select({ diff --git a/src/third_party/fmt/BUILD.bazel b/src/third_party/fmt/BUILD.bazel index ee3d7a6e863..4fc149ea183 100644 --- a/src/third_party/fmt/BUILD.bazel +++ b/src/third_party/fmt/BUILD.bazel @@ -6,8 +6,11 @@ mongo_cc_library( name = "fmt", srcs = [ "dist/src/format.cc", - "dist/src/os.cc", - ], + ] + select({ + # os.cc uses POSIX I/O (dup, dup2, pipe) not available in WASI + "@platforms//os:wasi": [], + "//conditions:default": ["dist/src/os.cc"], + }), hdrs = glob([ "dist/include/fmt/*.h", ]), @@ -21,4 +24,9 @@ mongo_cc_library( "//conditions:default": [], }), includes = ["dist/include"], + local_defines = select({ + # Disable POSIX file operations for WASI + "@platforms//os:wasi": ["FMT_USE_FCNTL=0"], + "//conditions:default": [], + }), )