Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion Benchmarks/Sources/Generated/BridgeJS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,34 @@ fileprivate func _bjs_ArrayRoundtrip_wrap_extern(_ pointer: UnsafeMutableRawPoin
return _bjs_ArrayRoundtrip_wrap_extern(pointer)
}

extension SimpleStruct: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = SimpleStruct.bridgeJSMakeTypeHandle()
}

extension Address: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = Address.bridgeJSMakeTypeHandle()
}

extension Person: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = Person.bridgeJSMakeTypeHandle()
}

extension ComplexStruct: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = ComplexStruct.bridgeJSMakeTypeHandle()
}

extension Point: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = Point.bridgeJSMakeTypeHandle()
}

extension APIResult: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = APIResult.bridgeJSMakeTypeHandle()
}

extension ComplexResult: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = ComplexResult.bridgeJSMakeTypeHandle()
}

#if arch(wasm32)
@_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoop")
fileprivate func bjs_benchmarkHelperNoop_extern() -> Void
Expand Down Expand Up @@ -2238,4 +2266,40 @@ func _$benchmarkRunner(_ name: String, _ body: JSObject) throws(JSException) ->
if let error = _swift_js_take_exception() {
throw error
}
}
}

#if arch(wasm32)
@_extern(wasm, module: "bjs", name: "bjs_Benchmarks_register_type_handles")
fileprivate func _bjs_Benchmarks_register_type_handles_extern(_ base: UnsafePointer<Int32>?, _ count: Int32)

@_expose(wasm, "bjs_Benchmarks_register_type_handles")
public func _bjs_Benchmarks_register_type_handles() {
let typeIds: [Int32] = [
Bool.bridgeJSTypeID,
Int.bridgeJSTypeID,
Int8.bridgeJSTypeID,
UInt8.bridgeJSTypeID,
Int16.bridgeJSTypeID,
UInt16.bridgeJSTypeID,
Int32.bridgeJSTypeID,
UInt32.bridgeJSTypeID,
UInt.bridgeJSTypeID,
Int64.bridgeJSTypeID,
UInt64.bridgeJSTypeID,
Float.bridgeJSTypeID,
Double.bridgeJSTypeID,
String.bridgeJSTypeID,
JSValue.bridgeJSTypeID,
SimpleStruct.bridgeJSTypeID,
Address.bridgeJSTypeID,
Person.bridgeJSTypeID,
ComplexStruct.bridgeJSTypeID,
Point.bridgeJSTypeID,
APIResult.bridgeJSTypeID,
ComplexResult.bridgeJSTypeID,
]
typeIds.withUnsafeBufferPointer { buffer in
_bjs_Benchmarks_register_type_handles_extern(buffer.baseAddress, Int32(buffer.count))
}
}
#endif
3 changes: 3 additions & 0 deletions Examples/Embedded/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ let package = Package(
swiftSettings: [
.enableExperimentalFeature("Extern")
],
plugins: [
.plugin(name: "BridgeJS", package: "JavaScriptKit")
]
)
],
swiftLanguageModes: [.v5]
Expand Down
21 changes: 21 additions & 0 deletions Examples/Embedded/Sources/EmbeddedApp/main.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import JavaScriptKit

// BridgeJS generic imports work under Embedded Swift: `echoValue` is
// implemented in JavaScript (see index.html) and a single piece of generated
// glue serves every conforming type, selected by a runtime type ID.
@JS struct CounterLabel {
var count: Int
var text: String
}

@JSFunction func echoValue<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T

let alert = JSObject.global.alert.object!
let document = JSObject.global.document

Expand Down Expand Up @@ -46,6 +56,17 @@ _ = encoderContainer.appendChild(textInputElement)
_ = encoderContainer.appendChild(encodeResultElement)
_ = document.body.appendChild(encoderContainer)

let genericResultElement = document.createElement("pre")
do {
let number = try echoValue(42)
let text = try echoValue("hello")
let label = try echoValue(CounterLabel(count: number, text: text))
genericResultElement.innerText = .string("Generic import round-trip: \(label.text) \(label.count)")
} catch {
genericResultElement.innerText = "Generic import round-trip failed"
}
_ = document.body.appendChild(genericResultElement)

func print(_ message: String) {
_ = JSObject.global.console.log(message)
}
Expand Down
9 changes: 8 additions & 1 deletion Examples/Embedded/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
<body>
<script type="module">
import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
init();
init({
getImports() {
return {
// Implements the generic `echoValue` imported by main.swift.
echoValue: (value) => value,
};
},
});
</script>
</body>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,18 @@ fileprivate func _bjs_PlayBridgeJS_wrap_extern(_ pointer: UnsafeMutableRawPointe
return _bjs_PlayBridgeJS_wrap_extern(pointer)
}

extension PlayBridgeJSOutput: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = PlayBridgeJSOutput.bridgeJSMakeTypeHandle()
}

extension PlayBridgeJSDiagnostic: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = PlayBridgeJSDiagnostic.bridgeJSMakeTypeHandle()
}

extension PlayBridgeJSResult: BridgedSwiftGenericBridgeable {
@_spi(BridgeJS) public static let bridgeJSTypeHandle = PlayBridgeJSResult.bridgeJSMakeTypeHandle()
}

#if arch(wasm32)
@_extern(wasm, module: "PlayBridgeJS", name: "bjs_createTS2Swift")
fileprivate func bjs_createTS2Swift_extern() -> Int32
Expand Down Expand Up @@ -274,4 +286,36 @@ func _$TS2Swift_convert(_ self: JSObject, _ ts: String) throws(JSException) -> S
throw error
}
return String.bridgeJSLiftReturn(ret)
}
}

#if arch(wasm32)
@_extern(wasm, module: "bjs", name: "bjs_PlayBridgeJS_register_type_handles")
fileprivate func _bjs_PlayBridgeJS_register_type_handles_extern(_ base: UnsafePointer<Int32>?, _ count: Int32)

@_expose(wasm, "bjs_PlayBridgeJS_register_type_handles")
public func _bjs_PlayBridgeJS_register_type_handles() {
let typeIds: [Int32] = [
Bool.bridgeJSTypeID,
Int.bridgeJSTypeID,
Int8.bridgeJSTypeID,
UInt8.bridgeJSTypeID,
Int16.bridgeJSTypeID,
UInt16.bridgeJSTypeID,
Int32.bridgeJSTypeID,
UInt32.bridgeJSTypeID,
UInt.bridgeJSTypeID,
Int64.bridgeJSTypeID,
UInt64.bridgeJSTypeID,
Float.bridgeJSTypeID,
Double.bridgeJSTypeID,
String.bridgeJSTypeID,
JSValue.bridgeJSTypeID,
PlayBridgeJSOutput.bridgeJSTypeID,
PlayBridgeJSDiagnostic.bridgeJSTypeID,
PlayBridgeJSResult.bridgeJSTypeID,
]
typeIds.withUnsafeBufferPointer { buffer in
_bjs_PlayBridgeJS_register_type_handles_extern(buffer.baseAddress, Int32(buffer.count))
}
}
#endif
2 changes: 1 addition & 1 deletion Plugins/BridgeJS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ graph LR
| `Dictionary<K, V>` | `Record<K, V>` | - | [#495](https://github.com/swiftwasm/JavaScriptKit/issues/495) |
| `Set<T>` | `Set<T>` | - | [#397](https://github.com/swiftwasm/JavaScriptKit/issues/397) |
| `Foundation.URL` | `string` | - | [#496](https://github.com/swiftwasm/JavaScriptKit/issues/496) |
| Generics | - | - | [#398](https://github.com/swiftwasm/JavaScriptKit/issues/398) |
| 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) |

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

Expand Down
119 changes: 117 additions & 2 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ public class ExportSwift {
}
}

withSpan("Render Generic Bridgeable Conformances") { [self] in
// Emitted unconditionally: a module cannot know whether a dependent
// module declares a generic imported function that will be called
// with one of this module's types, so every bridgeable `@JS` type
// must conform to `BridgedSwiftGenericBridgeable`.
let genericConformanceCodegen = GenericConformanceCodegen()
for entry in skeleton.genericBridgeableTypeEntries {
decls.append(contentsOf: genericConformanceCodegen.renderConformance(typeName: entry.swiftName))
}
}

try withSpan("Render Async Promise Helpers") { [self] in
let asyncResolveTypes = skeleton.asyncPromiseResolveReturnTypes
if !asyncResolveTypes.isEmpty {
Expand Down Expand Up @@ -875,6 +886,73 @@ public class ExportSwift {
}
}

// MARK: - GenericConformanceCodegen

/// Renders `BridgedSwiftGenericBridgeable` conformances for `@JS` types so they
/// can be used as the generic argument of a generic imported `@JSFunction`.
///
/// The conformance gives each type a unique `BridgeJSTypeHandle`; the handle
/// object's address is the type's runtime type ID (pointer identity, so it is
/// unique across all linked modules without relying on type names).
struct GenericConformanceCodegen {
func renderConformance(typeName: String) -> [DeclSyntax] {
let printer = CodeFragmentPrinter()
printer.write("extension \(typeName): BridgedSwiftGenericBridgeable {")
printer.indent {
printer.write(
"@_spi(BridgeJS) public static let bridgeJSTypeHandle = \(typeName).bridgeJSMakeTypeHandle()"
)
}
printer.write("}")
return ["\(raw: printer.lines.joined(separator: "\n"))"]
}
}

// MARK: - GenericTypeRegistrationCodegen

/// Renders the per-module type-handle registration function.
///
/// The function is exported from the wasm module as
/// `bjs_<Module>_register_type_handles`. When the JS glue calls it during
/// initialization, it lowers the `bridgeJSTypeID` of every registered type into
/// a buffer — in the canonical order defined by
/// `BridgeJSSkeleton.typeRegistrationEntries` — and calls the JS import hook of
/// the same name with `(base, count)`. The JS side emits its codec array in the
/// same canonical order and pairs the two up index-by-index, so a type ID maps
/// to its codec without any string-based lookup.
public struct GenericTypeRegistrationCodegen {
public init() {}

public func render(for skeleton: BridgeJSSkeleton) -> String? {
guard let entries = skeleton.typeRegistrationEntries else { return nil }
let abiName = ABINameGenerator.typeRegistrationFunctionName(moduleName: skeleton.moduleName)
let printer = CodeFragmentPrinter()
printer.write("#if arch(wasm32)")
printer.write("@_extern(wasm, module: \"bjs\", name: \"\(abiName)\")")
printer.write("fileprivate func _\(abiName)_extern(_ base: UnsafePointer<Int32>?, _ count: Int32)")
printer.nextLine()
printer.write("@_expose(wasm, \"\(abiName)\")")
printer.write("public func _\(abiName)() {")
printer.indent {
printer.write("let typeIds: [Int32] = [")
printer.indent {
for entry in entries {
printer.write("\(entry.swiftName).bridgeJSTypeID,")
}
}
printer.write("]")
printer.write("typeIds.withUnsafeBufferPointer { buffer in")
printer.indent {
printer.write("_\(abiName)_extern(buffer.baseAddress, Int32(buffer.count))")
}
printer.write("}")
}
printer.write("}")
printer.write("#endif")
return printer.lines.joined(separator: "\n")
}
}

// MARK: - StackCodegen

/// Helper for stack-based lifting and lowering operations.
Expand All @@ -896,6 +974,10 @@ struct StackCodegen {
return "JSObject.bridgeJSStackPop()"
case .void, .namespaceEnum:
return "()"
case .generic:
fatalError(
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
)
}
}

Expand All @@ -908,7 +990,7 @@ struct StackCodegen {
return "\(raw: typeName)<\(raw: wrappedType.swiftType)>.bridgeJSStackPop()"
case .jsObject(let className?):
return "\(raw: typeName)<JSObject>.bridgeJSStackPop().map { \(raw: className)(unsafelyWrapping: $0) }"
case .nullable, .void, .namespaceEnum, .closure, .unsafePointer, .swiftProtocol:
case .nullable, .void, .namespaceEnum, .closure, .unsafePointer, .swiftProtocol, .generic:
fatalError("Invalid nullable wrapped type: \(wrappedType)")
}
}
Expand Down Expand Up @@ -941,6 +1023,10 @@ struct StackCodegen {
return lowerArrayStatements(elementType: elementType, accessor: accessor, varPrefix: varPrefix)
case .dictionary(let valueType):
return lowerDictionaryStatements(valueType: valueType, accessor: accessor, varPrefix: varPrefix)
case .generic:
fatalError(
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
)
}
}

Expand Down Expand Up @@ -1596,12 +1682,32 @@ extension BridgeType {
case .associatedValueEnum:
return ["_BridgedSwiftAssociatedValueEnum"]
case .rawValueEnum, .void, .unsafePointer, .namespaceEnum,
.swiftProtocol, .closure, .nullable, .array, .dictionary, .alias:
.swiftProtocol, .closure, .nullable, .array, .dictionary, .alias, .generic:
// Not supported yet.
return nil
}
}

/// Wrapped generic values bridge through the same `Array`/`Optional`/`Dictionary`
/// conditional conformances that concrete types use.
var genericStackPopExpression: String? {
switch self {
case .generic(let name): return "\(name).bridgeJSStackPop()"
case .array(.generic(let name)): return "Array<\(name)>.bridgeJSStackPop()"
case .nullable(.generic(let name), _): return "Optional<\(name)>.bridgeJSStackPop()"
case .dictionary(.generic(let name)): return "Dictionary<String, \(name)>.bridgeJSStackPop()"
default: return nil
}
}

func genericStackPushStatement(value: String) -> String? {
switch self {
case .generic, .array(.generic), .nullable(.generic, _), .dictionary(.generic):
return "\(value).bridgeJSStackPush()"
default: return nil
}
}

var swiftType: String {
switch self {
case .bool: return "Bool"
Expand Down Expand Up @@ -1631,6 +1737,7 @@ extension BridgeType {
let closureType = "(\(paramTypes))\(effectsStr) -> \(signature.returnType.swiftType)"
return useJSTypedClosure ? "JSTypedClosure<\(closureType)>" : closureType
case .alias(let name, _): return name
case .generic(let name): return name
}
}

Expand Down Expand Up @@ -1717,6 +1824,10 @@ extension BridgeType {
return LiftingIntrinsicInfo(parameters: [])
case .alias(_, let underlying):
return try underlying.liftParameterInfo()
case .generic:
throw BridgeJSCoreError(
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
)
}
}

Expand Down Expand Up @@ -1770,6 +1881,10 @@ extension BridgeType {
return .array
case .alias(_, let underlying):
return try underlying.loweringReturnInfo()
case .generic:
throw BridgeJSCoreError(
"Generic parameters are only supported on imported declarations, not exported concrete-type codegen"
)
}
}
}
Expand Down
Loading
Loading