-
-
Notifications
You must be signed in to change notification settings - Fork 144
refactor: move embedded runtime JS to real .js files (js2c) #1989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // Lint setup for the runtime's builtin JavaScript | ||
| // (test-app/runtime/src/main/cpp/js). Each file is compiled by BuiltinLoader | ||
| // as a FUNCTION BODY with the fixed parameters `exports`, `module` and | ||
| // `binding` (see that directory's README.md), which are declared as globals | ||
| // here. no-undef is the typo net for binding-bag destructures and | ||
| // native-global usage alike. | ||
| import globals from 'globals'; | ||
|
|
||
| export default [ | ||
| { | ||
| files: ['test-app/runtime/src/main/cpp/js/**/*.js'], | ||
| languageOptions: { | ||
| ecmaVersion: 2022, | ||
| sourceType: 'script', | ||
| globals: { | ||
| ...globals.es2021, | ||
| exports: 'readonly', | ||
| module: 'readonly', | ||
| binding: 'readonly', | ||
| global: 'readonly', | ||
| console: 'readonly', | ||
| URL: 'readonly', | ||
| URLSearchParams: 'readonly', | ||
| Blob: 'readonly', | ||
| File: 'readonly', | ||
| WebAssembly: 'readonly', | ||
| // Java package roots resolved through the metadata interceptor at | ||
| // runtime: | ||
| java: 'readonly', | ||
| org: 'readonly', | ||
| }, | ||
| }, | ||
| rules: { | ||
| 'no-undef': 'error', | ||
| 'no-unused-vars': ['error', { args: 'none', caughtErrors: 'none' }], | ||
| }, | ||
| }, | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,6 +58,41 @@ include_directories( | |
| src/main/cpp/ada | ||
| ) | ||
|
|
||
| # The runtime's builtin JavaScript (src/main/cpp/js) embedded into a generated | ||
| # C++ table by tools/js2c.mjs. The list is explicit rather than globbed so that | ||
| # adding a file is a visible build change; --check-dir fails the build when it | ||
| # drifts from the directory contents. | ||
| set(RUNTIME_BUILTIN_JS_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp/js) | ||
| set(RUNTIME_BUILTIN_JS | ||
| ${RUNTIME_BUILTIN_JS_DIR}/blob-url.js | ||
| ${RUNTIME_BUILTIN_JS_DIR}/error-events.js | ||
| ${RUNTIME_BUILTIN_JS_DIR}/events.js | ||
| ${RUNTIME_BUILTIN_JS_DIR}/json-helper.js | ||
| ${RUNTIME_BUILTIN_JS_DIR}/message-loop-timer.js | ||
| ${RUNTIME_BUILTIN_JS_DIR}/require-factory.js | ||
| ${RUNTIME_BUILTIN_JS_DIR}/smart-stringify.js | ||
| ${RUNTIME_BUILTIN_JS_DIR}/weak-ref.js | ||
| ) | ||
| set(RUNTIME_BUILTINS_GENERATED_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp/generated) | ||
| get_filename_component(RUNTIME_BUILTINS_JS2C ${PROJECT_SOURCE_DIR}/../../tools/js2c.mjs ABSOLUTE) | ||
|
|
||
| find_program(NODE_EXECUTABLE NAMES node nodejs) | ||
| if (NOT NODE_EXECUTABLE) | ||
| message(FATAL_ERROR "node was not found on PATH; it is required to generate RuntimeBuiltins") | ||
| endif () | ||
|
|
||
| add_custom_command( | ||
| OUTPUT ${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.h | ||
| ${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.cpp | ||
| COMMAND ${NODE_EXECUTABLE} ${RUNTIME_BUILTINS_JS2C} | ||
| --out-dir ${RUNTIME_BUILTINS_GENERATED_DIR} | ||
| --check-dir ${RUNTIME_BUILTIN_JS_DIR} | ||
| ${RUNTIME_BUILTIN_JS} | ||
| DEPENDS ${RUNTIME_BUILTIN_JS} ${RUNTIME_BUILTINS_JS2C} | ||
| COMMENT "Generating RuntimeBuiltins from src/main/cpp/js" | ||
| VERBATIM | ||
| ) | ||
|
Comment on lines
+84
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 3 'RuntimeBuiltins\.h|GetBuiltinSource' \
test-app/runtime/src/main/cpp/BuiltinLoader.cpp
rg -n -C 8 'add_custom_command|add_custom_target|add_dependencies|RuntimeBuiltins' \
test-app/runtime/CMakeLists.txtRepository: NativeScript/android Length of output: 2688 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== CMakeLists relevant sections =="
sed -n '76,110p;200,212p' test-app/runtime/CMakeLists.txt
echo
echo "== BuiltinLoader includes and GetBuiltinSource body =="
sed -n '1,70p;90,130p' test-app/runtime/src/main/cpp/BuiltinLoader.cpp
echo
echo "== Library directive with source order context =="
sed -n '170,208p' test-app/runtime/CMakeLists.txtRepository: NativeScript/android Length of output: 7860 🌐 Web query:
💡 Result: In CMake, the add_custom_command function technically supports multiple files listed under the OUTPUT keyword. However, historical and practical usage patterns indicate that relying on multiple outputs within a single add_custom_command can lead to inconsistent behavior in how build tools (like Make or Ninja) track dependencies [1]. While the documentation notes that you can list multiple outputs to indicate a command generates several files [2], build systems often struggle to track which specific output was generated or whether the rule needs to run if only one of the outputs is missing. In older versions of CMake, this caused specific issues where the build system treated the first output differently or failed to correctly track dependencies for subsequent outputs [1]. To ensure robust and reliable build behavior, the recommended approach is as follows: 1. Use add_custom_command for each unique output file individually, if the commands to generate them are different [2]. 2. If a single command generates multiple files simultaneously, define the primary output in the add_custom_command and use the BYPRODUCTS option (available in modern CMake) to list the additional files produced by that command. This informs the build system that these files are generated by the same rule [3]. 3. If you need to ensure that multiple outputs are generated together and are required by different, independent targets, wrap the generation command in an add_custom_target. This forces the generation to happen as a single atomic unit before any dependent targets attempt to use the files, preventing race conditions or incomplete builds [4][5][6]. By using add_custom_target to drive the generation, you decouple the execution of the command from the individual file dependencies, ensuring that all necessary outputs are created before any part of the build process relies on them [4][7]. Citations:
🌐 Web query:
💡 Result: To manage a generated header file that is used by multiple source files or targets in CMake, you must ensure that the generation command runs before any consuming files are compiled [1][2]. The recommended approach depends on whether the generated header is internal to a single target or shared across multiple independent targets. ### Scenario 1: Internal to a Single Target If the generated header is used only by source files within the same library or executable, simply adding the generated file to the target's source list is often sufficient [3][2]. CMake's dependency scanner will typically recognize that the source files include the generated header and trigger the custom command automatically [4][2]. 1. Use add_custom_command with an OUTPUT parameter pointing to the full path of the generated header (usually in CMAKE_CURRENT_BINARY_DIR) [1][5]. 2. Add the generated file directly to the add_library or add_executable call (or use target_sources) [3][6]. 3. Ensure the directory containing the header is added to your target's include directories using target_include_directories [4][7]. ### Scenario 2: Shared Across Multiple Independent Targets If multiple independent targets need the same generated header, you must avoid race conditions where multiple targets attempt to run the same command in parallel [1]. 1. Wrap the generation command in an add_custom_target to act as a central "build step" [1][3]. Citations:
Make The current command attaches only to 🤖 Prompt for AI Agents |
||
|
|
||
| # This branch also produces runtime-regular-release.aar, shipped as | ||
| # nativescript-regular.aar and selected for apps that set useV8Symbols, so it | ||
| # must carry the release flags. Only a local Debug build keeps plain -g. | ||
|
|
@@ -106,6 +141,7 @@ add_library( | |
| src/main/cpp/ArrayElementAccessor.cpp | ||
| src/main/cpp/ArrayHelper.cpp | ||
| src/main/cpp/AssetExtractor.cpp | ||
| src/main/cpp/BuiltinLoader.cpp | ||
| src/main/cpp/CallbackHandlers.cpp | ||
| src/main/cpp/ConcurrentQueue.cpp | ||
| src/main/cpp/Constants.cpp | ||
|
|
@@ -165,6 +201,8 @@ add_library( | |
| src/main/cpp/HMRSupport.cpp | ||
| src/main/cpp/DevFlags.cpp | ||
|
|
||
| ${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.cpp | ||
|
|
||
| # V8 inspector source files will be included only in Release mode | ||
| ${INSPECTOR_SOURCES} | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| #include "BuiltinLoader.h" | ||
|
|
||
| #include <mutex> | ||
| #include <vector> | ||
|
|
||
| #include "ArgConverter.h" | ||
|
|
||
| using namespace v8; | ||
|
|
||
| namespace tns { | ||
|
|
||
| namespace { | ||
|
|
||
| /* | ||
| * Process-wide bytecode cache shared across isolates. Worker runtimes | ||
| * initialize on their own threads, so every access is under the mutex. | ||
| */ | ||
| std::mutex builtinCacheMutex; | ||
| std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)]; | ||
|
|
||
| /* | ||
| * Every builtin is compiled as a function body receiving these fixed | ||
| * parameters, mirroring Node's module wrapper: a file exports through | ||
| * `module.exports`/`exports`, and natives arrive as properties of the | ||
| * `binding` bag (Node's internalBinding idiom) for each file to destructure. | ||
| */ | ||
| constexpr const char* kExportsParamName = "exports"; | ||
| constexpr const char* kModuleParamName = "module"; | ||
| constexpr const char* kBindingParamName = "binding"; | ||
| constexpr size_t kParamCount = 3; | ||
|
|
||
| MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) { | ||
| Isolate* isolate = v8::Isolate::GetCurrent(); | ||
| const BuiltinSource& builtin = GetBuiltinSource(id); | ||
| const unsigned index = static_cast<unsigned>(id); | ||
|
|
||
| // Copy the blob out so the shared slot can be refreshed concurrently while | ||
| // this compile still reads from the copy. | ||
| std::vector<uint8_t> blob; | ||
| { | ||
| std::lock_guard<std::mutex> lock(builtinCacheMutex); | ||
| blob = builtinCache[index]; | ||
| } | ||
|
|
||
| ScriptOrigin origin(ArgConverter::ConvertToV8String(isolate, builtin.name)); | ||
| Local<v8::String> sourceText = ArgConverter::ConvertToV8String( | ||
| isolate, builtin.source, static_cast<int>(builtin.length)); | ||
| Local<v8::String> params[] = { | ||
| ArgConverter::ConvertToV8String(isolate, kExportsParamName), | ||
| ArgConverter::ConvertToV8String(isolate, kModuleParamName), | ||
| ArgConverter::ConvertToV8String(isolate, kBindingParamName)}; | ||
|
|
||
| Local<v8::Function> fn; | ||
| if (!blob.empty()) { | ||
| // The Source owns and deletes the CachedData object; BufferNotOwned | ||
| // keeps the underlying bytes (our copy) out of its hands. | ||
| auto* cachedData = new ScriptCompiler::CachedData( | ||
| blob.data(), static_cast<int>(blob.size()), | ||
| ScriptCompiler::CachedData::BufferNotOwned); | ||
| ScriptCompiler::Source source(sourceText, origin, cachedData); | ||
| if (ScriptCompiler::CompileFunction(context, &source, kParamCount, params, 0, nullptr, | ||
| ScriptCompiler::kConsumeCodeCache) | ||
| .ToLocal(&fn) && | ||
| !cachedData->rejected) { | ||
| return fn; | ||
| } | ||
| // Rejected cache (e.g. produced under different flags): fall through | ||
| // and recompile eagerly so the refreshed blob covers inner functions | ||
| // again. | ||
| } | ||
|
|
||
| ScriptCompiler::Source source(sourceText, origin); | ||
| if (!ScriptCompiler::CompileFunction(context, &source, kParamCount, params, 0, nullptr, | ||
| ScriptCompiler::kEagerCompile) | ||
| .ToLocal(&fn)) { | ||
| return MaybeLocal<v8::Function>(); | ||
| } | ||
|
|
||
| std::unique_ptr<ScriptCompiler::CachedData> produced( | ||
| ScriptCompiler::CreateCodeCacheForFunction(fn)); | ||
| if (produced != nullptr && produced->data != nullptr && produced->length > 0) { | ||
| std::lock_guard<std::mutex> lock(builtinCacheMutex); | ||
| builtinCache[index].assign(produced->data, produced->data + produced->length); | ||
| } | ||
|
|
||
| return fn; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context, BuiltinId id, | ||
| Local<Value> binding) { | ||
| Isolate* isolate = v8::Isolate::GetCurrent(); | ||
|
|
||
| Local<v8::Function> fn; | ||
| if (!CompileBuiltin(context, id).ToLocal(&fn)) { | ||
| return MaybeLocal<Value>(); | ||
| } | ||
|
|
||
| Local<Object> exportsObj = Object::New(isolate); | ||
| Local<Object> moduleObj = Object::New(isolate); | ||
| Local<v8::String> exportsKey = ArgConverter::ConvertToV8String(isolate, kExportsParamName); | ||
| if (!moduleObj->Set(context, exportsKey, exportsObj).FromMaybe(false)) { | ||
| return MaybeLocal<Value>(); | ||
| } | ||
|
|
||
| Local<Value> args[] = {exportsObj, moduleObj, | ||
| binding.IsEmpty() ? Undefined(isolate).As<Value>() : binding}; | ||
| if (fn->Call(context, Undefined(isolate), static_cast<int>(kParamCount), args).IsEmpty()) { | ||
| return MaybeLocal<Value>(); | ||
| } | ||
|
|
||
| return moduleObj->Get(context, exportsKey); | ||
| } | ||
|
|
||
| } // namespace tns |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| #ifndef BUILTINLOADER_H_ | ||
| #define BUILTINLOADER_H_ | ||
|
|
||
| #include "generated/RuntimeBuiltins.h" | ||
| #include "v8.h" | ||
|
|
||
| namespace tns { | ||
|
|
||
| class BuiltinLoader { | ||
| public: | ||
| /* | ||
| * Compiles the builtin identified by id as a function body with the fixed | ||
| * parameters `exports`, `module` and `binding` (Node's module wrapper plus | ||
| * its internalBinding idiom), calls it with the given bag of natives (or | ||
| * undefined when omitted), and returns the resulting `module.exports`. | ||
| * Scripts carry an "internal/<name>.js" origin so runtime frames are | ||
| * identifiable in stack traces. Compilation goes through a process-wide | ||
| * bytecode cache: the first run in the process compiles eagerly and | ||
| * populates the cache, later isolates (workers, which run on their own | ||
| * threads) consume it instead of re-parsing the source. | ||
| */ | ||
| static v8::MaybeLocal<v8::Value> RunBuiltin( | ||
| v8::Local<v8::Context> context, BuiltinId id, | ||
| v8::Local<v8::Value> binding = v8::Local<v8::Value>()); | ||
| }; | ||
|
|
||
| } // namespace tns | ||
|
|
||
| #endif /* BUILTINLOADER_H_ */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Detect directory drift before incremental builds.
--check-dirruns only after CMake schedules this custom command. Adding an unlisted.jsfile does not change anOUTPUTor a listedDEPENDSentry, so an incremental build can skip the command and silently omit the builtin.Use a configure-time
file(GLOB ... CONFIGURE_DEPENDS)check againstRUNTIME_BUILTIN_JS, or add an always-run validation target.🤖 Prompt for AI Agents