Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ option(JSRUNTIMEHOST_POLYFILL_URL "Include JsRuntimeHost Polyfill URL and URLSea
option(JSRUNTIMEHOST_POLYFILL_ABORT_CONTROLLER "Include JsRuntimeHost Polyfills AbortController and AbortSignal." ON)
option(JSRUNTIMEHOST_POLYFILL_WEBSOCKET "Include JsRuntimeHost Polyfill WebSocket." ON)
option(JSRUNTIMEHOST_POLYFILL_BLOB "Include JsRuntimeHost Polyfill Blob." ON)
option(JSRUNTIMEHOST_POLYFILL_FILE "Include JsRuntimeHost Polyfill File and FileReader." ON)
option(JSRUNTIMEHOST_POLYFILL_PERFORMANCE "Include JsRuntimeHost Polyfill Performance." ON)
option(JSRUNTIMEHOST_POLYFILL_TEXTDECODER "Include JsRuntimeHost Polyfill TextDecoder." ON)
option(JSRUNTIMEHOST_POLYFILL_TEXTENCODER "Include JsRuntimeHost Polyfill TextEncoder." ON)
Expand Down
4 changes: 4 additions & 0 deletions Polyfills/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ if(JSRUNTIMEHOST_POLYFILL_BLOB)
add_subdirectory(Blob)
endif()

if(JSRUNTIMEHOST_POLYFILL_FILE)
add_subdirectory(File)
endif()

if(JSRUNTIMEHOST_POLYFILL_PERFORMANCE)
add_subdirectory(Performance)
endif()
Expand Down
17 changes: 17 additions & 0 deletions Polyfills/File/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
set(SOURCES
"Include/Babylon/Polyfills/File.h"
"Source/File.h"
"Source/File.cpp"
"Source/FileReader.h"
"Source/FileReader.cpp")

add_library(File ${SOURCES})
warnings_as_errors(File)

target_include_directories(File PUBLIC "Include")

target_link_libraries(File
PUBLIC JsRuntime)

set_property(TARGET File PROPERTY FOLDER Polyfills)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
9 changes: 9 additions & 0 deletions Polyfills/File/Include/Babylon/Polyfills/File.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include <napi/env.h>
#include <Babylon/Api.h>

namespace Babylon::Polyfills::File
{
void BABYLON_API Initialize(Napi::Env env);
}
35 changes: 35 additions & 0 deletions Polyfills/File/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# File

Implements the `File` and `FileReader` web APIs on top of the native `Blob`
polyfill provided by JsRuntimeHost. Babylon.js GLTF/OBJ serializer
round-trip codepaths construct `new File([blob], 'scene.glb')` and read it
back via `FileReader.readAsArrayBuffer(...)`, so the runtime needs both
constructors for those tests (and any consumer code that wraps serializer
output) to work.

## Behaviour

* No-op when the runtime already exposes a global `File` / `FileReader`
(e.g. V8 in some embeddings).
* `File` is self-contained: the constructor delegates to the global
`Blob` constructor to build the underlying byte storage, then decorates
the instance with `name` and `lastModified`. Methods (`size`, `type`,
`arrayBuffer`, `text`, `bytes`) forward to the inner `Blob`.
* `FileReader` supports `readAsArrayBuffer`, `readAsText`, and
`readAsDataURL`, plus `abort`, `addEventListener` /
`removeEventListener` / `dispatchEvent`, and the standard `onload` /
`onerror` / `onloadstart` / `onloadend` / `onprogress` / `onabort`
handler slots. `abort()` invalidates in-flight reads via a monotonic
token so late-resolving `arrayBuffer()` promises cannot dispatch a
phantom `load` event after a user-initiated abort.
* `File` extends `Blob`: the JS-visible prototype chain is wired so
`new File(...) instanceof Blob === true`. Babylon.js core branches on
`instanceof Blob` in several places (fileTools, Offline/database,
abstractEngine, thinNativeEngine).

## Prerequisites

`Babylon::Polyfills::Blob::Initialize(env)` (from JsRuntimeHost) must be
called before `Babylon::Polyfills::File::Initialize(env)`; if `Blob` is
missing from the global object when `File::Initialize` runs, the `File`
constructor will not be registered.
216 changes: 216 additions & 0 deletions Polyfills/File/Source/File.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
#include "File.h"
#include "FileReader.h"

#include <Babylon/Polyfills/File.h>

#include <chrono>
#include <string>

namespace Babylon::Polyfills::Internal
{
namespace
{
constexpr auto JS_FILE_CONSTRUCTOR_NAME = "File";
constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob";

// Wire File.prototype to inherit from Blob.prototype so that
// `new File(...) instanceof Blob === true`. The shim runs entirely
// in JS so each engine's quirks are handled by the JS try/catch:
//
// - V8 / Chakra: `File.prototype` is the real prototype that
// instances use, so the direct setPrototypeOf succeeds and the
// probe path is skipped.
// - JSC: `File.prototype` aliases Object.prototype (napi_define_class
// wraps JSObjectMakeConstructor; see JsRH#172). The direct call
// throws TypeError, the catch swallows it, and the probe path
// discovers the real napi-internal prototype via
// `Object.getPrototypeOf(new File())` and sets its [[Prototype]]
// to Blob.prototype.
//
// Doing this in JS rather than via Napi::Function::Call avoids a
// JSC napi-shim quirk where setPrototypeOf on Object.prototype
// escapes as an uncaught error instead of being capturable via
// IsExceptionPending.
constexpr auto JS_PROTOTYPE_CHAIN_SHIM = R"JS(
(function() {
if (typeof File !== 'function' || typeof Blob !== 'function') return;
var blobProto = Blob.prototype;
try { Object.setPrototypeOf(File.prototype, blobProto); } catch (e) {}
try {
var probe = new File([], '');
if (!(probe instanceof Blob)) {
Object.setPrototypeOf(Object.getPrototypeOf(probe), blobProto);
}
} catch (e) {}
})();
)JS";
Comment on lines +34 to +46
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If #172 lands first, this whole shim collapses to a direct setPrototypeOf in C++ (no JS eval, no try/catch fallback, no probe). Recommending we land #172 first and drop the shim entirely.

}

void File::Initialize(Napi::Env env)
{
auto global = env.Global();

// No-op if the runtime already provides a global File. Cheapest
// check, and the common path on platforms with a native File.
if (!global.Get(JS_FILE_CONSTRUCTOR_NAME).IsUndefined())
{
return;
}

// 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.
auto blob = global.Get(JS_BLOB_CONSTRUCTOR_NAME);
if (blob.IsUndefined() || blob.IsNull())
{
throw Napi::Error::New(env,
"File polyfill requires the Blob polyfill to be installed first.");
}

Napi::Function func = DefineClass(
env,
JS_FILE_CONSTRUCTOR_NAME,
{
InstanceAccessor("size", &File::GetSize, nullptr),
InstanceAccessor("type", &File::GetType, nullptr),
InstanceAccessor("name", &File::GetName, nullptr),
InstanceAccessor("lastModified", &File::GetLastModified, nullptr),
InstanceMethod("arrayBuffer", &File::ArrayBuffer),
InstanceMethod("text", &File::Text),
InstanceMethod("bytes", &File::Bytes),
});

global.Set(JS_FILE_CONSTRUCTOR_NAME, func);

// Wire File.prototype -> Blob.prototype via a tiny JS shim. See
// the JS_PROTOTYPE_CHAIN_SHIM comment for engine-specific rationale.
Napi::Eval(env, JS_PROTOTYPE_CHAIN_SHIM, "JsRuntimeHost-File-PrototypeChainShim.js");
if (env.IsExceptionPending())
{
// The shim itself wraps every operation in try/catch, so this
// should never fire. Belt-and-braces: clear so Initialize stays
// best-effort and the rest of the polyfill remains installed.
env.GetAndClearPendingException();
}
}

File::File(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<File>{info}
{
auto env = info.Env();

// The WHATWG File constructor takes (fileBits, fileName, [options]).
// Both fileBits and fileName are required (USVString without
// `optional`), so missing either is a TypeError per WebIDL bindings.
if (info.Length() < 2)
{
throw Napi::TypeError::New(env,
"Failed to construct 'File': 2 arguments required, but only " +
std::to_string(info.Length()) + " present.");
}

Napi::Value parts = info[0];
Napi::Value name = info[1];
Napi::Value options = info.Length() > 2 ? info[2] : env.Undefined();

// USVString conversion: undefined -> "undefined", null -> "null",
// numbers/objects -> their .toString() representation. Napi::Value::
// ToString() routes through napi_coerce_to_string, which matches
// these semantics on all three engines.
m_name = name.ToString().Utf8Value();

// Default lastModified to the current wall clock in milliseconds,
// matching Date.now() semantics used by the JS File constructor.
m_lastModified = static_cast<double>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count());

auto blobOptions = Napi::Object::New(env);

if (options.IsObject())
{
auto optsObj = options.As<Napi::Object>();
if (optsObj.Has("type"))
{
blobOptions.Set("type", optsObj.Get("type"));
}
if (optsObj.Has("lastModified"))
{
auto lm = optsObj.Get("lastModified");
if (lm.IsNumber())
{
m_lastModified = lm.As<Napi::Number>().DoubleValue();
}
}
}

Napi::Value partsArray;
if (parts.IsArray())
{
partsArray = parts;
}
else
{
partsArray = Napi::Array::New(env, 0);
}

// Delegate byte-buffer construction to the native Blob polyfill so
// we benefit from its existing BlobPart handling (ArrayBuffer,
// typed array, string, Blob).
auto blobCtor = env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).As<Napi::Function>();
auto blobInstance = blobCtor.New({partsArray, blobOptions});
m_blob = Napi::Persistent(blobInstance);
}

Napi::Value File::GetSize(const Napi::CallbackInfo&)
{
return m_blob.Value().Get("size");
}

Napi::Value File::GetType(const Napi::CallbackInfo&)
{
return m_blob.Value().Get("type");
}

Napi::Value File::GetName(const Napi::CallbackInfo& info)
{
return Napi::String::New(info.Env(), m_name);
}

Napi::Value File::GetLastModified(const Napi::CallbackInfo& info)
{
return Napi::Number::New(info.Env(), m_lastModified);
}

Napi::Value File::ArrayBuffer(const Napi::CallbackInfo&)
{
auto blob = m_blob.Value();
return blob.Get("arrayBuffer").As<Napi::Function>().Call(blob, {});
}

Napi::Value File::Text(const Napi::CallbackInfo&)
{
auto blob = m_blob.Value();
return blob.Get("text").As<Napi::Function>().Call(blob, {});
}

Napi::Value File::Bytes(const Napi::CallbackInfo&)
{
auto blob = m_blob.Value();
return blob.Get("bytes").As<Napi::Function>().Call(blob, {});
}
}

namespace Babylon::Polyfills::File
{
void BABYLON_API Initialize(Napi::Env env)
{
Internal::File::Initialize(env);
Internal::FileReader::Initialize(env);
}
}
30 changes: 30 additions & 0 deletions Polyfills/File/Source/File.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once

#include <napi/napi.h>

#include <string>

namespace Babylon::Polyfills::Internal
{
class File final : public Napi::ObjectWrap<File>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File should behave as a subtype of Blob for WebAPI compatibility. As implemented, this looks like composition (m_blob) rather than inheritance, so file instanceof Blob may be false unless the prototype chain is explicitly wired up. Can we make File extend the polyfilled Blob type, or otherwise ensure File.prototype inherits from Blob.prototype?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4480ad8: wired File.prototype → Blob.prototype via Object.setPrototypeOf in File::Initialize.
ew File(...) instanceof Blob is now rue.

This matters because Babylon.js core branches on instanceof Blob in packages/dev/core/src/Misc/fileTools.pure.ts, Offline/database.pure.ts, Engines/abstractEngine.pure.ts, and Engines/thinNativeEngine.pure.ts — without the prototype chain wired up those checks silently fail for File inputs and the wrong branch is taken (the serializer round-trip path bypasses the Blob handling).

Internal m_blob composition stays as the implementation detail; only the JS-visible prototype chain is wired. New test in ests.ts:

it("is an instance of Blob (prototype chain wired up)", function () {
    const file = new File(["x"], "x.txt");
    expect(file instanceof Blob).to.equal(true);
    expect(file instanceof File).to.equal(true);
});

{
public:
static void Initialize(Napi::Env env);

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

private:
Napi::Value GetSize(const Napi::CallbackInfo& info);
Napi::Value GetType(const Napi::CallbackInfo& info);
Napi::Value GetName(const Napi::CallbackInfo& info);
Napi::Value GetLastModified(const Napi::CallbackInfo& info);

Napi::Value ArrayBuffer(const Napi::CallbackInfo& info);
Napi::Value Text(const Napi::CallbackInfo& info);
Napi::Value Bytes(const Napi::CallbackInfo& info);

Napi::ObjectReference m_blob;
std::string m_name;
double m_lastModified{0.0};
};
}
Loading
Loading