Skip to content

fix: preserve object identity across the host boundary (#4)#84

Merged
rot1024 merged 2 commits into
mainfrom
fix/marshal-identity
Jun 9, 2026
Merged

fix: preserve object identity across the host boundary (#4)#84
rot1024 merged 2 commits into
mainfrom
fix/marshal-identity

Conversation

@rot1024

@rot1024 rot1024 commented Jun 9, 2026

Copy link
Copy Markdown
Member

Closes part of #4.

Problem

Reference identity was not preserved across the host boundary in the default (sync-on) mode:

arena.expose({ id: (x) => x });
arena.evalCode(`let foo = id({}); foo === id(foo)`); // was false

const shared = { k: 1 };
arena.expose({ get: () => shared });
arena.evalCode(`get() === get()`); // was false

Root cause

A host function's return value is disposed by the VM once consumed. The VMMap retains marshalled object handles for identity while sync is on — but the handle returned to the VM was the map's own handle, so the VM disposed it and the entry went stale. Marshalling the same value again then missed the map, created a fresh VM object (breaking ===) and orphaned the sibling handle (the leak fixed in #83 was the same root mechanism surfacing as a dispose-time abort).

Fix

Hand the VM a dup() of the handle when the VMMap retains it (prepareReturn), keeping the map's copy alive. find then returns a live handle on the next marshal, so identity holds.

  • Scoped to host function returns (the consume point that disposed the map's handle).
  • Gated on syncEnabled: with syncEnabled: false nothing is retained, so no dup — behaviour unchanged.

Behavioural note (decided with maintainer)

Identity retention means a host object marshalled once is identity-cached: a later host-side mutation is not picked up by re-marshalling. arena.sync(obj) remains the supported way to propagate host writes to the VM. The edge.test.ts > getter test documented the old re-marshal-each-time behaviour (which was also what leaked); it's updated to the new semantics, plus a new getter (synced) test shows live tracking via arena.sync. Its ctx.dispose() is un-commented now that the leak is gone.

Tests

New src/identity.test.ts pins identity (round-trip, repeated returns, retained refs, and the sync-off negative case). Full suite: 168 passing, typecheck + lint clean.

rot1024 added 2 commits June 9, 2026 19:46
A host function's return value is disposed by the VM once consumed. The
VMMap retains marshalled object handles for identity while sync is on, but
the returned handle WAS the map's own handle, so the VM disposed it and the
map entry went stale: marshalling the same value twice produced two distinct
VM objects (foo !== id(foo), get() !== get()) and leaked the orphaned sibling.

Hand the VM a dup of the handle when the map retains it (prepareReturn),
keeping the map's copy alive. Identity now holds across round-trips and
repeated returns; with syncEnabled:false nothing is retained, so behaviour
is unchanged (no dup).

Host-side mutations of an already-marshalled object are no longer picked up
by re-marshalling (the value is identity-cached); arena.sync remains the way
to propagate host writes. Updated the edge 'getter' test accordingly and
un-commented its ctx.dispose() now that the leak is gone.
…outs

Identity assertions don't need the debug-sync runtime's leak detection (that
is covered by remarshalleak.test.ts), so run them on the default runtime —
under coverage the debug-sync variant pushed each in-VM call past the 30s
timeout in CI. The remaining debug-sync leak tests get a 90s timeout, matching
the existing slow 'many newFunction' edge test.
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 97.5% 703 / 721
🔵 Statements 93.45% 785 / 840
🔵 Functions 95.18% 178 / 187
🔵 Branches 84.83% 498 / 587
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/index.ts 89.08% 82.2% 89.13% 91.77% 99-102, 188, 327, 341, 374-386, 397, 414, 554, 569-570
src/marshal/function.ts 100% 91.66% 100% 100%
src/marshal/index.ts 100% 90.9% 100% 100%
Generated in workflow #546 for commit 226aab6 by the Vitest Coverage Report Action

@rot1024 rot1024 merged commit fdc1d74 into main Jun 9, 2026
2 checks passed
@rot1024 rot1024 deleted the fix/marshal-identity branch June 9, 2026 11:34
@reearth-app reearth-app Bot mentioned this pull request Jun 9, 2026
4 tasks
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.

1 participant