Skip to content
Merged
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ FetchContent_Declare(llhttp
EXCLUDE_FROM_ALL)
FetchContent_Declare(UrlLib
GIT_REPOSITORY https://github.com/BabylonJS/UrlLib.git
GIT_TAG e86ffb34e77092266145497681efc74e0a920ffe
GIT_TAG 0c991337a1160ba7a2d062bf8e342d0a66f48dc9
EXCLUDE_FROM_ALL)
FetchContent_Declare(quickjs-ng
GIT_REPOSITORY https://github.com/quickjs-ng/quickjs.git
Expand Down
7 changes: 6 additions & 1 deletion Core/Node-API/Source/js_native_api_javascriptcore.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1547,7 +1547,12 @@ napi_status napi_typeof(napi_env env, napi_value value, napi_valuetype* result)
case kJSTypeSymbol: *result = napi_symbol; break;
default:
JSObjectRef object{ToJSObject(env, value)};
if (JSObjectIsFunction(env->context, object)) {
// Consult JSObjectIsConstructor in addition to JSObjectIsFunction: some JSC builds (e.g.
// libjavascriptcoregtk) report constructors created via JSObjectMakeConstructor -- such as
// node-addon-api DefineClass constructors -- as not-a-function, which would otherwise be
// classified as napi_object and make Napi IsFunction() reject otherwise-valid constructors
// (see issue #194).
if (JSObjectIsFunction(env->context, object) || JSObjectIsConstructor(env->context, object)) {
*result = napi_function;
} else {
NativeInfo* info = NativeInfo::Get<NativeInfo>(object);
Expand Down
11 changes: 11 additions & 0 deletions Polyfills/Blob/Include/Babylon/Polyfills/Blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
#include <napi/env.h>
#include <Babylon/Api.h>

#include <cstddef>
#include <memory>
#include <string>
#include <vector>

namespace Babylon::Polyfills::Blob
{
void BABYLON_API Initialize(Napi::Env env);

// Synchronously reads the bytes and MIME type of a Blob JS object created by this polyfill.
// Returns false if `object` is not a Blob. A Blob is immutable once constructed, so `outData`
// is a shared reference to its buffer rather than a copy, and it stays valid for as long as the
// caller holds it -- even if `object` is collected.
bool BABYLON_API TryGetData(const Napi::Object& object, std::shared_ptr<const std::vector<std::byte>>& outData, std::string& outType);
Comment thread
bghgary marked this conversation as resolved.
Outdated
}
94 changes: 79 additions & 15 deletions Polyfills/Blob/Source/Blob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ namespace Babylon::Polyfills::Internal

Napi::Value Blob::GetSize(const Napi::CallbackInfo&)
{
return Napi::Value::From(Env(), m_data.size());
return Napi::Value::From(Env(), m_data->size());
}

Napi::Value Blob::GetType(const Napi::CallbackInfo&)
Expand All @@ -67,8 +67,8 @@ namespace Babylon::Polyfills::Internal
Napi::Value Blob::Text(const Napi::CallbackInfo&)
{
// NOTE: This will not check for UTF-8 validity
const auto begin = reinterpret_cast<const char*>(m_data.data());
std::string text(begin, m_data.size());
const auto begin = reinterpret_cast<const char*>(m_data->data());
std::string text(begin, m_data->size());

const auto deferred = Napi::Promise::Deferred::New(Env());
deferred.Resolve(Napi::String::New(Env(), text));
Expand All @@ -77,10 +77,10 @@ namespace Babylon::Polyfills::Internal

Napi::Value Blob::ArrayBuffer(const Napi::CallbackInfo&)
{
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size());
if (m_data.data())
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data->size());
if (m_data->data())
{
std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size());
std::memcpy(arrayBuffer.Data(), m_data->data(), m_data->size());
}

const auto deferred = Napi::Promise::Deferred::New(Env());
Expand All @@ -90,12 +90,12 @@ namespace Babylon::Polyfills::Internal

Napi::Value Blob::Bytes(const Napi::CallbackInfo&)
{
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size());
if (m_data.data())
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data->size());
if (m_data->data())
{
std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size());
std::memcpy(arrayBuffer.Data(), m_data->data(), m_data->size());
}
const auto uint8Array = Napi::Uint8Array::New(Env(), m_data.size(), arrayBuffer, 0);
const auto uint8Array = Napi::Uint8Array::New(Env(), m_data->size(), arrayBuffer, 0);

const auto deferred = Napi::Promise::Deferred::New(Env());
deferred.Resolve(uint8Array);
Expand All @@ -108,27 +108,28 @@ namespace Babylon::Polyfills::Internal
{
const auto buffer = blobPart.As<Napi::ArrayBuffer>();
const auto begin = static_cast<const std::byte*>(buffer.Data());
m_data.assign(begin, begin + buffer.ByteLength());
m_data = std::make_shared<const std::vector<std::byte>>(begin, begin + buffer.ByteLength());
}
else if (blobPart.IsTypedArray() || blobPart.IsDataView())
{
const auto array = blobPart.As<Napi::TypedArray>();
const auto buffer = array.ArrayBuffer();
const auto begin = static_cast<const std::byte*>(buffer.Data()) + array.ByteOffset();
m_data.assign(begin, begin + array.ByteLength());
m_data = std::make_shared<const std::vector<std::byte>>(begin, begin + array.ByteLength());
}
else if (blobPart.IsString())
{
const auto str = blobPart.As<Napi::String>().Utf8Value();
const auto begin = reinterpret_cast<const std::byte*>(str.data());
m_data.assign(begin, begin + str.length());
m_data = std::make_shared<const std::vector<std::byte>>(begin, begin + str.length());
}
else
{
// Assume it's another Blob object
// Assume it's another Blob object. Blobs are immutable, so the buffer can be shared
// rather than copied.
const auto obj = blobPart.As<Napi::Object>();
const auto blobObj = Napi::ObjectWrap<Blob>::Unwrap(obj);
m_data.assign(blobObj->m_data.begin(), blobObj->m_data.end());
m_data = blobObj->m_data;
}
}
}
Expand All @@ -139,4 +140,67 @@ namespace Babylon::Polyfills::Blob
{
Internal::Blob::Initialize(env);
}

bool BABYLON_API TryGetData(const Napi::Object& object, std::shared_ptr<const std::vector<std::byte>>& outData, std::string& outType)
{
const auto env = object.Env();
const auto global = env.Global();

// Verify `object` is a Blob (or a subclass such as File) by walking its prototype chain and
// comparing each link against Blob.prototype, using the JS-level Object.getPrototypeOf.
//
// We use Object.getPrototypeOf rather than the raw napi_get_prototype C API because the
// latter is not exposed by every adapter (e.g. JSI) and, on JSC, returns the raw
// [[Prototype]] which differs from the JS-visible prototype of a DefineClass instance.
// This keeps the check portable across QuickJS, V8, JavaScriptCore, Chakra and JSI, and it
// also accepts Blob subclasses (e.g. File). We deliberately avoid napi_instanceof, whose
// node-addon-api wrapper requires a Napi::Function.
const auto blobConstructor = global.Get("Blob");
if (!blobConstructor.IsFunction())
{
return false;
}

const auto blobPrototype = blobConstructor.As<Napi::Object>().Get("prototype");
if (!blobPrototype.IsObject())
{
return false;
}

const auto objectConstructor = global.Get("Object");
if (!objectConstructor.IsObject())
{
return false;
}

const auto getPrototypeOf = objectConstructor.As<Napi::Object>().Get("getPrototypeOf");
if (!getPrototypeOf.IsFunction())
{
return false;
}

const auto getPrototypeOfFn = getPrototypeOf.As<Napi::Function>();

bool isBlob = false;
Napi::Value current = getPrototypeOfFn.Call({object});
while (current.IsObject())
{
if (current.StrictEquals(blobPrototype))
{
isBlob = true;
break;
}
current = getPrototypeOfFn.Call({current});
}

if (!isBlob)
{
return false;
}

const auto* blob = Internal::Blob::Unwrap(object);
outData = blob->Data();
outType = blob->Type();
return true;
}
}
10 changes: 9 additions & 1 deletion Polyfills/Blob/Source/Blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include <napi/napi.h>

#include <cstddef>
#include <memory>
#include <vector>
#include <string>

Expand All @@ -14,6 +16,12 @@ namespace Babylon::Polyfills::Internal

explicit Blob(const Napi::CallbackInfo& info);

// Synchronous accessors for internal cross-polyfill use (e.g. URL.createObjectURL).
// A Blob is immutable once constructed, so the bytes are held in a shared_ptr and can be
// shared with consumers rather than copied. Never null.
const std::shared_ptr<const std::vector<std::byte>>& Data() const { return m_data; }
const std::string& Type() const { return m_type; }

private:
Napi::Value GetSize(const Napi::CallbackInfo& info);
Napi::Value GetType(const Napi::CallbackInfo& info);
Expand All @@ -23,7 +31,7 @@ namespace Babylon::Polyfills::Internal

void ProcessBlobPart(const Napi::Value& blobPart);

std::vector<std::byte> m_data;
std::shared_ptr<const std::vector<std::byte>> m_data{std::make_shared<const std::vector<std::byte>>()};
std::string m_type;
};
}
15 changes: 4 additions & 11 deletions Polyfills/Fetch/Source/Fetch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,9 @@ namespace Babylon::Polyfills::Internal
// when an error is thrown) -- in that case the rejection simply carries no synthetic frames.
std::string CaptureCallSiteStack(Napi::Env env)
{
// Detect the global Error constructor with IsUndefined()/IsNull() rather than
// IsFunction(): some JavaScriptCore/JSI builds classify constructor functions as
// typeof 'object', so napi_typeof reports napi_object and IsFunction() would
// incorrectly skip stack capture even though Error is callable (see the Blob check
// below for the same rationale). Error is always present, so this guard is defensive.
// Error is always present and callable; this guard is defensive.
const Napi::Value errorCtor = env.Global().Get("Error");
if (errorCtor.IsUndefined() || errorCtor.IsNull())
if (!errorCtor.IsFunction())
{
return {};
}
Expand Down Expand Up @@ -305,12 +301,9 @@ namespace Babylon::Polyfills::Internal
Napi::Env env = info.Env();
const auto deferred = Napi::Promise::Deferred::New(env);

// Use IsUndefined()/IsNull() rather than IsFunction() to detect the Blob
// polyfill: some JavaScriptCore/JSI builds classify constructor functions as
// typeof 'object', so napi_typeof reports napi_object and IsFunction() would
// incorrectly reject even when the Blob polyfill is installed.
// Require the Blob polyfill to be installed.
const auto blobConstructor = env.Global().Get("Blob");
if (blobConstructor.IsUndefined() || blobConstructor.IsNull())
if (!blobConstructor.IsFunction())
{
deferred.Reject(Napi::Error::New(env, "fetch: Blob is not available in this environment").Value());
return deferred.Promise();
Expand Down
8 changes: 2 additions & 6 deletions Polyfills/File/Source/File.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,9 @@ namespace Babylon::Polyfills::Internal

// Require the native Blob polyfill: File delegates byte storage to
// a Blob, so without it the constructor cannot produce useful
// instances. Use IsUndefined() rather than IsFunction() because
// some JavaScriptCore builds (notably libjavascriptcoregtk on
// Linux) classify constructors created via JSObjectMakeConstructor
// as typeof 'object', not 'function', so napi_typeof returns
// napi_object for them.
// instances.
auto blob = global.Get(JS_BLOB_CONSTRUCTOR_NAME);
if (blob.IsUndefined() || blob.IsNull())
if (!blob.IsFunction())
{
throw Napi::Error::New(env,
"File polyfill requires the Blob polyfill to be installed first.");
Expand Down
2 changes: 2 additions & 0 deletions Polyfills/URL/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ add_library(URL ${SOURCES})
target_include_directories(URL PUBLIC "Include")

target_link_libraries(URL
PRIVATE Blob
PRIVATE UrlLib
PUBLIC JsRuntime)

set_property(TARGET URL PROPERTY FOLDER Polyfills)
Expand Down
34 changes: 34 additions & 0 deletions Polyfills/URL/Include/Babylon/Polyfills/URL.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,41 @@
#include <napi/env.h>
#include <Babylon/Api.h>

#include <cstddef>
#include <memory>
#include <string>
#include <vector>

namespace Babylon::Polyfills::URL
{
void BABYLON_API Initialize(Napi::Env env);

// Blob URL store backing URL.createObjectURL / URL.revokeObjectURL. Native has no browser
// blob: URL store, so the URL polyfill keeps an in-memory registry keyed by a minted
// blob: URL. Consumers do not resolve these URLs directly: the URL polyfill registers a
// process-global blob: resolver with UrlLib, so fetch, XMLHttpRequest, and any other UrlLib
// consumer serve blob: URLs uniformly through the transport layer.
//
// The store is process-global (shared across all JS environments in the process); the minted
// URLs embed a UUID so entries never collide between environments. It is process-global because
// no Node-API adapter implements napi_add_env_cleanup_hook, so there is no portable hook on
// which to free per-environment state (tracked by #215). Consequently entries outlive the
// environment that created them: a browser drops its blob URL store at unload, which
// corresponds to environment teardown here rather than process exit, so an embedder that
// creates and destroys environments accumulates every un-revoked entry for the life of the
// process. Call revokeObjectURL when done with a URL.
//
// The store shares the Blob's byte buffer through a shared_ptr, so registering a URL does not
// duplicate it and resolving one hands out a reference rather than a copy. Bytes are released
// once the entry is revoked and every outstanding resolver has dropped its shared_ptr, matching
// how a browser Blob's bytes stay valid for an in-flight read even if the URL is revoked
// mid-flight.

// Registers `data` under a freshly minted blob: URL and returns it. The Blob's buffer is shared
// rather than copied, so createObjectURL does not duplicate a large blob.
// The `env` parameter is currently unused but kept for API symmetry and future per-env scoping.
std::string BABYLON_API RegisterObjectURL(Napi::Env env, std::shared_ptr<const std::vector<std::byte>> data, std::string type);

// Releases the entry for `url`, if any. Unknown URLs are ignored (matching the web platform).
void BABYLON_API RevokeObjectURL(Napi::Env env, const std::string& url);
Comment thread
bghgary marked this conversation as resolved.
Outdated
}
6 changes: 2 additions & 4 deletions Polyfills/URL/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ Partial implementation for [`URL`](https://developer.mozilla.org/en-US/docs/Web/
- [`toJSON`](https://developer.mozilla.org/en-US/docs/Web/API/URL/toJSON)
- [`parse`](https://developer.mozilla.org/en-US/docs/Web/API/URL/parse_static)
- [`canParse`](https://developer.mozilla.org/en-US/docs/Web/API/URL/canParse_static)

## Not implemented
- [`createObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static)
- [`revokeObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static)
- [`createObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static) (Blob only; mints a `blob:` URL backed by an in-memory, process-global object-URL store)
- [`revokeObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static) (releases the stored bytes for a `blob:` URL previously returned by `createObjectURL`; subsequent fetch/XMLHttpRequest against that URL fail as a network error)

# URLSearchParams
Partial implementatioin for [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)
Expand Down
Loading
Loading