Skip to content

Commit 16d0767

Browse files
committed
BridgeJS: Support generic functions on imported JS APIs
1 parent 83eae16 commit 16d0767

21 files changed

Lines changed: 1499 additions & 303 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: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ 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 passes its types to a generic imported function.
97+
let genericConformanceCodegen = GenericConformanceCodegen()
98+
for entry in skeleton.genericBridgeableTypeEntries {
99+
decls.append(contentsOf: genericConformanceCodegen.renderConformance(typeName: entry.swiftName))
100+
}
101+
}
102+
94103
try withSpan("Render Async Promise Helpers") { [self] in
95104
let asyncResolveTypes = skeleton.asyncPromiseResolveReturnTypes
96105
if !asyncResolveTypes.isEmpty {
@@ -875,6 +884,63 @@ public class ExportSwift {
875884
}
876885
}
877886

887+
// MARK: - GenericConformanceCodegen
888+
889+
/// Renders `BridgedSwiftGenericBridgeable` conformances for `@JS` types so they
890+
/// can be used as the generic argument of a generic imported `@JSFunction`.
891+
struct GenericConformanceCodegen {
892+
func renderConformance(typeName: String) -> [DeclSyntax] {
893+
let printer = CodeFragmentPrinter()
894+
printer.write("extension \(typeName): BridgedSwiftGenericBridgeable {")
895+
printer.indent {
896+
printer.write(
897+
"@_spi(BridgeJS) public static let bridgeJSTypeHandle = \(typeName).bridgeJSMakeTypeHandle()"
898+
)
899+
}
900+
printer.write("}")
901+
return ["\(raw: printer.lines.joined(separator: "\n"))"]
902+
}
903+
}
904+
905+
// MARK: - GenericTypeRegistrationCodegen
906+
907+
/// Renders the `bjs_<Module>_register_type_handles` wasm export: it lowers each
908+
/// registered type's `bridgeJSTypeID` into a buffer, in the canonical order of
909+
/// `BridgeJSSkeleton.typeRegistrationEntries`, and passes it to the JS import
910+
/// hook of the same name, which pairs the IDs with its codec array by index.
911+
public struct GenericTypeRegistrationCodegen {
912+
public init() {}
913+
914+
public func render(for skeleton: BridgeJSSkeleton) -> String? {
915+
guard let entries = skeleton.typeRegistrationEntries else { return nil }
916+
let abiName = ABINameGenerator.typeRegistrationFunctionName(moduleName: skeleton.moduleName)
917+
let printer = CodeFragmentPrinter()
918+
printer.write("#if arch(wasm32)")
919+
printer.write("@_extern(wasm, module: \"bjs\", name: \"\(abiName)\")")
920+
printer.write("fileprivate func _\(abiName)_extern(_ base: UnsafePointer<Int32>?, _ count: Int32)")
921+
printer.nextLine()
922+
printer.write("@_expose(wasm, \"\(abiName)\")")
923+
printer.write("public func _\(abiName)() {")
924+
printer.indent {
925+
printer.write("let typeIds: [Int32] = [")
926+
printer.indent {
927+
for entry in entries {
928+
printer.write("\(entry.swiftName).bridgeJSTypeID,")
929+
}
930+
}
931+
printer.write("]")
932+
printer.write("typeIds.withUnsafeBufferPointer { buffer in")
933+
printer.indent {
934+
printer.write("_\(abiName)_extern(buffer.baseAddress, Int32(buffer.count))")
935+
}
936+
printer.write("}")
937+
}
938+
printer.write("}")
939+
printer.write("#endif")
940+
return printer.lines.joined(separator: "\n")
941+
}
942+
}
943+
878944
// MARK: - StackCodegen
879945

880946
/// Helper for stack-based lifting and lowering operations.
@@ -896,6 +962,10 @@ struct StackCodegen {
896962
return "JSObject.bridgeJSStackPop()"
897963
case .void, .namespaceEnum:
898964
return "()"
965+
case .generic:
966+
fatalError(
967+
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
968+
)
899969
}
900970
}
901971

@@ -908,7 +978,7 @@ struct StackCodegen {
908978
return "\(raw: typeName)<\(raw: wrappedType.swiftType)>.bridgeJSStackPop()"
909979
case .jsObject(let className?):
910980
return "\(raw: typeName)<JSObject>.bridgeJSStackPop().map { \(raw: className)(unsafelyWrapping: $0) }"
911-
case .nullable, .void, .namespaceEnum, .closure, .unsafePointer, .swiftProtocol:
981+
case .nullable, .void, .namespaceEnum, .closure, .unsafePointer, .swiftProtocol, .generic:
912982
fatalError("Invalid nullable wrapped type: \(wrappedType)")
913983
}
914984
}
@@ -941,6 +1011,10 @@ struct StackCodegen {
9411011
return lowerArrayStatements(elementType: elementType, accessor: accessor, varPrefix: varPrefix)
9421012
case .dictionary(let valueType):
9431013
return lowerDictionaryStatements(valueType: valueType, accessor: accessor, varPrefix: varPrefix)
1014+
case .generic:
1015+
fatalError(
1016+
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
1017+
)
9441018
}
9451019
}
9461020

@@ -1596,12 +1670,34 @@ extension BridgeType {
15961670
case .associatedValueEnum:
15971671
return ["_BridgedSwiftAssociatedValueEnum"]
15981672
case .rawValueEnum, .void, .unsafePointer, .namespaceEnum,
1599-
.swiftProtocol, .closure, .nullable, .array, .dictionary, .alias:
1673+
.swiftProtocol, .closure, .nullable, .array, .dictionary, .alias, .generic:
16001674
// Not supported yet.
16011675
return nil
16021676
}
16031677
}
16041678

1679+
/// Stack expressions for bare `T` and `T?`, the only generic shapes that
1680+
/// cannot reuse the concrete emission: `bridgeJSLowerParameter()` names
1681+
/// per-type members that the generic constraint erases to the stack, so
1682+
/// `bridgeJSStackPush()`/`bridgeJSStackPop()` is the shared spelling.
1683+
/// `[T]` and `[String: T]` go through the ordinary paths via the `Array`
1684+
/// and `Dictionary` stack conformances.
1685+
var genericStackPopExpression: String? {
1686+
switch self {
1687+
case .generic(let name): return "\(name).bridgeJSStackPop()"
1688+
case .nullable(.generic(let name), _): return "Optional<\(name)>.bridgeJSStackPop()"
1689+
default: return nil
1690+
}
1691+
}
1692+
1693+
func genericStackPushStatement(value: String) -> String? {
1694+
switch self {
1695+
case .generic, .nullable(.generic, _):
1696+
return "\(value).bridgeJSStackPush()"
1697+
default: return nil
1698+
}
1699+
}
1700+
16051701
var swiftType: String {
16061702
switch self {
16071703
case .bool: return "Bool"
@@ -1631,6 +1727,7 @@ extension BridgeType {
16311727
let closureType = "(\(paramTypes))\(effectsStr) -> \(signature.returnType.swiftType)"
16321728
return useJSTypedClosure ? "JSTypedClosure<\(closureType)>" : closureType
16331729
case .alias(let name, _): return name
1730+
case .generic(let name): return name
16341731
}
16351732
}
16361733

@@ -1717,6 +1814,10 @@ extension BridgeType {
17171814
return LiftingIntrinsicInfo(parameters: [])
17181815
case .alias(_, let underlying):
17191816
return try underlying.liftParameterInfo()
1817+
case .generic:
1818+
throw BridgeJSCoreError(
1819+
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
1820+
)
17201821
}
17211822
}
17221823

@@ -1770,6 +1871,10 @@ extension BridgeType {
17701871
return .array
17711872
case .alias(_, let underlying):
17721873
return try underlying.loweringReturnInfo()
1874+
case .generic:
1875+
throw BridgeJSCoreError(
1876+
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
1877+
)
17731878
}
17741879
}
17751880
}

0 commit comments

Comments
 (0)