Skip to content

BridgeJS: Module-qualify internal ABI names to fix cross-module type collisions#17

Closed
krodak wants to merge 2 commits into
mainfrom
kr/module-qualified-abi-names
Closed

BridgeJS: Module-qualify internal ABI names to fix cross-module type collisions#17
krodak wants to merge 2 commits into
mainfrom
kr/module-qualified-abi-names

Conversation

@krodak

@krodak krodak commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes swiftwasm#785.

Problem

When two Swift modules in a single BridgeJS build both export a @JS type with the same name (e.g. Point), the generated glue collides silently:

  • duplicated @_expose(wasm, "bjs_Point_init") thunk names,
  • duplicated swift_js_struct_lower_Point / swift_js_struct_lift_Point extern hooks and their JS-side bjs[...] handlers,
  • colliding structHelpers.Point / enumHelpers.Point keys in the shared JS glue.

The JS handlers are plain assignments, so the last-linked module wins and every other module's values are decoded with the wrong field schema — silent data corruption rather than a build error.

Approach

Type identity in the generated glue is now module-aware, while the public JS surface stays flat:

  1. Module-qualified payloads. Exported-type BridgeType payloads are minted as Module.[Nesting.]Name at skeleton-generation time. Cross-module references resolve through ExternalModuleIndex, which indexes by module-stripped path (so existing Point / Dep.Point references keep working) while carrying the defining module's qualified payload.
  2. abiName derived from the qualified name by construction. abiName = swiftCallName.replacing(".", "_"), so the invariant JS key derived from payload == defining type's helper key holds structurally instead of by parallel derivations. All bjs_* thunks, swift_js_struct_lower/lift_* externs, and structHelpers/enumHelpers keys become Module_…-qualified.
  3. Public surface unchanged. JS class names, exports keys, and TypeScript declarations are untouched — the re-record produced zero .d.ts diffs. Display names strip the module prefix.
  4. Build-time diagnostic. The linker now rejects two different modules exporting the same public name at the same namespace path (classes, structs, non-namespace enums, top-level functions), with an error naming both modules. Namespace enums are exempt (cross-module namespace merging is intentional).

Protocol method hooks (wasm imports, already scoped per module by the import object) and TS-imported types keep their existing naming.

Testing

  • sameNamedTypesAcrossModulesLinkWithDistinctABINames — links two skeletons that each define @JS struct Point plus same-named top-level functions; asserts distinct module-qualified externs and helper keys in the output (snapshot: SameNamedTypesAcrossModules.js/.d.ts).
  • duplicatePublicNamesAcrossModulesProduceDiagnostic / duplicatePublicFunctionNamesAcrossModulesProduceDiagnostic — the new linker error.
  • Full snapshot suite re-recorded (~150 fixtures churn from the abiName rename; internal names only).
  • ./Utilities/bridge-js-generate.sh zero drift; npm run check:bridgejs-dts passes; WASM E2E (make unittest) 32/32 suites pass.

Notes for reviewers

  • ABI names are internal and regenerated atomically on both sides of the bridge, but this is a rename of every generated symbol — anyone snapshot-diffing generated output downstream will see churn.
  • @JS(namespace:) components no longer appear in type abiNames (module + Swift nesting path is used instead, which is unique per module and fixes a latent invariant violation for attribute-namespaced structs).
  • Same-named classes/enums across modules in different namespaces still collide on public JS declarations (duplicate class X / XValues emissions). That's a pre-existing public-surface limitation, distinct from the ABI corruption fixed here — happy to file a follow-up issue.

krodak added 2 commits July 9, 2026 12:50
…collisions

When two Swift modules in one BridgeJS build exported a `@JS` type with
the same name, the generated glue collided silently: duplicated
`@_expose(wasm, ...)` thunk names, duplicated
`swift_js_struct_lower_<name>`/`swift_js_struct_lift_<name>` hooks, and
colliding `structHelpers`/`enumHelpers` JS keys - last writer wins,
causing silent data corruption.

Fix by module-qualifying the internal ABI identity of exported Swift
types while keeping the public JS/TS surface flat:

- `BridgeType` payloads for exported Swift types are minted as
  `<Module>.<Nesting>.<Name>` (still a valid Swift call-site expression).
- Type `abiName`s are derived from the module-qualified `swiftCallName`
  (dots replaced with underscores), so the JS helper key derived from a
  payload always equals the defining type's `abiName` by construction.
- Top-level function thunk names gain the module name as namespace
  context; enum static contexts carry the qualified `swiftCallName`
  (sanitized inside `ABINameGenerator`).
- `mangleTypeName` sanitizes dots since mangled names are embedded in
  Swift identifiers.
- `ExternalModuleIndex` keeps indexing dependency types by their
  module-stripped dot path while carrying qualified payloads, so
  cross-module references resolve to the same ABI names.
- Public JS class names, exports keys, and TS declarations stay flat via
  the new `BridgeType.dropModulePrefix` display accessor.
- BridgeJSLink now fails with a clear diagnostic when two modules export
  the same public name at the same namespace path.

Fixes swiftwasm#785
- Link two skeletons that both export a `Point` struct and a `run`
  function with identical Swift names but different public JS names, and
  assert the generated glue keeps module-qualified struct hooks, helper
  keys, and thunk names distinct while the TS surface stays flat
  (snapshot: SameNamedTypesAcrossModules).
- Assert that two modules exporting the same public name (type or
  function) at the same namespace path fail to link with the new
  duplicate-export diagnostic.
@krodak krodak closed this Jul 9, 2026
@krodak krodak deleted the kr/module-qualified-abi-names branch July 9, 2026 12:01
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.

BridgeJS: Same-named @JS types across modules collide silently in generated glue

1 participant