Skip to content

Commit cfb9e71

Browse files
committed
Merge remote-tracking branch 'origin/feat/ns-util' into feat/hmr-dev-sessions
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # NativeScript/runtime/Runtime.mm
2 parents 6ff01c8 + 1da68c5 commit cfb9e71

52 files changed

Lines changed: 3494 additions & 936 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ Thumbs.db
3737
# VSCode
3838
.vscode
3939

40+
# Generated by tools/js2c.mjs (Xcode "Generate RuntimeBuiltins" build phase)
41+
NativeScript/runtime/generated/
42+
4043
# Other
4144
node_modules/
4245
package-lock.json
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#include "BuiltinLoader.h"
2+
3+
#include <mutex>
4+
#include <vector>
5+
6+
#include "Caches.h"
7+
#include "Helpers.h"
8+
#include "NsBuiltinModules.h"
9+
10+
using namespace v8;
11+
12+
namespace tns {
13+
14+
namespace {
15+
16+
// Process-wide bytecode cache shared across isolates (main + workers).
17+
std::mutex builtinCacheMutex;
18+
std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)];
19+
20+
// Every builtin is compiled as a function body receiving these fixed
21+
// parameters, mirroring Node's module wrapper: a file exports through
22+
// `module.exports`/`exports`, reaches sibling builtin modules through
23+
// `require`, natives arrive as properties of the `binding` bag (Node's
24+
// internalBinding idiom) and intrinsics as properties of `primordials`; each
25+
// file destructures what it needs.
26+
constexpr const char* kExportsParamName = "exports";
27+
constexpr const char* kRequireParamName = "require";
28+
constexpr const char* kModuleParamName = "module";
29+
constexpr const char* kBindingParamName = "binding";
30+
constexpr const char* kPrimordialsParamName = "primordials";
31+
constexpr int kParamCount = 5;
32+
33+
// The `require` every builtin receives: builtin specifiers only, so a builtin
34+
// can never reach application code or the filesystem.
35+
void BuiltinRequireCallback(const FunctionCallbackInfo<Value>& info) {
36+
Isolate* isolate = info.GetIsolate();
37+
if (info.Length() < 1 || !info[0]->IsString()) {
38+
isolate->ThrowException(Exception::TypeError(
39+
tns::ToV8String(isolate, "require() expects a specifier string")));
40+
return;
41+
}
42+
43+
Local<Context> context = isolate->GetCurrentContext();
44+
std::string specifier = tns::ToString(isolate, info[0].As<v8::String>());
45+
Local<Object> exports;
46+
if (NsBuiltinModules::GetExports(context, specifier).ToLocal(&exports)) {
47+
info.GetReturnValue().Set(exports);
48+
} else if (!NsBuiltinModules::IsRegistered(specifier)) {
49+
isolate->ThrowException(Exception::Error(tns::ToV8String(
50+
isolate, NsBuiltinModules::NotFoundMessage(specifier))));
51+
}
52+
}
53+
54+
MaybeLocal<v8::Function> GetBuiltinRequire(Local<Context> context) {
55+
Isolate* isolate = v8::Isolate::GetCurrent();
56+
std::shared_ptr<Caches> cache = Caches::Get(isolate);
57+
if (cache->BuiltinRequire != nullptr) {
58+
return cache->BuiltinRequire->Get(isolate);
59+
}
60+
61+
Local<v8::Function> require;
62+
if (!v8::Function::New(context, BuiltinRequireCallback, Local<Value>(), 1,
63+
ConstructorBehavior::kThrow)
64+
.ToLocal(&require)) {
65+
return MaybeLocal<v8::Function>();
66+
}
67+
cache->BuiltinRequire =
68+
std::make_unique<Persistent<v8::Function>>(isolate, require);
69+
return require;
70+
}
71+
72+
MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
73+
Isolate* isolate = v8::Isolate::GetCurrent();
74+
const BuiltinSource& builtin = GetBuiltinSource(id);
75+
const unsigned index = static_cast<unsigned>(id);
76+
77+
// Copy the blob out so the shared slot can be refreshed concurrently while
78+
// this compile still reads from the copy.
79+
std::vector<uint8_t> blob;
80+
{
81+
std::lock_guard<std::mutex> lock(builtinCacheMutex);
82+
blob = builtinCache[index];
83+
}
84+
85+
ScriptOrigin origin(tns::ToV8String(isolate, builtin.name),
86+
0, // line offset
87+
0, // column offset
88+
false, // shared_cross_origin
89+
-1, // script_id
90+
Local<Value>(),
91+
false, // is_opaque
92+
false, // is_wasm
93+
false // is_module
94+
);
95+
Local<v8::String> sourceText = tns::ToV8String(
96+
isolate, builtin.source, static_cast<int>(builtin.length));
97+
Local<v8::String> params[] = {
98+
tns::ToV8String(isolate, kExportsParamName),
99+
tns::ToV8String(isolate, kRequireParamName),
100+
tns::ToV8String(isolate, kModuleParamName),
101+
tns::ToV8String(isolate, kBindingParamName),
102+
tns::ToV8String(isolate, kPrimordialsParamName)};
103+
104+
Local<v8::Function> fn;
105+
if (!blob.empty()) {
106+
// The Source owns and deletes the CachedData object; BufferNotOwned keeps
107+
// the underlying bytes (our copy) out of its hands.
108+
auto* cachedData = new ScriptCompiler::CachedData(
109+
blob.data(), static_cast<int>(blob.size()),
110+
ScriptCompiler::CachedData::BufferNotOwned);
111+
ScriptCompiler::Source source(sourceText, origin, cachedData);
112+
if (ScriptCompiler::CompileFunction(context, &source, kParamCount, params,
113+
0, nullptr,
114+
ScriptCompiler::kConsumeCodeCache)
115+
.ToLocal(&fn) &&
116+
!cachedData->rejected) {
117+
return fn;
118+
}
119+
// Rejected cache (e.g. produced under different flags): fall through and
120+
// recompile eagerly so the refreshed blob covers inner functions again.
121+
}
122+
123+
ScriptCompiler::Source source(sourceText, origin);
124+
if (!ScriptCompiler::CompileFunction(context, &source, kParamCount, params, 0,
125+
nullptr, ScriptCompiler::kEagerCompile)
126+
.ToLocal(&fn)) {
127+
return MaybeLocal<v8::Function>();
128+
}
129+
130+
std::unique_ptr<ScriptCompiler::CachedData> produced(
131+
ScriptCompiler::CreateCodeCacheForFunction(fn));
132+
if (produced != nullptr && produced->data != nullptr &&
133+
produced->length > 0) {
134+
std::lock_guard<std::mutex> lock(builtinCacheMutex);
135+
builtinCache[index].assign(produced->data,
136+
produced->data + produced->length);
137+
}
138+
139+
return fn;
140+
}
141+
142+
MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id,
143+
Local<Value> binding, Local<Value> primordials) {
144+
Isolate* isolate = v8::Isolate::GetCurrent();
145+
146+
Local<v8::Function> fn;
147+
if (!CompileBuiltin(context, id).ToLocal(&fn)) {
148+
return MaybeLocal<Value>();
149+
}
150+
151+
Local<v8::Function> require;
152+
if (!GetBuiltinRequire(context).ToLocal(&require)) {
153+
return MaybeLocal<Value>();
154+
}
155+
156+
Local<Object> exportsObj = Object::New(isolate);
157+
Local<Object> moduleObj = Object::New(isolate);
158+
Local<v8::String> exportsKey = tns::ToV8String(isolate, kExportsParamName);
159+
if (!moduleObj->Set(context, exportsKey, exportsObj).FromMaybe(false)) {
160+
return MaybeLocal<Value>();
161+
}
162+
163+
Local<Value> args[] = {
164+
exportsObj, require, moduleObj,
165+
binding.IsEmpty() ? v8::Undefined(isolate).As<Value>() : binding,
166+
primordials};
167+
if (fn->Call(context, v8::Undefined(isolate), kParamCount, args).IsEmpty()) {
168+
return MaybeLocal<Value>();
169+
}
170+
171+
return moduleObj->Get(context, exportsKey);
172+
}
173+
174+
// Snapshot of the intrinsics, taken the first time any builtin runs in this
175+
// isolate — during runtime init, before user code can replace a global.
176+
// Builtins compiled later in the isolate's life get the same pristine
177+
// snapshot.
178+
MaybeLocal<Object> GetPrimordials(Local<Context> context) {
179+
Isolate* isolate = v8::Isolate::GetCurrent();
180+
std::shared_ptr<Caches> cache = Caches::Get(isolate);
181+
if (cache->Primordials != nullptr) {
182+
return cache->Primordials->Get(isolate);
183+
}
184+
185+
Local<Value> result;
186+
if (!CallBuiltin(context, BuiltinId::kPrimordials, Local<Value>(),
187+
v8::Undefined(isolate))
188+
.ToLocal(&result) ||
189+
!result->IsObject()) {
190+
return MaybeLocal<Object>();
191+
}
192+
193+
Local<Object> primordials = result.As<Object>();
194+
cache->Primordials =
195+
std::make_unique<Persistent<Object>>(isolate, primordials);
196+
return primordials;
197+
}
198+
199+
} // namespace
200+
201+
MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context,
202+
BuiltinId id,
203+
Local<Value> binding) {
204+
Local<Object> primordials;
205+
if (!GetPrimordials(context).ToLocal(&primordials)) {
206+
return MaybeLocal<Value>();
207+
}
208+
209+
return CallBuiltin(context, id, binding, primordials);
210+
}
211+
212+
} // namespace tns
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#ifndef BuiltinLoader_h
2+
#define BuiltinLoader_h
3+
4+
#include "Common.h"
5+
#include "RuntimeBuiltins.h"
6+
7+
namespace tns {
8+
9+
class BuiltinLoader {
10+
public:
11+
// Compiles the builtin identified by id as a function body with the fixed
12+
// parameters `exports`, `require`, `module`, `binding` (Node's module wrapper
13+
// plus its internalBinding idiom) and `primordials`, calls it with the given
14+
// bag of natives (or undefined when omitted) plus this isolate's frozen
15+
// intrinsics snapshot, and returns the resulting `module.exports`. `require`
16+
// reaches the builtin modules (NsBuiltinModules) and nothing else. The
17+
// snapshot is
18+
// produced by the kPrimordials builtin on first use and cached per isolate,
19+
// so it is taken before any user code can replace a global. Scripts carry
20+
// an "internal/<name>.js" origin so runtime
21+
// frames are identifiable in stack traces. Compilation goes through a
22+
// process-wide bytecode cache: the first run in the process compiles
23+
// eagerly and populates the cache, later isolates (workers) consume it
24+
// instead of re-parsing the source.
25+
static v8::MaybeLocal<v8::Value> RunBuiltin(
26+
v8::Local<v8::Context> context, BuiltinId id,
27+
v8::Local<v8::Value> binding = v8::Local<v8::Value>());
28+
};
29+
30+
} // namespace tns
31+
32+
#endif /* BuiltinLoader_h */

NativeScript/runtime/Caches.h

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,13 @@ class Caches {
152152
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
153153
std::unique_ptr<v8::Persistent<v8::Function>> WeakRefClearFunc =
154154
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
155-
std::unique_ptr<v8::Persistent<v8::Function>> SmartJSONStringifyFunc =
155+
// console formatter (internal/inspect.js), initialized by Console::Init.
156+
std::unique_ptr<v8::Persistent<v8::Function>> InspectFunc =
156157
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
158+
// ns:util's format, used by console.* for %-substitution.
159+
std::unique_ptr<v8::Persistent<v8::Function>> FormatFunc =
160+
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
161+
bool FormatFuncUnavailable = false;
157162
std::unique_ptr<v8::Persistent<v8::Function>> InteropReferenceCtorFunc =
158163
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
159164
std::unique_ptr<v8::Persistent<v8::Function>> PointerCtorFunc =
@@ -163,6 +168,28 @@ class Caches {
163168
std::unique_ptr<v8::Persistent<v8::Function>> UnmanagedTypeCtorFunc =
164169
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
165170

171+
// `ns:`/`node:` builtin modules (NsBuiltinModules), keyed by specifier. Both
172+
// are per isolate: a builtin module is a singleton per realm, so workers get
173+
// their own exports objects and their own synthetic modules.
174+
robin_hood::unordered_map<std::string,
175+
std::unique_ptr<v8::Persistent<v8::Object>>>
176+
BuiltinModuleExports;
177+
robin_hood::unordered_map<std::string,
178+
std::unique_ptr<v8::Persistent<v8::Module>>>
179+
BuiltinModules;
180+
// Specifiers currently being built, so a shim requiring back into the module
181+
// that is loading it fails instead of recursing.
182+
robin_hood::unordered_set<std::string> BuiltinModulesInProgress;
183+
// The `require` handed to every builtin, resolving builtin specifiers only.
184+
std::unique_ptr<v8::Persistent<v8::Function>> BuiltinRequire =
185+
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
186+
187+
// Frozen intrinsics snapshot returned by internal/primordials.js, passed to
188+
// every builtin as its second fixed parameter (BuiltinLoader::RunBuiltin).
189+
// Per isolate, so workers snapshot their own realm's intrinsics.
190+
std::unique_ptr<v8::Persistent<v8::Object>> Primordials =
191+
std::unique_ptr<v8::Persistent<v8::Object>>(nullptr);
192+
166193
// Internal EventTarget instance backing the global, returned by the generic
167194
// event-primitives bootstrap IIFE (Events::Init). Holds the real listener
168195
// store, so native layers dispatch through it without going through

NativeScript/runtime/ClassBuilder.mm

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <numeric>
44
#include <sstream>
55
#include "ArgConverter.h"
6+
#include "BuiltinLoader.h"
67
#include "Caches.h"
78
#include "FastEnumerationAdapter.h"
89
#include "Helpers.h"
@@ -162,27 +163,10 @@
162163
return;
163164
}
164165

165-
std::string extendsFuncScript = "(function() { "
166-
" function __extends(d, b) { "
167-
" for (var p in b) {"
168-
" if (b.hasOwnProperty(p)) {"
169-
" d[p] = b[p];"
170-
" }"
171-
" }"
172-
" function __() { this.constructor = d; }"
173-
" d.prototype = b === null ? Object.create(b) : "
174-
"(__.prototype = b.prototype, new __());"
175-
" } "
176-
" return __extends;"
177-
"})()";
178-
179-
Local<Script> script;
180-
tns::Assert(Script::Compile(context, tns::ToV8String(isolate, extendsFuncScript.c_str()))
181-
.ToLocal(&script),
182-
isolate);
183-
184166
Local<Value> extendsFunc;
185-
tns::Assert(script->Run(context).ToLocal(&extendsFunc) && extendsFunc->IsFunction(), isolate);
167+
tns::Assert(BuiltinLoader::RunBuiltin(context, BuiltinId::kClassExtends).ToLocal(&extendsFunc) &&
168+
extendsFunc->IsFunction(),
169+
isolate);
186170

187171
cache->OriginalExtendsFunc =
188172
std::make_unique<Persistent<v8::Function>>(isolate, extendsFunc.As<v8::Function>());

0 commit comments

Comments
 (0)