Skip to content
Draft
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
185 changes: 185 additions & 0 deletions NativeScript/runtime/Base64.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#include "Base64.h"

#include <vector>

#include "BuiltinLoader.h"
#include "Helpers.h"

using namespace v8;

namespace tns {

namespace {

constexpr char kAlphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

// 6-bit value per ASCII byte; 0xFF marks everything outside the alphabet.
constexpr uint8_t kInvalid = 0xFF;

uint8_t SixBits(uint8_t c) {
if (c >= 'A' && c <= 'Z') {
return static_cast<uint8_t>(c - 'A');
}
if (c >= 'a' && c <= 'z') {
return static_cast<uint8_t>(c - 'a' + 26);
}
if (c >= '0' && c <= '9') {
return static_cast<uint8_t>(c - '0' + 52);
}
if (c == '+') {
return 62;
}
if (c == '/') {
return 63;
}
return kInvalid;
}

bool IsAsciiWhitespace(uint8_t c) {
return c == '\t' || c == '\n' || c == '\f' || c == '\r' || c == ' ';
}

// The string's code units as bytes. Fails when any unit is above U+00FF,
// which neither op can represent.
bool GetLatin1Bytes(Isolate* isolate, Local<Value> value,
std::vector<uint8_t>* out) {
if (!value->IsString()) {
return false;
}
Local<v8::String> str = value.As<v8::String>();
if (!str->ContainsOnlyOneByte()) {
return false;
}
const int length = str->Length();
out->resize(static_cast<size_t>(length));
if (length > 0) {
str->WriteOneByteV2(isolate, 0, static_cast<uint32_t>(length), out->data());
}
return true;
}

// btoa: base64-encode the input's code units.
void BtoaCallback(const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
std::vector<uint8_t> input;
if (!GetLatin1Bytes(isolate, info[0], &input)) {
info.GetReturnValue().SetNull();
return;
}

std::vector<uint8_t> out;
out.reserve((input.size() + 2) / 3 * 4);
size_t i = 0;
for (; i + 3 <= input.size(); i += 3) {
const uint32_t group = (static_cast<uint32_t>(input[i]) << 16) |
(static_cast<uint32_t>(input[i + 1]) << 8) |
input[i + 2];
out.push_back(kAlphabet[(group >> 18) & 0x3F]);
out.push_back(kAlphabet[(group >> 12) & 0x3F]);
out.push_back(kAlphabet[(group >> 6) & 0x3F]);
out.push_back(kAlphabet[group & 0x3F]);
}
const size_t remaining = input.size() - i;
if (remaining == 1) {
const uint32_t group = static_cast<uint32_t>(input[i]) << 16;
out.push_back(kAlphabet[(group >> 18) & 0x3F]);
out.push_back(kAlphabet[(group >> 12) & 0x3F]);
out.push_back('=');
out.push_back('=');
} else if (remaining == 2) {
const uint32_t group = (static_cast<uint32_t>(input[i]) << 16) |
(static_cast<uint32_t>(input[i + 1]) << 8);
out.push_back(kAlphabet[(group >> 18) & 0x3F]);
out.push_back(kAlphabet[(group >> 12) & 0x3F]);
out.push_back(kAlphabet[(group >> 6) & 0x3F]);
out.push_back('=');
}

if (out.empty()) {
info.GetReturnValue().Set(v8::String::Empty(isolate));
return;
}
Local<v8::String> result;
if (v8::String::NewFromOneByte(isolate, out.data(), NewStringType::kNormal,
static_cast<int>(out.size()))
.ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}

// atob: forgiving-base64 decode
// (https://infra.spec.whatwg.org/#forgiving-base64-decode).
void AtobCallback(const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
std::vector<uint8_t> raw;
if (!GetLatin1Bytes(isolate, info[0], &raw)) {
info.GetReturnValue().SetNull();
return;
}

std::vector<uint8_t> data;
data.reserve(raw.size());
for (uint8_t c : raw) {
if (!IsAsciiWhitespace(c)) {
data.push_back(c);
}
}

if (data.size() % 4 == 0) {
size_t strip = 0;
while (strip < 2 && !data.empty() && data.back() == '=') {
data.pop_back();
strip++;
}
}
if (data.size() % 4 == 1) {
info.GetReturnValue().SetNull();
return;
}

std::vector<uint8_t> out;
out.reserve(data.size() / 4 * 3 + 2);
uint32_t accumulator = 0;
uint32_t bits = 0;
for (uint8_t c : data) {
const uint8_t value = SixBits(c);
if (value == kInvalid) {
info.GetReturnValue().SetNull();
return;
}
accumulator = (accumulator << 6) | value;
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back(static_cast<uint8_t>((accumulator >> bits) & 0xFF));
}
}

if (out.empty()) {
info.GetReturnValue().Set(v8::String::Empty(isolate));
return;
}
Local<v8::String> result;
if (v8::String::NewFromOneByte(isolate, out.data(), NewStringType::kNormal,
static_cast<int>(out.size()))
.ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}

MaybeLocal<Object> CreateBinding(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
Local<Object> binding = Object::New(isolate);
tns::SetMethodNoSideEffect(context, binding, "btoa", BtoaCallback);
tns::SetMethodNoSideEffect(context, binding, "atob", AtobCallback);
return binding;
}

} // namespace

MaybeLocal<Object> Base64::GetExports(Local<Context> context) {
return BuiltinLoader::GetExports(context, BuiltinId::kBase64, CreateBinding);
}

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

#include "Common.h"

namespace tns {

// Native ops behind the base64 builtin (internal/base64.js): the WHATWG
// forgiving-base64 codec backing the atob / btoa globals. Both ops answer
// null instead of throwing, so the builtin owns the error shape.
class Base64 {
public:
// The builtin's exports, `{ atob, btoa }`, from the one run it gets per
// isolate.
static v8::MaybeLocal<v8::Object> GetExports(v8::Local<v8::Context> context);
};

} // namespace tns

#endif /* Base64_h */
37 changes: 37 additions & 0 deletions NativeScript/runtime/BuiltinLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ constexpr const char* kPrimordialsParamName = "primordials";
constexpr const char* kInternalsParamName = "internals";
constexpr int kParamCount = 6;

// `module.exports` of every builtin that has run in this isolate, indexed by
// id. Per isolate because a builtin is a singleton per realm, so workers run
// their own copy of a file and export their own objects.
struct BuiltinExportsState {
Persistent<Object> exports[static_cast<unsigned>(BuiltinId::kCount)];
};

// Per-isolate `internals` object handed to every builtin: the private
// channel for cross-builtin capabilities (hook keys, setters) that must
// never reach app code. Producers publish during their init, consumers read
Expand Down Expand Up @@ -242,4 +249,34 @@ MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context,
return CallBuiltin(context, id, binding, primordials, internals);
}

MaybeLocal<Object> BuiltinLoader::GetExports(Local<Context> context,
BuiltinId id,
BindingFactory bindingFactory) {
Isolate* isolate = v8::Isolate::GetCurrent();
auto* state = Caches::StateFor<BuiltinExportsState>(isolate);
if (state == nullptr) {
return MaybeLocal<Object>();
}

const unsigned index = static_cast<unsigned>(id);
if (!state->exports[index].IsEmpty()) {
return state->exports[index].Get(isolate);
}

Local<Object> binding;
if (bindingFactory != nullptr && !bindingFactory(context).ToLocal(&binding)) {
return MaybeLocal<Object>();
}

Local<Value> result;
if (!RunBuiltin(context, id, binding).ToLocal(&result) ||
!result->IsObject()) {
return MaybeLocal<Object>();
}

Local<Object> exports = result.As<Object>();
state->exports[index].Reset(isolate, exports);
return exports;
}

} // namespace tns
15 changes: 15 additions & 0 deletions NativeScript/runtime/BuiltinLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace tns {

class BuiltinLoader {
public:
// Builds the bag of natives a builtin receives as its `binding` parameter.
// GetExports calls it only when the builtin actually runs, so a call site
// that hits the cache pays nothing for it.
using BindingFactory = v8::MaybeLocal<v8::Object> (*)(v8::Local<v8::Context>);

// Compiles the builtin identified by id as a function body with the fixed
// parameters `exports`, `require`, `module`, `binding` (Node's module wrapper
// plus its internalBinding idiom), `primordials` and `internals`, calls it
Expand All @@ -29,6 +34,16 @@ class BuiltinLoader {
static v8::MaybeLocal<v8::Value> RunBuiltin(
v8::Local<v8::Context> context, BuiltinId id,
v8::Local<v8::Value> binding = v8::Local<v8::Value>());

// The builtin's `module.exports`, running it at most once per isolate. Every
// entry point that reaches the same file — the `ns:`/`node:` module registry,
// the lazy-global tier, another builtin's `require` — shares that one run, so
// a value a file exports is the same object through all of them. Empty when
// the builtin failed to run (an exception is pending) or exported a
// non-object.
static v8::MaybeLocal<v8::Object> GetExports(v8::Local<v8::Context> context,
BuiltinId id,
BindingFactory binding);
};

} // namespace tns
Expand Down
10 changes: 4 additions & 6 deletions NativeScript/runtime/Caches.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,10 @@ class Caches {
std::unique_ptr<v8::Persistent<v8::Function>> UnmanagedTypeCtorFunc =
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);

// `ns:`/`node:` builtin modules (NsBuiltinModules), keyed by specifier. Both
// are per isolate: a builtin module is a singleton per realm, so workers get
// their own exports objects and their own synthetic modules.
robin_hood::unordered_map<std::string,
std::unique_ptr<v8::Persistent<v8::Object>>>
BuiltinModuleExports;
// Synthetic ES modules wrapping the `ns:`/`node:` builtin modules
// (NsBuiltinModules), keyed by specifier. Per isolate: a builtin module is a
// singleton per realm, so workers get their own. The exports they wrap live
// in the per-isolate builtin exports cache (BuiltinLoader::GetExports).
robin_hood::unordered_map<std::string,
std::unique_ptr<v8::Persistent<v8::Module>>>
BuiltinModules;
Expand Down
7 changes: 7 additions & 0 deletions NativeScript/runtime/Helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,13 @@ void SetMethod(v8::Local<v8::Context> context, v8::Local<v8::Object> that, const
// Similar to SetProtoMethod but without receiver signature checks.
void SetMethod(v8::Isolate* isolate, v8::Local<v8::Template> that, const char* name,
v8::FunctionCallback callback, v8::Local<v8::Value> data = v8::Local<v8::Value>());
// Whether the runtime registers v8::CFunction fast-call overloads next to the
// slow callbacks. A registered overload is inert wherever the optimizing tiers
// are absent — iOS ships V8 in lite mode, which implies jitless, so every call
// there goes through the slow callback — and only fires on JIT-enabled embeds.
#ifndef NATIVESCRIPT_ENABLE_FAST_API
#define NATIVESCRIPT_ENABLE_FAST_API 1
#endif
void SetFastMethod(v8::Isolate* isolate, v8::Local<v8::Template> that, const char* name,
v8::FunctionCallback slow_callback, const v8::CFunction* c_function,
v8::Local<v8::Value> data = v8::Local<v8::Value>());
Expand Down
77 changes: 77 additions & 0 deletions NativeScript/runtime/LazyGlobals.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#include "LazyGlobals.h"

#include "Base64.h"
#include "Helpers.h"
#include "TextEncoding.h"

using namespace v8;

namespace tns {

namespace {

// The builtin's `module.exports`, from the single run it gets per isolate
// (BuiltinLoader::GetExports). Two globals out of the same file therefore cost
// one run, and so does a module that exports the same interfaces.
using ExportsAccessor = MaybeLocal<Object> (*)(Local<Context>);

struct LazyGlobalEntry {
const char* name;
const char* exportName; // key of `name` in the builtin's module.exports
ExportsAccessor exports;
};

constexpr LazyGlobalEntry kLazyGlobals[] = {
{"TextEncoder", "TextEncoder", TextEncoding::GetExports},
{"TextDecoder", "TextDecoder", TextEncoding::GetExports},
{"atob", "atob", Base64::GetExports},
{"btoa", "btoa", Base64::GetExports},
};

void LazyGlobalGetter(Local<v8::Name> property,
const PropertyCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
const auto* entry = static_cast<const LazyGlobalEntry*>(
info.Data().As<External>()->Value(v8::kExternalPointerTypeTagDefault));
Local<Context> context = isolate->GetCurrentContext();

Local<Object> exports;
if (!entry->exports(context).ToLocal(&exports)) {
return;
}
Local<Value> value;
if (!exports->Get(context, tns::ToV8String(isolate, entry->exportName))
.ToLocal(&value)) {
return;
}
info.GetReturnValue().Set(value);
}

} // namespace

void LazyGlobals::Init(Isolate* isolate, Local<ObjectTemplate> globalTemplate) {
for (const LazyGlobalEntry& entry : kLazyGlobals) {
Local<External> data =
External::New(isolate, const_cast<LazyGlobalEntry*>(&entry),
v8::kExternalPointerTypeTagDefault);
// SetLazyDataProperty, not a getter that rewrites the property itself:
// defining over an API accessor reads its current descriptor, which calls
// the getter again and recurses. V8 does the rewrite from the outside, and
// gives a setter-less accessor its ReconfigureToDataProperty setter, so an
// assignment landing before the first read replaces the global as well.
globalTemplate->SetLazyDataProperty(tns::ToV8String(isolate, entry.name),
LazyGlobalGetter, data,
PropertyAttribute::DontEnum);
}
}

bool LazyGlobals::IsLazyGlobal(const std::string& name) {
for (const LazyGlobalEntry& entry : kLazyGlobals) {
if (name == entry.name) {
return true;
}
}
return false;
}

} // namespace tns
Loading
Loading