Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 69 additions & 6 deletions docs/ns-builtin-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`,
Expand Down
1 change: 1 addition & 0 deletions test-app/app/src/main/assets/app/mainpage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
7 changes: 7 additions & 0 deletions test-app/app/src/main/assets/app/testNsInteropImport.mjs
Original file line number Diff line number Diff line change
@@ -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 };
14 changes: 14 additions & 0 deletions test-app/app/src/main/assets/app/tests/nsInteropWorker.js
Original file line number Diff line number Diff line change
@@ -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,
});
};
150 changes: 150 additions & 0 deletions test-app/app/src/main/assets/app/tests/testNsInterop.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
3 changes: 2 additions & 1 deletion test-app/app/src/main/assets/app/tests/testNsRuntime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
3 changes: 3 additions & 0 deletions test-app/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 9 additions & 12 deletions test-app/runtime/src/main/cpp/ArrayHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "ArgConverter.h"
#include "NativeScriptException.h"
#include "Runtime.h"
#include <mutex>
#include <sstream>

using namespace v8;
Expand All @@ -12,27 +13,23 @@ using namespace tns;
ArrayHelper::ArrayHelper() {
}

void ArrayHelper::Init(const Local<Context>& context) {
void ArrayHelper::ResolveRuntimeMethods() {
JEnv env;

RUNTIME_CLASS = env.FindClass("com/tns/Runtime");
NS_CHECK(RUNTIME_CLASS != nullptr);

CREATE_ARRAY_HELPER = env.GetStaticMethodID(RUNTIME_CLASS, "createArrayHelper", "(Ljava/lang/String;I)Ljava/lang/Object;");
NS_CHECK(CREATE_ARRAY_HELPER != nullptr);
}

MaybeLocal<Function> ArrayHelper::CreateNewArray(Local<Context> 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<Value> arrVal;
auto success = arr.ToLocal(&arrVal);
if (success) {
auto arrayObj = arrVal.As<Object>();
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<Value>& info) {
Expand Down
8 changes: 7 additions & 1 deletion test-app/runtime/src/main/cpp/ArrayHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
namespace tns {
class ArrayHelper {
public:
static void Init(const v8::Local<v8::Context>& context);
/*
* A new Java array factory, `newArray(type, length)`, for the
* `ns:interop/android` binding.
*/
static v8::MaybeLocal<v8::Function> CreateNewArray(v8::Local<v8::Context> context);

private:
ArrayHelper();
Expand All @@ -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;
Expand Down
5 changes: 2 additions & 3 deletions test-app/runtime/src/main/cpp/CallbackHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -973,9 +973,8 @@ void CallbackHandlers::ExitMethodCallback(const v8::FunctionCallbackInfo<v8::Val
exit(-1);
}

void CallbackHandlers::CreateGlobalCastFunctions(Isolate *isolate,
const Local<ObjectTemplate> &globalTemplate) {
castFunctions.CreateGlobalCastFunctions(isolate, globalTemplate);
bool CallbackHandlers::AddCastFunctions(Local<Context> context, Local<Object> target) {
return castFunctions.AddCastFunctions(context, target);
}

vector<string> CallbackHandlers::GetTypeMetadata(const string &name, int index) {
Expand Down
Loading
Loading