From 712c3a7509280e61ec3742b548415dc29cb094a9 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Mon, 7 Sep 2026 15:19:12 +0200 Subject: [PATCH] BridgeJS: Diagnose unsupported protocol conformers before lowering --- .../Sources/BridgeJSCore/ClosureCodegen.swift | 8 +- .../Sources/BridgeJSCore/ExportSwift.swift | 30 +++--- .../Sources/BridgeJSCore/ImportTS.swift | 8 +- .../BridgeJSCore/SwiftToSkeleton.swift | 95 +++++++++++++++++++ .../BridgeJSCore/TypeDeclResolver.swift | 7 ++ .../CrossModuleResolutionTests.swift | 21 ++++ .../BridgeJSToolTests/DiagnosticsTests.swift | 92 ++++++++++++++++++ .../BridgeJSCodegenTests/Alias.swift | 2 +- .../BridgeJSCodegenTests/Protocol.swift | 12 +-- .../ProtocolInClosure.swift | 10 +- .../JavaScriptKit/BridgeJSIntrinsics.swift | 17 ++++ .../Generated/BridgeJS.swift | 26 ++--- 12 files changed, 283 insertions(+), 45 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift index 969b3c01e..25de94da1 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift @@ -182,15 +182,15 @@ public struct ClosureCodegen { } else { printer.write("let result = \(closureCallExpr)") switch signature.returnType { - case .swiftProtocol: + case .swiftProtocol(let protocolName): printer.write( - "return (result as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()" + "return _bridgeJSUnwrapProtocolExportable(result, \"\(protocolName)\").bridgeJSLowerAsProtocolReturn()" ) - case .nullable(.swiftProtocol, _): + case .nullable(.swiftProtocol(let protocolName), _): printer.write("if let result {") printer.indent { printer.write( - "_swift_js_return_optional_object(1, (result as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn())" + "_swift_js_return_optional_object(1, _bridgeJSUnwrapProtocolExportable(result, \"\(protocolName)\").bridgeJSLowerAsProtocolReturn())" ) } printer.write("} else {") diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index 55b5889fe..d976c9a39 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -262,8 +262,8 @@ public class ExportSwift { private func protocolCastSuffix(for returnType: BridgeType) -> (prefix: String, suffix: String) { switch returnType { - case .swiftProtocol: - return ("", " as! _BridgedSwiftProtocolExportable") + case .swiftProtocol(let name): + return ("_bridgeJSUnwrapProtocolExportable(", ", \"\(name)\")") default: return ("", "") } @@ -424,11 +424,11 @@ public class ExportSwift { } case .swiftProtocol: append("return ret.bridgeJSLowerAsProtocolReturn()") - case .nullable(.swiftProtocol, _): + case .nullable(.swiftProtocol(let protocolName), _): append( """ if let ret { - _swift_js_return_optional_object(1, (ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_return_optional_object(1, _bridgeJSUnwrapProtocolExportable(ret, "\(raw: protocolName)").bridgeJSLowerAsProtocolReturn()) } else { _swift_js_return_optional_object(0, 0) } @@ -993,9 +993,9 @@ struct StackCodegen { return ["\(raw: accessor).bridgeJSStackPush()"] case .jsObject(_?): return ["\(raw: accessor).jsObject.bridgeJSStackPush()"] - case .swiftProtocol: + case .swiftProtocol(let protocolName): return [ - "_swift_js_push_i32((\(raw: accessor) as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn())" + "_swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(\(raw: accessor), \"\(raw: protocolName)\").bridgeJSLowerAsProtocolReturn())" ] case .void, .namespaceEnum: return [] @@ -1016,8 +1016,8 @@ struct StackCodegen { varPrefix: String ) -> [CodeBlockItemSyntax] { switch elementType { - case .swiftProtocol: - return lowerProtocolArrayStatements(accessor: accessor, varPrefix: varPrefix) + case .swiftProtocol(let protocolName): + return lowerProtocolArrayStatements(protocolName: protocolName, accessor: accessor, varPrefix: varPrefix) case .void, .namespaceEnum: fatalError("Invalid array element type: \(elementType)") default: @@ -1026,6 +1026,7 @@ struct StackCodegen { } private func lowerProtocolArrayStatements( + protocolName: String, accessor: String, varPrefix: String ) -> [CodeBlockItemSyntax] { @@ -1033,7 +1034,7 @@ struct StackCodegen { return [ """ for \(raw: elemVar) in \(raw: accessor) { - _swift_js_push_i32((\(raw: elemVar) as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(\(raw: elemVar), "\(raw: protocolName)").bridgeJSLowerAsProtocolReturn()) } """, "_swift_js_push_i32(Int32(\(raw: accessor).count))", @@ -1048,8 +1049,12 @@ struct StackCodegen { switch valueType { case .jsObject(let className?) where className != "JSObject": return ["\(raw: accessor).mapValues { $0.jsObject }.bridgeJSStackPush()"] - case .swiftProtocol: - return lowerProtocolDictionaryStatements(accessor: accessor, varPrefix: varPrefix) + case .swiftProtocol(let protocolName): + return lowerProtocolDictionaryStatements( + protocolName: protocolName, + accessor: accessor, + varPrefix: varPrefix + ) case .nullable, .closure: return lowerDictionaryStatementsInline( valueType: valueType, @@ -1107,6 +1112,7 @@ struct StackCodegen { } private func lowerProtocolDictionaryStatements( + protocolName: String, accessor: String, varPrefix: String ) -> [CodeBlockItemSyntax] { @@ -1115,7 +1121,7 @@ struct StackCodegen { """ for \(raw: pairVar) in \(raw: accessor) { \(raw: pairVar).key.bridgeJSStackPush() - _swift_js_push_i32((\(raw: pairVar).value as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(\(raw: pairVar).value, "\(raw: protocolName)").bridgeJSLowerAsProtocolReturn()) } """, "_swift_js_push_i32(Int32(\(raw: accessor).count))", diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index cff1aa979..09bee37d6 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -188,12 +188,12 @@ public struct ImportTS { } ) ) - } else if case .nullable(.swiftProtocol, _) = param.type, context == .exportSwift { + } else if case .nullable(.swiftProtocol(let protocolName), _) = param.type, context == .exportSwift { body.write("let \(pattern): (Int32, Int32)") body.write("if let \(param.name) {") body.indent { body.write( - "\(pattern) = (1, (\(param.name) as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn())" + "\(pattern) = (1, _bridgeJSUnwrapProtocolExportable(\(param.name), \"\(protocolName)\").bridgeJSLowerAsProtocolReturn())" ) } body.write("} else {") @@ -203,9 +203,9 @@ public struct ImportTS { body.write("}") } else { let initializerExpr: ExprSyntax - if case .swiftProtocol = param.type, context == .exportSwift { + if case .swiftProtocol(let protocolName) = param.type, context == .exportSwift { initializerExpr = ExprSyntax( - "(\(raw: param.name) as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()" + "_bridgeJSUnwrapProtocolExportable(\(raw: param.name), \"\(raw: protocolName)\").bridgeJSLowerAsProtocolReturn()" ) } else { initializerExpr = ExprSyntax("\(raw: param.name).bridgeJSLowerParameter()") diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index fb81d9089..2fc6f8db3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -295,6 +295,8 @@ public final class SwiftToSkeleton { collector.finalize(&exported) } + perSourceErrors.append(contentsOf: diagnoseProtocolConformances(in: exported)) + if !perSourceErrors.isEmpty { let diagnostics = perSourceErrors.flatMap { inputFilePath, errors in errors.map { (file: inputFilePath, diagnostic: $0) } @@ -318,6 +320,99 @@ public final class SwiftToSkeleton { ) } + private func diagnoseProtocolConformances( + in exported: ExportedSkeleton + ) -> [(inputFilePath: String, errors: [DiagnosticError])] { + var loweredProtocols: Set = [] + func collect(_ type: BridgeType, loweredBySwift: Bool) { + switch type { + case .swiftProtocol(let name): + if loweredBySwift { loweredProtocols.insert(name) } + case .array(let element), .dictionary(let element), .nullable(let element, _), .alias(_, let element): + collect(element, loweredBySwift: loweredBySwift) + case .closure(let signature, _): + for parameter in signature.parameters { + collect(parameter, loweredBySwift: !loweredBySwift) + } + collect(signature.returnType, loweredBySwift: loweredBySwift) + default: + break + } + } + func collect(_ function: ExportedFunction, loweredReturn: Bool = true) { + for parameter in function.parameters { collect(parameter.type, loweredBySwift: !loweredReturn) } + collect(function.returnType, loweredBySwift: loweredReturn) + } + + for function in exported.functions + exported.classes.flatMap(\.methods) + + exported.structs.flatMap(\.methods) + exported.enums.flatMap(\.staticMethods) + { + collect(function) + } + for constructor in exported.classes.compactMap(\.constructor) + exported.structs.compactMap(\.constructor) { + for parameter in constructor.parameters { collect(parameter.type, loweredBySwift: false) } + } + for property in exported.classes.flatMap(\.properties) + exported.enums.flatMap(\.staticProperties) { + collect(property.type, loweredBySwift: true) + if !property.isReadonly { collect(property.type, loweredBySwift: false) } + } + for property in exported.structs.flatMap(\.properties) { + collect(property.type, loweredBySwift: true) + if !property.isStatic || !property.isReadonly { collect(property.type, loweredBySwift: false) } + } + for value in exported.enums.flatMap(\.cases).flatMap(\.associatedValues) { + collect(value.type, loweredBySwift: true) + } + for protocolDef in exported.protocols { + for method in protocolDef.methods { collect(method, loweredReturn: false) } + for property in protocolDef.properties { + collect(property.type, loweredBySwift: false) + if !property.isReadonly { collect(property.type, loweredBySwift: true) } + } + } + + guard !loweredProtocols.isEmpty else { return [] } + var diagnostics: [(inputFilePath: String, errors: [DiagnosticError])] = [] + for declaration in typeDeclResolver.declarationsWithInheritance where !declaration.is(ProtocolDeclSyntax.self) { + let extendedType = declaration.as(ExtensionDeclSyntax.self)?.extendedType + let target = extendedType.flatMap { typeDeclResolver.resolveExtensionTarget($0) } + let classDecl = declaration.as(ClassDeclSyntax.self) ?? target?.as(ClassDeclSyntax.self) + if classDecl?.attributes.hasJSAttribute() == true { continue } + if let extendedType, target == nil { + var errors: [DiagnosticError] = [] + if case .swiftHeapObject = resolveExternal(for: extendedType, errors: &errors) { continue } + } + guard + let name = declaration.asProtocol(NamedDeclSyntax.self)?.name.text ?? extendedType?.trimmedDescription, + let inputFilePath = sourceFiles.first(where: { $0.sourceFile.id == declaration.root.id })?.inputFilePath + else { continue } + for inherited in declaration.inheritanceClause?.inheritedTypes ?? [] { + guard let protocolDecl = typeDeclResolver.resolve(inherited.type)?.as(ProtocolDeclSyntax.self), + protocolDecl.attributes.hasJSAttribute(), loweredProtocols.contains(protocolDecl.name.text) + else { continue } + let protocolName = protocolDecl.name.text + diagnostics.append( + ( + inputFilePath, + [ + DiagnosticError( + node: declaration, + message: + "'\(name)' conforms to '\(protocolName)', a @JS protocol that exported APIs " + + "bridge to JavaScript, but '\(name)' is not a '@JS class'. Passing it to " + + "JavaScript as 'any \(protocolName)' would trap at runtime.", + hint: + "Mark '\(name)' as a '@JS class' so it can cross the bridge, or avoid using " + + "'\(protocolName)' as an existential in exported APIs." + ) + ] + ) + ) + } + } + return diagnostics + } + private static let jsTypedArrayTypealiasNames: [String: String] = [ "Int8": "JSInt8Array", "UInt8": "JSUint8Array", diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/TypeDeclResolver.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/TypeDeclResolver.swift index 4df546b8a..f88f67084 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/TypeDeclResolver.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/TypeDeclResolver.swift @@ -9,6 +9,7 @@ class TypeDeclResolver { typealias QualifiedName = [String] private var typeDeclByQualifiedName: [QualifiedName: TypeDecl] = [:] private var typeAliasByQualifiedName: [QualifiedName: TypeAliasDeclSyntax] = [:] + private(set) var declarationsWithInheritance: [any DeclGroupSyntax] = [] enum Error: Swift.Error { case typeNotFound(QualifiedName) @@ -24,6 +25,9 @@ class TypeDeclResolver { } func visitNominalDecl(_ node: TypeDecl) -> SyntaxVisitorContinueKind { + if node.inheritanceClause != nil { + resolver.declarationsWithInheritance.append(node) + } let name = node.name.text let qualifiedName = scope + [name] resolver.typeDeclByQualifiedName[qualifiedName] = node @@ -74,6 +78,9 @@ class TypeDeclResolver { } override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + if node.inheritanceClause != nil { + resolver.declarationsWithInheritance.append(node) + } guard let components = node.memberScopeComponents else { return .skipChildren } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/CrossModuleResolutionTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/CrossModuleResolutionTests.swift index 3f0ee88db..afd87b9df 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/CrossModuleResolutionTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/CrossModuleResolutionTests.swift @@ -487,6 +487,27 @@ import Testing } } + @Test func exportedClassConformanceInAnotherModule() throws { + let core = try makeSkeleton( + """ + @JS public class MyImpl { + @JS public init() {} + @JS public func ok() -> Int { 42 } + } + """, + moduleName: "Core" + ) + _ = try makeSkeleton( + """ + import Core + @JS protocol P { func ok() -> Int } + extension MyImpl: P {} + @JS func get() -> P { MyImpl() } + """, + dependencies: [(moduleName: "Core", skeleton: core)] + ) + } + // MARK: - Utillites private func resolveApp( diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift index 4f45a9880..ea8ae9402 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift @@ -791,4 +791,96 @@ import Testing // No line 3 in source, so output must not show a " 3 |" context line after the pointer #expect(!description.contains(" 3 |")) } + + @Test(arguments: [ + "struct MyImpl: P { func ok() -> Int { 42 } }", + "class MyImpl: P { func ok() -> Int { 42 } }", + "struct MyImpl {}\nextension MyImpl: P { func ok() -> Int { 42 } }", + ]) + func unsupportedProtocolConformer(conformer: String) { + expectDiagnostic( + source: """ + @JS protocol P { func ok() -> Int } + \(conformer) + @JS func get() -> P { MyImpl() } + """, + contains: "'MyImpl' is not a '@JS class'" + ) + } + + @Test(arguments: [ + "@JS func get() -> [P] { fatalError() }", + "@JS func get() -> P? { fatalError() }", + "@JS func get() -> [String: P] { fatalError() }", + "@JS func subscribe(_ callback: (P) -> Void) {}", + "@JS class Holder { @JS var value: P = MyImpl() }", + "@JS protocol Sink { func consume(_ value: P) }", + "@JS class Holder { @JS var callback: (P) -> Void = { _ in } }", + "@JS struct Holder { let callback: (P) -> Void }", + "@JS enum Holder { @JS static var callback: (P) -> Void = { _ in } }", + "@JS protocol Holder { var callback: (P) -> Void { get set } }", + "@JS protocol Holder { var callback: (P) -> Void { get } }", + ]) + func loweredProtocolConformer(declaration: String) { + expectDiagnostic( + source: """ + @JS protocol P { func ok() -> Int } + struct MyImpl: P { func ok() -> Int { 42 } } + \(declaration) + """, + contains: "'MyImpl' conforms to 'P'" + ) + } + + @Test(arguments: [ + "@JS func take(_ value: P) -> Int { value.ok() }", + "@JS class Holder { @JS let callback: (P) -> Void = { _ in } }", + ]) + func protocolConformerWithoutLowering(declaration: String) throws { + _ = try makeSkeleton( + """ + @JS protocol P { func ok() -> Int } + struct MyImpl: P { func ok() -> Int { 42 } } + \(declaration) + """ + ) + } + + @Test func exportedProtocolConformer() throws { + _ = try makeSkeleton( + """ + @JS protocol P { func ok() -> Int } + @JS class MyImpl: P { + @JS init() {} + @JS func ok() -> Int { 42 } + } + @JS func get() -> P { MyImpl() } + """ + ) + } + + @Test func unusedJSProtocolConformer() throws { + _ = try makeSkeleton( + """ + @JS protocol P { var id: String { get } } + @JS struct Building: P { var id: String } + @JS func describe(_ building: Building) -> String { building.id } + """ + ) + } + + @Test func protocolRefinementIsNotAConcreteConformance() throws { + _ = try makeSkeleton( + """ + @JS protocol Base { func ok() -> Int } + @JS protocol Refined: Base { func extra() -> Int } + @JS class MyImpl: Refined { + @JS init() {} + @JS func ok() -> Int { 1 } + @JS func extra() -> Int { 2 } + } + @JS func get() -> Base { MyImpl() } + """ + ) + } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Alias.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Alias.swift index 4483de428..b88c6717e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Alias.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Alias.swift @@ -133,7 +133,7 @@ public func _bjs_roundtripTags() -> Void { @_cdecl("bjs_describeUser") public func _bjs_describeUser(_ owner: Int32) -> Int32 { #if arch(wasm32) - let ret = describeUser(_: AnyHasOptionalUserId.bridgeJSLiftParameter(owner)) as! _BridgedSwiftProtocolExportable + let ret = _bridgeJSUnwrapProtocolExportable(describeUser(_: AnyHasOptionalUserId.bridgeJSLiftParameter(owner)), "HasOptionalUserId") return ret.bridgeJSLowerAsProtocolReturn() #else fatalError("Only available on WebAssembly") diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Protocol.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Protocol.swift index 7c2db9a98..6afb7e6b8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Protocol.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Protocol.swift @@ -698,7 +698,7 @@ public func _bjs_processDelegates() -> Void { #if arch(wasm32) let ret = processDelegates(_: [AnyMyViewControllerDelegate].bridgeJSStackPop()) for __bjs_elem_ret in ret { - _swift_js_push_i32((__bjs_elem_ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(__bjs_elem_ret, "MyViewControllerDelegate").bridgeJSLowerAsProtocolReturn()) } _swift_js_push_i32(Int32(ret.count)) #else @@ -713,7 +713,7 @@ public func _bjs_processDelegatesByName() -> Void { let ret = processDelegatesByName(_: [String: AnyMyViewControllerDelegate].bridgeJSLiftParameter()) for __bjs_kv_ret in ret { __bjs_kv_ret.key.bridgeJSStackPush() - _swift_js_push_i32((__bjs_kv_ret.value as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(__bjs_kv_ret.value, "MyViewControllerDelegate").bridgeJSLowerAsProtocolReturn()) } _swift_js_push_i32(Int32(ret.count)) #else @@ -871,7 +871,7 @@ public func _bjs_MyViewController_sendHelper(_ _self: UnsafeMutableRawPointer, _ @_cdecl("bjs_MyViewController_delegate_get") public func _bjs_MyViewController_delegate_get(_ _self: UnsafeMutableRawPointer) -> Int32 { #if arch(wasm32) - let ret = MyViewController.bridgeJSLiftParameter(_self).delegate as! _BridgedSwiftProtocolExportable + let ret = _bridgeJSUnwrapProtocolExportable(MyViewController.bridgeJSLiftParameter(_self).delegate, "MyViewControllerDelegate") return ret.bridgeJSLowerAsProtocolReturn() #else fatalError("Only available on WebAssembly") @@ -894,7 +894,7 @@ public func _bjs_MyViewController_secondDelegate_get(_ _self: UnsafeMutableRawPo #if arch(wasm32) let ret = MyViewController.bridgeJSLiftParameter(_self).secondDelegate if let ret { - _swift_js_return_optional_object(1, (ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_return_optional_object(1, _bridgeJSUnwrapProtocolExportable(ret, "MyViewControllerDelegate").bridgeJSLowerAsProtocolReturn()) } else { _swift_js_return_optional_object(0, 0) } @@ -971,7 +971,7 @@ public func _bjs_DelegateManager_delegates_get(_ _self: UnsafeMutableRawPointer) #if arch(wasm32) let ret = DelegateManager.bridgeJSLiftParameter(_self).delegates for __bjs_elem_ret in ret { - _swift_js_push_i32((__bjs_elem_ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(__bjs_elem_ret, "MyViewControllerDelegate").bridgeJSLowerAsProtocolReturn()) } _swift_js_push_i32(Int32(ret.count)) #else @@ -996,7 +996,7 @@ public func _bjs_DelegateManager_delegatesByName_get(_ _self: UnsafeMutableRawPo let ret = DelegateManager.bridgeJSLiftParameter(_self).delegatesByName for __bjs_kv_ret in ret { __bjs_kv_ret.key.bridgeJSStackPush() - _swift_js_push_i32((__bjs_kv_ret.value as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(__bjs_kv_ret.value, "MyViewControllerDelegate").bridgeJSLowerAsProtocolReturn()) } _swift_js_push_i32(Int32(ret.count)) #else diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ProtocolInClosure.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ProtocolInClosure.swift index d956a6f3a..78765e3f5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ProtocolInClosure.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ProtocolInClosure.swift @@ -27,7 +27,7 @@ private enum _BJS_Closure_10TestModule10RenderableP_10RenderableP { let callback = JSObject.bridgeJSLiftParameter(callbackId) return { [callback] param0 in #if arch(wasm32) - let param0ObjectId = (param0 as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn() + let param0ObjectId = _bridgeJSUnwrapProtocolExportable(param0, "Renderable").bridgeJSLowerAsProtocolReturn() let callbackValue = callback.bridgeJSLowerParameter() let ret = invoke_js_callback_TestModule_10TestModule10RenderableP_10RenderableP(callbackValue, param0ObjectId) return AnyRenderable.bridgeJSLiftReturn(ret) @@ -55,7 +55,7 @@ public func _invoke_swift_closure_TestModule_10TestModule10RenderableP_10Rendera #if arch(wasm32) let closure = Unmanaged<_BridgeJSTypedClosureBox<(any Renderable) -> any Renderable>>.fromOpaque(boxPtr).takeUnretainedValue().closure let result = closure(AnyRenderable.bridgeJSLiftParameter(param0)) - return (result as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn() + return _bridgeJSUnwrapProtocolExportable(result, "Renderable").bridgeJSLowerAsProtocolReturn() #else fatalError("Only available on WebAssembly") #endif @@ -90,7 +90,7 @@ private enum _BJS_Closure_10TestModule10RenderableP_SS { let callback = JSObject.bridgeJSLiftParameter(callbackId) return { [callback] param0 in #if arch(wasm32) - let param0ObjectId = (param0 as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn() + let param0ObjectId = _bridgeJSUnwrapProtocolExportable(param0, "Renderable").bridgeJSLowerAsProtocolReturn() let callbackValue = callback.bridgeJSLowerParameter() let ret = invoke_js_callback_TestModule_10TestModule10RenderableP_SS(callbackValue, param0ObjectId) return String.bridgeJSLiftReturn(ret) @@ -155,7 +155,7 @@ private enum _BJS_Closure_10TestModuleSq10RenderableP_SS { #if arch(wasm32) let (param0IsSome, param0ObjectId): (Int32, Int32) if let param0 { - (param0IsSome, param0ObjectId) = (1, (param0 as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + (param0IsSome, param0ObjectId) = (1, _bridgeJSUnwrapProtocolExportable(param0, "Renderable").bridgeJSLowerAsProtocolReturn()) } else { (param0IsSome, param0ObjectId) = (0, 0) } @@ -248,7 +248,7 @@ public func _invoke_swift_closure_TestModule_10TestModuley_10RenderableP(_ boxPt #if arch(wasm32) let closure = Unmanaged<_BridgeJSTypedClosureBox<() -> any Renderable>>.fromOpaque(boxPtr).takeUnretainedValue().closure let result = closure() - return (result as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn() + return _bridgeJSUnwrapProtocolExportable(result, "Renderable").bridgeJSLowerAsProtocolReturn() #else fatalError("Only available on WebAssembly") #endif diff --git a/Sources/JavaScriptKit/BridgeJSIntrinsics.swift b/Sources/JavaScriptKit/BridgeJSIntrinsics.swift index ab0a9f4f0..f62c6aff0 100644 --- a/Sources/JavaScriptKit/BridgeJSIntrinsics.swift +++ b/Sources/JavaScriptKit/BridgeJSIntrinsics.swift @@ -1145,6 +1145,23 @@ extension _BridgedSwiftProtocolExportable where Self: _BridgedSwiftProtocolWrapp } } +#if !hasFeature(Embedded) +/// Checks that a protocol value can be lowered to JavaScript. +@_spi(BridgeJS) public func _bridgeJSUnwrapProtocolExportable( + _ value: Any, + _ protocolName: StaticString +) -> any _BridgedSwiftProtocolExportable { + guard let exportable = value as? any _BridgedSwiftProtocolExportable else { + fatalError( + "BridgeJS: cannot bridge a value of type '\(type(of: value))' to JavaScript as " + + "'any \(protocolName)': the concrete type is not exported with @JS. Only '@JS class' " + + "types and JavaScript-provided implementations can cross the bridge as protocol values." + ) + } + return exportable +} +#endif + /// A protocol that Swift enum types that do not have a payload can conform to. /// /// The conformance is automatically synthesized by the BridgeJS code generator. diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index f16bce9f2..748a95129 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -100,7 +100,7 @@ private enum _BJS_Closure_20BridgeJSRuntimeTests13DataProcessorP_13DataProcessor let callback = JSObject.bridgeJSLiftParameter(callbackId) return { [callback] param0 in #if arch(wasm32) - let param0ObjectId = (param0 as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn() + let param0ObjectId = _bridgeJSUnwrapProtocolExportable(param0, "DataProcessor").bridgeJSLowerAsProtocolReturn() let callbackValue = callback.bridgeJSLowerParameter() let ret = invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests13DataProcessorP_13DataProcessorP(callbackValue, param0ObjectId) return AnyDataProcessor.bridgeJSLiftReturn(ret) @@ -128,7 +128,7 @@ public func _invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests13D #if arch(wasm32) let closure = Unmanaged<_BridgeJSTypedClosureBox<(any DataProcessor) -> any DataProcessor>>.fromOpaque(boxPtr).takeUnretainedValue().closure let result = closure(AnyDataProcessor.bridgeJSLiftParameter(param0)) - return (result as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn() + return _bridgeJSUnwrapProtocolExportable(result, "DataProcessor").bridgeJSLowerAsProtocolReturn() #else fatalError("Only available on WebAssembly") #endif @@ -163,7 +163,7 @@ private enum _BJS_Closure_20BridgeJSRuntimeTests13DataProcessorP_SS { let callback = JSObject.bridgeJSLiftParameter(callbackId) return { [callback] param0 in #if arch(wasm32) - let param0ObjectId = (param0 as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn() + let param0ObjectId = _bridgeJSUnwrapProtocolExportable(param0, "DataProcessor").bridgeJSLowerAsProtocolReturn() let callbackValue = callback.bridgeJSLowerParameter() let ret = invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests13DataProcessorP_SS(callbackValue, param0ObjectId) return String.bridgeJSLiftReturn(ret) @@ -1670,7 +1670,7 @@ private enum _BJS_Closure_20BridgeJSRuntimeTestsSq13DataProcessorP_SS { #if arch(wasm32) let (param0IsSome, param0ObjectId): (Int32, Int32) if let param0 { - (param0IsSome, param0ObjectId) = (1, (param0 as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + (param0IsSome, param0ObjectId) = (1, _bridgeJSUnwrapProtocolExportable(param0, "DataProcessor").bridgeJSLowerAsProtocolReturn()) } else { (param0IsSome, param0ObjectId) = (0, 0) } @@ -3480,7 +3480,7 @@ public func _invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsy_1 #if arch(wasm32) let closure = Unmanaged<_BridgeJSTypedClosureBox<() -> any DataProcessor>>.fromOpaque(boxPtr).takeUnretainedValue().closure let result = closure() - return (result as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn() + return _bridgeJSUnwrapProtocolExportable(result, "DataProcessor").bridgeJSLowerAsProtocolReturn() #else fatalError("Only available on WebAssembly") #endif @@ -4581,7 +4581,7 @@ public func _bjs_ArraySupportExports_static_roundTripProtocolArray() -> Void { #if arch(wasm32) let ret = ArraySupportExports.roundTripProtocolArray(_: [AnyArrayElementProtocol].bridgeJSStackPop()) for __bjs_elem_ret in ret { - _swift_js_push_i32((__bjs_elem_ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(__bjs_elem_ret, "ArrayElementProtocol").bridgeJSLowerAsProtocolReturn()) } _swift_js_push_i32(Int32(ret.count)) #else @@ -12699,7 +12699,7 @@ public func _bjs_DataProcessorManager_setProcessorAPIResult(_ _self: UnsafeMutab @_cdecl("bjs_DataProcessorManager_processor_get") public func _bjs_DataProcessorManager_processor_get(_ _self: UnsafeMutableRawPointer) -> Int32 { #if arch(wasm32) - let ret = DataProcessorManager.bridgeJSLiftParameter(_self).processor as! _BridgedSwiftProtocolExportable + let ret = _bridgeJSUnwrapProtocolExportable(DataProcessorManager.bridgeJSLiftParameter(_self).processor, "DataProcessor") return ret.bridgeJSLowerAsProtocolReturn() #else fatalError("Only available on WebAssembly") @@ -12722,7 +12722,7 @@ public func _bjs_DataProcessorManager_backupProcessor_get(_ _self: UnsafeMutable #if arch(wasm32) let ret = DataProcessorManager.bridgeJSLiftParameter(_self).backupProcessor if let ret { - _swift_js_return_optional_object(1, (ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_return_optional_object(1, _bridgeJSUnwrapProtocolExportable(ret, "DataProcessor").bridgeJSLowerAsProtocolReturn()) } else { _swift_js_return_optional_object(0, 0) } @@ -13136,7 +13136,7 @@ fileprivate func _bjs_SwiftDataProcessor_wrap_extern(_ pointer: UnsafeMutableRaw @_cdecl("bjs_ProtocolReturnTests_static_createNativeProcessor") public func _bjs_ProtocolReturnTests_static_createNativeProcessor() -> Int32 { #if arch(wasm32) - let ret = ProtocolReturnTests.createNativeProcessor() as! _BridgedSwiftProtocolExportable + let ret = _bridgeJSUnwrapProtocolExportable(ProtocolReturnTests.createNativeProcessor(), "DataProcessor") return ret.bridgeJSLowerAsProtocolReturn() #else fatalError("Only available on WebAssembly") @@ -13149,7 +13149,7 @@ public func _bjs_ProtocolReturnTests_static_createNativeProcessorOptional() -> V #if arch(wasm32) let ret = ProtocolReturnTests.createNativeProcessorOptional() if let ret { - _swift_js_return_optional_object(1, (ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_return_optional_object(1, _bridgeJSUnwrapProtocolExportable(ret, "DataProcessor").bridgeJSLowerAsProtocolReturn()) } else { _swift_js_return_optional_object(0, 0) } @@ -13164,7 +13164,7 @@ public func _bjs_ProtocolReturnTests_static_createNativeProcessorNil() -> Void { #if arch(wasm32) let ret = ProtocolReturnTests.createNativeProcessorNil() if let ret { - _swift_js_return_optional_object(1, (ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_return_optional_object(1, _bridgeJSUnwrapProtocolExportable(ret, "DataProcessor").bridgeJSLowerAsProtocolReturn()) } else { _swift_js_return_optional_object(0, 0) } @@ -13179,7 +13179,7 @@ public func _bjs_ProtocolReturnTests_static_createNativeProcessorArray() -> Void #if arch(wasm32) let ret = ProtocolReturnTests.createNativeProcessorArray() for __bjs_elem_ret in ret { - _swift_js_push_i32((__bjs_elem_ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(__bjs_elem_ret, "DataProcessor").bridgeJSLowerAsProtocolReturn()) } _swift_js_push_i32(Int32(ret.count)) #else @@ -13194,7 +13194,7 @@ public func _bjs_ProtocolReturnTests_static_createNativeProcessorDictionary() -> let ret = ProtocolReturnTests.createNativeProcessorDictionary() for __bjs_kv_ret in ret { __bjs_kv_ret.key.bridgeJSStackPush() - _swift_js_push_i32((__bjs_kv_ret.value as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()) + _swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(__bjs_kv_ret.value, "DataProcessor").bridgeJSLowerAsProtocolReturn()) } _swift_js_push_i32(Int32(ret.count)) #else