diff --git a/integration-tests/environments/browser/package.json b/integration-tests/environments/browser/package.json index 846b5bec8f2..23df6967d1f 100644 --- a/integration-tests/environments/browser/package.json +++ b/integration-tests/environments/browser/package.json @@ -12,7 +12,10 @@ "wireit": { "test": { "command": "mocha-remote -- concurrently npm:vite npm:runner", - "dependencies": ["../../../packages/realm:build:ts"] + "dependencies": [ + "../../../packages/realm:build:ts", + "../../../packages/realm:prebuild-wasm" + ] } }, "dependencies": { @@ -33,4 +36,4 @@ "puppeteer": "^22.12.0", "vite": "^5.2.0" } -} +} \ No newline at end of file diff --git a/integration-tests/tests/src/browser/index.ts b/integration-tests/tests/src/browser/index.ts index 13a8f0408a2..046dc18e94d 100644 --- a/integration-tests/tests/src/browser/index.ts +++ b/integration-tests/tests/src/browser/index.ts @@ -20,6 +20,9 @@ if (typeof navigator.userAgent !== "string") { throw new Error("This file is only supposed to be imported from a browser environment!"); } +import { ready } from "realm"; +await ready; + // Import all the regular tests first import "./setup-globals"; diff --git a/integration-tests/tests/src/setup-globals.ts b/integration-tests/tests/src/setup-globals.ts index 28a38e52872..f25989a3c55 100644 --- a/integration-tests/tests/src/setup-globals.ts +++ b/integration-tests/tests/src/setup-globals.ts @@ -74,8 +74,7 @@ describe("Test Harness", function (this: Mocha.Suite) { Context.prototype.longTimeout = longTimeout; }); -import Realm, { ready } from "realm"; -await ready; +import Realm from "realm"; // Disable the logger to avoid console flooding const { defaultLogLevel = "off" } = environment; diff --git a/packages/realm/bindgen/src/realm_js_wasm_helpers.h b/packages/realm/bindgen/src/realm_js_wasm_helpers.h new file mode 100644 index 00000000000..da52629406f --- /dev/null +++ b/packages/realm/bindgen/src/realm_js_wasm_helpers.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +namespace realm::js::wasm { +namespace { + +REALM_NOINLINE inline emscripten::val toEmscriptenErrorCode(const std::error_code& e) noexcept +{ + REALM_ASSERT_RELEASE(e); + auto jsErr = emscripten::val::global("Error")(emscripten::val(e.message())); + jsErr.set("code", e.value()); + jsErr.set("category", e.category().name()); + + return jsErr; +} +REALM_NOINLINE inline emscripten::val toEmscriptenException(const std::exception& e) noexcept +{ + return emscripten::val::global("Error")(std::string(e.what())); +} +REALM_NOINLINE inline emscripten::val toEmscriptenException(const std::exception_ptr& e) noexcept +{ + try { + std::rethrow_exception(e); + } + catch (const std::exception& e) { + return toEmscriptenException(e); + } + catch (...) { + return emscripten::val::global("Error")(std::string("Unknown error")); + } +} +// Allocate a new C++ buffer big enough to fit the JS buffer +// Create a JS memory view around the C++ buffer +// Call TypedArray.prototype.set to efficiently copy the JS buffer into the C++ buffer via the view +REALM_NOINLINE inline std::string toBinaryData(const emscripten::val array_buffer) noexcept +{ + REALM_ASSERT(array_buffer.instanceof (emscripten::val::global("ArrayBuffer"))); + std::string buf; + buf.resize(array_buffer["byteLength"].as()); + + emscripten::val mv(emscripten::typed_memory_view(buf.length(), buf.data())); + mv.call("set", emscripten::val::global("Uint8Array").new_(array_buffer)); + + return buf; +} +REALM_NOINLINE inline OwnedBinaryData toOwnedBinaryData(const emscripten::val array_buffer) noexcept +{ + REALM_ASSERT(array_buffer.instanceof (emscripten::val::global("ArrayBuffer"))); + auto length = array_buffer["byteLength"].as(); + + std::unique_ptr buf(new char[length]); + + emscripten::val mv(emscripten::typed_memory_view(length, buf.get())); + mv.call("set", emscripten::val::global("Uint8Array").new_(array_buffer)); + + return OwnedBinaryData(std::move(buf), length); +} +} // namespace +} // namespace realm::js::wasm diff --git a/packages/realm/bindgen/src/templates/wasm.ts b/packages/realm/bindgen/src/templates/wasm.ts index 1f1f41504fb..8b872be0875 100644 --- a/packages/realm/bindgen/src/templates/wasm.ts +++ b/packages/realm/bindgen/src/templates/wasm.ts @@ -169,6 +169,9 @@ function convertPrimToEmscripten(addon: BrowserAddon, type: string, expr: string case "uint64_t": return `emscripten::val(${expr})`; + case "std::chrono::milliseconds": + return `emscripten::val(std::chrono::milliseconds(${expr}).count())`; + case "StringData": case "std::string_view": case "std::string": @@ -259,6 +262,9 @@ function convertPrimFromEmscripten(addon: BrowserAddon, type: string, expr: stri case "uint64_t": return `${expr}.as()`; + case "std::chrono::milliseconds": + return `std::chrono::milliseconds(${expr}.as())`; + case "std::string": return `${addon.get()}->wrapString((${expr}).as())`; @@ -341,7 +347,7 @@ function convertToEmscripten(addon: BrowserAddon, type: Type, expr: string): str case "Nullable": { return `[&] (auto&& val) { return !val ? emscripten::val::null() : ${c(inner, "FWD(val)")}; }(${expr})`; } - case "util::Optional": + case "std::optional": return `[&] (auto&& opt) { return !opt ? emscripten::val::undefined() : ${c(inner, "*FWD(opt)")}; }(${expr})`; case "std::vector": return `[&] (auto&& vec) { @@ -454,7 +460,7 @@ function convertFromEmscripten(addon: BrowserAddon, type: Type, expr: string): s inner, "val", )}; }(${expr})`; - case "util::Optional": + case "std::optional": return `[&] (emscripten::val val) { return val.isUndefined() ? ${type.toCpp()}() : ${c(inner, "val")}; }(${expr})`; @@ -498,6 +504,7 @@ function convertFromEmscripten(addon: BrowserAddon, type: Type, expr: string): s return out; }(${expr})`; case "AsyncCallback": + case "util::UniqueFunction": case "std::function": return `${type.toCpp()}(${c(inner, expr)})`; } @@ -876,7 +883,7 @@ class BrowserCppDecls extends CppDecls { outputDefsTo(out: (...parts: string[]) => void) { super.outputDefsTo(out); out(` - void browser_init() + void wasm_init() { if (!RealmAddon::self) { RealmAddon::self = std::make_unique(); @@ -900,7 +907,7 @@ class BrowserCppDecls extends CppDecls { // }); out(`\nfunction("_internal_iterator", &_internal_iterator);`); - out(`\nfunction("browserInit", &browser_init);`); + out(`\nfunction("wasmInit", &wasm_init);`); out(`\nfunction("injectInjectables", &injectExternalTypes);`); out("\n}"); @@ -908,7 +915,7 @@ class BrowserCppDecls extends CppDecls { } export function generate({ rawSpec, spec, file: makeFile }: TemplateContext): void { - const out = makeFile("browser_init.cpp", clangFormat); + const out = makeFile("wasm_init.cpp", clangFormat); // HEADER out(`// This file is generated: Update the spec instead of editing this file directly`); @@ -920,9 +927,9 @@ export function generate({ rawSpec, spec, file: makeFile }: TemplateContext): vo out(` #include #include - #include + #include - namespace realm::js::browser { + namespace realm::js::wasm { namespace { `); @@ -930,6 +937,6 @@ export function generate({ rawSpec, spec, file: makeFile }: TemplateContext): vo out(` } // namespace - } // namespace realm::js::browser + } // namespace realm::js::wasm `); } diff --git a/packages/realm/binding/wasm/CMakeLists.txt b/packages/realm/binding/wasm/CMakeLists.txt index 36d71de0cf9..b2649b79d3b 100644 --- a/packages/realm/binding/wasm/CMakeLists.txt +++ b/packages/realm/binding/wasm/CMakeLists.txt @@ -55,7 +55,7 @@ bindgen( target_sources(realm-js PRIVATE wasm_init.cpp ${CMAKE_JS_SRC} ${BINDING_DIR}/wasm/platform.cpp) add_executable(realm-js-wasm) -target_link_options(realm-js-wasm PRIVATE -d -sALLOW_MEMORY_GROWTH=1 -sLLD_REPORT_UNDEFINED -sFETCH=1 -lembind -fwasm-exceptions -sEXPORT_ES6=1 -sWASM_BIGINT=1 -sENVIRONMENT=web -sSTACK_SIZE=131072 --pre-js=../web_polyfill.js) +target_link_options(realm-js-wasm PRIVATE -d -sALLOW_MEMORY_GROWTH=1 -sLLD_REPORT_UNDEFINED -sFETCH=1 -lembind -fwasm-exceptions -sEXPORT_ES6=1 -sWASM_BIGINT=1 -sENVIRONMENT=web -sSTACK_SIZE=131072) target_link_libraries(realm-js-wasm realm-js) set_target_properties(realm-js-wasm PROPERTIES diff --git a/packages/realm/binding/wasm/platform.cpp b/packages/realm/binding/wasm/platform.cpp index c772c10d344..56c8b1cfd17 100644 --- a/packages/realm/binding/wasm/platform.cpp +++ b/packages/realm/binding/wasm/platform.cpp @@ -19,14 +19,27 @@ #include #include #include +#include #include "../platform.hpp" +static std::string s_default_realm_directory; + namespace realm { +void JsPlatformHelpers::set_default_realm_file_directory(std::string dir) +{ + s_default_realm_directory = dir; +} + std::string JsPlatformHelpers::default_realm_file_directory() { - return std::string(""); + if (!s_default_realm_directory.empty()) { + return s_default_realm_directory; + } + else { + return std::string(""); + } } void JsPlatformHelpers::ensure_directory_exists_for_file(const std::string&) diff --git a/packages/realm/package.json b/packages/realm/package.json index 4814a5f6c1f..bbf84a4b1d2 100644 --- a/packages/realm/package.json +++ b/packages/realm/package.json @@ -209,6 +209,27 @@ "src/binding/wrapper.generated.ts" ] }, + "bindgen:generate:wasm-wrapper": { + "command": "realm-bindgen --template bindgen/src/templates/wasm-wrapper.ts --spec bindgen/vendor/realm-core/bindgen/spec.yml --spec bindgen/js_spec.yml --opt-in bindgen/wasm_opt_in_spec.yml --output binding/generated", + "dependencies": [ + "bindgen:generate:spec-schema" + ], + "files": [ + "bindgen/vendor/realm-core/bindgen/spec.yml", + "bindgen/vendor/realm-core/bindgen/src", + "bindgen/js_spec.yml", + "bindgen/wasm_opt_in_spec.yml", + "bindgen/src", + "!bindgen/src/templates", + "bindgen/src/templates/base-wrapper.ts", + "bindgen/src/templates/wasm-wrapper.ts" + ], + "output": [ + "binding/generated/native.wasm.mjs", + "binding/generated/native.wasm.d.mts", + "binding/generated/native.wasm.d.cts" + ] + }, "bindgen:generate:spec-schema": { "command": "typescript-json-schema bindgen/vendor/realm-core/bindgen/tsconfig.json RelaxedSpec --include bindgen/vendor/realm-core/bindgen/src/spec/relaxed-model.ts --out bindgen/vendor/realm-core/bindgen/generated/spec.schema.json --required --noExtraProps", "files": [ @@ -237,10 +258,7 @@ "command": "madge --circular --extensions ts src", "dependencies": [ "../fetch:build", - "bindgen:generate:typescript", - "bindgen:generate:node-wrapper", - "bindgen:generate:react-native-wrapper", - "bindgen:transpile" + "bindgen:wrapper" ] }, "docs": { diff --git a/packages/realm/src/platform/browser/binding.ts b/packages/realm/src/platform/browser/binding.ts new file mode 100644 index 00000000000..67d206b41d2 --- /dev/null +++ b/packages/realm/src/platform/browser/binding.ts @@ -0,0 +1,22 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import { injectAndPatch } from "../binding"; +const bindingPromise = import("../../../binding/generated/native.wasm.cjs"); + +bindingPromise.then(injectAndPatch); diff --git a/packages/realm/src/platform/browser/index.ts b/packages/realm/src/platform/browser/index.ts index d81286bcdb6..c9b91a9ee3d 100644 --- a/packages/realm/src/platform/browser/index.ts +++ b/packages/realm/src/platform/browser/index.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -// import "./binding"; +import "./binding"; import "./fs"; import "./device-info"; // import "./sync-proxy-config";