diff --git a/docs/ns-builtin-modules.md b/docs/ns-builtin-modules.md index fde20fdc7..c2d66a5e4 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 @@ -729,16 +785,19 @@ resolvers read the same table differently: - The **`ns:`/`node:` resolver** — the app-facing one, behind `require()`, `import` and `import()` — serves only rows *not* marked internal-only. An internal-only specifier fails exactly as a name absent from the table does. - Seven rows are public today: `ns:module`, `ns:runtime`, `ns:util`, - `node:module`, `node:url`, `node:util`, `node:worker_threads`. + Nine rows are public today: `ns:interop`, `ns:interop/android`, `ns:module`, + `ns:runtime`, `ns:util`, `node:module`, `node:url`, `node:util`, + `node:worker_threads`. - The **internal require** builtins receive (previous section) is the only - thing that can name an internal-only row. Six rows are marked that way: + thing that can name an internal-only row. Seven rows are marked that way: `internal/broadcast-channel`, `internal/dom-exception`, `internal/events`, - `internal/message-channel`, `internal/message-event`, - `internal/worker-events`. Their exports carry capabilities app code must not - hold — listener-accounting hook keys, the error-reporter setter, the key a - worker's rebuilt error travels under, base classes that must be the - runtime's own rather than whatever a global currently names. + `internal/interop-globals`, `internal/message-channel`, + `internal/message-event`, `internal/worker-events`. Their exports carry + capabilities app code must not hold — listener-accounting hook keys, the + error-reporter setter, the key a worker's rebuilt error travels under, base + classes that must be the runtime's own rather than whatever a global + currently names — or, for `internal/interop-globals`, install the legacy + globals. - Builtins with **no row at all** (the intrinsics snapshot, the require factory, the console formatter) are invoked straight from their native call sites. There is no specifier that could reach them and nothing to mark. @@ -777,8 +836,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 4bf18777f..d39ad51f0 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -103,6 +103,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 eb2ca29ac..bdac8c19d 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -76,6 +76,7 @@ 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}/message-channel.js ${RUNTIME_BUILTIN_JS_DIR}/message-event.js @@ -83,6 +84,8 @@ set(RUNTIME_BUILTIN_JS ${RUNTIME_BUILTIN_JS_DIR}/node-url.js ${RUNTIME_BUILTIN_JS_DIR}/node-util.js ${RUNTIME_BUILTIN_JS_DIR}/node-worker-threads.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 803a0805c..594bd6db6 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -975,9 +975,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 239522cae..5edb4c069 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 69c8b5c49..997adcfd7 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 "Messaging.h" #include "ModuleInternalCallbacks.h" #include "NativeScriptAssert.h" @@ -27,6 +30,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); @@ -54,6 +58,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}, @@ -66,6 +72,7 @@ constexpr Registration kRegistry[] = { {"internal/dom-exception", BuiltinId::kDomException, serialization::DomExceptionBinding, true}, {"internal/events", BuiltinId::kEvents, nullptr, true}, + {"internal/interop-globals", BuiltinId::kInteropGlobals, nullptr, true}, {"internal/message-channel", BuiltinId::kMessageChannel, messaging::CreateBinding, true}, {"internal/message-event", BuiltinId::kMessageEvent, nullptr, true}, {"internal/worker-events", BuiltinId::kWorkerEvents, nullptr, true}, @@ -182,6 +189,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); @@ -197,12 +217,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 40533fcda..b5addb8eb 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 a55687fed..743e36ab9 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 dd45ea3d4..1ccd3a204 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" @@ -1107,8 +1105,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. @@ -1139,8 +1135,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); @@ -1201,8 +1203,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 5c4665d45..cbf5ecdc3 100644 --- a/test-app/runtime/src/main/cpp/js/README.md +++ b/test-app/runtime/src/main/cpp/js/README.md @@ -70,6 +70,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);