diff --git a/NativeScript/runtime/Base64.cpp b/NativeScript/runtime/Base64.cpp new file mode 100644 index 00000000..221ba91d --- /dev/null +++ b/NativeScript/runtime/Base64.cpp @@ -0,0 +1,185 @@ +#include "Base64.h" + +#include + +#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(c - 'A'); + } + if (c >= 'a' && c <= 'z') { + return static_cast(c - 'a' + 26); + } + if (c >= '0' && c <= '9') { + return static_cast(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, + std::vector* out) { + if (!value->IsString()) { + return false; + } + Local str = value.As(); + if (!str->ContainsOnlyOneByte()) { + return false; + } + const int length = str->Length(); + out->resize(static_cast(length)); + if (length > 0) { + str->WriteOneByteV2(isolate, 0, static_cast(length), out->data()); + } + return true; +} + +// btoa: base64-encode the input's code units. +void BtoaCallback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + std::vector input; + if (!GetLatin1Bytes(isolate, info[0], &input)) { + info.GetReturnValue().SetNull(); + return; + } + + std::vector 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(input[i]) << 16) | + (static_cast(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(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(input[i]) << 16) | + (static_cast(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 result; + if (v8::String::NewFromOneByte(isolate, out.data(), NewStringType::kNormal, + static_cast(out.size())) + .ToLocal(&result)) { + info.GetReturnValue().Set(result); + } +} + +// atob: forgiving-base64 decode +// (https://infra.spec.whatwg.org/#forgiving-base64-decode). +void AtobCallback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + std::vector raw; + if (!GetLatin1Bytes(isolate, info[0], &raw)) { + info.GetReturnValue().SetNull(); + return; + } + + std::vector 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 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((accumulator >> bits) & 0xFF)); + } + } + + if (out.empty()) { + info.GetReturnValue().Set(v8::String::Empty(isolate)); + return; + } + Local result; + if (v8::String::NewFromOneByte(isolate, out.data(), NewStringType::kNormal, + static_cast(out.size())) + .ToLocal(&result)) { + info.GetReturnValue().Set(result); + } +} + +MaybeLocal CreateBinding(Local context) { + Isolate* isolate = v8::Isolate::GetCurrent(); + Local binding = Object::New(isolate); + tns::SetMethodNoSideEffect(context, binding, "btoa", BtoaCallback); + tns::SetMethodNoSideEffect(context, binding, "atob", AtobCallback); + return binding; +} + +} // namespace + +MaybeLocal Base64::GetExports(Local context) { + return BuiltinLoader::GetExports(context, BuiltinId::kBase64, CreateBinding); +} + +} // namespace tns diff --git a/NativeScript/runtime/Base64.h b/NativeScript/runtime/Base64.h new file mode 100644 index 00000000..9408d3bb --- /dev/null +++ b/NativeScript/runtime/Base64.h @@ -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 GetExports(v8::Local context); +}; + +} // namespace tns + +#endif /* Base64_h */ diff --git a/NativeScript/runtime/BuiltinLoader.cpp b/NativeScript/runtime/BuiltinLoader.cpp index 888645a6..4da2aec4 100644 --- a/NativeScript/runtime/BuiltinLoader.cpp +++ b/NativeScript/runtime/BuiltinLoader.cpp @@ -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 exports[static_cast(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 @@ -242,4 +249,34 @@ MaybeLocal BuiltinLoader::RunBuiltin(Local context, return CallBuiltin(context, id, binding, primordials, internals); } +MaybeLocal BuiltinLoader::GetExports(Local context, + BuiltinId id, + BindingFactory bindingFactory) { + Isolate* isolate = v8::Isolate::GetCurrent(); + auto* state = Caches::StateFor(isolate); + if (state == nullptr) { + return MaybeLocal(); + } + + const unsigned index = static_cast(id); + if (!state->exports[index].IsEmpty()) { + return state->exports[index].Get(isolate); + } + + Local binding; + if (bindingFactory != nullptr && !bindingFactory(context).ToLocal(&binding)) { + return MaybeLocal(); + } + + Local result; + if (!RunBuiltin(context, id, binding).ToLocal(&result) || + !result->IsObject()) { + return MaybeLocal(); + } + + Local exports = result.As(); + state->exports[index].Reset(isolate, exports); + return exports; +} + } // namespace tns diff --git a/NativeScript/runtime/BuiltinLoader.h b/NativeScript/runtime/BuiltinLoader.h index fdea44ab..3373789e 100644 --- a/NativeScript/runtime/BuiltinLoader.h +++ b/NativeScript/runtime/BuiltinLoader.h @@ -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::Local); + // 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 @@ -29,6 +34,16 @@ class BuiltinLoader { static v8::MaybeLocal RunBuiltin( v8::Local context, BuiltinId id, v8::Local binding = v8::Local()); + + // 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 GetExports(v8::Local context, + BuiltinId id, + BindingFactory binding); }; } // namespace tns diff --git a/NativeScript/runtime/Caches.h b/NativeScript/runtime/Caches.h index 9c983318..bc4641e4 100644 --- a/NativeScript/runtime/Caches.h +++ b/NativeScript/runtime/Caches.h @@ -218,12 +218,10 @@ class Caches { std::unique_ptr> UnmanagedTypeCtorFunc = std::unique_ptr>(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>> - 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>> BuiltinModules; diff --git a/NativeScript/runtime/Helpers.h b/NativeScript/runtime/Helpers.h index a7ef57e9..1e55f422 100644 --- a/NativeScript/runtime/Helpers.h +++ b/NativeScript/runtime/Helpers.h @@ -525,6 +525,13 @@ void SetMethod(v8::Local context, v8::Local that, const // Similar to SetProtoMethod but without receiver signature checks. void SetMethod(v8::Isolate* isolate, v8::Local that, const char* name, v8::FunctionCallback callback, v8::Local data = v8::Local()); +// 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 that, const char* name, v8::FunctionCallback slow_callback, const v8::CFunction* c_function, v8::Local data = v8::Local()); diff --git a/NativeScript/runtime/LazyGlobals.cpp b/NativeScript/runtime/LazyGlobals.cpp new file mode 100644 index 00000000..dcc60ec9 --- /dev/null +++ b/NativeScript/runtime/LazyGlobals.cpp @@ -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 (*)(Local); + +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 property, + const PropertyCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + const auto* entry = static_cast( + info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); + Local context = isolate->GetCurrentContext(); + + Local exports; + if (!entry->exports(context).ToLocal(&exports)) { + return; + } + Local value; + if (!exports->Get(context, tns::ToV8String(isolate, entry->exportName)) + .ToLocal(&value)) { + return; + } + info.GetReturnValue().Set(value); +} + +} // namespace + +void LazyGlobals::Init(Isolate* isolate, Local globalTemplate) { + for (const LazyGlobalEntry& entry : kLazyGlobals) { + Local data = + External::New(isolate, const_cast(&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 diff --git a/NativeScript/runtime/LazyGlobals.h b/NativeScript/runtime/LazyGlobals.h new file mode 100644 index 00000000..3123e926 --- /dev/null +++ b/NativeScript/runtime/LazyGlobals.h @@ -0,0 +1,36 @@ +#ifndef LazyGlobals_h +#define LazyGlobals_h + +#include + +#include "Common.h" + +namespace tns { + +// Globals whose implementation is a runtime builtin that must not run until +// someone actually reaches for the name. Each entry is registered on the +// global template as a lazy data property; the first read runs the builtin +// through the per-isolate exports cache (BuiltinLoader::GetExports), so sibling +// names (TextEncoder and TextDecoder) share the run — as does a module +// exporting the same interfaces — and V8 then replaces the property with a +// plain data property so later reads cost nothing. +// +// A builtin behind this tier runs at an arbitrary point in the isolate's life +// rather than during init, so it may only consume `internals` keys published +// by eager builtins (see NativeScript/runtime/js/README.md). +class LazyGlobals { + public: + // Registers every lazy global. Must run before Context::New, on the same + // template the eager globals use. + static void Init(v8::Isolate* isolate, + v8::Local globalTemplate); + + // Whether `name` is one of this tier's globals. The global named-property + // interceptor declines these so an ObjC metadata symbol sharing a name can + // never resolve ahead of the runtime's own global. + static bool IsLazyGlobal(const std::string& name); +}; + +} // namespace tns + +#endif /* LazyGlobals_h */ diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index ce9f6417..604e4933 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -6,6 +6,7 @@ #include "Helpers.h" #include "InlineFunctions.h" #include "Interop.h" +#include "LazyGlobals.h" #include "NativeScriptException.h" #include "ObjectManager.h" #include "Runtime.h" @@ -52,7 +53,10 @@ NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, return v8::Intercepted::kNo; } - if (InlineFunctions::IsGlobalFunction(propName)) { + // Globals the runtime owns: the eager ones inline-functions.js installs and + // the lazy tier's, which are only accessors until first read and so would + // otherwise lose the name to a metadata symbol that shares it. + if (InlineFunctions::IsGlobalFunction(propName) || LazyGlobals::IsLazyGlobal(propName)) { return v8::Intercepted::kNo; } diff --git a/NativeScript/runtime/NsBuiltinModules.cpp b/NativeScript/runtime/NsBuiltinModules.cpp index 534a5255..63f97132 100644 --- a/NativeScript/runtime/NsBuiltinModules.cpp +++ b/NativeScript/runtime/NsBuiltinModules.cpp @@ -8,6 +8,7 @@ #include "Helpers.h" #include "ModuleInternalCallbacks.h" #include "Runtime.h" +#include "TextEncoding.h" using namespace v8; @@ -18,9 +19,17 @@ namespace { constexpr const char* kNsPrefix = "ns:"; constexpr const char* kNodePrefix = "node:"; +// Defined below, each next to the natives it gathers. +MaybeLocal NsModuleBinding(Local context); +MaybeLocal NsRuntimeBinding(Local context); +MaybeLocal NsUtilBinding(Local context); + struct Registration { const char* specifier; BuiltinId builtin; + // Natives the file receives as `binding`, null when it needs none (every + // `node:` shim, which reaches its `ns:` module through require instead). + BuiltinLoader::BindingFactory binding; }; // The public registry (docs/ns-builtin-modules.md). One specifier, one source @@ -28,12 +37,12 @@ struct Registration { // adapts, so the two module objects stay distinct and the standard module // never carries compatibility code. constexpr Registration kRegistry[] = { - {"ns:module", BuiltinId::kNsModule}, - {"ns:runtime", BuiltinId::kNsRuntime}, - {"ns:util", BuiltinId::kNsUtil}, - {"node:module", BuiltinId::kNodeModule}, - {"node:url", BuiltinId::kNodeUrl}, - {"node:util", BuiltinId::kNodeUtil}, + {"ns:module", BuiltinId::kNsModule, NsModuleBinding}, + {"ns:runtime", BuiltinId::kNsRuntime, NsRuntimeBinding}, + {"ns:util", BuiltinId::kNsUtil, NsUtilBinding}, + {"node:module", BuiltinId::kNodeModule, nullptr}, + {"node:url", BuiltinId::kNodeUrl, nullptr}, + {"node:util", BuiltinId::kNodeUtil, nullptr}, }; // ns:runtime config keys. Each key defines its value domain and scope here; @@ -145,97 +154,77 @@ bool HasPrefix(const std::string& specifier, const char* prefix) { return specifier.rfind(prefix, 0) == 0; } -MaybeLocal BuildBinding(Local context, BuiltinId builtin) { +MaybeLocal NsModuleBinding(Local context) { Isolate* isolate = v8::Isolate::GetCurrent(); Local binding = Object::New(isolate); - - switch (builtin) { - case BuiltinId::kNsModule: { - // The HTTP-loader control surface (HttpLoader.mm). The binding builder - // decides build-dependent membership; ns-module.js only shapes - // and freezes whatever arrives. - if (!BuildNsModuleBinding(context, binding)) { - return MaybeLocal(); - } - break; - } - case BuiltinId::kNsRuntime: { - Local setConfig, getConfig; - if (!v8::Function::New(context, SetConfigCallback).ToLocal(&setConfig) || - !v8::Function::New(context, GetConfigCallback).ToLocal(&getConfig) || - !binding - ->Set(context, tns::ToV8String(isolate, "setConfig"), setConfig) - .FromMaybe(false) || - !binding - ->Set(context, tns::ToV8String(isolate, "getConfig"), getConfig) - .FromMaybe(false)) { - return MaybeLocal(); - } - break; - } - case BuiltinId::kNsUtil: { - // The console formatter is built once per realm; ns:util re-exports that - // instance instead of creating a second one. - Console::InitInspect(context); - std::shared_ptr cache = Caches::Get(isolate); - if (cache->InspectFunc == nullptr) { - return MaybeLocal(); - } - if (!binding - ->Set(context, tns::ToV8String(isolate, "inspect"), - cache->InspectFunc->Get(isolate)) - .FromMaybe(false)) { - return MaybeLocal(); - } - break; - } - default: - break; + // The HTTP-loader control surface (HttpLoader.mm). The binding builder + // decides build-dependent membership; ns-module.js only shapes and freezes + // whatever arrives. + if (!BuildNsModuleBinding(context, binding)) { + return MaybeLocal(); } - return binding; } -// Runs a module's builtin and caches its exports. Always leaves an exception -// pending when it returns false. -bool Instantiate(Local context, const Registration& requested) { +MaybeLocal NsRuntimeBinding(Local context) { Isolate* isolate = v8::Isolate::GetCurrent(); - std::shared_ptr cache = Caches::Get(isolate); + Local binding = Object::New(isolate); + Local setConfig, getConfig; + if (!v8::Function::New(context, SetConfigCallback).ToLocal(&setConfig) || + !v8::Function::New(context, GetConfigCallback).ToLocal(&getConfig) || + !binding->Set(context, tns::ToV8String(isolate, "setConfig"), setConfig) + .FromMaybe(false) || + !binding->Set(context, tns::ToV8String(isolate, "getConfig"), getConfig) + .FromMaybe(false)) { + return MaybeLocal(); + } + return binding; +} - // A shim reaches its ns: module through the builtin require, so the graph is - // walked while a module is still being built; a cycle would otherwise - // recurse until the stack runs out. - if (cache->BuiltinModulesInProgress.count(requested.specifier) > 0) { - isolate->ThrowException(Exception::Error( - tns::ToV8String(isolate, "Circular require of built-in module: " + - std::string(requested.specifier)))); - return false; +// TextEncoder / TextDecoder for ns:util, read straight out of the +// text-encoding builtin's per-isolate run, so the module's classes are the +// objects the globals of the same name expose. +void TextEncodingClassGetter(Local property, + const PropertyCallbackInfo& info) { + Local context = info.GetIsolate()->GetCurrentContext(); + Local exports; + Local value; + if (TextEncoding::GetExports(context).ToLocal(&exports) && + exports->Get(context, property).ToLocal(&value)) { + info.GetReturnValue().Set(value); } - cache->BuiltinModulesInProgress.emplace(requested.specifier); +} - TryCatch tc(isolate); - Local binding; - Local result; - bool built = BuildBinding(context, requested.builtin).ToLocal(&binding) && - BuiltinLoader::RunBuiltin(context, requested.builtin, binding) - .ToLocal(&result) && - result->IsObject(); - cache->BuiltinModulesInProgress.erase(requested.specifier); +MaybeLocal NsUtilBinding(Local context) { + Isolate* isolate = v8::Isolate::GetCurrent(); + Local binding = Object::New(isolate); - if (built) { - cache->BuiltinModuleExports[requested.specifier] = - std::make_unique>(isolate, result.As()); - return true; + // The console formatter is built once per realm; ns:util re-exports that + // instance instead of creating a second one. + Console::InitInspect(context); + std::shared_ptr cache = Caches::Get(isolate); + if (cache->InspectFunc == nullptr) { + return MaybeLocal(); + } + if (!binding + ->Set(context, tns::ToV8String(isolate, "inspect"), + cache->InspectFunc->Get(isolate)) + .FromMaybe(false)) { + return MaybeLocal(); } - if (tc.HasCaught()) { - tc.ReThrow(); - return false; + // Lazy so that requiring ns:util does not run the text-encoding builtin; + // ns-util.js keeps the read inside its own getters to preserve that. + for (const char* name : {"TextEncoder", "TextDecoder"}) { + if (binding + ->SetLazyDataProperty(context, tns::ToV8String(isolate, name), + TextEncodingClassGetter) + .IsNothing()) { + return MaybeLocal(); + } } - isolate->ThrowException(Exception::Error( - tns::ToV8String(isolate, "Failed to initialize built-in module '" + - std::string(requested.specifier) + "'"))); - return false; + + return binding; } // The exports a synthetic module re-exports by name, in the order used both @@ -327,17 +316,34 @@ MaybeLocal NsBuiltinModules::GetExports(Local context, Isolate* isolate = v8::Isolate::GetCurrent(); std::shared_ptr cache = Caches::Get(isolate); - auto it = cache->BuiltinModuleExports.find(specifier); - if (it == cache->BuiltinModuleExports.end()) { - if (!Instantiate(context, *registration)) { - return MaybeLocal(); - } - it = cache->BuiltinModuleExports.find(specifier); - if (it == cache->BuiltinModuleExports.end()) { - return MaybeLocal(); - } + + // A shim reaches its ns: module through the builtin require, so the graph is + // walked while a module is still being built; a cycle would otherwise + // recurse until the stack runs out. + if (cache->BuiltinModulesInProgress.count(specifier) > 0) { + isolate->ThrowException(Exception::Error(tns::ToV8String( + isolate, "Circular require of built-in module: " + specifier))); + return MaybeLocal(); + } + cache->BuiltinModulesInProgress.emplace(specifier); + + TryCatch tc(isolate); + Local exports; + bool built = BuiltinLoader::GetExports(context, registration->builtin, + registration->binding) + .ToLocal(&exports); + cache->BuiltinModulesInProgress.erase(specifier); + + if (built) { + return exports; + } + if (tc.HasCaught()) { + tc.ReThrow(); + return MaybeLocal(); } - return it->second->Get(isolate); + isolate->ThrowException(Exception::Error(tns::ToV8String( + isolate, "Failed to initialize built-in module '" + specifier + "'"))); + return MaybeLocal(); } MaybeLocal NsBuiltinModules::GetModule(Local context, diff --git a/NativeScript/runtime/Runtime.mm b/NativeScript/runtime/Runtime.mm index 863d762a..a5d7a58f 100644 --- a/NativeScript/runtime/Runtime.mm +++ b/NativeScript/runtime/Runtime.mm @@ -12,6 +12,7 @@ #include "InlineFunctions.h" #include "Interop.h" #include "IsolateTracked.h" +#include "LazyGlobals.h" #include "NativeScriptException.h" #include "NativeScriptPlatform.h" #include "ObjectManager.h" @@ -401,6 +402,7 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { globalTemplate->Set(tns::ToV8String(isolate, "queueMicrotask"), qmtTemplate); } ObjectManager::Init(isolate, globalTemplate); + LazyGlobals::Init(isolate, globalTemplate); MetadataBuilder::RegisterConstantsOnGlobalObject(isolate, globalTemplate, isWorker); isolate->SetCaptureStackTraceForUncaughtExceptions(true, 100, StackTrace::kOverview); diff --git a/NativeScript/runtime/TextEncoding.cpp b/NativeScript/runtime/TextEncoding.cpp new file mode 100644 index 00000000..bfeecd7a --- /dev/null +++ b/NativeScript/runtime/TextEncoding.cpp @@ -0,0 +1,669 @@ +#include "TextEncoding.h" + +#include +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" +#include "v8-fast-api-calls.h" +#pragma clang diagnostic pop + +#include "BuiltinLoader.h" +#include "Helpers.h" + +using namespace v8; + +namespace tns { + +namespace { + +// Encoding ids shared with text-encoding.js, which maps them back to the +// canonical names through its kEncodingNames array — keep the two in step. +enum Encoding : uint32_t { + kUtf8 = 0, + kUtf16le = 1, + kUtf16be = 2, + kWindows1252 = 3, +}; + +// Decode option bits, mirrored by kFlag* in text-encoding.js. +constexpr uint32_t kFlagFatal = 1; +constexpr uint32_t kFlagIgnoreBOM = 2; +constexpr uint32_t kFlagStream = 4; + +constexpr uint16_t kReplacementCharacter = 0xFFFD; + +struct EncodingLabel { + const char* label; + Encoding encoding; +}; + +// The complete label set of the four encodings the runtime supports +// (https://encoding.spec.whatwg.org/#names-and-labels), sorted by label so +// lookup is a binary search over a table with no runtime setup cost. +constexpr EncodingLabel kLabels[] = { + {"ansi_x3.4-1968", kWindows1252}, + {"ascii", kWindows1252}, + {"cp1252", kWindows1252}, + {"cp819", kWindows1252}, + {"csisolatin1", kWindows1252}, + {"csunicode", kUtf16le}, + {"ibm819", kWindows1252}, + {"iso-10646-ucs-2", kUtf16le}, + {"iso-8859-1", kWindows1252}, + {"iso-ir-100", kWindows1252}, + {"iso8859-1", kWindows1252}, + {"iso88591", kWindows1252}, + {"iso_8859-1", kWindows1252}, + {"iso_8859-1:1987", kWindows1252}, + {"l1", kWindows1252}, + {"latin1", kWindows1252}, + {"ucs-2", kUtf16le}, + {"unicode", kUtf16le}, + {"unicode-1-1-utf-8", kUtf8}, + {"unicode11utf8", kUtf8}, + {"unicode20utf8", kUtf8}, + {"unicodefeff", kUtf16le}, + {"unicodefffe", kUtf16be}, + {"us-ascii", kWindows1252}, + {"utf-16", kUtf16le}, + {"utf-16be", kUtf16be}, + {"utf-16le", kUtf16le}, + {"utf-8", kUtf8}, + {"utf8", kUtf8}, + {"windows-1252", kWindows1252}, + {"x-cp1252", kWindows1252}, + {"x-unicode20utf8", kUtf8}, +}; + +// windows-1252 index, pointers 0x80-0x9F. Everything outside that block is +// Latin-1 (identity), including the C1 controls this table maps to +// themselves. +constexpr uint16_t kWindows1252Index[32] = { + 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, + 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, + 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, + 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178}; + +bool IsAsciiWhitespace(char c) { + return c == '\t' || c == '\n' || c == '\f' || c == '\r' || c == ' '; +} + +int32_t LookupEncoding(const std::string& rawLabel) { + size_t begin = 0; + size_t end = rawLabel.size(); + while (begin < end && IsAsciiWhitespace(rawLabel[begin])) { + begin++; + } + while (end > begin && IsAsciiWhitespace(rawLabel[end - 1])) { + end--; + } + + std::string label = rawLabel.substr(begin, end - begin); + for (char& c : label) { + if (c >= 'A' && c <= 'Z') { + c = static_cast(c - 'A' + 'a'); + } + } + + size_t lo = 0; + size_t hi = sizeof(kLabels) / sizeof(kLabels[0]); + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + int cmp = label.compare(kLabels[mid].label); + if (cmp == 0) { + return static_cast(kLabels[mid].encoding); + } + if (cmp < 0) { + hi = mid; + } else { + lo = mid + 1; + } + } + return -1; +} + +// Everything a decoder must remember between streaming calls. It lives in a +// Uint8Array the builtin allocates per TextDecoder, so the native side stays +// stateless and no instance needs a finalizer. +constexpr int kDecoderStateBytes = 10; // bytes Store writes +static_assert(kDecoderStateBytes <= TextEncoding::kDecoderStateSize, + "the builtin allocates too little decoder state"); + +struct DecoderState { + bool bomSeen = false; + uint8_t utf8PendingLength = 0; + uint8_t utf8Pending[3] = {0, 0, 0}; + bool hasLeadByte = false; + uint8_t leadByte = 0; + bool hasLeadSurrogate = false; + uint16_t leadSurrogate = 0; + + void Load(const uint8_t* raw) { + bomSeen = (raw[0] & 1) != 0; + utf8PendingLength = raw[1] > 3 ? 0 : raw[1]; + utf8Pending[0] = raw[2]; + utf8Pending[1] = raw[3]; + utf8Pending[2] = raw[4]; + hasLeadByte = raw[5] != 0; + leadByte = raw[6]; + hasLeadSurrogate = raw[7] != 0; + leadSurrogate = static_cast(raw[8] | (raw[9] << 8)); + } + + void Store(uint8_t* raw) const { + raw[0] = bomSeen ? 1 : 0; + raw[1] = utf8PendingLength; + raw[2] = utf8Pending[0]; + raw[3] = utf8Pending[1]; + raw[4] = utf8Pending[2]; + raw[5] = hasLeadByte ? 1 : 0; + raw[6] = leadByte; + raw[7] = hasLeadSurrogate ? 1 : 0; + raw[8] = static_cast(leadSurrogate & 0xFF); + raw[9] = static_cast(leadSurrogate >> 8); + } + + void Reset() { *this = DecoderState(); } +}; + +// Collects decoded UTF-16 code units and tracks whether they all fit in one +// byte, so the finished string can take V8's one-byte representation. +class Utf16Sink { + public: + void Append(uint16_t unit) { + units_.push_back(unit); + orAll_ |= unit; + } + + void AppendCodePoint(uint32_t codePoint) { + if (codePoint <= 0xFFFF) { + Append(static_cast(codePoint)); + return; + } + codePoint -= 0x10000; + Append(static_cast(0xD800 + (codePoint >> 10))); + Append(static_cast(0xDC00 + (codePoint & 0x3FF))); + } + + MaybeLocal Finish(Isolate* isolate) const { + if (units_.empty()) { + return v8::String::Empty(isolate); + } + if (orAll_ <= 0xFF) { + std::vector oneByte(units_.size()); + for (size_t i = 0; i < units_.size(); i++) { + oneByte[i] = static_cast(units_[i]); + } + return v8::String::NewFromOneByte(isolate, oneByte.data(), + NewStringType::kNormal, + static_cast(oneByte.size())); + } + return v8::String::NewFromTwoByte(isolate, units_.data(), + NewStringType::kNormal, + static_cast(units_.size())); + } + + private: + std::vector units_; + uint32_t orAll_ = 0; +}; + +// A code point leaving a utf-8 / utf-16 decoder, with the leading-BOM removal +// TextDecoder performs once per stream. +class BomFilter { + public: + BomFilter(Utf16Sink& sink, DecoderState& state, bool ignoreBOM) + : sink_(sink), state_(state), ignoreBOM_(ignoreBOM) {} + + void Emit(uint32_t codePoint) { + if (!state_.bomSeen) { + state_.bomSeen = true; + if (!ignoreBOM_ && codePoint == 0xFEFF) { + return; + } + } + sink_.AppendCodePoint(codePoint); + } + + private: + Utf16Sink& sink_; + DecoderState& state_; + const bool ignoreBOM_; +}; + +// WHATWG utf-8 decoder. Incomplete trailing sequences are kept as raw bytes +// and replayed at the head of the next call, so the boundary constraints of a +// split sequence are re-derived from its own lead byte rather than carried in +// the saved state. Returns false when fatal mode hits invalid input. +bool DecodeUtf8(const uint8_t* input, size_t inputLength, DecoderState& state, + bool stream, bool fatal, bool ignoreBOM, Utf16Sink& sink) { + const size_t pendingLength = state.utf8PendingLength; + const size_t total = pendingLength + inputLength; + auto byteAt = [&](size_t index) -> uint8_t { + return index < pendingLength ? state.utf8Pending[index] + : input[index - pendingLength]; + }; + state.utf8PendingLength = 0; + + BomFilter out(sink, state, ignoreBOM); + uint32_t codePoint = 0; + uint32_t bytesNeeded = 0; + uint32_t bytesSeen = 0; + uint32_t lowerBoundary = 0x80; + uint32_t upperBoundary = 0xBF; + size_t sequenceStart = 0; + size_t index = 0; + + while (index < total) { + const uint8_t byte = byteAt(index); + if (bytesNeeded == 0) { + sequenceStart = index; + index++; + if (byte <= 0x7F) { + out.Emit(byte); + } else if (byte >= 0xC2 && byte <= 0xDF) { + bytesNeeded = 1; + codePoint = byte & 0x1F; + } else if (byte >= 0xE0 && byte <= 0xEF) { + if (byte == 0xE0) { + lowerBoundary = 0xA0; + } else if (byte == 0xED) { + upperBoundary = 0x9F; + } + bytesNeeded = 2; + codePoint = byte & 0x0F; + } else if (byte >= 0xF0 && byte <= 0xF4) { + if (byte == 0xF0) { + lowerBoundary = 0x90; + } else if (byte == 0xF4) { + upperBoundary = 0x8F; + } + bytesNeeded = 3; + codePoint = byte & 0x07; + } else { + if (fatal) { + state.Reset(); + return false; + } + out.Emit(kReplacementCharacter); + } + continue; + } + + if (byte < lowerBoundary || byte > upperBoundary) { + // One replacement for the maximal subpart consumed so far; the offending + // byte is reprocessed as the start of a new sequence (index unchanged). + codePoint = 0; + bytesNeeded = 0; + bytesSeen = 0; + lowerBoundary = 0x80; + upperBoundary = 0xBF; + if (fatal) { + state.Reset(); + return false; + } + out.Emit(kReplacementCharacter); + continue; + } + + lowerBoundary = 0x80; + upperBoundary = 0xBF; + codePoint = (codePoint << 6) | (byte & 0x3F); + bytesSeen++; + index++; + if (bytesSeen != bytesNeeded) { + continue; + } + const uint32_t finished = codePoint; + codePoint = 0; + bytesNeeded = 0; + bytesSeen = 0; + out.Emit(finished); + } + + if (bytesNeeded == 0) { + return true; + } + if (!stream) { + if (fatal) { + state.Reset(); + return false; + } + out.Emit(kReplacementCharacter); + return true; + } + // At most three bytes, and each is read before the slot it overwrites. + const size_t carried = total - sequenceStart; + for (size_t i = 0; i < carried; i++) { + state.utf8Pending[i] = byteAt(sequenceStart + i); + } + state.utf8PendingLength = static_cast(carried); + return true; +} + +// WHATWG shared utf-16 decoder, both endiannesses. +bool DecodeUtf16(const uint8_t* input, size_t inputLength, bool bigEndian, + DecoderState& state, bool stream, bool fatal, bool ignoreBOM, + Utf16Sink& sink) { + BomFilter out(sink, state, ignoreBOM); + + auto process = [&](uint16_t unit) -> bool { + if (state.hasLeadSurrogate) { + const uint16_t lead = state.leadSurrogate; + state.hasLeadSurrogate = false; + if (unit >= 0xDC00 && unit <= 0xDFFF) { + out.Emit(0x10000u + (static_cast(lead - 0xD800) << 10) + + (unit - 0xDC00)); + return true; + } + if (fatal) { + return false; + } + // The unpaired lead is replaced and `unit` starts over below. + out.Emit(kReplacementCharacter); + } + if (unit >= 0xD800 && unit <= 0xDBFF) { + state.hasLeadSurrogate = true; + state.leadSurrogate = unit; + return true; + } + if (unit >= 0xDC00 && unit <= 0xDFFF) { + if (fatal) { + return false; + } + out.Emit(kReplacementCharacter); + return true; + } + out.Emit(unit); + return true; + }; + + for (size_t i = 0; i < inputLength; i++) { + const uint8_t byte = input[i]; + if (!state.hasLeadByte) { + state.hasLeadByte = true; + state.leadByte = byte; + continue; + } + const uint16_t unit = + bigEndian ? static_cast((state.leadByte << 8) | byte) + : static_cast((byte << 8) | state.leadByte); + state.hasLeadByte = false; + if (!process(unit)) { + state.Reset(); + return false; + } + } + + if (stream) { + return true; + } + if (state.hasLeadByte || state.hasLeadSurrogate) { + state.hasLeadByte = false; + state.hasLeadSurrogate = false; + if (fatal) { + state.Reset(); + return false; + } + out.Emit(kReplacementCharacter); + } + return true; +} + +void DecodeWindows1252(const uint8_t* input, size_t inputLength, + Utf16Sink& sink) { + for (size_t i = 0; i < inputLength; i++) { + const uint8_t byte = input[i]; + sink.Append(byte >= 0x80 && byte <= 0x9F ? kWindows1252Index[byte - 0x80] + : byte); + } +} + +bool AllBytesBelow(const uint8_t* input, size_t length, uint8_t limit) { + for (size_t i = 0; i < length; i++) { + if (input[i] >= limit) { + return false; + } + } + return true; +} + +bool NoC1Bytes(const uint8_t* input, size_t length) { + for (size_t i = 0; i < length; i++) { + if (input[i] >= 0x80 && input[i] <= 0x9F) { + return false; + } + } + return true; +} + +// Bytes of an ArrayBuffer, SharedArrayBuffer or any ArrayBufferView; a +// detached buffer reads as empty. Only the builtin calls in, so anything else +// is a programming error rather than a user-visible one. +bool GetByteSource(Local value, const uint8_t** data, size_t* length) { + *data = nullptr; + *length = 0; + if (value.IsEmpty() || value->IsUndefined()) { + return true; + } + if (value->IsArrayBufferView()) { + Local view = value.As(); + Local buffer = view->Buffer(); + void* base = buffer->Data(); + if (base == nullptr) { + return true; + } + *data = static_cast(base) + view->ByteOffset(); + *length = view->ByteLength(); + return true; + } + if (value->IsArrayBuffer()) { + Local buffer = value.As(); + if (buffer->Data() == nullptr) { + return true; + } + *data = static_cast(buffer->Data()); + *length = buffer->ByteLength(); + return true; + } + if (value->IsSharedArrayBuffer()) { + Local buffer = value.As(); + if (buffer->Data() == nullptr) { + return true; + } + *data = static_cast(buffer->Data()); + *length = buffer->ByteLength(); + return true; + } + return false; +} + +void LabelToEncodingCallback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + info.GetReturnValue().Set(LookupEncoding(tns::ToString(isolate, info[0]))); +} + +void DecodeCallback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + + const uint8_t* input = nullptr; + size_t inputLength = 0; + if (!GetByteSource(info[0], &input, &inputLength)) { + isolate->ThrowException(Exception::TypeError(tns::ToV8String( + isolate, + "The \"input\" argument must be an ArrayBuffer, SharedArrayBuffer or " + "ArrayBufferView"))); + return; + } + + const uint32_t encoding = + static_cast(info[1].As()->Value()); + const uint32_t flags = + static_cast(info[2].As()->Value()); + const bool fatal = (flags & kFlagFatal) != 0; + const bool ignoreBOM = (flags & kFlagIgnoreBOM) != 0; + const bool stream = (flags & kFlagStream) != 0; + + Local stateArray = info[3].As(); + uint8_t* rawState = static_cast(stateArray->Buffer()->Data()) + + stateArray->ByteOffset(); + DecoderState state; + state.Load(rawState); + + // Byte-for-byte one-byte results skip the intermediate code-unit buffer. + if (input != nullptr && !stream && state.utf8PendingLength == 0 && + !state.hasLeadByte && !state.hasLeadSurrogate) { + const bool asciiUtf8 = + encoding == kUtf8 && AllBytesBelow(input, inputLength, 0x80); + const bool latin1Windows1252 = + encoding == kWindows1252 && NoC1Bytes(input, inputLength); + if (asciiUtf8 || latin1Windows1252) { + state.Reset(); + state.Store(rawState); + Local result; + if (v8::String::NewFromOneByte(isolate, input, NewStringType::kNormal, + static_cast(inputLength)) + .ToLocal(&result)) { + info.GetReturnValue().Set(result); + } + return; + } + } + + Utf16Sink sink; + bool ok = true; + switch (encoding) { + case kUtf8: + ok = + DecodeUtf8(input, inputLength, state, stream, fatal, ignoreBOM, sink); + break; + case kUtf16le: + case kUtf16be: + ok = DecodeUtf16(input, inputLength, encoding == kUtf16be, state, stream, + fatal, ignoreBOM, sink); + break; + default: + DecodeWindows1252(input, inputLength, sink); + break; + } + + // A non-streaming call is the end of a stream: the next one starts from a + // clean decoder, BOM tracking included. + if (!stream) { + state.Reset(); + } + state.Store(rawState); + if (!ok) { + isolate->ThrowException(Exception::TypeError( + tns::ToV8String(isolate, "The encoded data was not valid"))); + return; + } + + Local result; + if (sink.Finish(isolate).ToLocal(&result)) { + info.GetReturnValue().Set(result); + } +} + +void EncodeUtf8Callback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + Local source = info[0].As(); + + const size_t length = source->Utf8LengthV2(isolate); + std::unique_ptr store = + ArrayBuffer::NewBackingStore(isolate, length); + if (length > 0) { + source->WriteUtf8V2(isolate, static_cast(store->Data()), length, + v8::String::WriteFlags::kReplaceInvalidUtf8); + } + Local buffer = ArrayBuffer::New(isolate, std::move(store)); + info.GetReturnValue().Set(Uint8Array::New(buffer, 0, length)); +} + +// Writes as much of `source` as fits into `destination` without splitting an +// encoded code point, and reports {read, written} through `results` — a +// Uint32Array the builtin owns, so the op returns only the destination +// type check and stays expressible as a fast call. A destination that is a +// Uint8Array but detached or empty is a zero-length write, not a failure. +bool EncodeIntoImpl(Isolate* isolate, Local sourceValue, + Local destinationValue, Local resultsValue) { + if (!destinationValue->IsUint8Array()) { + return false; + } + if (!sourceValue->IsString() || !resultsValue->IsUint32Array()) { + return true; + } + + Local results = resultsValue.As(); + uint32_t* resultData = static_cast(results->Buffer()->Data()); + if (resultData == nullptr || results->Length() < 2) { + return true; + } + resultData += results->ByteOffset() / sizeof(uint32_t); + resultData[0] = 0; + resultData[1] = 0; + + Local destination = destinationValue.As(); + void* base = destination->Buffer()->Data(); + const size_t capacity = destination->ByteLength(); + if (base == nullptr || capacity == 0) { + return true; + } + + size_t read = 0; + const size_t written = sourceValue.As()->WriteUtf8V2( + isolate, static_cast(base) + destination->ByteOffset(), capacity, + v8::String::WriteFlags::kReplaceInvalidUtf8, &read); + resultData[0] = static_cast(read); + resultData[1] = static_cast(written); + return true; +} + +void EncodeIntoCallback(const FunctionCallbackInfo& info) { + info.GetReturnValue().Set( + EncodeIntoImpl(info.GetIsolate(), info[0], info[1], info[2])); +} + +#if NATIVESCRIPT_ENABLE_FAST_API +// Fast-call overload of encodeInto. Inert wherever V8's optimizing tiers are +// absent (iOS runs jitless), so the slow callback above stays the only path +// there. It allocates nothing on the V8 heap and calls no JS. +bool FastEncodeInto(Local receiver, Local source, + Local destination, Local results, + // NOLINTNEXTLINE(runtime/references) + FastApiCallbackOptions& options) { + HandleScope scope(options.isolate); + return EncodeIntoImpl(options.isolate, source, destination, results); +} + +const CFunction kFastEncodeInto = CFunction::Make(FastEncodeInto); +#endif + +MaybeLocal CreateBinding(Local context) { + Isolate* isolate = v8::Isolate::GetCurrent(); + Local binding = Object::New(isolate); + + tns::SetMethodNoSideEffect(context, binding, "labelToEncoding", + LabelToEncodingCallback); + tns::SetMethod(context, binding, "decode", DecodeCallback); + tns::SetMethodNoSideEffect(context, binding, "encodeUtf8", + EncodeUtf8Callback); +#if NATIVESCRIPT_ENABLE_FAST_API + tns::SetFastMethod(context, binding, "encodeInto", EncodeIntoCallback, + &kFastEncodeInto); +#else + tns::SetMethod(context, binding, "encodeInto", EncodeIntoCallback); +#endif + + return binding; +} + +} // namespace + +MaybeLocal TextEncoding::GetExports(Local context) { + return BuiltinLoader::GetExports(context, BuiltinId::kTextEncoding, + CreateBinding); +} + +} // namespace tns diff --git a/NativeScript/runtime/TextEncoding.h b/NativeScript/runtime/TextEncoding.h new file mode 100644 index 00000000..854b5b2d --- /dev/null +++ b/NativeScript/runtime/TextEncoding.h @@ -0,0 +1,26 @@ +#ifndef TextEncoding_h +#define TextEncoding_h + +#include "Common.h" + +namespace tns { + +// Native ops behind the text-encoding builtin (internal/text-encoding.js): +// the WHATWG label table, UTF-8 encoding and the decoders for the encodings +// the runtime supports (utf-8, utf-16le, utf-16be, windows-1252). The builtin +// owns the web-facing shapes; everything that touches bytes lives here. +class TextEncoding { + public: + // The builtin's exports, `{ TextEncoder, TextDecoder }`, from the one run it + // gets per isolate. The lazy globals and `ns:util` both hand out these + // objects, so require("ns:util").TextDecoder === globalThis.TextDecoder + // whichever is reached first. + static v8::MaybeLocal GetExports(v8::Local context); + + // Bytes of decoder state the builtin must hand back on every decode call. + static constexpr int kDecoderStateSize = 16; +}; + +} // namespace tns + +#endif /* TextEncoding_h */ diff --git a/NativeScript/runtime/js/README.md b/NativeScript/runtime/js/README.md index 76d8c7b4..144e358e 100644 --- a/NativeScript/runtime/js/README.md +++ b/NativeScript/runtime/js/README.md @@ -61,12 +61,40 @@ module.exports = somethingTheCallSiteNeeds; - Destructure `binding` and `primordials` once, at the top of the file, so the file's dependencies are visible and greppable. +## Eager and lazy builtins + +Most builtins run during `Runtime::Init` and install their globals themselves. +A **lazy** builtin instead exports its interfaces and is run by +`LazyGlobals` (`runtime/LazyGlobals.cpp`), which registers each global it backs +as a lazy data property on the global template: the first read of the name runs +the file through the per-isolate exports cache (`BuiltinLoader::GetExports`) so +sibling names share one run, and V8 replaces the property with a plain data +property. That cache is the same one the `ns:`/`node:` module registry uses, so +a module re-exporting a lazy builtin's interfaces (`ns:util`'s `TextEncoder`) +hands out the objects the globals hold, in either access order. Until then +nothing of it exists — no compile, no run, no allocation. `text-encoding.js` +(`TextEncoder`/`TextDecoder`) and `base64.js` (`atob`/`btoa`) are the current +ones; new globals join by adding a row to `kLazyGlobals`. + +The two extra rules a lazy builtin lives by: + +- **It runs at an arbitrary point in the isolate's life, not at init.** The + `internals` channel is therefore off limits: its producers publish during + their own init, and a consumer that reads a key it does not find fails at + first use instead of loudly at boot. Anything a lazy builtin needs from + another builtin has to come through `require` or its `binding`. +- **It must not install anything on `globalThis`.** The C++ tier owns + placement; a file that self-installs would have to run to do it, which is + the thing being avoided. + ## Rules -- Run at isolate init, before any user code: capture any global you rely on - (e.g. `globalThis.Event`) eagerly so later monkey-patching can't break you. - For intrinsics that is what `primordials` is; for everything else - (`URLSearchParams`, …) capture it into a file-level `const`. +- Eager builtins run at isolate init, before any user code: capture any global + you rely on (e.g. `globalThis.Event`) eagerly so later monkey-patching can't + break you. For intrinsics that is what `primordials` is; for everything else + (`URLSearchParams`, …) capture it into a file-level `const`. A lazy builtin + gets the same pristine `primordials`, but the live globals it would capture + are whatever user code left behind, so it should not reach for them at all. - No `import`/`export` — these are classic function bodies, not modules. - ESLint (`eslint.config.mjs` at the repo root, run by lint-staged) declares `exports`, `require`, `module`, `binding`, `primordials` and the reachable diff --git a/NativeScript/runtime/js/base64.js b/NativeScript/runtime/js/base64.js new file mode 100644 index 00000000..3fa07542 --- /dev/null +++ b/NativeScript/runtime/js/base64.js @@ -0,0 +1,46 @@ +"use strict"; +// atob / btoa (HTML Standard §8.3, base64 utility methods) over the WHATWG +// forgiving-base64 codec in Base64.cpp. +// +// This file exports the two functions instead of installing them; the C++ +// lazy-global tier (LazyGlobals) places them and is what runs this file, on +// the first read of either name. Nothing here may depend on a builtin that +// runs after it, so `internals` is off limits — see the README. +// +// Deliberate deviation from the spec: no DOMException in this runtime, so the +// failure is an Error with `name` patched to "InvalidCharacterError", the same +// stand-in abort-signal.js and performance.js use. The native ops answer null +// on failure rather than throwing, so that shape stays here. +const { Error, TypeError } = primordials; + +const { atob: decodeBase64, btoa: encodeBase64 } = binding; + +function invalidCharacterError() { + const e = new Error("Invalid character"); + e.name = "InvalidCharacterError"; + return e; +} + +function btoa(data) { + if (arguments.length < 1) { + throw new TypeError("btoa requires 1 argument"); + } + const result = encodeBase64(`${data}`); + if (result === null) { + throw invalidCharacterError(); + } + return result; +} + +function atob(data) { + if (arguments.length < 1) { + throw new TypeError("atob requires 1 argument"); + } + const result = decodeBase64(`${data}`); + if (result === null) { + throw invalidCharacterError(); + } + return result; +} + +module.exports = { atob, btoa }; diff --git a/NativeScript/runtime/js/node-util.js b/NativeScript/runtime/js/node-util.js index a89c2826..fd965d47 100644 --- a/NativeScript/runtime/js/node-util.js +++ b/NativeScript/runtime/js/node-util.js @@ -11,6 +11,18 @@ // surfaces can diverge without either one carrying the other's baggage. const { ObjectFreeze } = primordials; -const { inspect, format } = require("ns:util"); +const util = require("ns:util"); +const { inspect, format } = util; -module.exports = ObjectFreeze({ inspect, format }); +// Node exposes the two encoding interfaces on util; they stay lazy here for +// the same reason they are lazy on ns:util. +module.exports = ObjectFreeze({ + inspect, + format, + get TextEncoder() { + return util.TextEncoder; + }, + get TextDecoder() { + return util.TextDecoder; + }, +}); diff --git a/NativeScript/runtime/js/ns-util.js b/NativeScript/runtime/js/ns-util.js index edc47327..ef886bbb 100644 --- a/NativeScript/runtime/js/ns-util.js +++ b/NativeScript/runtime/js/ns-util.js @@ -148,4 +148,16 @@ function format(...args) { return str; } -module.exports = ObjectFreeze({ inspect, format }); +// `binding.TextEncoder`/`.TextDecoder` are lazy: reading either one runs the +// text-encoding builtin, so the reads stay inside these getters instead of +// joining the destructuring at the top of the file. +module.exports = ObjectFreeze({ + inspect, + format, + get TextEncoder() { + return binding.TextEncoder; + }, + get TextDecoder() { + return binding.TextDecoder; + }, +}); diff --git a/NativeScript/runtime/js/primordials.js b/NativeScript/runtime/js/primordials.js index 984e3262..37381979 100644 --- a/NativeScript/runtime/js/primordials.js +++ b/NativeScript/runtime/js/primordials.js @@ -30,6 +30,8 @@ const intrinsics = { Set, String, TypeError, + Uint8Array, + Uint32Array, URL, WeakRef, SymbolHasInstance: Symbol.hasInstance, diff --git a/NativeScript/runtime/js/text-encoding.js b/NativeScript/runtime/js/text-encoding.js new file mode 100644 index 00000000..37129060 --- /dev/null +++ b/NativeScript/runtime/js/text-encoding.js @@ -0,0 +1,172 @@ +"use strict"; +// TextEncoder / TextDecoder (WHATWG Encoding Standard, +// https://encoding.spec.whatwg.org). +// +// This file exports the two interfaces instead of installing them; the C++ +// lazy-global tier (LazyGlobals) places them and is what runs this file, on +// the first read of either name. Nothing here may depend on a builtin that +// runs after it, so `internals` is off limits — see the README. +// +// The supported encodings (utf-8, utf-16le, utf-16be, windows-1252) with +// their complete label sets, the decoders and the UTF-8 encoder all live in +// TextEncoding.cpp. Per-decoder streaming state is the Uint8Array this file +// allocates and the native decoder reads and rewrites, so a TextDecoder needs +// neither a native handle nor a finalizer. +const { + ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, + RangeError, + SymbolToStringTag, + TypeError, + Uint8Array, + Uint32Array, +} = primordials; + +const { labelToEncoding, decode, encodeUtf8, encodeInto } = binding; + +// Indexed by the encoding ids labelToEncoding returns. +const kEncodingNames = ["utf-8", "utf-16le", "utf-16be", "windows-1252"]; + +// Mirror the kFlag* constants in TextEncoding.cpp. +const kFlagFatal = 1; +const kFlagIgnoreBOM = 2; +const kFlagStream = 4; + +// Mirrors TextEncoding::kDecoderStateSize. +const kDecoderStateSize = 16; + +// encodeInto reports {read, written} through this rather than allocating a +// result object natively; the op is synchronous, so one buffer serves every +// encoder in the isolate. +const encodeIntoResults = new Uint32Array(2); + +// WebIDL dictionary conversion: undefined and null mean "all defaults", +// anything else must be an object. +function toDictionary(value, name) { + if (value === undefined || value === null) { + return undefined; + } + if (typeof value !== "object" && typeof value !== "function") { + throw new TypeError(`The "${name}" argument must be an object`); + } + return value; +} + +class TextEncoder { + #brand; + + static #check(receiver) { + if (!(#brand in receiver)) { + throw new TypeError("Illegal invocation"); + } + } + + get encoding() { + TextEncoder.#check(this); + return "utf-8"; + } + + encode(input = "") { + TextEncoder.#check(this); + return encodeUtf8(`${input}`); + } + + encodeInto(source, destination) { + TextEncoder.#check(this); + const text = `${source}`; + if (!encodeInto(text, destination, encodeIntoResults)) { + throw new TypeError( + 'The "destination" argument must be an instance of Uint8Array' + ); + } + return { read: encodeIntoResults[0], written: encodeIntoResults[1] }; + } +} + +class TextDecoder { + #encoding; + #fatal; + #ignoreBOM; + #flags; + #state; + + static #check(receiver) { + if (!(#encoding in receiver)) { + throw new TypeError("Illegal invocation"); + } + } + + constructor(label = "utf-8", options = undefined) { + const name = `${label}`; + const dictionary = toDictionary(options, "options"); + const encoding = labelToEncoding(name); + if (encoding < 0) { + throw new RangeError(`The encoding "${name}" is not supported`); + } + const fatal = dictionary !== undefined && !!dictionary.fatal; + const ignoreBOM = dictionary !== undefined && !!dictionary.ignoreBOM; + this.#encoding = encoding; + this.#fatal = fatal; + this.#ignoreBOM = ignoreBOM; + this.#flags = (fatal ? kFlagFatal : 0) | (ignoreBOM ? kFlagIgnoreBOM : 0); + this.#state = new Uint8Array(kDecoderStateSize); + } + + get encoding() { + TextDecoder.#check(this); + return kEncodingNames[this.#encoding]; + } + + get fatal() { + TextDecoder.#check(this); + return this.#fatal; + } + + get ignoreBOM() { + TextDecoder.#check(this); + return this.#ignoreBOM; + } + + decode(input = undefined, options = undefined) { + TextDecoder.#check(this); + const dictionary = toDictionary(options, "options"); + const stream = dictionary !== undefined && !!dictionary.stream; + return decode( + input, + this.#encoding, + stream ? this.#flags | kFlagStream : this.#flags, + this.#state + ); + } +} + +// WebIDL shape: interface members are enumerable prototype properties and the +// class string is a configurable, non-writable Symbol.toStringTag; class +// syntax alone yields non-enumerable members. +function finishInterface(ctor, tag, members) { + const proto = ctor.prototype; + ObjectDefineProperty(proto, SymbolToStringTag, { + value: tag, + writable: false, + enumerable: false, + configurable: true, + }); + for (let i = 0; i < members.length; i++) { + const desc = ObjectGetOwnPropertyDescriptor(proto, members[i]); + desc.enumerable = true; + ObjectDefineProperty(proto, members[i], desc); + } +} +finishInterface(TextEncoder, "TextEncoder", [ + "encoding", + "encode", + "encodeInto", +]); +finishInterface(TextDecoder, "TextDecoder", [ + "encoding", + "fatal", + "ignoreBOM", + "decode", +]); + +module.exports = { TextEncoder, TextDecoder }; diff --git a/TestRunner/app/shared b/TestRunner/app/shared index 0baab7cc..0f45dc87 160000 --- a/TestRunner/app/shared +++ b/TestRunner/app/shared @@ -1 +1 @@ -Subproject commit 0baab7cceaca2bb5fdb7b697c08b19be1e46d925 +Subproject commit 0f45dc8776207618c2dce3202f8767d2275dfb5a diff --git a/TestRunner/app/tests/NsUtilTests.js b/TestRunner/app/tests/NsUtilTests.js index 50cd3e69..16f2215c 100644 --- a/TestRunner/app/tests/NsUtilTests.js +++ b/TestRunner/app/tests/NsUtilTests.js @@ -11,7 +11,45 @@ describe("ns:util", function () { // The export set is public API, declared in types/ns-util.d.ts and // docs/ns-builtin-modules.md — all three must change together. it("exposes exactly the declared surface", function () { - expect(Object.keys(util).sort()).toEqual(["format", "inspect"]); + expect(Object.keys(util).sort()).toEqual(["TextDecoder", "TextEncoder", "format", "inspect"]); + }); + + it("exposes the encoding interfaces the globals expose", function () { + expect(typeof util.TextEncoder).toBe("function"); + expect(typeof util.TextDecoder).toBe("function"); + // One run of the text-encoding builtin backs both entry points, so the + // classes are identical objects no matter which is reached first. + expect(util.TextEncoder).toBe(globalThis.TextEncoder); + expect(util.TextDecoder).toBe(globalThis.TextDecoder); + }); + + it("round trips text through the module's encoding interfaces", function () { + var bytes = new util.TextEncoder().encode("héllo"); + expect(bytes instanceof Uint8Array).toBe(true); + expect(bytes.length).toBe(6); + expect(new util.TextDecoder().decode(bytes)).toBe("héllo"); + }); + + it("keeps the classes identical in a fresh isolate, whichever is touched first", function (done) { + var orders = ["global-first", "util-first"]; + var replies = 0; + orders.forEach(function (order) { + var worker = new Worker("./nsUtilEncodingOrderWorker.js"); + worker.onmessage = function (msg) { + expect(msg.data).toEqual({ + order: order, + encoder: true, + decoder: true, + roundTrip: "ok", + }); + worker.terminate(); + replies++; + if (replies === orders.length) { + done(); + } + }; + worker.postMessage(order); + }); }); it("is a singleton per realm", function () { @@ -149,6 +187,15 @@ describe("node:util", function () { expect(Object.isFrozen(nodeUtil)).toBe(true); expect(nodeUtil.inspect).toBe(util.inspect); expect(nodeUtil.format).toBe(util.format); + expect(nodeUtil.TextEncoder).toBe(util.TextEncoder); + expect(nodeUtil.TextDecoder).toBe(util.TextDecoder); + }); + + it("exposes Node's encoding interfaces, identical to the globals", function () { + expect(Object.keys(nodeUtil).sort()).toEqual(["TextDecoder", "TextEncoder", "format", "inspect"]); + expect(nodeUtil.TextEncoder).toBe(globalThis.TextEncoder); + expect(nodeUtil.TextDecoder).toBe(globalThis.TextDecoder); + expect(new nodeUtil.TextDecoder("utf-8").decode(new nodeUtil.TextEncoder().encode("ok"))).toBe("ok"); }); it("is a singleton per realm", function () { diff --git a/TestRunner/app/tests/nsUtilEncodingOrderWorker.js b/TestRunner/app/tests/nsUtilEncodingOrderWorker.js new file mode 100644 index 00000000..3a7e6c70 --- /dev/null +++ b/TestRunner/app/tests/nsUtilEncodingOrderWorker.js @@ -0,0 +1,28 @@ +// A worker is a fresh isolate, which is what makes the access order testable: +// the parent realm has already materialized TextEncoder/TextDecoder by the +// time any spec runs. Nothing here may touch either name before the handler, +// or the requested order is lost. +onmessage = function (msg) { + var order = msg.data; + var results = { order: order }; + + if (order === "global-first") { + var globalEncoder = globalThis.TextEncoder; + var globalDecoder = globalThis.TextDecoder; + var nsUtil = require("ns:util"); + var nodeUtil = require("node:util"); + results.encoder = nsUtil.TextEncoder === globalEncoder && nodeUtil.TextEncoder === globalEncoder; + results.decoder = nsUtil.TextDecoder === globalDecoder && nodeUtil.TextDecoder === globalDecoder; + results.roundTrip = new nsUtil.TextDecoder().decode(new nodeUtil.TextEncoder().encode("ok")); + } else { + var util = require("ns:util"); + var node = require("node:util"); + var utilEncoder = util.TextEncoder; + var utilDecoder = node.TextDecoder; + results.encoder = globalThis.TextEncoder === utilEncoder && node.TextEncoder === utilEncoder; + results.decoder = globalThis.TextDecoder === utilDecoder && util.TextDecoder === utilDecoder; + results.roundTrip = new node.TextDecoder().decode(new util.TextEncoder().encode("ok")); + } + + postMessage(results); +}; diff --git a/docs/ns-builtin-modules.md b/docs/ns-builtin-modules.md index ba2ce433..bf298c8b 100644 --- a/docs/ns-builtin-modules.md +++ b/docs/ns-builtin-modules.md @@ -59,6 +59,7 @@ Rules: |---|---| | `inspect(value[, options])` | Formats any value for human consumption: depth-limited, output-capped, cycle-safe, never invokes getters (except a guarded `error.stack` read and custom `toString` overrides, which are honored). `options.depth` (number) overrides the default depth of 2. Other option keys are reserved. | | `format(fmt, ...args)` | Node-style printf formatting: `%s`, `%d`, `%i`, `%f`, `%j`, `%o`, `%O`, `%%`. Extra arguments are appended space-separated, objects rendered via `inspect`. When `fmt` is not a string or contains no substitutions, all arguments are formatted and joined with spaces. `console.*` routes its arguments through this, so `console.log("%d apples", 3)` works. | +| `TextEncoder` / `TextDecoder` | The WHATWG encoding interfaces, **the very same class objects the globals of those names hold** (`require("ns:util").TextDecoder === globalThis.TextDecoder`). Reading either member is what materializes them, so requiring the module costs nothing extra. | ```js const { inspect, format } = require("ns:util"); @@ -78,6 +79,13 @@ format("%j", { ok: true }); // '{"ok":true}' format("100% sure", "extra"); // "100% sure extra" (no placeholder consumed) ``` +```js +const { TextEncoder, TextDecoder } = require("ns:util"); + +TextDecoder === globalThis.TextDecoder; // true +new TextDecoder().decode(new TextEncoder().encode("héllo")); // "héllo" +``` + **Stability caveat (verbatim from Node's contract):** the output of `inspect` (and therefore `format`'s object rendering) may change between runtime versions for readability; it is intended for humans and must not be parsed @@ -426,7 +434,7 @@ unmodified where a shim exists: | module | exports | notes | |---|---|---| -| `node:util` | `inspect`, `format` | Re-exports `ns:util`'s members unchanged (`nodeUtil.inspect === nsUtil.inspect`) from a **distinct, separately frozen module object**. Documented as partial. | +| `node:util` | `inspect`, `format`, `TextEncoder`, `TextDecoder` | Re-exports `ns:util`'s members unchanged (`nodeUtil.inspect === nsUtil.inspect`) from a **distinct, separately frozen module object**. `TextEncoder`/`TextDecoder` are the globals of those names, as they are in Node. Documented as partial. | | `node:url` | `fileURLToPath`, `pathToFileURL` | Node-strict converters between `file:` URLs and paths. Documented as partial — no `URL`/`URLSearchParams` re-exports (both are globals), no legacy `url.parse`/`format`/`resolve`. | | `node:module` | `createRequire` | Re-exports `ns:module`'s `createRequire` unchanged from a **distinct, separately frozen module object**. `createPumpingRequire` is deliberately absent: it has no Node counterpart, so code written against this shim keeps running on Node. `require.resolve`/`.cache`/`.main` are not implemented, and neither is any other `node:module` member (`Module`, `builtinModules`, `isBuiltin`, `register`, `syncBuiltinESMExports`). Documented as partial. | diff --git a/tools/js2c-inputs.xcfilelist b/tools/js2c-inputs.xcfilelist index 1913e2ab..18c55154 100644 --- a/tools/js2c-inputs.xcfilelist +++ b/tools/js2c-inputs.xcfilelist @@ -1,5 +1,6 @@ $(SRCROOT)/tools/js2c.mjs $(SRCROOT)/NativeScript/runtime/js/abort-signal.js +$(SRCROOT)/NativeScript/runtime/js/base64.js $(SRCROOT)/NativeScript/runtime/js/blob-url.js $(SRCROOT)/NativeScript/runtime/js/class-extends.js $(SRCROOT)/NativeScript/runtime/js/error-events.js @@ -17,5 +18,6 @@ $(SRCROOT)/NativeScript/runtime/js/performance.js $(SRCROOT)/NativeScript/runtime/js/promise-proxy.js $(SRCROOT)/NativeScript/runtime/js/require-factory.js $(SRCROOT)/NativeScript/runtime/js/structured-clone.js +$(SRCROOT)/NativeScript/runtime/js/text-encoding.js $(SRCROOT)/NativeScript/runtime/js/ts-helpers.js $(SRCROOT)/NativeScript/runtime/js/weak-ref.js diff --git a/types/ns-util.d.ts b/types/ns-util.d.ts index fd32d948..534f2c63 100644 --- a/types/ns-util.d.ts +++ b/types/ns-util.d.ts @@ -23,4 +23,39 @@ declare module "ns:util" { * `console.*` routes its arguments through this. */ export function format(format?: unknown, ...args: unknown[]): string; + + export interface TextEncoderInstance { + readonly encoding: "utf-8"; + encode(input?: string): Uint8Array; + encodeInto( + source: string, + destination: Uint8Array + ): { read: number; written: number }; + } + + export interface TextDecoderInstance { + readonly encoding: string; + readonly fatal: boolean; + readonly ignoreBOM: boolean; + decode( + input?: ArrayBuffer | ArrayBufferView | null, + options?: { stream?: boolean } + ): string; + } + + /** + * The WHATWG `TextEncoder`, which is the very object the global of that name + * holds: `require("ns:util").TextEncoder === globalThis.TextEncoder`. The + * members are declared here rather than taken from `globalThis` so the + * declaration stands on its own, without a DOM lib in the program. + */ + export const TextEncoder: { new (): TextEncoderInstance }; + + /** The WHATWG `TextDecoder`, likewise identical to the global. */ + export const TextDecoder: { + new ( + label?: string, + options?: { fatal?: boolean; ignoreBOM?: boolean } + ): TextDecoderInstance; + }; } diff --git a/v8ios.xcodeproj/project.pbxproj b/v8ios.xcodeproj/project.pbxproj index c3083b3e..ad53635c 100644 --- a/v8ios.xcodeproj/project.pbxproj +++ b/v8ios.xcodeproj/project.pbxproj @@ -22,6 +22,12 @@ 2BFE22062AC1C93100307752 /* metadata-arm64.bin in Resources */ = {isa = PBXBuildFile; fileRef = 2BFE22052AC1C93100307752 /* metadata-arm64.bin */; }; 3C1850542A6DCB2D002ACC81 /* Timers.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3C1850522A6DCB2D002ACC81 /* Timers.cpp */; }; 3C1850552A6DCB2D002ACC81 /* Timers.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C1850532A6DCB2D002ACC81 /* Timers.hpp */; }; + 3CAE10112F900001002ACC81 /* TextEncoding.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3CAE10012F900001002ACC81 /* TextEncoding.cpp */; }; + 3CAE10122F900001002ACC81 /* TextEncoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CAE10022F900001002ACC81 /* TextEncoding.h */; }; + 3CAE10132F900001002ACC81 /* Base64.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3CAE10032F900001002ACC81 /* Base64.cpp */; }; + 3CAE10142F900001002ACC81 /* Base64.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CAE10042F900001002ACC81 /* Base64.h */; }; + 3CAE10152F900001002ACC81 /* LazyGlobals.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3CAE10052F900001002ACC81 /* LazyGlobals.cpp */; }; + 3CAE10162F900001002ACC81 /* LazyGlobals.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CAE10062F900001002ACC81 /* LazyGlobals.h */; }; 3CFCA0032E5A0001002ACC81 /* AnimationFrame.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3CFCA0012E5A0001002ACC81 /* AnimationFrame.mm */; }; 3CFCA0042E5A0001002ACC81 /* AnimationFrame.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3CFCA0022E5A0001002ACC81 /* AnimationFrame.hpp */; }; 3C48F68D2F57905500C14231 /* json.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C48F68B2F57905500C14231 /* json.hpp */; }; @@ -469,6 +475,12 @@ 2BFE22052AC1C93100307752 /* metadata-arm64.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = "metadata-arm64.bin"; sourceTree = ""; }; 3C1850522A6DCB2D002ACC81 /* Timers.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Timers.cpp; sourceTree = ""; }; 3C1850532A6DCB2D002ACC81 /* Timers.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Timers.hpp; sourceTree = ""; }; + 3CAE10012F900001002ACC81 /* TextEncoding.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = TextEncoding.cpp; sourceTree = ""; }; + 3CAE10022F900001002ACC81 /* TextEncoding.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TextEncoding.h; sourceTree = ""; }; + 3CAE10032F900001002ACC81 /* Base64.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Base64.cpp; sourceTree = ""; }; + 3CAE10042F900001002ACC81 /* Base64.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Base64.h; sourceTree = ""; }; + 3CAE10052F900001002ACC81 /* LazyGlobals.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = LazyGlobals.cpp; sourceTree = ""; }; + 3CAE10062F900001002ACC81 /* LazyGlobals.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LazyGlobals.h; sourceTree = ""; }; 3CFCA0012E5A0001002ACC81 /* AnimationFrame.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AnimationFrame.mm; sourceTree = ""; }; 3CFCA0022E5A0001002ACC81 /* AnimationFrame.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AnimationFrame.hpp; sourceTree = ""; }; 3C48F68B2F57905500C14231 /* json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json.hpp; sourceTree = ""; }; @@ -1547,6 +1559,12 @@ 3C78BA5B2A0D600100C20A88 /* ModuleBinding.hpp */, 3C1850522A6DCB2D002ACC81 /* Timers.cpp */, 3C1850532A6DCB2D002ACC81 /* Timers.hpp */, + 3CAE10012F900001002ACC81 /* TextEncoding.cpp */, + 3CAE10022F900001002ACC81 /* TextEncoding.h */, + 3CAE10032F900001002ACC81 /* Base64.cpp */, + 3CAE10042F900001002ACC81 /* Base64.h */, + 3CAE10052F900001002ACC81 /* LazyGlobals.cpp */, + 3CAE10062F900001002ACC81 /* LazyGlobals.h */, 3CFCA0012E5A0001002ACC81 /* AnimationFrame.mm */, 3CFCA0022E5A0001002ACC81 /* AnimationFrame.hpp */, 3C5333332B0E683100BE0C47 /* Message.hpp */, @@ -1644,6 +1662,9 @@ C22536B8241A318900192740 /* ffi.h in Headers */, C247C16F22F82842001D2CA2 /* v8-util.h in Headers */, 3C1850552A6DCB2D002ACC81 /* Timers.hpp in Headers */, + 3CAE10122F900001002ACC81 /* TextEncoding.h in Headers */, + 3CAE10142F900001002ACC81 /* Base64.h in Headers */, + 3CAE10162F900001002ACC81 /* LazyGlobals.h in Headers */, 3CFCA0042E5A0001002ACC81 /* AnimationFrame.hpp in Headers */, C2C8EE7222CE323C001F8CEC /* ConcurrentMap.h in Headers */, C2A6EF3123745A0B00E8FBE7 /* MetadataInlines.h in Headers */, @@ -2289,6 +2310,9 @@ 6573B9D4291FE29F00B0ED7C /* V8RuntimeFactory.cpp in Sources */, C2DDEBB4229EAC8300345BFE /* DictionaryAdapter.mm in Sources */, 3C1850542A6DCB2D002ACC81 /* Timers.cpp in Sources */, + 3CAE10112F900001002ACC81 /* TextEncoding.cpp in Sources */, + 3CAE10132F900001002ACC81 /* Base64.cpp in Sources */, + 3CAE10152F900001002ACC81 /* LazyGlobals.cpp in Sources */, 3CFCA0032E5A0001002ACC81 /* AnimationFrame.mm in Sources */, C298C027233C9AEA000DDF54 /* TSHelpers.cpp in Sources */, C2FEA16F22A3C75C00A5C0FC /* InlineFunctions.cpp in Sources */,