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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ Thumbs.db
# VSCode
.vscode

# Generated by tools/js2c.mjs (Xcode "Generate RuntimeBuiltins" build phase)
NativeScript/runtime/generated/

# Other
node_modules/
package-lock.json
Expand Down
104 changes: 104 additions & 0 deletions NativeScript/runtime/BuiltinLoader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#include "BuiltinLoader.h"

#include <mutex>
#include <vector>

#include "Helpers.h"

using namespace v8;

namespace tns {

namespace {

// Process-wide bytecode cache shared across isolates (main + workers).
std::mutex builtinCacheMutex;
std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)];

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

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(tns::ToV8String(isolate, builtin.name),
0, // line offset
0, // column offset
false, // shared_cross_origin
-1, // script_id
Local<Value>(),
false, // is_opaque
false, // is_wasm
false // is_module
);
Local<v8::String> sourceText = tns::ToV8String(
isolate, builtin.source, static_cast<int>(builtin.length));
Local<v8::String> params[] = {tns::ToV8String(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, 1, 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, 1, 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<Value> args[] = {binding.IsEmpty() ? v8::Undefined(isolate).As<Value>()
: binding};
return fn->Call(context, v8::Undefined(isolate), 1, args);
}

} // namespace tns
26 changes: 26 additions & 0 deletions NativeScript/runtime/BuiltinLoader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#ifndef BuiltinLoader_h
#define BuiltinLoader_h

#include "Common.h"
#include "RuntimeBuiltins.h"

namespace tns {

class BuiltinLoader {
public:
// Compiles the builtin identified by id as a function body with the single
// fixed parameter `binding` (Node's internalBinding idiom), calls it with
// the given bag of natives (or undefined when omitted), and returns its
// return value. 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) 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 */
24 changes: 4 additions & 20 deletions NativeScript/runtime/ClassBuilder.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <numeric>
#include <sstream>
#include "ArgConverter.h"
#include "BuiltinLoader.h"
#include "Caches.h"
#include "FastEnumerationAdapter.h"
#include "Helpers.h"
Expand Down Expand Up @@ -162,27 +163,10 @@
return;
}

std::string extendsFuncScript = "(function() { "
" function __extends(d, b) { "
" for (var p in b) {"
" if (b.hasOwnProperty(p)) {"
" d[p] = b[p];"
" }"
" }"
" function __() { this.constructor = d; }"
" d.prototype = b === null ? Object.create(b) : "
"(__.prototype = b.prototype, new __());"
" } "
" return __extends;"
"})()";

Local<Script> script;
tns::Assert(Script::Compile(context, tns::ToV8String(isolate, extendsFuncScript.c_str()))
.ToLocal(&script),
isolate);

Local<Value> extendsFunc;
tns::Assert(script->Run(context).ToLocal(&extendsFunc) && extendsFunc->IsFunction(), isolate);
tns::Assert(BuiltinLoader::RunBuiltin(context, BuiltinId::kClassExtends).ToLocal(&extendsFunc) &&
extendsFunc->IsFunction(),
isolate);

cache->OriginalExtendsFunc =
std::make_unique<Persistent<v8::Function>>(isolate, extendsFunc.As<v8::Function>());
Expand Down
14 changes: 13 additions & 1 deletion NativeScript/runtime/DataWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,26 @@ class BaseDataWrapper {

class EnumDataWrapper : public BaseDataWrapper {
public:
EnumDataWrapper(std::string jsCode) : jsCode_(jsCode) {}
EnumDataWrapper(std::string jsCode)
: jsCode_(jsCode), hasCachedValue_(false), cachedValue_(0) {}

const WrapperType Type() { return WrapperType::Enum; }

std::string JSCode() { return jsCode_; }

// Numeric value memoized by Interop::WriteValue so the enum's JS snippet is
// compiled and evaluated at most once per wrapper.
bool HasCachedValue() const { return hasCachedValue_; }
double CachedValue() const { return cachedValue_; }
void SetCachedValue(double value) {
cachedValue_ = value;
hasCachedValue_ = true;
}

private:
std::string jsCode_;
bool hasCachedValue_;
double cachedValue_;
};

class PointerTypeWrapper : public BaseDataWrapper {
Expand Down
144 changes: 30 additions & 114 deletions NativeScript/runtime/ErrorEvents.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "ErrorEvents.h"

#include "BuiltinLoader.h"
#include "Caches.h"
#include "Helpers.h"
#include "NativeScriptException.h"
Expand All @@ -8,8 +9,9 @@ using namespace v8;

namespace tns {

// Native function handed to the bootstrap IIFE as `nativeReportFatal(error,
// stackString)`. It runs the terminal tail (shim + fatal log) WITHOUT
// Native function handed to the error-events builtin as
// `binding.nativeReportFatal(error, stackString)`. It runs the terminal tail
// (shim + fatal log) WITHOUT
// re-dispatching an event: reportError and listener-thrown errors have already
// gone through JS dispatch, so dispatching again here would recurse.
static void NativeReportFatalCallback(const FunctionCallbackInfo<Value>& info) {
Expand All @@ -22,128 +24,42 @@ static void NativeReportFatalCallback(const FunctionCallbackInfo<Value>& info) {
}

void ErrorEvents::Init(Local<Context> context) {
// WHATWG error-events layer, layered on top of the generic event primitives
// installed by Events::Init. Plain (module-free) script, strict inside the
// IIFE, ES5-ish so it never depends on other runtime extensions. The IIFE is
// invoked with two arguments — the internal EventTarget backing the global
// (so native dispatch survives app code overwriting globalThis.dispatchEvent)
// and the native nativeReportFatal(error, stack) function that runs the
// terminal tail — and returns three closures bound to that backing store.
// ErrorEvent/PromiseRejectionEvent subclass the Event captured off globalThis
// at init time, which runs before any user code.
std::string source = R"(
(function (globalTarget, nativeReportFatal) {
"use strict";
var g = globalThis;
var Event = g.Event;

function ErrorEvent(type, opts) {
opts = opts || {};
Event.call(this, type, opts);
this.message = opts.message !== undefined ? String(opts.message) : "";
this.filename = opts.filename !== undefined ? String(opts.filename) : "";
this.lineno = opts.lineno !== undefined ? (opts.lineno | 0) : 0;
this.colno = opts.colno !== undefined ? (opts.colno | 0) : 0;
this.error = opts.error !== undefined ? opts.error : null;
}
ErrorEvent.prototype = Object.create(Event.prototype);
ErrorEvent.prototype.constructor = ErrorEvent;

function PromiseRejectionEvent(type, opts) {
opts = opts || {};
Event.call(this, type, opts);
this.promise = opts.promise;
this.reason = opts.reason;
}
PromiseRejectionEvent.prototype = Object.create(Event.prototype);
PromiseRejectionEvent.prototype.constructor = PromiseRejectionEvent;

// A listener that throws must not stop other listeners: route the thrown
// value to the native fatal tail instead of ever recursively dispatching
// another `error` event from inside dispatch.
globalTarget._installListenerErrorReporter(function (e) {
try { nativeReportFatal(e, (e && e.stack) || ""); } catch (ignored) {}
});

g.reportError = function (e) {
if (arguments.length === 0) {
throw new TypeError("Failed to execute 'reportError': 1 argument required, but only 0 present.");
}
var ev = new ErrorEvent("error", {
message: (e && e.message !== undefined && e.message !== null) ? String(e.message) : String(e),
error: e,
cancelable: true
});
if (globalTarget.dispatchEvent(ev)) {
nativeReportFatal(e, (e && e.stack) || "");
}
};

g.ErrorEvent = ErrorEvent;
g.PromiseRejectionEvent = PromiseRejectionEvent;

// Closures called by C++. They never look up globalThis.dispatchEvent, so
// they keep working even if app code overwrites it.
function dispatchErrorEvent(error, message, stack) {
var ev = new ErrorEvent("error", {
message: message !== undefined && message !== null ? String(message) : "",
error: error,
cancelable: true
});
globalTarget.dispatchEvent(ev);
return ev.defaultPrevented;
}
function dispatchUnhandledRejection(promise, reason) {
var ev = new PromiseRejectionEvent("unhandledrejection", {
promise: promise,
reason: reason,
cancelable: true
});
globalTarget.dispatchEvent(ev);
return ev.defaultPrevented;
}
function dispatchRejectionHandled(promise, reason) {
var ev = new PromiseRejectionEvent("rejectionhandled", {
promise: promise,
reason: reason,
cancelable: false
});
globalTarget.dispatchEvent(ev);
}

return [dispatchErrorEvent, dispatchUnhandledRejection, dispatchRejectionHandled];
})
)";

// WHATWG error-events layer (internal/error-events.js), layered on top of
// the generic event primitives installed by Events::Init. The builtin
// receives — via its binding bag — the internal EventTarget backing the
// global (so native dispatch survives app code overwriting
// globalThis.dispatchEvent) and the native nativeReportFatal(error, stack)
// function that runs the terminal tail, and returns three closures bound to
// that backing store. ErrorEvent/PromiseRejectionEvent subclass the Event
// captured off globalThis at init time, which runs before any user code.
Isolate* isolate = v8::Isolate::GetCurrent();

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

Local<Script> script;
bool success = Script::Compile(context, tns::ToV8String(isolate, source))
.ToLocal(&script);
tns::Assert(success && !script.IsEmpty(), isolate);

Local<Value> result;
success = script->Run(context).ToLocal(&result);
tns::Assert(success && result->IsFunction(), isolate);

Local<v8::Function> iife = result.As<v8::Function>();

Local<v8::Function> nativeReportFatal;
success = v8::Function::New(context, NativeReportFatalCallback)
.ToLocal(&nativeReportFatal);
bool success = v8::Function::New(context, NativeReportFatalCallback)
.ToLocal(&nativeReportFatal);
tns::Assert(success, isolate);

Local<Value> installArgs[] = {globalTarget, nativeReportFatal};
Local<Value> iifeResult;
success = iife->Call(context, context->Global(), 2, installArgs)
.ToLocal(&iifeResult);
tns::Assert(success && iifeResult->IsArray(), isolate);
Local<Object> binding = Object::New(isolate);
success =
binding
->Set(context, tns::ToV8String(isolate, "globalTarget"), globalTarget)
.FromMaybe(false) &&
binding
->Set(context, tns::ToV8String(isolate, "nativeReportFatal"),
nativeReportFatal)
.FromMaybe(false);
tns::Assert(success, isolate);

Local<Value> result;
success = BuiltinLoader::RunBuiltin(context, BuiltinId::kErrorEvents, binding)
.ToLocal(&result);
tns::Assert(success && result->IsArray(), isolate);

Local<v8::Array> closures = iifeResult.As<v8::Array>();
Local<v8::Array> closures = result.As<v8::Array>();
Local<Value> errorFn, rejectionFn, handledFn;
tns::Assert(
closures->Get(context, 0).ToLocal(&errorFn) && errorFn->IsFunction(),
Expand Down
Loading
Loading