diff --git a/docs/ns-builtin-modules.md b/docs/ns-builtin-modules.md index ce4a5290d..e2fb5e8f3 100644 --- a/docs/ns-builtin-modules.md +++ b/docs/ns-builtin-modules.md @@ -95,13 +95,22 @@ Node's single-quoted style. ### `ns:runtime` -Runtime-level configuration. Keys, value domains, and scope are defined and -validated natively; the module surface is a thin frozen wrapper. +Runtime-level configuration and runtime plumbing. Keys, value domains, and +scope are defined and validated natively; the module surface is a thin frozen +wrapper. | export | description | |---|---| | `setConfig(key, value)` | Sets a runtime config key. Throws `TypeError` on an unknown key, an invalid value, or (for process-wide keys) when called from a worker isolate. | | `getConfig(key)` | Returns the current value of a config key. Throws `TypeError` on an unknown key. Readable from any isolate. | +| `escapeException(x)` | Returns an `Error` branded so that, when it is thrown back across a native callback boundary, the native caller receives the original native exception `x` carries rather than the runtime's wrapper exception. Idempotent: a branded error is returned unchanged. Throws `TypeError` when called with no argument. The global `interop.escapeException` is this same function (see [Legacy globals](#legacy-globals)). | + +*Platform note (Android):* the original exception is the Java throwable `x` +wraps, or its `nativeException`; a Java `catch` of the concrete type then +matches instead of receiving a `com.tns.NativeScriptException`. A branded error +with no underlying throwable escapes as a `com.tns.NativeScriptException` +carrying the JS message and stack, as an unbranded one does, but bypasses +`discardUncaughtJsExceptions`. Config keys: @@ -170,6 +179,53 @@ a release build that cannot be diagnosed. `TNS.esm`, `TNS.fetch`, `TNS.registry` — so `adb logcat -s TNS.esm` can filter them without matching message text. +### `ns:interop` + +The cross-platform interop surface: working with native values, as opposed to +finding native symbols. It exists on every platform with whatever members that +platform supports, so a feature check (`"Pointer" in interop`) is how portable +code asks; a member a platform lacks is absent, never present-but-throwing. + +On Android the namespace is **empty**: Android has none of the native-memory +members iOS exports here (`Pointer`, `Reference`, `sizeof`, …). It is still a +frozen module, so `import * as interop from "ns:interop"` resolves everywhere. + +### `ns:interop/android` + +Java-specific interop helpers. Android only: on other platforms the specifier +is not registered and fails like any unknown builtin. + +| export | description | +|---|---| +| `long(x)`, `byte(x)`, `short(x)`, `char(x)`, `float(x)`, `double(x)` | Mark a JS value as the named Java primitive, for overload resolution and argument conversion: `method(long("123456789012"))` picks the `long` overload and passes the full 64-bit value. Each takes exactly one argument. | +| `newArray(type, length)` | Creates a Java array of `length` elements. `type` is a primitive type name (`"int"`, `"char"`, `"boolean"`, …), a fully qualified Java class name (`"java.lang.String"`), or a Java class constructor (`java.lang.String`). Throws on any other arity, a non-integer or negative length, or a class name that does not resolve. | + +```js +import { long, newArray } from "ns:interop/android"; + +const bytes = newArray("byte", 1024); // byte[1024] +const names = newArray(java.lang.String, 3); // String[3] +someJavaObject.setTimestamp(long(Date.now())); +``` + +### Legacy globals + +Before these modules existed the same functions were reachable only as +globals, and app code and `@nativescript/core` read them at module top level. +They remain, installed before any app code runs, as **aliases of the module +exports** — the very same function objects, per realm: + +| global | module export | +|---|---| +| `interop.escapeException` | `ns:runtime`'s `escapeException` | +| `long`, `byte`, `short`, `char`, `float`, `double` (Android) | `ns:interop/android`'s members of those names | +| `Array.create` (Android) | `ns:interop/android`'s `newArray` | + +The global `interop` object, the cast globals and `Array.create` are ordinary +writable, enumerable, configurable data properties. On Android the global +`interop` has exactly one member, `escapeException`; it is a plain object, not +the `ns:interop` namespace. New code should import from the modules. + ### `ns:module` The module-loader control surface: import-map vocabulary, registry @@ -724,8 +780,9 @@ separate loading paths**, not one registry with a per-entry flag: - The **public registry** is a table mapping specifier → builtin, and it is the only thing the `ns:`/`node:` resolver consults. A specifier absent from it - does not resolve, full stop. Today it holds six entries: `ns:module`, - `ns:runtime`, `ns:util`, `node:module`, `node:url`, `node:util`. + does not resolve, full stop. Today it holds eight entries: `ns:interop`, + `ns:interop/android`, `ns:module`, `ns:runtime`, `ns:util`, `node:module`, + `node:url`, `node:util`. - **Internal builtins** (the intrinsics snapshot, the require factory, the console formatter, and so on) are invoked directly from their own native call sites. They are never named in the public registry, so there is no specifier @@ -765,8 +822,14 @@ internal require is a fixed parameter of the builtin function wrapper The files under `js/` that are *not* in the public registry — `primordials.js`, `require-factory.js`, `inspect.js`, `json-helper.js`, `events.js`, `error-events.js`, `structured-clone.js`, `blob-url.js`, `performance.js`, -`weak-ref.js` — are the internal builtins: each is run from its own native call -site and none is named in the registry table. +`weak-ref.js`, `interop-globals.js` — are the internal builtins: each is run +from its own native call site and none is named in the public registry. + +`interop-globals.js` is the [legacy globals](#legacy-globals) layer. It runs +eagerly from `Runtime::PrepareV8Runtime` in every isolate, requires +`ns:runtime` and `ns:interop/android` through the internal require and assigns +their functions to the globals, so the compat knowledge lives only in that +consumer and neither module refers to it. Per-realm builtin state — the exports objects, the synthetic modules, the in-progress set that produces the circular-require error, the cached `format`, diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 0d0f26200..243203616 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -96,6 +96,7 @@ require('./tests/testInspect'); // The ns:/node: builtin modules require('./tests/testNsUtil'); require('./tests/testNsRuntime'); +require('./tests/testNsInterop'); require('./tests/testNsModule'); // Remote-module security gate (debug builds allow remote modules outright) require('./tests/testRemoteModuleSecurity'); diff --git a/test-app/app/src/main/assets/app/testNsInteropImport.mjs b/test-app/app/src/main/assets/app/testNsInteropImport.mjs new file mode 100644 index 000000000..af6607f71 --- /dev/null +++ b/test-app/app/src/main/assets/app/testNsInteropImport.mjs @@ -0,0 +1,7 @@ +// Static `import` of the interop builtins, so the ES module resolve callback +// handles a specifier with a path segment (`ns:interop/android`). +import interopDefault from "ns:interop"; +import androidDefault, { long, newArray } from "ns:interop/android"; +import { escapeException } from "ns:runtime"; + +export { interopDefault, androidDefault, long, newArray, escapeException }; diff --git a/test-app/app/src/main/assets/app/tests/nsInteropWorker.js b/test-app/app/src/main/assets/app/tests/nsInteropWorker.js new file mode 100644 index 000000000..df939115c --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/nsInteropWorker.js @@ -0,0 +1,14 @@ +// The compat globals are assembled per isolate, so a worker's globals must be +// its own module instance's functions. +onmessage = function () { + var android = require("ns:interop/android"); + var runtime = require("ns:runtime"); + postMessage({ + casts: ["long", "byte", "short", "char", "float", "double"].every(function (name) { + return globalThis[name] === android[name]; + }), + newArray: Array.create === android.newArray, + escapeException: interop.escapeException === runtime.escapeException, + arrayLength: android.newArray("int", 4).length, + }); +}; diff --git a/test-app/app/src/main/assets/app/tests/testNsInterop.js b/test-app/app/src/main/assets/app/tests/testNsInterop.js new file mode 100644 index 000000000..aceb2dcc1 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testNsInterop.js @@ -0,0 +1,150 @@ +var CAST_NAMES = ["long", "byte", "short", "char", "float", "double"]; + +describe("ns:interop", function () { + var nsInterop = require("ns:interop"); + + // Android has none of the cross-platform interop members iOS exports. + it("is an empty frozen namespace", function () { + expect(Object.isFrozen(nsInterop)).toBe(true); + expect(Object.keys(nsInterop)).toEqual([]); + }); + + it("is a singleton across require calls", function () { + expect(require("ns:interop")).toBe(nsInterop); + }); +}); + +describe("ns:interop/android", function () { + var android = require("ns:interop/android"); + + it("exposes exactly the declared surface, frozen", function () { + expect(Object.isFrozen(android)).toBe(true); + expect(Object.keys(android).sort()).toEqual( + ["byte", "char", "double", "float", "long", "newArray", "short"]); + Object.keys(android).forEach(function (name) { + expect(typeof android[name]).toBe("function"); + }); + }); + + it("is a singleton across require calls", function () { + expect(require("ns:interop/android")).toBe(android); + }); + + it("backs the cast globals with the same function objects", function () { + CAST_NAMES.forEach(function (name) { + expect(globalThis[name]).toBe(android[name]); + }); + }); + + it("backs Array.create with newArray", function () { + expect(Array.create).toBe(android.newArray); + }); + + it("keeps the globals ordinary writable, enumerable, configurable properties", function () { + CAST_NAMES.forEach(function (name) { + var descriptor = Object.getOwnPropertyDescriptor(globalThis, name); + expect(descriptor.writable).toBe(true); + expect(descriptor.enumerable).toBe(true); + expect(descriptor.configurable).toBe(true); + }); + var create = Object.getOwnPropertyDescriptor(Array, "create"); + expect(create.writable).toBe(true); + expect(create.enumerable).toBe(true); + expect(create.configurable).toBe(true); + }); + + it("marks numbers for Java overload resolution", function () { + var n = new com.tns.tests.NumericConversionTest(); + expect(n.method5(android.long("123456789012"))).toBe("long=123456789012"); + expect(n.method5(android.float(1.23))).toBe("float=1.23"); + expect(n.method5(android.double(1))).toBe("double=1.0"); + expect(n.method2(android.double(65536 + 2))).toBe("double=65538.0"); + expect(n.method2(android.byte(65536 + 2))).toBe("float=2.0"); + expect(n.method2(android.short(65536 + 2))).toBe("float=2.0"); + }); + + it("rejects a cast with the wrong arity", function () { + expect(function () { + android.long(); + }).toThrow(); + }); + + it("creates Java arrays from a primitive type name or a class", function () { + var ints = android.newArray("int", 3); + expect(ints.length).toBe(3); + expect(ints.getClass().getName()).toBe("[I"); + + var strings = android.newArray(java.lang.String, 2); + expect(strings.length).toBe(2); + expect(strings.getClass().getName()).toBe("[Ljava.lang.String;"); + }); + + it("rejects bad newArray arguments", function () { + expect(function () { + android.newArray("int"); + }).toThrow(); + expect(function () { + android.newArray("int", -1); + }).toThrow(); + }); + + it("resolves through a static import", function (done) { + import("~/testNsInteropImport.mjs").then(function (module) { + expect(module.interopDefault).toBe(require("ns:interop")); + expect(module.androidDefault).toBe(android); + expect(module.long).toBe(android.long); + expect(module.newArray).toBe(android.newArray); + expect(module.escapeException).toBe(require("ns:runtime").escapeException); + done(); + }, function (error) { + fail("static import of ns:interop/android should resolve: " + error); + done(); + }); + }); + + it("resolves through a dynamic import", function (done) { + import("ns:interop/android").then(function (namespace) { + expect(namespace.default).toBe(android); + expect(namespace.long).toBe(android.long); + expect(namespace.newArray).toBe(android.newArray); + done(); + }, function (error) { + fail("dynamic import of ns:interop/android should resolve: " + error); + done(); + }); + }); + + it("gives a worker its own instance behind its own globals", function (done) { + var worker = new Worker("./nsInteropWorker.js"); + worker.onmessage = function (msg) { + expect(msg.data).toEqual({ + casts: true, + newArray: true, + escapeException: true, + arrayLength: 4, + }); + worker.terminate(); + done(); + }; + worker.onerror = function (error) { + fail("worker failed: " + error.message); + worker.terminate(); + done(); + }; + worker.postMessage("go"); + }); +}); + +describe("interop global", function () { + it("holds exactly ns:runtime's escapeException", function () { + expect(Object.keys(interop)).toEqual(["escapeException"]); + expect(interop.escapeException).toBe(require("ns:runtime").escapeException); + }); + + it("is an ordinary writable, enumerable, configurable global", function () { + var descriptor = Object.getOwnPropertyDescriptor(globalThis, "interop"); + expect(descriptor.writable).toBe(true); + expect(descriptor.enumerable).toBe(true); + expect(descriptor.configurable).toBe(true); + }); +}); diff --git a/test-app/app/src/main/assets/app/tests/testNsRuntime.js b/test-app/app/src/main/assets/app/tests/testNsRuntime.js index f57d1c5bd..3542f0622 100644 --- a/test-app/app/src/main/assets/app/tests/testNsRuntime.js +++ b/test-app/app/src/main/assets/app/tests/testNsRuntime.js @@ -5,12 +5,13 @@ describe("ns:runtime", function () { expect(Object.isFrozen(runtime)).toBe(true); expect(typeof runtime.setConfig).toBe("function"); expect(typeof runtime.getConfig).toBe("function"); + expect(typeof runtime.escapeException).toBe("function"); }); // The export set is public API, declared alongside docs/ns-builtin-modules.md // — all of them change together. it("exposes exactly the declared surface", function () { - expect(Object.keys(runtime).sort()).toEqual(["getConfig", "setConfig"]); + expect(Object.keys(runtime).sort()).toEqual(["escapeException", "getConfig", "setConfig"]); }); it("rejects unknown keys", function () { diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index f638b92a1..aa9afd850 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -75,10 +75,13 @@ set(RUNTIME_BUILTIN_JS ${RUNTIME_BUILTIN_JS_DIR}/error-events.js ${RUNTIME_BUILTIN_JS_DIR}/events.js ${RUNTIME_BUILTIN_JS_DIR}/inspect.js + ${RUNTIME_BUILTIN_JS_DIR}/interop-globals.js ${RUNTIME_BUILTIN_JS_DIR}/json-helper.js ${RUNTIME_BUILTIN_JS_DIR}/node-module.js ${RUNTIME_BUILTIN_JS_DIR}/node-url.js ${RUNTIME_BUILTIN_JS_DIR}/node-util.js + ${RUNTIME_BUILTIN_JS_DIR}/ns-interop-android.js + ${RUNTIME_BUILTIN_JS_DIR}/ns-interop.js ${RUNTIME_BUILTIN_JS_DIR}/ns-module.js ${RUNTIME_BUILTIN_JS_DIR}/ns-runtime.js ${RUNTIME_BUILTIN_JS_DIR}/ns-util.js diff --git a/test-app/runtime/src/main/cpp/ArrayHelper.cpp b/test-app/runtime/src/main/cpp/ArrayHelper.cpp index 668993dbd..fee06dd19 100644 --- a/test-app/runtime/src/main/cpp/ArrayHelper.cpp +++ b/test-app/runtime/src/main/cpp/ArrayHelper.cpp @@ -3,6 +3,7 @@ #include "ArgConverter.h" #include "NativeScriptException.h" #include "Runtime.h" +#include #include using namespace v8; @@ -12,7 +13,7 @@ using namespace tns; ArrayHelper::ArrayHelper() { } -void ArrayHelper::Init(const Local& context) { +void ArrayHelper::ResolveRuntimeMethods() { JEnv env; RUNTIME_CLASS = env.FindClass("com/tns/Runtime"); @@ -20,19 +21,15 @@ void ArrayHelper::Init(const Local& context) { CREATE_ARRAY_HELPER = env.GetStaticMethodID(RUNTIME_CLASS, "createArrayHelper", "(Ljava/lang/String;I)Ljava/lang/Object;"); NS_CHECK(CREATE_ARRAY_HELPER != nullptr); +} + +MaybeLocal ArrayHelper::CreateNewArray(Local context) { + // Every isolate's binding gets here, workers on their own threads. + static std::once_flag resolved; + std::call_once(resolved, ResolveRuntimeMethods); auto isolate = v8::Isolate::GetCurrent(); - auto global = context->Global(); - auto arr = global->Get(context, ArgConverter::ConvertToV8String(isolate, "Array")); - - if (!arr.IsEmpty()) { - Local arrVal; - auto success = arr.ToLocal(&arrVal); - if (success) { - auto arrayObj = arrVal.As(); - arrayObj->Set(context, ArgConverter::ConvertToV8String(isolate, "create"), FunctionTemplate::New(isolate, CreateJavaArrayCallback)->GetFunction(context).ToLocalChecked()); - } - } + return FunctionTemplate::New(isolate, CreateJavaArrayCallback)->GetFunction(context); } void ArrayHelper::CreateJavaArrayCallback(const FunctionCallbackInfo& info) { diff --git a/test-app/runtime/src/main/cpp/ArrayHelper.h b/test-app/runtime/src/main/cpp/ArrayHelper.h index b32744174..9fadf0ee2 100644 --- a/test-app/runtime/src/main/cpp/ArrayHelper.h +++ b/test-app/runtime/src/main/cpp/ArrayHelper.h @@ -8,7 +8,11 @@ namespace tns { class ArrayHelper { public: - static void Init(const v8::Local& context); + /* + * A new Java array factory, `newArray(type, length)`, for the + * `ns:interop/android` binding. + */ + static v8::MaybeLocal CreateNewArray(v8::Local context); private: ArrayHelper(); @@ -21,6 +25,8 @@ class ArrayHelper { static jobject CreateArrayByClassName(const std::string& typeName, int length); + static void ResolveRuntimeMethods(); + static jclass RUNTIME_CLASS; static jmethodID CREATE_ARRAY_HELPER; diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp index 00bd2576c..b48fb23d8 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -973,9 +973,8 @@ void CallbackHandlers::ExitMethodCallback(const v8::FunctionCallbackInfo &globalTemplate) { - castFunctions.CreateGlobalCastFunctions(isolate, globalTemplate); +bool CallbackHandlers::AddCastFunctions(Local context, Local target) { + return castFunctions.AddCastFunctions(context, target); } vector CallbackHandlers::GetTypeMetadata(const string &name, int index) { diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.h b/test-app/runtime/src/main/cpp/CallbackHandlers.h index 86dba64a5..7aa3a062e 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.h +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.h @@ -93,8 +93,7 @@ namespace tns { static void ExitMethodCallback(const v8::FunctionCallbackInfo &args); - static void CreateGlobalCastFunctions(v8::Isolate *isolate, - const v8::Local &globalTemplate); + static bool AddCastFunctions(v8::Local context, v8::Local target); static std::vector GetTypeMetadata(const std::string &name, int index); diff --git a/test-app/runtime/src/main/cpp/Interop.cpp b/test-app/runtime/src/main/cpp/Interop.cpp index 644aff4a8..04bc26a3e 100644 --- a/test-app/runtime/src/main/cpp/Interop.cpp +++ b/test-app/runtime/src/main/cpp/Interop.cpp @@ -4,7 +4,6 @@ #include "ArgConverter.h" #include "JniLocalRef.h" -#include "NativeScriptException.h" #include "ObjectManager.h" #include "Runtime.h" #include "V8StringConstants.h" @@ -173,24 +172,8 @@ static void EscapeExceptionCallback(const FunctionCallbackInfo& info) { info.GetReturnValue().Set(errObj); } -void Interop::Init(Local context) { - auto isolate = v8::Isolate::GetCurrent(); - auto global = context->Global(); - - auto interop = Object::New(isolate); - - Local escapeException; - if (!Function::New(context, EscapeExceptionCallback).ToLocal(&escapeException)) { - throw NativeScriptException("Interop::Init: failed to create escapeException"); - } - - if (!interop->Set(context, ArgConverter::ConvertToV8String(isolate, "escapeException"), - escapeException) - .FromMaybe(false) || - !global->Set(context, ArgConverter::ConvertToV8String(isolate, "interop"), interop) - .FromMaybe(false)) { - throw NativeScriptException("Interop::Init: failed to install the interop global"); - } +MaybeLocal Interop::CreateEscapeException(Local context) { + return Function::New(context, EscapeExceptionCallback); } static string GetPayloadString(Local context, Local payload, diff --git a/test-app/runtime/src/main/cpp/Interop.h b/test-app/runtime/src/main/cpp/Interop.h index b5dec0310..c6bab561c 100644 --- a/test-app/runtime/src/main/cpp/Interop.h +++ b/test-app/runtime/src/main/cpp/Interop.h @@ -7,10 +7,8 @@ namespace tns { /* - * The `interop` global, mirroring the iOS runtime's interop namespace. On - * Android it currently exposes a single member: - * - * interop.escapeException(x) + * escapeException(x), exported by `ns:runtime` (and by the legacy `interop` + * global, which js/interop-globals.js assembles from that module). * * Brands an error so the JS->Java boundary (NativeScriptException:: * ReThrowToJava) forwards the ORIGINAL Java throwable carried by `x` (its @@ -25,7 +23,7 @@ namespace tns { class Interop { public: /* - * The brand payload of an interop.escapeException(...) error, decoded at + * The brand payload of an escapeException(...) error, decoded at * the JS->Java boundary. `original` (a new local reference the caller may * env.Throw() directly) is nullptr for a branded error that carries no * Java throwable - the synthesized-escape case. @@ -37,15 +35,14 @@ class Interop { std::string message; /* The JS stack of the escaped error itself (may be empty). */ std::string stack; - /* The JS stack of the interop.escapeException(...) call site. */ + /* The JS stack of the escapeException(...) call site. */ std::string escapeSiteStack; }; /* - * Installs the `interop` object on the global. Evaluated once per isolate - * during PrepareV8Runtime, for both the main and worker isolates. + * A new escapeException function for the `ns:runtime` binding. */ - static void Init(v8::Local context); + static v8::MaybeLocal CreateEscapeException(v8::Local context); /* * Decodes the brand payload of `errObj` into `out`. Returns true when the diff --git a/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp b/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp index 467917d93..c7cf7aff9 100644 --- a/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp +++ b/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp @@ -5,7 +5,10 @@ #include #include "ArgConverter.h" +#include "ArrayHelper.h" #include "BuiltinLoader.h" +#include "CallbackHandlers.h" +#include "Interop.h" #include "ModuleInternalCallbacks.h" #include "NativeScriptAssert.h" #include "Runtime.h" @@ -25,6 +28,7 @@ constexpr const char* kNsPrefix = "ns:"; constexpr const char* kNodePrefix = "node:"; /* Defined below, each next to the natives it gathers. */ +MaybeLocal NsInteropAndroidBinding(Local context); MaybeLocal NsModuleBinding(Local context); MaybeLocal NsRuntimeBinding(Local context); MaybeLocal NsUtilBinding(Local context); @@ -52,6 +56,8 @@ struct Registration { * standard module never carries compatibility code. */ constexpr Registration kRegistry[] = { + {"ns:interop", BuiltinId::kNsInterop, nullptr}, + {"ns:interop/android", BuiltinId::kNsInteropAndroid, NsInteropAndroidBinding}, {"ns:module", BuiltinId::kNsModule, NsModuleBinding}, {"ns:runtime", BuiltinId::kNsRuntime, NsRuntimeBinding}, {"ns:util", BuiltinId::kNsUtil, NsUtilBinding}, @@ -60,6 +66,7 @@ constexpr Registration kRegistry[] = { {"node:util", BuiltinId::kNodeUtil, nullptr}, {"internal/dom-exception", BuiltinId::kDomException, nullptr, true}, {"internal/events", BuiltinId::kEvents, nullptr, true}, + {"internal/interop-globals", BuiltinId::kInteropGlobals, nullptr, true}, }; constexpr const char* kDebugKey = "debug"; @@ -173,6 +180,19 @@ RealmState* GetRealm(Isolate* isolate) { return RuntimeState::For(isolate); } +MaybeLocal NsInteropAndroidBinding(Local context) { + Isolate* isolate = v8::Isolate::GetCurrent(); + Local binding = Object::New(isolate); + Local newArray; + if (!CallbackHandlers::AddCastFunctions(context, binding) || + !ArrayHelper::CreateNewArray(context).ToLocal(&newArray) || + !binding->Set(context, ArgConverter::ConvertToV8String(isolate, "newArray"), newArray) + .FromMaybe(false)) { + return MaybeLocal(); + } + return binding; +} + MaybeLocal NsModuleBinding(Local context) { Isolate* isolate = v8::Isolate::GetCurrent(); Local binding = Object::New(isolate); @@ -188,12 +208,16 @@ MaybeLocal NsModuleBinding(Local context) { MaybeLocal NsRuntimeBinding(Local context) { Isolate* isolate = v8::Isolate::GetCurrent(); Local binding = Object::New(isolate); - Local setConfig, getConfig; + Local setConfig, getConfig, escapeException; if (!v8::Function::New(context, SetConfigCallback).ToLocal(&setConfig) || !v8::Function::New(context, GetConfigCallback).ToLocal(&getConfig) || + !Interop::CreateEscapeException(context).ToLocal(&escapeException) || !binding->Set(context, ArgConverter::ConvertToV8String(isolate, "setConfig"), setConfig) .FromMaybe(false) || !binding->Set(context, ArgConverter::ConvertToV8String(isolate, "getConfig"), getConfig) + .FromMaybe(false) || + !binding->Set(context, ArgConverter::ConvertToV8String(isolate, "escapeException"), + escapeException) .FromMaybe(false)) { return MaybeLocal(); } diff --git a/test-app/runtime/src/main/cpp/NumericCasts.cpp b/test-app/runtime/src/main/cpp/NumericCasts.cpp index ba1806292..165099ee3 100644 --- a/test-app/runtime/src/main/cpp/NumericCasts.cpp +++ b/test-app/runtime/src/main/cpp/NumericCasts.cpp @@ -11,15 +11,30 @@ using namespace v8; using namespace std; using namespace tns; -void NumericCasts::CreateGlobalCastFunctions(Isolate* isolate, const Local& globalTemplate) { +bool NumericCasts::AddCastFunctions(Local context, Local target) { + auto isolate = v8::Isolate::GetCurrent(); auto ext = External::New(isolate, this, v8::kExternalPointerTypeTagDefault); - globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "long"), FunctionTemplate::New(isolate, NumericCasts::MarkAsLongCallbackStatic, ext)); - globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "byte"), FunctionTemplate::New(isolate, NumericCasts::MarkAsByteCallbackStatic, ext)); - globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "short"), FunctionTemplate::New(isolate, NumericCasts::MarkAsShortCallbackStatic, ext)); - globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "double"), FunctionTemplate::New(isolate, NumericCasts::MarkAsDoubleCallbackStatic, ext)); - globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "float"), FunctionTemplate::New(isolate, NumericCasts::MarkAsFloatCallbackStatic, ext)); - globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "char"), FunctionTemplate::New(isolate, NumericCasts::MarkAsCharCallbackStatic, ext)); + const struct { + const char* name; + FunctionCallback callback; + } casts[] = { + {"long", NumericCasts::MarkAsLongCallbackStatic}, + {"byte", NumericCasts::MarkAsByteCallbackStatic}, + {"short", NumericCasts::MarkAsShortCallbackStatic}, + {"double", NumericCasts::MarkAsDoubleCallbackStatic}, + {"float", NumericCasts::MarkAsFloatCallbackStatic}, + {"char", NumericCasts::MarkAsCharCallbackStatic}, + }; + + for (const auto& cast : casts) { + Local func; + if (!FunctionTemplate::New(isolate, cast.callback, ext)->GetFunction(context).ToLocal(&func) || + !target->Set(context, ArgConverter::ConvertToV8String(isolate, cast.name), func).FromMaybe(false)) { + return false; + } + } + return true; } CastType NumericCasts::GetCastType(Isolate* isolate, const Local& object) { diff --git a/test-app/runtime/src/main/cpp/NumericCasts.h b/test-app/runtime/src/main/cpp/NumericCasts.h index d66ee69c5..243438b41 100644 --- a/test-app/runtime/src/main/cpp/NumericCasts.h +++ b/test-app/runtime/src/main/cpp/NumericCasts.h @@ -17,7 +17,11 @@ enum class CastType { class NumericCasts { public: - void CreateGlobalCastFunctions(v8::Isolate* isolate, const v8::Local& globalTemplate); + /* + * Sets long, byte, short, char, float and double on target, as new + * functions bound to this instance. + */ + bool AddCastFunctions(v8::Local context, v8::Local target); static CastType GetCastType(v8::Isolate* isolate, const v8::Local& object); diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index d7ab015e8..00db45d61 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -11,7 +11,6 @@ #include #include "ArgConverter.h" -#include "ArrayHelper.h" #include "BuiltinLoader.h" #include "CallbackHandlers.h" #include "Constants.h" @@ -22,7 +21,6 @@ #include "File.h" #include "FrameCallbacks.h" #include "HttpLoader.h" -#include "Interop.h" #include "IsolateTracked.h" #include "JType.h" #include "JsArgToArrayConverter.h" @@ -989,8 +987,6 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, SimpleProfiler::Init(isolate, globalTemplate); - CallbackHandlers::CreateGlobalCastFunctions(isolate, globalTemplate); - // Lazy web globals (TextEncoder/TextDecoder, atob/btoa): registered on the // template so the builtins behind them run only on first use, in every // isolate — workers included. @@ -1017,8 +1013,14 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, // mark/measure `detail` is cloned through the structuredClone global. Performance::Init(context); - // The `interop` global (interop.escapeException), mirroring iOS. - Interop::Init(context); + // The legacy interop globals (`interop`, the numeric casts, Array.create), + // assembled from ns:runtime and ns:interop/android. Builtins run above this + // point cannot use them. + if (BuiltinLoader::GetExports(context, BuiltinId::kInteropGlobals, nullptr) + .IsEmpty()) { + throw NativeScriptException( + "PrepareV8Runtime: the interop globals bootstrap failed"); + } m_objectManager->Init(isolate); @@ -1079,8 +1081,6 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, MetadataNode::CreateTopLevelNamespaces(isolate, global); - ArrayHelper::Init(context); - m_arrayBufferHelper.CreateConvertFunctions(context, global, m_objectManager); this->m_context = new Persistent(isolate, context); diff --git a/test-app/runtime/src/main/cpp/js/README.md b/test-app/runtime/src/main/cpp/js/README.md index 8e0b5f5a0..548d267f9 100644 --- a/test-app/runtime/src/main/cpp/js/README.md +++ b/test-app/runtime/src/main/cpp/js/README.md @@ -64,6 +64,13 @@ module.exports = somethingTheCallSiteNeeds; - `node-module.js` re-exports `ns:module`'s `createRequire` as the `node:module` shim, and `node-url.js` is the `node:url` shim (`fileURLToPath` / `pathToFileURL`), the one shim with no `ns:` counterpart to adapt. +- `ns-interop.js` is `ns:interop` (empty on Android) and `ns-interop-android.js` + is `ns:interop/android` (the numeric casts and `newArray`). The legacy + globals they back — `interop`, `long`…`double`, `Array.create` — are + installed by `interop-globals.js`, an eager internal-tier builtin that + requires both modules (and `ns:runtime` for `escapeException`): the same + one-file-per-surface split as a `node:` shim, with the compat file as the + only consumer that knows the globals exist. - Destructure `binding` and `primordials` once, at the top of the file, so the file's dependencies are visible and greppable. diff --git a/test-app/runtime/src/main/cpp/js/interop-globals.js b/test-app/runtime/src/main/cpp/js/interop-globals.js new file mode 100644 index 000000000..4a2266e94 --- /dev/null +++ b/test-app/runtime/src/main/cpp/js/interop-globals.js @@ -0,0 +1,34 @@ +"use strict"; + +// The legacy interop globals, assembled from the modules that own them: +// `interop.escapeException` from ns:runtime, the numeric casts and +// `Array.create` from ns:interop/android. Each global holds the module's own +// function object, never a wrapper, so the two spellings cannot drift. +// +// Runs eagerly from Runtime::PrepareV8Runtime, before any app code: +// @nativescript/core reads these at module top level. They stay ordinary +// writable, enumerable, configurable data properties. + +const { ObjectDefineProperty } = primordials; +const { escapeException } = require("ns:runtime"); +const android = require("ns:interop/android"); + +function define(target, name, value) { + ObjectDefineProperty(target, name, { + value, + writable: true, + enumerable: true, + configurable: true, + }); +} + +define(globalThis, "interop", { escapeException }); + +define(globalThis, "long", android.long); +define(globalThis, "byte", android.byte); +define(globalThis, "short", android.short); +define(globalThis, "char", android.char); +define(globalThis, "float", android.float); +define(globalThis, "double", android.double); + +define(Array, "create", android.newArray); diff --git a/test-app/runtime/src/main/cpp/js/ns-interop-android.js b/test-app/runtime/src/main/cpp/js/ns-interop-android.js new file mode 100644 index 000000000..f873aaf21 --- /dev/null +++ b/test-app/runtime/src/main/cpp/js/ns-interop-android.js @@ -0,0 +1,18 @@ +"use strict"; + +// The `ns:interop/android` builtin module: Java-specific interop helpers +// (docs/ns-builtin-modules.md). The numeric casts mark a JS value as a Java +// primitive for overload resolution and argument conversion; `newArray` +// creates a Java array of a primitive type name or a Java class. + +const { long, byte, short, char, float, double, newArray } = binding; +const { ObjectFreeze } = primordials; + +exports.long = long; +exports.byte = byte; +exports.short = short; +exports.char = char; +exports.float = float; +exports.double = double; +exports.newArray = newArray; +ObjectFreeze(exports); diff --git a/test-app/runtime/src/main/cpp/js/ns-interop.js b/test-app/runtime/src/main/cpp/js/ns-interop.js new file mode 100644 index 000000000..568e60b76 --- /dev/null +++ b/test-app/runtime/src/main/cpp/js/ns-interop.js @@ -0,0 +1,11 @@ +"use strict"; + +// The `ns:interop` builtin module: the cross-platform interop surface +// (docs/ns-builtin-modules.md). It exists on every platform with whatever +// members that platform supports; Android has none of the native-memory +// pieces iOS exports here, so the namespace is empty. Java-specific helpers +// live in `ns:interop/android`. + +const { ObjectFreeze } = primordials; + +ObjectFreeze(exports); diff --git a/test-app/runtime/src/main/cpp/js/ns-runtime.js b/test-app/runtime/src/main/cpp/js/ns-runtime.js index fc026722e..b7e678bd4 100644 --- a/test-app/runtime/src/main/cpp/js/ns-runtime.js +++ b/test-app/runtime/src/main/cpp/js/ns-runtime.js @@ -1,14 +1,15 @@ "use strict"; -// The `ns:runtime` builtin module: runtime-level configuration and (future) -// runtime introspection. See docs/ns-builtin-modules.md for the contract and -// the key registry — keys, their value domains, and their scope (process-wide -// vs per-isolate) are defined and validated on the native side, so this file +// The `ns:runtime` builtin module: runtime-level configuration and runtime +// plumbing. See docs/ns-builtin-modules.md for the contract and the key +// registry — keys, their value domains, and their scope (process-wide vs +// per-isolate) are defined and validated on the native side, so this file // stays a thin, frozen surface. -const { setConfig, getConfig } = binding; +const { setConfig, getConfig, escapeException } = binding; const { ObjectFreeze } = primordials; exports.setConfig = setConfig; exports.getConfig = getConfig; +exports.escapeException = escapeException; ObjectFreeze(exports);