Skip to content

Commit f55d763

Browse files
committed
BridgeJS: Support generic functions on imported JS APIs
1 parent d58d2c6 commit f55d763

20 files changed

Lines changed: 1218 additions & 58 deletions

File tree

Benchmarks/Sources/Generated/BridgeJS.swift

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2179,6 +2179,34 @@ fileprivate func _bjs_ArrayRoundtrip_wrap_extern(_ pointer: UnsafeMutableRawPoin
21792179
return _bjs_ArrayRoundtrip_wrap_extern(pointer)
21802180
}
21812181

2182+
extension SimpleStruct: BridgedSwiftGenericBridgeable {
2183+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = SimpleStruct.bridgeJSMakeTypeHandle()
2184+
}
2185+
2186+
extension Address: BridgedSwiftGenericBridgeable {
2187+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = Address.bridgeJSMakeTypeHandle()
2188+
}
2189+
2190+
extension Person: BridgedSwiftGenericBridgeable {
2191+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = Person.bridgeJSMakeTypeHandle()
2192+
}
2193+
2194+
extension ComplexStruct: BridgedSwiftGenericBridgeable {
2195+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = ComplexStruct.bridgeJSMakeTypeHandle()
2196+
}
2197+
2198+
extension Point: BridgedSwiftGenericBridgeable {
2199+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = Point.bridgeJSMakeTypeHandle()
2200+
}
2201+
2202+
extension APIResult: BridgedSwiftGenericBridgeable {
2203+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = APIResult.bridgeJSMakeTypeHandle()
2204+
}
2205+
2206+
extension ComplexResult: BridgedSwiftGenericBridgeable {
2207+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = ComplexResult.bridgeJSMakeTypeHandle()
2208+
}
2209+
21822210
#if arch(wasm32)
21832211
@_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoop")
21842212
fileprivate func bjs_benchmarkHelperNoop_extern() -> Void
@@ -2238,4 +2266,40 @@ func _$benchmarkRunner(_ name: String, _ body: JSObject) throws(JSException) ->
22382266
if let error = _swift_js_take_exception() {
22392267
throw error
22402268
}
2241-
}
2269+
}
2270+
2271+
#if arch(wasm32)
2272+
@_extern(wasm, module: "bjs", name: "bjs_Benchmarks_register_type_handles")
2273+
fileprivate func _bjs_Benchmarks_register_type_handles_extern(_ base: UnsafePointer<Int32>?, _ count: Int32)
2274+
2275+
@_expose(wasm, "bjs_Benchmarks_register_type_handles")
2276+
public func _bjs_Benchmarks_register_type_handles() {
2277+
let typeIds: [Int32] = [
2278+
Bool.bridgeJSTypeID,
2279+
Int.bridgeJSTypeID,
2280+
Int8.bridgeJSTypeID,
2281+
UInt8.bridgeJSTypeID,
2282+
Int16.bridgeJSTypeID,
2283+
UInt16.bridgeJSTypeID,
2284+
Int32.bridgeJSTypeID,
2285+
UInt32.bridgeJSTypeID,
2286+
UInt.bridgeJSTypeID,
2287+
Int64.bridgeJSTypeID,
2288+
UInt64.bridgeJSTypeID,
2289+
Float.bridgeJSTypeID,
2290+
Double.bridgeJSTypeID,
2291+
String.bridgeJSTypeID,
2292+
JSValue.bridgeJSTypeID,
2293+
SimpleStruct.bridgeJSTypeID,
2294+
Address.bridgeJSTypeID,
2295+
Person.bridgeJSTypeID,
2296+
ComplexStruct.bridgeJSTypeID,
2297+
Point.bridgeJSTypeID,
2298+
APIResult.bridgeJSTypeID,
2299+
ComplexResult.bridgeJSTypeID,
2300+
]
2301+
typeIds.withUnsafeBufferPointer { buffer in
2302+
_bjs_Benchmarks_register_type_handles_extern(buffer.baseAddress, Int32(buffer.count))
2303+
}
2304+
}
2305+
#endif

Examples/Embedded/Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ let package = Package(
1616
swiftSettings: [
1717
.enableExperimentalFeature("Extern")
1818
],
19+
plugins: [
20+
.plugin(name: "BridgeJS", package: "JavaScriptKit")
21+
]
1922
)
2023
],
2124
swiftLanguageModes: [.v5]

Examples/Embedded/Sources/EmbeddedApp/main.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import JavaScriptKit
22

3+
// BridgeJS generic imports work under Embedded Swift: `echoValue` is
4+
// implemented in JavaScript (see index.html) and a single piece of generated
5+
// glue serves every conforming type, selected by a runtime type ID.
6+
@JS struct CounterLabel {
7+
var count: Int
8+
var text: String
9+
}
10+
11+
@JSFunction func echoValue<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
12+
313
let alert = JSObject.global.alert.object!
414
let document = JSObject.global.document
515

@@ -46,6 +56,17 @@ _ = encoderContainer.appendChild(textInputElement)
4656
_ = encoderContainer.appendChild(encodeResultElement)
4757
_ = document.body.appendChild(encoderContainer)
4858

59+
let genericResultElement = document.createElement("pre")
60+
do {
61+
let number = try echoValue(42)
62+
let text = try echoValue("hello")
63+
let label = try echoValue(CounterLabel(count: number, text: text))
64+
genericResultElement.innerText = .string("Generic import round-trip: \(label.text) \(label.count)")
65+
} catch {
66+
genericResultElement.innerText = "Generic import round-trip failed"
67+
}
68+
_ = document.body.appendChild(genericResultElement)
69+
4970
func print(_ message: String) {
5071
_ = JSObject.global.console.log(message)
5172
}

Examples/Embedded/index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88
<body>
99
<script type="module">
1010
import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
11-
init();
11+
init({
12+
getImports() {
13+
return {
14+
// Implements the generic `echoValue` imported by main.swift.
15+
echoValue: (value) => value,
16+
};
17+
},
18+
});
1219
</script>
1320
</body>
1421

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.swift

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,18 @@ fileprivate func _bjs_PlayBridgeJS_wrap_extern(_ pointer: UnsafeMutableRawPointe
231231
return _bjs_PlayBridgeJS_wrap_extern(pointer)
232232
}
233233

234+
extension PlayBridgeJSOutput: BridgedSwiftGenericBridgeable {
235+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = PlayBridgeJSOutput.bridgeJSMakeTypeHandle()
236+
}
237+
238+
extension PlayBridgeJSDiagnostic: BridgedSwiftGenericBridgeable {
239+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = PlayBridgeJSDiagnostic.bridgeJSMakeTypeHandle()
240+
}
241+
242+
extension PlayBridgeJSResult: BridgedSwiftGenericBridgeable {
243+
@_spi(BridgeJS) public static let bridgeJSTypeHandle = PlayBridgeJSResult.bridgeJSMakeTypeHandle()
244+
}
245+
234246
#if arch(wasm32)
235247
@_extern(wasm, module: "PlayBridgeJS", name: "bjs_createTS2Swift")
236248
fileprivate func bjs_createTS2Swift_extern() -> Int32
@@ -274,4 +286,36 @@ func _$TS2Swift_convert(_ self: JSObject, _ ts: String) throws(JSException) -> S
274286
throw error
275287
}
276288
return String.bridgeJSLiftReturn(ret)
277-
}
289+
}
290+
291+
#if arch(wasm32)
292+
@_extern(wasm, module: "bjs", name: "bjs_PlayBridgeJS_register_type_handles")
293+
fileprivate func _bjs_PlayBridgeJS_register_type_handles_extern(_ base: UnsafePointer<Int32>?, _ count: Int32)
294+
295+
@_expose(wasm, "bjs_PlayBridgeJS_register_type_handles")
296+
public func _bjs_PlayBridgeJS_register_type_handles() {
297+
let typeIds: [Int32] = [
298+
Bool.bridgeJSTypeID,
299+
Int.bridgeJSTypeID,
300+
Int8.bridgeJSTypeID,
301+
UInt8.bridgeJSTypeID,
302+
Int16.bridgeJSTypeID,
303+
UInt16.bridgeJSTypeID,
304+
Int32.bridgeJSTypeID,
305+
UInt32.bridgeJSTypeID,
306+
UInt.bridgeJSTypeID,
307+
Int64.bridgeJSTypeID,
308+
UInt64.bridgeJSTypeID,
309+
Float.bridgeJSTypeID,
310+
Double.bridgeJSTypeID,
311+
String.bridgeJSTypeID,
312+
JSValue.bridgeJSTypeID,
313+
PlayBridgeJSOutput.bridgeJSTypeID,
314+
PlayBridgeJSDiagnostic.bridgeJSTypeID,
315+
PlayBridgeJSResult.bridgeJSTypeID,
316+
]
317+
typeIds.withUnsafeBufferPointer { buffer in
318+
_bjs_PlayBridgeJS_register_type_handles_extern(buffer.baseAddress, Int32(buffer.count))
319+
}
320+
}
321+
#endif

Plugins/BridgeJS/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ graph LR
9898
| `Dictionary<K, V>` | `Record<K, V>` | - | [#495](https://github.com/swiftwasm/JavaScriptKit/issues/495) |
9999
| `Set<T>` | `Set<T>` | - | [#397](https://github.com/swiftwasm/JavaScriptKit/issues/397) |
100100
| `Foundation.URL` | `string` | - | [#496](https://github.com/swiftwasm/JavaScriptKit/issues/496) |
101-
| Generics | - | - | [#398](https://github.com/swiftwasm/JavaScriptKit/issues/398) |
101+
| Generic function or method (`T`, `[T]`, `T?`, `[String: T]`) | `<T>(value: T): T` | Depends on `T` | ✅ imports only ([#398](https://github.com/swiftwasm/JavaScriptKit/issues/398) for exports) |
102102

103103
### Import-specific (TypeScript -> Swift)
104104

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ public class ExportSwift {
9191
}
9292
}
9393

94+
withSpan("Render Generic Bridgeable Conformances") { [self] in
95+
// Emitted unconditionally: a module cannot know whether a dependent
96+
// module declares a generic imported function that will be called
97+
// with one of this module's types, so every bridgeable `@JS` type
98+
// must conform to `BridgedSwiftGenericBridgeable`.
99+
let genericConformanceCodegen = GenericConformanceCodegen()
100+
for entry in skeleton.genericBridgeableTypeEntries {
101+
decls.append(contentsOf: genericConformanceCodegen.renderConformance(typeName: entry.swiftName))
102+
}
103+
}
104+
94105
try withSpan("Render Async Promise Helpers") { [self] in
95106
let asyncResolveTypes = skeleton.asyncPromiseResolveReturnTypes
96107
if !asyncResolveTypes.isEmpty {
@@ -875,6 +886,73 @@ public class ExportSwift {
875886
}
876887
}
877888

889+
// MARK: - GenericConformanceCodegen
890+
891+
/// Renders `BridgedSwiftGenericBridgeable` conformances for `@JS` types so they
892+
/// can be used as the generic argument of a generic imported `@JSFunction`.
893+
///
894+
/// The conformance gives each type a unique `BridgeJSTypeHandle`; the handle
895+
/// object's address is the type's runtime type ID (pointer identity, so it is
896+
/// unique across all linked modules without relying on type names).
897+
struct GenericConformanceCodegen {
898+
func renderConformance(typeName: String) -> [DeclSyntax] {
899+
let printer = CodeFragmentPrinter()
900+
printer.write("extension \(typeName): BridgedSwiftGenericBridgeable {")
901+
printer.indent {
902+
printer.write(
903+
"@_spi(BridgeJS) public static let bridgeJSTypeHandle = \(typeName).bridgeJSMakeTypeHandle()"
904+
)
905+
}
906+
printer.write("}")
907+
return ["\(raw: printer.lines.joined(separator: "\n"))"]
908+
}
909+
}
910+
911+
// MARK: - GenericTypeRegistrationCodegen
912+
913+
/// Renders the per-module type-handle registration function.
914+
///
915+
/// The function is exported from the wasm module as
916+
/// `bjs_<Module>_register_type_handles`. When the JS glue calls it during
917+
/// initialization, it lowers the `bridgeJSTypeID` of every registered type into
918+
/// a buffer — in the canonical order defined by
919+
/// `BridgeJSSkeleton.typeRegistrationEntries` — and calls the JS import hook of
920+
/// the same name with `(base, count)`. The JS side emits its codec array in the
921+
/// same canonical order and pairs the two up index-by-index, so a type ID maps
922+
/// to its codec without any string-based lookup.
923+
public struct GenericTypeRegistrationCodegen {
924+
public init() {}
925+
926+
public func render(for skeleton: BridgeJSSkeleton) -> String? {
927+
guard let entries = skeleton.typeRegistrationEntries else { return nil }
928+
let abiName = ABINameGenerator.typeRegistrationFunctionName(moduleName: skeleton.moduleName)
929+
let printer = CodeFragmentPrinter()
930+
printer.write("#if arch(wasm32)")
931+
printer.write("@_extern(wasm, module: \"bjs\", name: \"\(abiName)\")")
932+
printer.write("fileprivate func _\(abiName)_extern(_ base: UnsafePointer<Int32>?, _ count: Int32)")
933+
printer.nextLine()
934+
printer.write("@_expose(wasm, \"\(abiName)\")")
935+
printer.write("public func _\(abiName)() {")
936+
printer.indent {
937+
printer.write("let typeIds: [Int32] = [")
938+
printer.indent {
939+
for entry in entries {
940+
printer.write("\(entry.swiftName).bridgeJSTypeID,")
941+
}
942+
}
943+
printer.write("]")
944+
printer.write("typeIds.withUnsafeBufferPointer { buffer in")
945+
printer.indent {
946+
printer.write("_\(abiName)_extern(buffer.baseAddress, Int32(buffer.count))")
947+
}
948+
printer.write("}")
949+
}
950+
printer.write("}")
951+
printer.write("#endif")
952+
return printer.lines.joined(separator: "\n")
953+
}
954+
}
955+
878956
// MARK: - StackCodegen
879957

880958
/// Helper for stack-based lifting and lowering operations.
@@ -896,6 +974,10 @@ struct StackCodegen {
896974
return "JSObject.bridgeJSStackPop()"
897975
case .void, .namespaceEnum:
898976
return "()"
977+
case .generic:
978+
fatalError(
979+
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
980+
)
899981
}
900982
}
901983

@@ -908,7 +990,7 @@ struct StackCodegen {
908990
return "\(raw: typeName)<\(raw: wrappedType.swiftType)>.bridgeJSStackPop()"
909991
case .jsObject(let className?):
910992
return "\(raw: typeName)<JSObject>.bridgeJSStackPop().map { \(raw: className)(unsafelyWrapping: $0) }"
911-
case .nullable, .void, .namespaceEnum, .closure, .unsafePointer, .swiftProtocol:
993+
case .nullable, .void, .namespaceEnum, .closure, .unsafePointer, .swiftProtocol, .generic:
912994
fatalError("Invalid nullable wrapped type: \(wrappedType)")
913995
}
914996
}
@@ -941,6 +1023,10 @@ struct StackCodegen {
9411023
return lowerArrayStatements(elementType: elementType, accessor: accessor, varPrefix: varPrefix)
9421024
case .dictionary(let valueType):
9431025
return lowerDictionaryStatements(valueType: valueType, accessor: accessor, varPrefix: varPrefix)
1026+
case .generic:
1027+
fatalError(
1028+
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
1029+
)
9441030
}
9451031
}
9461032

@@ -1596,12 +1682,32 @@ extension BridgeType {
15961682
case .associatedValueEnum:
15971683
return ["_BridgedSwiftAssociatedValueEnum"]
15981684
case .rawValueEnum, .void, .unsafePointer, .namespaceEnum,
1599-
.swiftProtocol, .closure, .nullable, .array, .dictionary, .alias:
1685+
.swiftProtocol, .closure, .nullable, .array, .dictionary, .alias, .generic:
16001686
// Not supported yet.
16011687
return nil
16021688
}
16031689
}
16041690

1691+
/// Wrapped generic values bridge through the same `Array`/`Optional`/`Dictionary`
1692+
/// conditional conformances that concrete types use.
1693+
var genericStackPopExpression: String? {
1694+
switch self {
1695+
case .generic(let name): return "\(name).bridgeJSStackPop()"
1696+
case .array(.generic(let name)): return "Array<\(name)>.bridgeJSStackPop()"
1697+
case .nullable(.generic(let name), _): return "Optional<\(name)>.bridgeJSStackPop()"
1698+
case .dictionary(.generic(let name)): return "Dictionary<String, \(name)>.bridgeJSStackPop()"
1699+
default: return nil
1700+
}
1701+
}
1702+
1703+
func genericStackPushStatement(value: String) -> String? {
1704+
switch self {
1705+
case .generic, .array(.generic), .nullable(.generic, _), .dictionary(.generic):
1706+
return "\(value).bridgeJSStackPush()"
1707+
default: return nil
1708+
}
1709+
}
1710+
16051711
var swiftType: String {
16061712
switch self {
16071713
case .bool: return "Bool"
@@ -1631,6 +1737,7 @@ extension BridgeType {
16311737
let closureType = "(\(paramTypes))\(effectsStr) -> \(signature.returnType.swiftType)"
16321738
return useJSTypedClosure ? "JSTypedClosure<\(closureType)>" : closureType
16331739
case .alias(let name, _): return name
1740+
case .generic(let name): return name
16341741
}
16351742
}
16361743

@@ -1717,6 +1824,10 @@ extension BridgeType {
17171824
return LiftingIntrinsicInfo(parameters: [])
17181825
case .alias(_, let underlying):
17191826
return try underlying.liftParameterInfo()
1827+
case .generic:
1828+
throw BridgeJSCoreError(
1829+
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
1830+
)
17201831
}
17211832
}
17221833

@@ -1770,6 +1881,10 @@ extension BridgeType {
17701881
return .array
17711882
case .alias(_, let underlying):
17721883
return try underlying.loweringReturnInfo()
1884+
case .generic:
1885+
throw BridgeJSCoreError(
1886+
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
1887+
)
17731888
}
17741889
}
17751890
}

0 commit comments

Comments
 (0)