Skip to content

Commit b4fa9d7

Browse files
committed
refactor: compile builtins as function bodies with a fixed binding parameter
Adopts Node's internals idiom: every builtin is compiled via ScriptCompiler::CompileFunction with the single parameter `binding`, and natives arrive as properties of one bag object that each file destructures at the top (const { isRuntimeRunloop } = binding). The visible IIFE wrappers are gone, top-level return is the way a builtin hands a value back to C++, and the bytecode cache moves to CreateCodeCacheForFunction. The parameter name is fixed and hardcoded in BuiltinLoader, so there is no per-builtin name to mistype in C++; on the JS side an ESLint gate (no-undef with `binding` and the reachable native globals declared) catches typos in the destructures, wired into lint-staged. Conventions are documented in NativeScript/runtime/js/README.md.
1 parent 885abe9 commit b4fa9d7

15 files changed

Lines changed: 526 additions & 429 deletions

NativeScript/runtime/BuiltinLoader.cpp

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ namespace {
1515
std::mutex builtinCacheMutex;
1616
std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)];
1717

18-
} // namespace
18+
// Every builtin is compiled as a function body receiving this single, fixed
19+
// parameter (Node's internalBinding idiom): natives arrive as properties of
20+
// one bag object and each file destructures what it needs.
21+
constexpr const char* kBindingParamName = "binding";
1922

20-
MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context,
21-
BuiltinId id) {
23+
MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
2224
Isolate* isolate = v8::Isolate::GetCurrent();
2325
const BuiltinSource& builtin = GetBuiltinSource(id);
2426
const unsigned index = static_cast<unsigned>(id);
@@ -43,41 +45,60 @@ MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context,
4345
);
4446
Local<v8::String> sourceText = tns::ToV8String(
4547
isolate, builtin.source, static_cast<int>(builtin.length));
48+
Local<v8::String> params[] = {tns::ToV8String(isolate, kBindingParamName)};
4649

47-
Local<Script> script;
50+
Local<v8::Function> fn;
4851
if (!blob.empty()) {
4952
// The Source owns and deletes the CachedData object; BufferNotOwned keeps
5053
// the underlying bytes (our copy) out of its hands.
5154
auto* cachedData = new ScriptCompiler::CachedData(
5255
blob.data(), static_cast<int>(blob.size()),
5356
ScriptCompiler::CachedData::BufferNotOwned);
5457
ScriptCompiler::Source source(sourceText, origin, cachedData);
55-
if (ScriptCompiler::Compile(context, &source,
56-
ScriptCompiler::kConsumeCodeCache)
57-
.ToLocal(&script) &&
58+
if (ScriptCompiler::CompileFunction(context, &source, 1, params, 0, nullptr,
59+
ScriptCompiler::kConsumeCodeCache)
60+
.ToLocal(&fn) &&
5861
!cachedData->rejected) {
59-
return script->Run(context);
62+
return fn;
6063
}
6164
// Rejected cache (e.g. produced under different flags): fall through and
6265
// recompile eagerly so the refreshed blob covers inner functions again.
6366
}
6467

6568
ScriptCompiler::Source source(sourceText, origin);
66-
if (!ScriptCompiler::Compile(context, &source, ScriptCompiler::kEagerCompile)
67-
.ToLocal(&script)) {
68-
return MaybeLocal<Value>();
69+
if (!ScriptCompiler::CompileFunction(context, &source, 1, params, 0, nullptr,
70+
ScriptCompiler::kEagerCompile)
71+
.ToLocal(&fn)) {
72+
return MaybeLocal<v8::Function>();
6973
}
7074

7175
std::unique_ptr<ScriptCompiler::CachedData> produced(
72-
ScriptCompiler::CreateCodeCache(script->GetUnboundScript()));
76+
ScriptCompiler::CreateCodeCacheForFunction(fn));
7377
if (produced != nullptr && produced->data != nullptr &&
7478
produced->length > 0) {
7579
std::lock_guard<std::mutex> lock(builtinCacheMutex);
7680
builtinCache[index].assign(produced->data,
7781
produced->data + produced->length);
7882
}
7983

80-
return script->Run(context);
84+
return fn;
85+
}
86+
87+
} // namespace
88+
89+
MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context,
90+
BuiltinId id,
91+
Local<Value> binding) {
92+
Isolate* isolate = v8::Isolate::GetCurrent();
93+
94+
Local<v8::Function> fn;
95+
if (!CompileBuiltin(context, id).ToLocal(&fn)) {
96+
return MaybeLocal<Value>();
97+
}
98+
99+
Local<Value> args[] = {binding.IsEmpty() ? v8::Undefined(isolate).As<Value>()
100+
: binding};
101+
return fn->Call(context, v8::Undefined(isolate), 1, args);
81102
}
82103

83104
} // namespace tns

NativeScript/runtime/BuiltinLoader.h

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ namespace tns {
88

99
class BuiltinLoader {
1010
public:
11-
// Compiles and runs the builtin script identified by id in the given context
12-
// and returns its completion value. Scripts carry an "internal/<name>.js"
13-
// origin so runtime frames are identifiable in stack traces. Compilation goes
14-
// through a process-wide bytecode cache: the first run in the process
15-
// compiles eagerly and populates the cache, later isolates (workers) consume
16-
// it instead of re-parsing the source.
17-
static v8::MaybeLocal<v8::Value> RunBuiltin(v8::Local<v8::Context> context,
18-
BuiltinId id);
11+
// Compiles the builtin identified by id as a function body with the single
12+
// fixed parameter `binding` (Node's internalBinding idiom), calls it with
13+
// the given bag of natives (or undefined when omitted), and returns its
14+
// return value. Scripts carry an "internal/<name>.js" origin so runtime
15+
// frames are identifiable in stack traces. Compilation goes through a
16+
// process-wide bytecode cache: the first run in the process compiles
17+
// eagerly and populates the cache, later isolates (workers) consume it
18+
// instead of re-parsing the source.
19+
static v8::MaybeLocal<v8::Value> RunBuiltin(
20+
v8::Local<v8::Context> context, BuiltinId id,
21+
v8::Local<v8::Value> binding = v8::Local<v8::Value>());
1922
};
2023

2124
} // namespace tns

NativeScript/runtime/ErrorEvents.cpp

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,40 +23,42 @@ static void NativeReportFatalCallback(const FunctionCallbackInfo<Value>& info) {
2323
}
2424

2525
void ErrorEvents::Init(Local<Context> context) {
26-
// WHATWG error-events layer, layered on top of the generic event primitives
27-
// installed by Events::Init. Plain (module-free) script, strict inside the
28-
// IIFE, ES5-ish so it never depends on other runtime extensions. The IIFE is
29-
// invoked with two arguments — the internal EventTarget backing the global
30-
// (so native dispatch survives app code overwriting globalThis.dispatchEvent)
31-
// and the native nativeReportFatal(error, stack) function that runs the
32-
// terminal tail — and returns three closures bound to that backing store.
33-
// ErrorEvent/PromiseRejectionEvent subclass the Event captured off globalThis
34-
// at init time, which runs before any user code.
26+
// WHATWG error-events layer (internal/error-events.js), layered on top of
27+
// the generic event primitives installed by Events::Init. The builtin
28+
// receives — via its binding bag — the internal EventTarget backing the
29+
// global (so native dispatch survives app code overwriting
30+
// globalThis.dispatchEvent) and the native nativeReportFatal(error, stack)
31+
// function that runs the terminal tail, and returns three closures bound to
32+
// that backing store. ErrorEvent/PromiseRejectionEvent subclass the Event
33+
// captured off globalThis at init time, which runs before any user code.
3534
Isolate* isolate = v8::Isolate::GetCurrent();
3635

3736
auto cache = Caches::Get(isolate);
3837
tns::Assert(cache != nullptr && cache->GlobalEventTarget != nullptr, isolate);
3938
Local<Object> globalTarget = cache->GlobalEventTarget->Get(isolate);
4039

41-
Local<Value> result;
42-
bool success = BuiltinLoader::RunBuiltin(context, BuiltinId::kErrorEvents)
43-
.ToLocal(&result);
44-
tns::Assert(success && result->IsFunction(), isolate);
45-
46-
Local<v8::Function> iife = result.As<v8::Function>();
47-
4840
Local<v8::Function> nativeReportFatal;
49-
success = v8::Function::New(context, NativeReportFatalCallback)
50-
.ToLocal(&nativeReportFatal);
41+
bool success = v8::Function::New(context, NativeReportFatalCallback)
42+
.ToLocal(&nativeReportFatal);
5143
tns::Assert(success, isolate);
5244

53-
Local<Value> installArgs[] = {globalTarget, nativeReportFatal};
54-
Local<Value> iifeResult;
55-
success = iife->Call(context, context->Global(), 2, installArgs)
56-
.ToLocal(&iifeResult);
57-
tns::Assert(success && iifeResult->IsArray(), isolate);
45+
Local<Object> binding = Object::New(isolate);
46+
success =
47+
binding
48+
->Set(context, tns::ToV8String(isolate, "globalTarget"), globalTarget)
49+
.FromMaybe(false) &&
50+
binding
51+
->Set(context, tns::ToV8String(isolate, "nativeReportFatal"),
52+
nativeReportFatal)
53+
.FromMaybe(false);
54+
tns::Assert(success, isolate);
55+
56+
Local<Value> result;
57+
success = BuiltinLoader::RunBuiltin(context, BuiltinId::kErrorEvents, binding)
58+
.ToLocal(&result);
59+
tns::Assert(success && result->IsArray(), isolate);
5860

59-
Local<v8::Array> closures = iifeResult.As<v8::Array>();
61+
Local<v8::Array> closures = result.As<v8::Array>();
6062
Local<Value> errorFn, rejectionFn, handledFn;
6163
tns::Assert(
6264
closures->Get(context, 0).ToLocal(&errorFn) && errorFn->IsFunction(),

NativeScript/runtime/Events.cpp

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ using namespace v8;
99
namespace tns {
1010

1111
void Events::Init(Local<Context> context) {
12-
// Generic WHATWG event primitives. Plain (module-free) script, strict inside
13-
// the IIFE, ES5-ish so it never depends on other runtime extensions. The IIFE
14-
// installs Event/EventTarget and the global EventTarget methods, then returns
15-
// the internal EventTarget instance backing the global so native dispatch
12+
// Generic WHATWG event primitives (internal/events.js). The builtin installs
13+
// Event/EventTarget and the global EventTarget methods, then returns the
14+
// internal EventTarget instance backing the global so native dispatch
1615
// survives app code overwriting globalThis.dispatchEvent. The error-events
1716
// layer (ErrorEvents::Init) runs immediately after and installs the native
1817
// listener-error reporter through _installListenerErrorReporter.
@@ -21,18 +20,11 @@ void Events::Init(Local<Context> context) {
2120
Local<Value> result;
2221
bool success =
2322
BuiltinLoader::RunBuiltin(context, BuiltinId::kEvents).ToLocal(&result);
24-
tns::Assert(success && result->IsFunction(), isolate);
25-
26-
Local<v8::Function> iife = result.As<v8::Function>();
27-
28-
Local<Value> iifeResult;
29-
success =
30-
iife->Call(context, context->Global(), 0, nullptr).ToLocal(&iifeResult);
31-
tns::Assert(success && iifeResult->IsObject(), isolate);
23+
tns::Assert(success && result->IsObject(), isolate);
3224

3325
auto cache = Caches::Get(isolate);
34-
cache->GlobalEventTarget = std::make_unique<Persistent<v8::Object>>(
35-
isolate, iifeResult.As<Object>());
26+
cache->GlobalEventTarget =
27+
std::make_unique<Persistent<v8::Object>>(isolate, result.As<Object>());
3628
}
3729

3830
} // namespace tns

NativeScript/runtime/PromiseProxy.cpp

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,22 @@ static void IsRuntimeRunloopCallback(const FunctionCallbackInfo<Value>& args) {
2424
void PromiseProxy::Init(v8::Local<v8::Context> context) {
2525
Isolate* isolate = v8::Isolate::GetCurrent();
2626

27-
Local<Value> result;
28-
bool success = BuiltinLoader::RunBuiltin(context, BuiltinId::kPromiseProxy)
29-
.ToLocal(&result);
30-
tns::Assert(success && result->IsFunction(), isolate);
31-
32-
Local<v8::Function> installProxy = result.As<v8::Function>();
33-
3427
Local<v8::Function> isRuntimeRunloop;
35-
success = v8::Function::New(context, IsRuntimeRunloopCallback)
36-
.ToLocal(&isRuntimeRunloop);
28+
bool success = v8::Function::New(context, IsRuntimeRunloopCallback)
29+
.ToLocal(&isRuntimeRunloop);
3730
tns::Assert(success, isolate);
3831

39-
Local<Value> installArgs[] = {isRuntimeRunloop};
40-
Local<Value> installResult;
41-
success = installProxy->Call(context, context->Global(), 1, installArgs)
42-
.ToLocal(&installResult);
32+
Local<Object> binding = Object::New(isolate);
33+
success = binding
34+
->Set(context, tns::ToV8String(isolate, "isRuntimeRunloop"),
35+
isRuntimeRunloop)
36+
.FromMaybe(false);
37+
tns::Assert(success, isolate);
38+
39+
Local<Value> result;
40+
success =
41+
BuiltinLoader::RunBuiltin(context, BuiltinId::kPromiseProxy, binding)
42+
.ToLocal(&result);
4343
tns::Assert(success, isolate);
4444
}
4545

NativeScript/runtime/js/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Runtime builtins
2+
3+
The `.js` files in this directory are the runtime's internal JavaScript. At
4+
build time the "Generate RuntimeBuiltins" Xcode phase runs `tools/js2c.mjs`,
5+
which embeds them into `NativeScript/runtime/generated/RuntimeBuiltins.cpp`;
6+
at runtime `BuiltinLoader::RunBuiltin` compiles and executes them with an
7+
`internal/<name>.js` script origin and a process-wide bytecode cache.
8+
9+
## Contract (Node's internalBinding idiom)
10+
11+
Every file is compiled as a **function body** via `v8::ScriptCompiler::CompileFunction`
12+
with one fixed parameter:
13+
14+
```js
15+
const { someNative, anotherNative } = binding;
16+
```
17+
18+
- `binding` is a plain object of natives built by the C++ call site; a file
19+
that needs nothing from C++ simply doesn't mention it.
20+
- Because the file is a function body, **top-level `return` is legal** — a
21+
builtin's return value is what `RunBuiltin` hands back to C++ (used to
22+
return factory functions and init results).
23+
- Strict mode is per-file: start the file with `"use strict";` to opt in.
24+
- Destructure `binding` once, at the top of the file, so the file's native
25+
dependencies are visible and greppable.
26+
27+
## Rules
28+
29+
- Run at isolate init, before any user code: capture any global you rely on
30+
(e.g. `globalThis.Event`) eagerly so later monkey-patching can't break you.
31+
- No `import`/`export` — these are classic function bodies, not modules.
32+
- ESLint (`eslint.config.mjs` at the repo root, run by lint-staged) declares
33+
`binding` and the reachable native globals; `no-undef` is the typo net.
34+
If a builtin starts using a new native global, add it there.
35+
- File names are kebab-case; the name determines the `BuiltinId` enum value
36+
(`promise-proxy.js``kPromiseProxy`) and the script origin. New files must
37+
also be added to `tools/js2c-inputs.xcfilelist`.

NativeScript/runtime/js/class-extends.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
(function() {
21
function __extends(d, b) {
32
for (var p in b) {
43
if (b.hasOwnProperty(p)) {
@@ -10,4 +9,3 @@
109
(__.prototype = b.prototype, new __());
1110
}
1211
return __extends;
13-
})()

0 commit comments

Comments
 (0)