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, andJSValue- 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
@JSFunctionthunks 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/@_cdeclthunk 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 reifiesTthrough 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 aBridgeTypesmap) as the trailing argument, since TypeScript erases generics. - Cross-module: a
@JS structdefined in a dependency module can be used asT(codegen registers it, synthesizing a conditional@retroactiveconformance 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
fatalErrorstub 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/.dictionaryof.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, fixedliftCoercefor 64-bit unsigned integers soUInt64(and the non-generic[UInt64]/UInt64?paths) lift to an unsignedBigInt.
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.shthengit diff --exit-code. - WebAssembly runtime round-trips:
make unittest SWIFT_SDK_ID=<wasm-sdk>—ImportGenericAPITestsandExportGenericAPITestscover every supportedTin 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 andnil). -
./Utilities/format.swiftclean.
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.