Add File / FileReader polyfill#169
Conversation
There was a problem hiding this comment.
Pull request overview
This PR moves File / FileReader support from a BabylonNative Playground-only JS shim into JsRuntimeHost as a native C++ polyfill, so all JsRuntimeHost consumers get the WHATWG-style APIs alongside existing polyfills (Blob, URL, XHR, etc.).
Changes:
- Add a new
Polyfills/File/C++ target implementingFileandFileReader(including event handler slots + EventTarget-style listeners). - Wire the polyfill into the build via a new
JSRUNTIMEHOST_POLYFILL_FILECMake option (ON by default) and link it into unit tests. - Add new Mocha unit tests covering
FileandFileReaderbehaviors (construction, reads, events, abort, and a JSC regression aroundObject.prototypepollution).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| CMakeLists.txt | Adds JSRUNTIMEHOST_POLYFILL_FILE option (ON by default). |
| Polyfills/CMakeLists.txt | Conditionally includes the new File polyfill subdirectory. |
| Polyfills/File/CMakeLists.txt | Defines the File polyfill library target and its sources. |
| Polyfills/File/Include/Babylon/Polyfills/File.h | Public API entrypoint for initializing the polyfill. |
| Polyfills/File/Readme.md | Documents behavior and prerequisites (Blob must be initialized first). |
| Polyfills/File/Source/File.h | Declares the internal File ObjectWrap implementation. |
| Polyfills/File/Source/File.cpp | Implements File over the existing Blob polyfill and initializes FileReader. |
| Polyfills/File/Source/FileReader.h | Declares the internal FileReader ObjectWrap implementation. |
| Polyfills/File/Source/FileReader.cpp | Implements async reads, base64 DataURL generation, events, and abort logic. |
| Tests/UnitTests/CMakeLists.txt | Links the new File target into the UnitTests executable. |
| Tests/UnitTests/Shared/Shared.cpp | Initializes the File polyfill in the unit test JS environment. |
| Tests/UnitTests/Scripts/tests.ts | Adds new Mocha tests for File and FileReader, including a JSC regression canary. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
6f8a827 to
67f9f5c
Compare
fa84e76 to
52f6338
Compare
|
@bghgary friendly ping — this is the upstream half of BabylonJS/BabylonNative#1706 (File / FileReader polyfill, which re-enables 19 GLTF/OBJ serializer tests in BN). Same flow as #171 (TextEncoder) which you helped me close out earlier today: the polyfill was moved here from the BN PR per your review ("this should be a proper C++ polyfill under State just refreshed:
Once this lands I'll bump BN #1706's pin back to |
ryantrem
left a comment
There was a problem hiding this comment.
Similar to my comment on the other PR, we may want to do a minimal implementation of the File polyfill that is just enough to support BJS usage (not sure if that is already the case).
…_ptr trick Addresses @ryantrem review on BabylonJS#169 (review id 4412098338): * Drop FileReader.readAsBinaryString and its supporting code paths. BJS has zero call sites (searched core, dev/, tools/). Removing it also eliminates the deferred Latin-1/UTF-8 follow-up that previously crashed Chakra during a combined-edit attempt. * Wire File.prototype to inherit from Blob.prototype via Object.setPrototypeOf in File::Initialize. Babylon.js core branches on `instanceof Blob` in fileTools, Offline/database, abstractEngine, and thinNativeEngine; without inheritance those checks silently fail for File inputs and the wrong branch is taken. Internal m_blob composition stays as the implementation detail; only the JS-visible prototype chain is wired. New test asserts `new File(...) instanceof Blob === true`. * Replace shared_ptr<ObjectReference>-in-lambda trick with a member Napi::ObjectReference m_selfRef anchored across the in-flight read. Matches the member-slot pattern used by WebSocket/XHR in this repo. Lambdas now only capture POD + this, so they remain copyable for jsi::Function's std::function-style callable slot. Every terminal path (load, error, abort) resets m_selfRef to break the self-cycle. Each lambda also guards on m_readId before dereferencing m_selfRef to handle the abort-then-late-resolve race.
|
@ryantrem on the minimization point — I surveyed Babylon.js usage and the only thing that wasn't already minimal is
Dropping Full set of changes in 4480ad8:
Local Win32 Chakra |
|
CI on The prototype-chain wire-up for
Ball back in your court for re-review. |
bghgary
left a comment
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
Recommend landing #172 first. The JSC napi-shim bug it describes is the root cause of several workarounds in this PR — JS_PROTOTYPE_CHAIN_SHIM collapses to one direct setPrototypeOf call, the Napi::Eval / env.RunScript dispatch goes away, and the 14-line FileReader dual-exposure comment shrinks to one line. Quite a bit of code evaporates before merge if #172 lands first.
Other concerns inline.
|
Filed #174 with the JSC napi shim fix for #172. It replaces the old Once #174 lands, I'll rebase this branch onto main and:
Will respond on the Re: splitting |
|
Update: closing #174 without merging — the So #169 keeps its JS-shim wire-up (
|
…abylonJS#169 File.cpp - Drop the misleading BABYLON_POLYFILL_USE_NAPI_JSI_EVAL macro and the __has_include(<napi/env.h>) guard. <napi/env.h> exists on every backend (V8, Chakra, JSC, JSI) and is pulled in transitively via <Babylon/Polyfills/File.h>, so the include guard always succeeded and the macro name implied a JSI-only path that doesn't exist — Napi::Eval is declared on all four backends (the Shared N-API impl in env.cc is a thin wrapper around env.RunScript). Call Napi::Eval directly. - Reorder Initialize: check "already provided" (cheap no-op, common path on platforms with a native File) before probing for Blob. - Throw Napi::Error on missing Blob instead of silently bailing. Consumers that wire up the File polyfill expect it to be installed; silent failures are hard to debug. FileReader.cpp - MakeEvent: replace the dangling "JS polyfill that this C++ implementation replaces" reference (no such JS polyfill exists in this PR) with a one-line description of the actual ProgressEvent contract. - DefineClass: collapse the 14-line dual-StaticValue/InstanceValue comment to one line pointing at JsRH#173. Per BabylonJS#173 this is the correct WHATWG IDL `const` member exposure pattern, not a workaround that needs in-line justification. tests.ts - Re-point the Chakra throw-from-constructor TODO at JsRH#175 (filed today) so the disabled "throws when fewer than 2 arguments are passed" test can be re-enabled atomically when that shim limitation is fixed. No behavior change; pure cleanup. Local Chakra build clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Pushed Changes in this commit:
Still to come (separate commits for review-clarity, in priority order):
#172 / #174 update: as noted in the follow-up comment on #174, the proper JSC |
…_ptr trick Addresses @ryantrem review on BabylonJS#169 (review id 4412098338): * Drop FileReader.readAsBinaryString and its supporting code paths. BJS has zero call sites (searched core, dev/, tools/). Removing it also eliminates the deferred Latin-1/UTF-8 follow-up that previously crashed Chakra during a combined-edit attempt. * Wire File.prototype to inherit from Blob.prototype via Object.setPrototypeOf in File::Initialize. Babylon.js core branches on `instanceof Blob` in fileTools, Offline/database, abstractEngine, and thinNativeEngine; without inheritance those checks silently fail for File inputs and the wrong branch is taken. Internal m_blob composition stays as the implementation detail; only the JS-visible prototype chain is wired. New test asserts `new File(...) instanceof Blob === true`. * Replace shared_ptr<ObjectReference>-in-lambda trick with a member Napi::ObjectReference m_selfRef anchored across the in-flight read. Matches the member-slot pattern used by WebSocket/XHR in this repo. Lambdas now only capture POD + this, so they remain copyable for jsi::Function's std::function-style callable slot. Every terminal path (load, error, abort) resets m_selfRef to break the self-cycle. Each lambda also guards on m_readId before dereferencing m_selfRef to handle the abort-then-late-resolve race.
…on Chakra ThrowAsJavaScriptException avoids the Chakra heap-corruption on throw-from-constructor, but JSI's napi shim implements `Error::ThrowAsJavaScriptException` as a no-op that only stores the error in `env->last_exception` without raising it. The script then continues normally with an incompletely-initialized File wrapper, the `expect(...).to.throw()` matcher sees no JS error, and the unhandled exception is later reported through AppRuntime's UnhandledExceptionHandler, failing the test harness exit code. Use the JRH-wide convention `throw Napi::TypeError::New(env, msg)` (matches Blob, XHR, URL, AbortSignal, TextDecoder, FileReader). On engines whose Node-API shim properly translates a C++ throw into a JS exception at the ObjectWrap construction boundary (V8 / JSC / JSI), this propagates as expected. Chakra cannot handle throwing from a constructor — there are five pre-existing TODOs in tests.ts noting this same limitation for URL parse() error cases. Apply the same workaround here: keep the WebIDL-conformant throw in C++, but comment out the test that exercises it (it would corrupt the Chakra heap on every CI run). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…_ptr trick Addresses @ryantrem review on BabylonJS#169 (review id 4412098338): * Drop FileReader.readAsBinaryString and its supporting code paths. BJS has zero call sites (searched core, dev/, tools/). Removing it also eliminates the deferred Latin-1/UTF-8 follow-up that previously crashed Chakra during a combined-edit attempt. * Wire File.prototype to inherit from Blob.prototype via Object.setPrototypeOf in File::Initialize. Babylon.js core branches on `instanceof Blob` in fileTools, Offline/database, abstractEngine, and thinNativeEngine; without inheritance those checks silently fail for File inputs and the wrong branch is taken. Internal m_blob composition stays as the implementation detail; only the JS-visible prototype chain is wired. New test asserts `new File(...) instanceof Blob === true`. * Replace shared_ptr<ObjectReference>-in-lambda trick with a member Napi::ObjectReference m_selfRef anchored across the in-flight read. Matches the member-slot pattern used by WebSocket/XHR in this repo. Lambdas now only capture POD + this, so they remain copyable for jsi::Function's std::function-style callable slot. Every terminal path (load, error, abort) resets m_selfRef to break the self-cycle. Each lambda also guards on m_readId before dereferencing m_selfRef to handle the abort-then-late-resolve race.
CI failed on JSC engines (Ubuntu_gcc, macOS, iOS, Android_JSC) with
'[Uncaught Error] setPrototypeOf@[native code]' during JS env init.
Root cause: the previous commit called
\Object.setPrototypeOf(func.Get('prototype'), Blob.prototype)\. On JSC,
the napi-defined class's \.prototype\ JS property points to
Object.prototype (per JSObjectMakeConstructor semantics; same quirk the
FileReader constants block documents). setPrototypeOf on Object.prototype
throws TypeError because Object.prototype's [[Prototype]] is immutable.
Fix: instantiate a throwaway File, fetch its real prototype via
Object.getPrototypeOf, and set THAT prototype's prototype to
Blob.prototype. On V8 / Chakra the real prototype is also
func.prototype, so this is correct everywhere. The temp instance is
GC-eligible immediately after.
Each call is guarded with IsExceptionPending + GetAndClearPendingException
so that if a non-Chromium JSC build still rejects setPrototypeOf on the
napi-internal prototype, Initialize stays best-effort: instances won't be
Blob subtypes on that engine but the rest of the polyfill still
installs and the JS env starts cleanly.
Commit ac19d77 swapped the direct setPrototypeOf for a temp-instance trick to fix JSC, but that broke Chakra (Win32_x64_Chakra and Win32_x86_Chakra now report `file instanceof Blob === false`). Restore the direct call (which previously passed on V8 and Chakra), verify the chain via a probe instance, and only fall back to the temp-instance approach when the chain isn't wired up. That recovers JSC (where `func.prototype` aliases `Object.prototype`) without regressing the engines where the direct call was already correct.
Commit 9e7b333 (C++ dual-path direct -> temp-instance fallback) passed Chakra but regressed JSC: on JSC, setPrototypeOf on Object.prototype throws TypeError that escapes the napi shim as an [Uncaught Error] instead of being capturable via IsExceptionPending. Move both setPrototypeOf calls into a single JS shim wrapped in try/catch. JS-level try/catch reliably traps the TypeError on every engine, the direct path takes care of V8/Chakra, and the probe path (instanceof Blob check + getPrototypeOf + setPrototypeOf on the real napi-internal prototype) takes care of JSC.
…abylonJS#169 File.cpp - Drop the misleading BABYLON_POLYFILL_USE_NAPI_JSI_EVAL macro and the __has_include(<napi/env.h>) guard. <napi/env.h> exists on every backend (V8, Chakra, JSC, JSI) and is pulled in transitively via <Babylon/Polyfills/File.h>, so the include guard always succeeded and the macro name implied a JSI-only path that doesn't exist — Napi::Eval is declared on all four backends (the Shared N-API impl in env.cc is a thin wrapper around env.RunScript). Call Napi::Eval directly. - Reorder Initialize: check "already provided" (cheap no-op, common path on platforms with a native File) before probing for Blob. - Throw Napi::Error on missing Blob instead of silently bailing. Consumers that wire up the File polyfill expect it to be installed; silent failures are hard to debug. FileReader.cpp - MakeEvent: replace the dangling "JS polyfill that this C++ implementation replaces" reference (no such JS polyfill exists in this PR) with a one-line description of the actual ProgressEvent contract. - DefineClass: collapse the 14-line dual-StaticValue/InstanceValue comment to one line pointing at JsRH#173. Per BabylonJS#173 this is the correct WHATWG IDL `const` member exposure pattern, not a workaround that needs in-line justification. tests.ts - Re-point the Chakra throw-from-constructor TODO at JsRH#175 (filed today) so the disabled "throws when fewer than 2 arguments are passed" test can be re-enabled atomically when that shim limitation is fixed. No behavior change; pure cleanup. Local Chakra build clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onJS#177 landed BabylonJS#177 fixed the JSC napi shim's Object.prototype pollution by passing the per-class JSClassRef to JSObjectMakeConstructor, so napi-defined classes now get a real per-class .prototype object instead of aliasing the global Object.prototype. That removes the entire reason for the JS_PROTOTYPE_CHAIN_SHIM and the Napi::Eval / try/catch dance: we can wire File.prototype's [[Prototype]] to Blob.prototype with a direct Object.setPrototypeOf call. Drops ~50 lines of explanatory comment + shim + Eval-with-IsExceptionPending guard in File::Initialize down to a 4-line napi call. `file instanceof Blob` regression coverage remains in tests.ts:1564 and BabylonJS#177's `describe("napi class prototype isolation (BabylonJS#172)")` block at tests.ts:1271 (uses Blob — and File extends Blob — to assert that napi-defined classes don't share Object.prototype). Local Chakra build clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ment Per bghgary's review nit: the "BabylonJS#177 fixed JSC .prototype pollution" context is now in main's git history, doesn't need to live in the comment block. Comment now focuses on WHY we wire File→Blob, not on which past bug enabled the wiring to be one-liner. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0bff942 to
fe457e5
Compare
|
Rebased onto current Both asks from your latest review addressed:
Rebase was clean — 8 commits replayed cleanly on top of |
…eAccessor readyState/result/error are WHATWG readonly attributes and the on* slots are EventHandler IDL attributes; both should be prototype accessors, not writable enumerable own data properties stamped onto every instance. - readyState/result/error -> InstanceAccessor getters reading m_readyState / m_result / m_error. State-machine checks now read the C++ members directly, so JS can no longer tamper with them by overwriting the property. - on* handlers -> a single InstanceAccessor get/set pair whose accessor data carries the event type, backed by a Napi::FunctionReference map. Dispatch consults the map instead of the on-prefixed instance property. - Constructor no longer stamps any instance properties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…impl Per review, avoid carrying a third hand-rolled base64 encoder. Promote the header-only azawadzki/base-n (already used by BabylonNative) into JsRuntimeHost as a FetchContent INTERFACE library, gated on JSRUNTIMEHOST_POLYFILL_FILE, and link it PRIVATE into the File polyfill. base-n's encode_b64 emits unpadded base64, so EncodeBase64 now just delegates to it and appends the RFC 4648 '=' padding for the final partial group. This keeps data: URL output byte-for-byte identical (e.g. "Hello" -> "SGVsbG8="). Pinned to the same commit BabylonNative uses (7573e77c). BN's direct base-n dependency can be dropped as a follow-up once it consumes JRH's. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the remaining review asks (pushed
All 18 review threads are now resolved. |
napi_create_reference only accepts heap values (object/function/symbol) on the V8/JSC N-API backends, so storing a primitive string result (readAsText/readAsDataURL) in a Napi::Reference<Value> threw there and the load/loadend events never fired (Chakra's shim tolerated it, which is why this passed locally). Box result/error as properties on a persistent holder object instead; referencing the holder object is valid for every engine while keeping the state C++-owned and tamper-proof. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
CI on Verified green (all FileReader tests pass — 179 passing):
The only red job is |
Replace the m_selfRef member self-reference with a shared_ptr<ObjectReference> captured by both promise reactions. The spec requires an in-flight read to keep the FileReader alive even if script drops its reference; this anchors the wrapper externally (owned by the lambdas) for exactly the read's duration. When the promise settles and the engine releases the reactions, the last shared_ptr copy drops and the anchor is released automatically. This drops the member self-cycle and the manual m_selfRef.Reset() on every terminal path, and removes the dead-this race the self-anchor left on the abort-then-drop path: the wrapper is now guaranteed alive whenever a reaction runs, so reading m_readId and calling anchor->Value() are always safe. m_readId is retained solely as the abort/restart guard. Matches the externally-anchored, lambda-owned-state convention used by the WebSocket polyfill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@bghgary thanks for the careful review — all of your inline concerns are addressed in 61d9924:
I also refreshed the PR description to match the current implementation (it had drifted): C++-backed readonly accessors for Verified Win32-x64 Chakra Debug green (180 JS + native gtests), and full CI is green across all engines/platforms. PTAL. |
bghgary
left a comment
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
LGTM. All concerns addressed; the FileReader in-flight lifecycle/UAF fix is verified in 61d9924 — shared_ptr<ObjectReference> anchor captured by both promise reactions, no self-ref member, no manual Reset().
### Problem When JsRuntimeHost is embedded via FetchContent in a parent project that already declares its own `base-n` INTERFACE target — e.g. **BabylonNative**, which uses base-n for its Window/Canvas polyfills — configuring fails: ``` CMake Error at .../jsruntimehost-src/CMakeLists.txt:150 (add_library): add_library cannot create target "base-n" because another target with the same name already exists. The existing target is an interface library created in source directory ".../Dependencies". ``` This surfaced now because the merged File / FileReader polyfill (#169) switched its base64 encoder over to the `base-n` dependency, and BabylonNative declares `base-n` before it fetches JsRuntimeHost. ### Fix Guard the `base-n` block with `NOT TARGET base-n` so that when a parent project already provides the target, JsRuntimeHost adopts it instead of redeclaring. This is the standard CMake subproject pattern and mirrors BabylonNative's own `if(NOT TARGET glslang)` guards. Standalone JsRuntimeHost builds are unaffected (no pre-existing target → block runs as before). ### Validation - Standalone `NAPI_JAVASCRIPT_ENGINE=Chakra` configure + `UnitTests` build: green, File polyfill + base-n link as before. - Unblocks BabylonJS/BabylonNative#1706 (the consuming PR that re-pins to merged main). Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Brings in: * Guard base-n target against parent-project collision (BabylonJS#184) * Add File / FileReader polyfill (BabylonJS#169) * Stop mutating instance [[Prototype]] chain in napi_wrap on Chakra (BabylonJS#179) * Fix .prototype pollution on JSC (BabylonJS#177) * Add TextEncoder polyfill (BabylonJS#171) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the WHATWG etch() API as a **native** polyfill under `Polyfills/Fetch/`, mirroring the `XMLHttpRequest` polyfill layout. Like `XMLHttpRequest`, it is built on the same platform-specific `UrlLib` transports, so libcurl/WinHTTP/etc. behavior is identical between the two. Closes #98. ### API `fetch(input, init)` returns a `Promise` that resolves to a `Response`-like object exposing: - `ok`, `status`, `statusText`, `url`, `redirected`, `type`, `bodyUsed` - `headers` with `get(name)` / `has(name)` / `forEach(cb)` (case-insensitive) - `text()`, `arrayBuffer()`, `json()`, `blob()` (each returns a `Promise`) - `clone()` ### Behavior / deliberate limitations - Per the fetch spec, the promise **rejects only on transport-level failures**; a completed request with a non-2xx status (e.g. 404) still resolves with `ok === false`. - The body is fully buffered before the promise resolves, so the body accessors may be called more than once (`bodyUsed` stays `false`) — a lenient deviation from the spec's single-use semantics. - `GET`/`POST` only and string request bodies only, matching the underlying `UrlLib` transport (same constraints as `XMLHttpRequest`). - `blob()` requires the `Blob` polyfill to be initialized. ### Wiring - New `JSRUNTIMEHOST_POLYFILL_FETCH` option (default `ON`). `UrlLib` is now fetched when **either** `XMLHttpRequest` or `Fetch` is enabled. - Initialized in the test harness and covered by a new `describe("fetch")` block (12 cases: status/ok, 404-resolves, statusText, text/arrayBuffer/json/blob, case-insensitive headers, clone, init method, no-arg rejection). A small `Assets/sample.json` backs the deterministic `json()` test. ### Verification Built the `Fetch` target under `warnings_as_errors` and ran the full UnitTests suite on Win32/Chakra: **192 passing**, including all 12 new fetch cases. ### Context This is step 2 of the fetch native-polyfill pivot discussed in BabylonNative#1707 (reviewers asked fetch to follow the same native pattern as File/FileReader (#169) and AbortController). Once this lands, BabylonNative#1707 becomes a small bump that initializes `Polyfills::Fetch`. --------- Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Branimir Karadžić <branimirk@microsoft.com>
The File polyfill in BabylonNative currently lives as a JS shim (
Apps/Playground/Scripts/file_polyfill.js, ~308 lines) that isLoadScript-ed from the Playground'sAppContext(see BabylonJS/BabylonNative#1706). Per @bghgary's review on that PR, it should live in JsRuntimeHost alongside Blob, URL, WebSocket, XMLHttpRequest, TextDecoder, AbortController, etc., so every JsRuntimeHost consumer -- not only the BabylonNative Playground -- getsFileandFileReader.This PR adds a native C++
Polyfills/File/target that implements the WHATWGFileandFileReaderweb APIs, plus 29 new Mocha unit tests (15 File + 14 FileReader) underTests/UnitTests/Scripts/tests.ts.Surface area
File(parts, name, options)constructor.Fileinstance accessors:size,type,name,lastModified.Fileinstance methods:arrayBuffer(),text(),bytes().File extends Blob: the prototype chain is wired sonew File(...) instanceof Blob === true(Babylon.js core branches oninstanceof Blobin fileTools, Offline/database, abstractEngine, thinNativeEngine).FileReaderwith state constantsEMPTY/LOADING/DONE(exposed both on the constructor and on instances, per WHATWG).FileReaderreadonly attributesreadyState,result,errorand theonloadstart/onprogress/onload/onabort/onerror/onloadendEventHandler attributes.FileReaderevents delivered via both theonXhandler slots andaddEventListener/removeEventListener/dispatchEvent.FileReader.readAsText/readAsArrayBuffer/readAsDataURL/abort.Implementation notes
Filedelegates its byte storage to the existingBlobpolyfill viaenv.Global().Get("Blob"), reusing Blob'sBlobParthandling (ArrayBuffer, typed array, string, Blob).Blob::Initializemust run beforeFile::Initialize.FileReader's readonly attributes (readyState/result/error) and theon*handler slots are backed by C++ state surfaced through prototype accessors, not by writable JS properties, so script can neither overwrite them nor fool the state-machine checks.result/errorare boxed on a persistent holder object rather thanNapi::Reference<Value>slots, becausenapi_create_referencerejects primitive values on the real N-API backends (V8/JSC) andreadAsText/readAsDataURLproduce primitive string results.readAsDataURLbase64-encodes via thebase-ndependency (added as a top-levelFetchContent), matching the encoder already used by other polyfills.FileReaderalive (per spec) by anchoring its JS wrapper in ashared_ptr<Napi::ObjectReference>captured by the promise reactions -- externally owned, released automatically when the promise settles. A monotonic read id letsabort()/ restart invalidate a stale continuation so a late-resolvingarrayBuffer()promise cannot dispatch a phantomloadafter a user-initiated abort.JSRUNTIMEHOST_POLYFILL_FILECMake option is on by default and gates building the target.Notable JSC-specific care
Two pieces of the implementation are shaped specifically to work correctly on JavaScriptCore:
FileReader.EMPTY/LOADING/DONEare registered viaStaticValueandInstanceValuedescriptors inside theDefineClassproperty list, not viafunc.Get("prototype").Set(...)after class creation. On JSC the napi shim'sConstructorInfo::Createdefaults the constructor's.prototypetoObject.prototype, so the latter pattern would polluteObject.prototypewithEMPTY/LOADING/DONEkeys and breakfor..inover plain objects throughout the runtime. The regression test"does not pollute Object.prototype with EMPTY/LOADING/DONE"pins this behaviour.The Blob-dependency guard in
File::InitializeusesIsUndefined()rather thanIsFunction(). Some JSC builds (notablylibjavascriptcoregtk-4.1on Linux) classify constructors created viaJSObjectMakeConstructorastypeof 'object', not'function', sonapi_typeofreturnsnapi_objectfor them and anIsFunction()guard would silently early-return on those engines.IsUndefined()matches the guard used by Blob and works on V8, JSI, Chakra, iOS-JSC, Android-JSC, and Linux JSC.Testing
Locally
UnitTests.exe(Win32 x64 Chakra Debug) passes all 180 JS tests (151 pre-existing + 29 new) and the native gtests, including theObject.prototype-pollution regression canary and the abort / late-resolve continuation guards. CI is green across all engines/platforms (V8, Chakra, JSI/JSC, iOS, Android, UWP, macOS, Ubuntu, sanitizers).After this merges
BabylonJS/BabylonNative#1706re-pinsCMakeLists.txt'sGIT_REPOSITORY/GIT_TAGto the merged SHA and removes the JS shim.