Skip to content

Commit 05dc54b

Browse files
committed
refactor: move embedded runtime JS to real .js files (js2c)
The runtime's internal JavaScript lived as C++ string literals across eight files, unlintable and invisible to tooling. It now lives in real .js files under test-app/runtime/src/main/cpp/js, embedded into a generated C++ table by tools/js2c.mjs at build time and executed through a new BuiltinLoader. Each file is compiled with v8::ScriptCompiler::CompileFunction as a function body with the fixed parameters `exports`, `module` and `binding` (Node's module wrapper plus its internalBinding idiom): natives arrive as properties of a binding bag built at the C++ call site, results come back through module.exports, and the script origin is internal/<name>.js so runtime frames stay identifiable in stack traces. Compilation goes through a process-wide bytecode cache guarded by a mutex, since worker runtimes initialize on their own threads. Extracted: weak-ref, message-loop-timer, smart-stringify, require-factory, json-helper, events, error-events and blob-url. Each extraction was verified AST-identical to the original literal by byte-comparing esbuild-minified output of both. tools/js2c.mjs is taken from the iOS runtime's feat/ns-util branch, which includes the later `unsigned char` fix for source bytes >= 0x80 (a narrowing error in a plain char array). Its --filelist drift check is adapted to --check-dir, comparing the explicit RUNTIME_BUILTIN_JS list in CMakeLists.txt against the directory contents so a new builtin cannot be silently skipped on incremental builds. Two behavioural notes: - JSONObjectHelper recompiled its JS->org.json serializer on every MetadataNode `from` registration. It is now compiled once per isolate and released via the isolate-dispose hook. - __messageLoopTimerStart/__messageLoopTimerStop are no longer installed on the global object. Nothing outside MessageLoopTimer referenced them, and the timer's start/stop pair now reaches its builtin through the binding bag. Mirrors NativeScript/ios#411.
1 parent c08a91b commit 05dc54b

28 files changed

Lines changed: 907 additions & 531 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ thumbs.db
2222

2323
.classpath
2424
android-runtime.iml
25+
26+
# Emitted by tools/js2c.mjs from test-app/runtime/src/main/cpp/js during the build.
27+
test-app/runtime/src/main/cpp/generated/
28+
2529
test-app/build-tools/*.log
2630
test-app/analytics/build-statistics.json
2731
package-lock.json

eslint.config.mjs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Lint setup for the runtime's builtin JavaScript
2+
// (test-app/runtime/src/main/cpp/js). Each file is compiled by BuiltinLoader
3+
// as a FUNCTION BODY with the fixed parameters `exports`, `module` and
4+
// `binding` (see that directory's README.md), which are declared as globals
5+
// here. no-undef is the typo net for binding-bag destructures and
6+
// native-global usage alike.
7+
import globals from 'globals';
8+
9+
export default [
10+
{
11+
files: ['test-app/runtime/src/main/cpp/js/**/*.js'],
12+
languageOptions: {
13+
ecmaVersion: 2022,
14+
sourceType: 'script',
15+
globals: {
16+
...globals.es2021,
17+
exports: 'readonly',
18+
module: 'readonly',
19+
binding: 'readonly',
20+
global: 'readonly',
21+
console: 'readonly',
22+
URL: 'readonly',
23+
URLSearchParams: 'readonly',
24+
Blob: 'readonly',
25+
File: 'readonly',
26+
WebAssembly: 'readonly',
27+
// Java package roots resolved through the metadata interceptor at
28+
// runtime:
29+
java: 'readonly',
30+
org: 'readonly',
31+
},
32+
},
33+
rules: {
34+
'no-undef': 'error',
35+
'no-unused-vars': ['error', { args: 'none', caughtErrors: 'none' }],
36+
},
37+
},
38+
];

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@
2727
},
2828
"scripts": {
2929
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
30+
"lint": "eslint test-app/runtime/src/main/cpp/js",
3031
"version": "npm run changelog && git add CHANGELOG.md"
3132
},
3233
"devDependencies": {
3334
"conventional-changelog-cli": "^2.1.1",
3435
"dayjs": "^1.11.7",
36+
"eslint": "^9.15.0",
37+
"globals": "^15.12.0",
3538
"semver": "^7.5.0"
3639
}
3740
}

test-app/runtime/CMakeLists.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,41 @@ include_directories(
5858
src/main/cpp/ada
5959
)
6060

61+
# The runtime's builtin JavaScript (src/main/cpp/js) embedded into a generated
62+
# C++ table by tools/js2c.mjs. The list is explicit rather than globbed so that
63+
# adding a file is a visible build change; --check-dir fails the build when it
64+
# drifts from the directory contents.
65+
set(RUNTIME_BUILTIN_JS_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp/js)
66+
set(RUNTIME_BUILTIN_JS
67+
${RUNTIME_BUILTIN_JS_DIR}/blob-url.js
68+
${RUNTIME_BUILTIN_JS_DIR}/error-events.js
69+
${RUNTIME_BUILTIN_JS_DIR}/events.js
70+
${RUNTIME_BUILTIN_JS_DIR}/json-helper.js
71+
${RUNTIME_BUILTIN_JS_DIR}/message-loop-timer.js
72+
${RUNTIME_BUILTIN_JS_DIR}/require-factory.js
73+
${RUNTIME_BUILTIN_JS_DIR}/smart-stringify.js
74+
${RUNTIME_BUILTIN_JS_DIR}/weak-ref.js
75+
)
76+
set(RUNTIME_BUILTINS_GENERATED_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp/generated)
77+
get_filename_component(RUNTIME_BUILTINS_JS2C ${PROJECT_SOURCE_DIR}/../../tools/js2c.mjs ABSOLUTE)
78+
79+
find_program(NODE_EXECUTABLE NAMES node nodejs)
80+
if (NOT NODE_EXECUTABLE)
81+
message(FATAL_ERROR "node was not found on PATH; it is required to generate RuntimeBuiltins")
82+
endif ()
83+
84+
add_custom_command(
85+
OUTPUT ${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.h
86+
${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.cpp
87+
COMMAND ${NODE_EXECUTABLE} ${RUNTIME_BUILTINS_JS2C}
88+
--out-dir ${RUNTIME_BUILTINS_GENERATED_DIR}
89+
--check-dir ${RUNTIME_BUILTIN_JS_DIR}
90+
${RUNTIME_BUILTIN_JS}
91+
DEPENDS ${RUNTIME_BUILTIN_JS} ${RUNTIME_BUILTINS_JS2C}
92+
COMMENT "Generating RuntimeBuiltins from src/main/cpp/js"
93+
VERBATIM
94+
)
95+
6196
# This branch also produces runtime-regular-release.aar, shipped as
6297
# nativescript-regular.aar and selected for apps that set useV8Symbols, so it
6398
# must carry the release flags. Only a local Debug build keeps plain -g.
@@ -106,6 +141,7 @@ add_library(
106141
src/main/cpp/ArrayElementAccessor.cpp
107142
src/main/cpp/ArrayHelper.cpp
108143
src/main/cpp/AssetExtractor.cpp
144+
src/main/cpp/BuiltinLoader.cpp
109145
src/main/cpp/CallbackHandlers.cpp
110146
src/main/cpp/ConcurrentQueue.cpp
111147
src/main/cpp/Constants.cpp
@@ -165,6 +201,8 @@ add_library(
165201
src/main/cpp/HMRSupport.cpp
166202
src/main/cpp/DevFlags.cpp
167203

204+
${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.cpp
205+
168206
# V8 inspector source files will be included only in Release mode
169207
${INSPECTOR_SOURCES}
170208
)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#include "BuiltinLoader.h"
2+
3+
#include <mutex>
4+
#include <vector>
5+
6+
#include "ArgConverter.h"
7+
8+
using namespace v8;
9+
10+
namespace tns {
11+
12+
namespace {
13+
14+
/*
15+
* Process-wide bytecode cache shared across isolates. Worker runtimes
16+
* initialize on their own threads, so every access is under the mutex.
17+
*/
18+
std::mutex builtinCacheMutex;
19+
std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)];
20+
21+
/*
22+
* Every builtin is compiled as a function body receiving these fixed
23+
* parameters, mirroring Node's module wrapper: a file exports through
24+
* `module.exports`/`exports`, and natives arrive as properties of the
25+
* `binding` bag (Node's internalBinding idiom) for each file to destructure.
26+
*/
27+
constexpr const char* kExportsParamName = "exports";
28+
constexpr const char* kModuleParamName = "module";
29+
constexpr const char* kBindingParamName = "binding";
30+
constexpr size_t kParamCount = 3;
31+
32+
MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
33+
Isolate* isolate = v8::Isolate::GetCurrent();
34+
const BuiltinSource& builtin = GetBuiltinSource(id);
35+
const unsigned index = static_cast<unsigned>(id);
36+
37+
// Copy the blob out so the shared slot can be refreshed concurrently while
38+
// this compile still reads from the copy.
39+
std::vector<uint8_t> blob;
40+
{
41+
std::lock_guard<std::mutex> lock(builtinCacheMutex);
42+
blob = builtinCache[index];
43+
}
44+
45+
ScriptOrigin origin(ArgConverter::ConvertToV8String(isolate, builtin.name));
46+
Local<v8::String> sourceText = ArgConverter::ConvertToV8String(
47+
isolate, builtin.source, static_cast<int>(builtin.length));
48+
Local<v8::String> params[] = {
49+
ArgConverter::ConvertToV8String(isolate, kExportsParamName),
50+
ArgConverter::ConvertToV8String(isolate, kModuleParamName),
51+
ArgConverter::ConvertToV8String(isolate, kBindingParamName)};
52+
53+
Local<v8::Function> fn;
54+
if (!blob.empty()) {
55+
// The Source owns and deletes the CachedData object; BufferNotOwned
56+
// keeps the underlying bytes (our copy) out of its hands.
57+
auto* cachedData = new ScriptCompiler::CachedData(
58+
blob.data(), static_cast<int>(blob.size()),
59+
ScriptCompiler::CachedData::BufferNotOwned);
60+
ScriptCompiler::Source source(sourceText, origin, cachedData);
61+
if (ScriptCompiler::CompileFunction(context, &source, kParamCount, params, 0, nullptr,
62+
ScriptCompiler::kConsumeCodeCache)
63+
.ToLocal(&fn) &&
64+
!cachedData->rejected) {
65+
return fn;
66+
}
67+
// Rejected cache (e.g. produced under different flags): fall through
68+
// and recompile eagerly so the refreshed blob covers inner functions
69+
// again.
70+
}
71+
72+
ScriptCompiler::Source source(sourceText, origin);
73+
if (!ScriptCompiler::CompileFunction(context, &source, kParamCount, params, 0, nullptr,
74+
ScriptCompiler::kEagerCompile)
75+
.ToLocal(&fn)) {
76+
return MaybeLocal<v8::Function>();
77+
}
78+
79+
std::unique_ptr<ScriptCompiler::CachedData> produced(
80+
ScriptCompiler::CreateCodeCacheForFunction(fn));
81+
if (produced != nullptr && produced->data != nullptr && produced->length > 0) {
82+
std::lock_guard<std::mutex> lock(builtinCacheMutex);
83+
builtinCache[index].assign(produced->data, produced->data + produced->length);
84+
}
85+
86+
return fn;
87+
}
88+
89+
} // namespace
90+
91+
MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context, BuiltinId id,
92+
Local<Value> binding) {
93+
Isolate* isolate = v8::Isolate::GetCurrent();
94+
95+
Local<v8::Function> fn;
96+
if (!CompileBuiltin(context, id).ToLocal(&fn)) {
97+
return MaybeLocal<Value>();
98+
}
99+
100+
Local<Object> exportsObj = Object::New(isolate);
101+
Local<Object> moduleObj = Object::New(isolate);
102+
Local<v8::String> exportsKey = ArgConverter::ConvertToV8String(isolate, kExportsParamName);
103+
if (!moduleObj->Set(context, exportsKey, exportsObj).FromMaybe(false)) {
104+
return MaybeLocal<Value>();
105+
}
106+
107+
Local<Value> args[] = {exportsObj, moduleObj,
108+
binding.IsEmpty() ? Undefined(isolate).As<Value>() : binding};
109+
if (fn->Call(context, Undefined(isolate), static_cast<int>(kParamCount), args).IsEmpty()) {
110+
return MaybeLocal<Value>();
111+
}
112+
113+
return moduleObj->Get(context, exportsKey);
114+
}
115+
116+
} // namespace tns
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#ifndef BUILTINLOADER_H_
2+
#define BUILTINLOADER_H_
3+
4+
#include "generated/RuntimeBuiltins.h"
5+
#include "v8.h"
6+
7+
namespace tns {
8+
9+
class BuiltinLoader {
10+
public:
11+
/*
12+
* Compiles the builtin identified by id as a function body with the fixed
13+
* parameters `exports`, `module` and `binding` (Node's module wrapper plus
14+
* its internalBinding idiom), calls it with the given bag of natives (or
15+
* undefined when omitted), and returns the resulting `module.exports`.
16+
* Scripts carry an "internal/<name>.js" origin so runtime frames are
17+
* identifiable in stack traces. Compilation goes through a process-wide
18+
* bytecode cache: the first run in the process compiles eagerly and
19+
* populates the cache, later isolates (workers, which run on their own
20+
* threads) consume it instead of re-parsing the source.
21+
*/
22+
static v8::MaybeLocal<v8::Value> RunBuiltin(
23+
v8::Local<v8::Context> context, BuiltinId id,
24+
v8::Local<v8::Value> binding = v8::Local<v8::Value>());
25+
};
26+
27+
} // namespace tns
28+
29+
#endif /* BUILTINLOADER_H_ */

0 commit comments

Comments
 (0)