Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ thumbs.db

.classpath
android-runtime.iml

# Emitted by tools/js2c.mjs from test-app/runtime/src/main/cpp/js during the build.
test-app/runtime/src/main/cpp/generated/

test-app/build-tools/*.log
test-app/analytics/build-statistics.json
package-lock.json
Expand Down
38 changes: 38 additions & 0 deletions eslint.config.mjs
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' }],
},
},
];
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@
},
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"lint": "eslint test-app/runtime/src/main/cpp/js",
"version": "npm run changelog && git add CHANGELOG.md"
},
"devDependencies": {
"conventional-changelog-cli": "^2.1.1",
"dayjs": "^1.11.7",
"eslint": "^9.15.0",
"globals": "^15.12.0",
"semver": "^7.5.0"
}
}
38 changes: 38 additions & 0 deletions test-app/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

if (OPTIMIZED_BUILD OR OPTIMIZED_WITH_INSPECTOR_BUILD)
set(CMAKE_CXX_FLAGS "${COMMON_CMAKE_ARGUMENTS} -O3 -fvisibility=hidden -ffunction-sections -fno-data-sections")
else ()
Expand Down Expand Up @@ -99,6 +134,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
Expand Down Expand Up @@ -158,6 +194,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}
)
Expand Down
116 changes: 116 additions & 0 deletions test-app/runtime/src/main/cpp/BuiltinLoader.cpp
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
29 changes: 29 additions & 0 deletions test-app/runtime/src/main/cpp/BuiltinLoader.h
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_ */
Loading