Skip to content

Add [Symbol.dispose]() to free objects without waiting for GC - #240

Open
dannyball710 wants to merge 1 commit into
harfbuzz:mainfrom
dannyball710:symbol-dispose
Open

dannyball710 wants to merge 1 commit into
harfbuzz:mainfrom
dannyball710:symbol-dispose

Conversation

@dannyball710

@dannyball710 dannyball710 commented Oct 13, 2026 •

Copy link
Copy Markdown

Resolves: #239.

Adds [Symbol.dispose]() to the classes that own a native object (Blob, Face, Font, FontFuncs, Buffer, DrawFuncs, PaintFuncs), so an object can be freed right away, or at the end of a using scope, instead of when it is garbage collected. See the commit message for details.

  • The classes declare the built-in Disposable interface, and the generated typings reference the esnext.disposable lib, so consumers on older lib settings still type-check.
  • dispose frees the native object, unregisters it from the FinalizationRegistry so it is not freed twice, and does nothing when called again. Any later use of the object throws. The logic lives in a new untrack() helper next to track(). DrawFuncs and PaintFuncs now register through track() too.
  • Runtimes without Symbol.dispose (Safari) need the polyfill from the TypeScript 5.2 release notes, run before harfbuzzjs is imported. MIGRATING.md mentions it. A correction to my earlier comment: TypeScript's using helper does not fall back to Symbol.for("Symbol.dispose"). It throws when Symbol.dispose is missing, so the polyfill is the way, as you said.

This touches track() in src/helpers.ts like #238 does. The conflict is trivial (keep both), and I'll rebase whichever lands second.

Tested with make test and make doc (Emscripten 4.0.13, Node.js 22; the native using test is skipped there because Node.js 22 can't parse it), and the test suite on Node.js 24, where all 115 tests pass.

FinalizationRegistry callbacks only run on a later task, so a synchronous
loop that keeps creating large blobs and faces can fill the 2 GiB WASM
memory before any of them is freed. Give every class that owns a native
object (Blob, Face, Font, FontFuncs, Buffer, DrawFuncs, PaintFuncs) a
[Symbol.dispose]() method, so it can be freed right away, or with a
`using` declaration. The classes declare that they implement the built-in
Disposable interface, and the generated typings reference the
esnext.disposable lib, so consumers that target older libs still get its
declarations.

The method frees the native object now and unregisters it from the
registry, so the finalizer does not free it again. A second call does
nothing, and any later use of the object's `ptr` throws. This lives in a
new untrack() helper next to track(); DrawFuncs and PaintFuncs now
register through track() like the other classes.

Runtimes without Symbol.dispose (Safari at the time of writing) need a
polyfill such as `Symbol.dispose ??= Symbol("Symbol.dispose");` from the
TypeScript 5.2 release notes, and it must run before harfbuzzjs is
imported, since the method key is evaluated when the module is loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Opt-in destroy() alongside FinalizationRegistry?

1 participant