◐ Shell
clean mode source ↗

BridgeJS: Support generic functions at the Swift and JavaScript boundary by krodak · Pull Request #14 · PassiveLogic/JavaScriptKit

Overview

Adds support for generic functions to BridgeJS, in both directions, constrained to a new bridgeable bound T: _BridgedSwiftGenericBridgeable. Values cross the boundary using each type's existing stack ABI; a runtime type-ID registry selects the correct per-type codec, so the per-function glue stays type-agnostic. The @JS / @JSFunction macros are unchanged.

// Import: call a generic JS function from Swift
@JSFunction func parse<T: _BridgedSwiftGenericBridgeable>(_ json: String) -> T
let user: User = try parse(jsonString)          // T monomorphized at the call site

// Export: call a generic Swift function from JavaScript
@JS public func identity<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
import { BridgeTypes } from "./bridge-js.js";
const n = exports.identity(42, BridgeTypes.Int);          // token recovers T (TS erases generics)
const p = exports.identity({ x: 1, y: 2 }, BridgeTypes.Point);

Supported T

  • Bool, Float, Double, String, and JSValue
  • every fixed-width integer: Int, UInt, Int8/Int16/Int32/Int64, UInt8/UInt16/UInt32/UInt64
  • any @JS struct (including nested/namespaced and dependency-module structs), final @JS class, and @JS enum (case, raw-value, or associated-value)

The generic parameter may be used bare (T) or wrapped as [T], T?, or [String: T]. JSObject cannot be used as T (it is a non-final class that cannot satisfy the protocol's StackLiftResult == Self requirement); use JSValue instead.

What's included

1. Import direction

  • Generic @JSFunction thunks are generic and monomorphized by the Swift compiler at the call site; one type-agnostic wasm import carries a trailing type-ID per generic parameter, and JS dispatches through a shared codec table.
  • Supports a generic used in one or many parameters, multiple distinct generic parameters, and return-only generics (make<T>() -> T, where JS produces the value).

2. Export direction

  • The wasm entry point is a concrete @_expose/@_cdecl thunk taking a trailing type-ID per generic parameter; the generic value crosses on the stack. The thunk looks the type-ID up in a codegen-emitted [Int32: any _BridgedSwiftGenericBridgeable.Type] registry and reifies T through an opened existential. Multiple distinct generic parameters use a nested opening chain.
  • Concrete parameters of any supported bridged type may be mixed with the generic value, with correct stack ordering.
  • JavaScript callers pass a generated BridgeType<T> token (exported as a BridgeTypes map) as the trailing argument, since TypeScript erases generics.
  • Cross-module: a @JS struct defined in a dependency module can be used as T (codegen registers it, synthesizing a conditional @retroactive conformance when the defining module has no generics of its own).
  • Embedded Swift: opened existentials are unavailable, so the exported generic thunk is emitted as a runtime fatalError stub under #if hasFeature(Embedded) (codegen cannot know the consumer's Embedded mode); the import side remains Embedded-compatible.

3. Wrapped generics ([T], T?, [String: T])

  • Resolved by the parser to .array/.nullable/.dictionary of .generic, then bridged element-by-element through dedicated per-element stack helpers (_bridgeJSStackPush/PopArray/Optional/DictGeneric) on the Swift side and codec-driven composition helpers on the JS side. This keeps the wrapper ABI uniform and independent of the element type.

4. Codec generation

  • Every generic codec ({ lower, lift }) is generated from the canonical per-type stack fragments (stackLowerFragment / stackLiftFragment) — the same code path the non-generic glue uses — so there is no hand-written/duplicated lowering to drift. While unifying this, fixed liftCoerce for 64-bit unsigned integers so UInt64 (and the non-generic [UInt64] / UInt64? paths) lift to an unsigned BigInt.

5. Diagnostics
Build-time, source-located diagnostics for unsupported forms: missing/incorrect constraint, where clauses, async generics, generics inside @JSClass/static members, nested or unsupported wrappings ([[T]], [T?], [Int: T]), and (export) a return type that is neither a declared generic (optionally wrapped) nor Void.

6. Documentation
DocC articles updated (exporting/importing functions, supported types, unsupported features, internals rationale) and the BridgeJS README bridged-type table.

Test plan

  • Host snapshot + diagnostics tests: swift test --package-path ./Plugins/BridgeJS --disable-experimental-prebuilts.
  • swift-syntax compatibility matrix: BRIDGEJS_OVERRIDE_SWIFT_SYNTAX_VERSION={601,602,603}.0.0 swift test --package-path ./Plugins/BridgeJS --disable-experimental-prebuilts.
  • AoT generated trees in sync: ./Utilities/bridge-js-generate.sh then git diff --exit-code.
  • WebAssembly runtime round-trips: make unittest SWIFT_SDK_ID=<wasm-sdk>ImportGenericAPITests and ExportGenericAPITests cover every supported T in both directions, multiple/distinct generic parameters, value-type concrete parameters mixed with the generic, return-only imports, and the [T] / T? / [String: T] wrappers (including empty collections and nil).
  • ./Utilities/format.swift clean.

Notes

  • Organized as five commits for review: (1) generic bridging infrastructure, (2) generic function import and export codegen, (3) tests and fixtures, (4) documentation, (5) review-driven fixes and simplifications.
  • BridgeJS remains experimental; no API stability is implied.