From 7efb593975392efcc2b6f0fa19cb7705f49b00fa Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Wed, 8 Oct 2025 14:29:48 +0200 Subject: [PATCH] BridgeJS: Initial protocol support [fe09c4b1] Simplify export swift [b42bb7c1] WIP: Simplify protocol method generation [75e1ed37] BridgeJSLink simplification [b1fcc4d0] Cleanup [70aa0332] WIP: Final wrap [2f648eaf] WIP: Protocols simplification [f2f89b33] WIP: Fix for parameters in methods [6026fc1d] WIP: Test protocol methods with parameters --- .../JavaScript/BridgeJS.ExportSwift.json | 5 +- .../JavaScript/BridgeJS.ExportSwift.json | 5 +- .../Sources/BridgeJSCore/ExportSwift.swift | 422 +++++++++++++--- .../Sources/BridgeJSCore/ImportTS.swift | 4 + .../Sources/BridgeJSLink/BridgeJSLink.swift | 80 ++- .../Sources/BridgeJSLink/JSGlueGen.swift | 26 + .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 26 +- .../BridgeJSToolTests/Inputs/Protocol.swift | 41 ++ .../BridgeJSLinkTests/Protocol.Export.d.ts | 44 ++ .../BridgeJSLinkTests/Protocol.Export.js | 286 +++++++++++ .../__Snapshots__/ExportSwiftTests/Async.json | 5 +- .../ExportSwiftTests/DefaultParameters.json | 5 +- .../ExportSwiftTests/EnumAssociatedValue.json | 5 +- .../ExportSwiftTests/EnumCase.json | 5 +- .../ExportSwiftTests/EnumNamespace.json | 5 +- .../ExportSwiftTests/EnumRawType.json | 5 +- .../ExportSwiftTests/Namespaces.json | 5 +- .../ExportSwiftTests/Optionals.json | 5 +- .../ExportSwiftTests/PrimitiveParameters.json | 5 +- .../ExportSwiftTests/PrimitiveReturn.json | 5 +- .../ExportSwiftTests/PropertyTypes.json | 5 +- .../ExportSwiftTests/Protocol.json | 305 ++++++++++++ .../ExportSwiftTests/Protocol.swift | 174 +++++++ .../ExportSwiftTests/StaticFunctions.json | 5 +- .../ExportSwiftTests/StaticProperties.json | 5 +- .../ExportSwiftTests/StringParameter.json | 5 +- .../ExportSwiftTests/StringReturn.json | 5 +- .../ExportSwiftTests/SwiftClass.json | 5 +- .../ExportSwiftTests/Throws.json | 5 +- .../VoidParameterVoidReturn.json | 5 +- .../JavaScriptKit/BridgeJSInstrincics.swift | 65 +++ .../BridgeJS/Exporting-Swift-to-JavaScript.md | 1 + .../Exporting-Swift-Protocols.md | 209 ++++++++ .../BridgeJSRuntimeTests/ExportAPITests.swift | 81 +++ .../Generated/BridgeJS.ExportSwift.swift | 285 +++++++++++ .../JavaScript/BridgeJS.ExportSwift.json | 465 +++++++++++++++++- Tests/prelude.mjs | 105 +++- 37 files changed, 2612 insertions(+), 107 deletions(-) create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Protocol.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.Export.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.Export.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Protocol.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Protocol.swift create mode 100644 Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md diff --git a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json index cbd7d10e..cfec1d46 100644 --- a/Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -720,5 +720,8 @@ } } ], - "moduleName" : "Benchmarks" + "moduleName" : "Benchmarks", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json index 5ef85519..3e00433e 100644 --- a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -138,5 +138,8 @@ "functions" : [ ], - "moduleName" : "PlayBridgeJS" + "moduleName" : "PlayBridgeJS", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index 00902ee9..9179a966 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -24,6 +24,8 @@ public class ExportSwift { private var exportedFunctions: [ExportedFunction] = [] private var exportedClasses: [ExportedClass] = [] private var exportedEnums: [ExportedEnum] = [] + private var exportedProtocols: [ExportedProtocol] = [] + private var exportedProtocolNameByKey: [String: String] = [:] private var typeDeclResolver: TypeDeclResolver = TypeDeclResolver() private let enumCodegen: EnumCodegen = EnumCodegen() @@ -64,7 +66,8 @@ public class ExportSwift { moduleName: moduleName, functions: exportedFunctions, classes: exportedClasses, - enums: exportedEnums + enums: exportedEnums, + protocols: exportedProtocols ) ) } @@ -77,10 +80,13 @@ public class ExportSwift { /// The names of the exported enums, in the order they were written in the source file var exportedEnumNames: [String] = [] var exportedEnumByName: [String: ExportedEnum] = [:] + /// The names of the exported protocols, in the order they were written in the source file + var exportedProtocolNames: [String] = [] + var exportedProtocolByName: [String: ExportedProtocol] = [:] var errors: [DiagnosticError] = [] - /// Creates a unique key for a class by combining name and namespace - private func classKey(name: String, namespace: [String]?) -> String { + /// Creates a unique key by combining name and namespace + private func makeKey(name: String, namespace: [String]?) -> String { if let namespace = namespace, !namespace.isEmpty { return "\(namespace.joined(separator: ".")).\(name)" } else { @@ -88,19 +94,39 @@ public class ExportSwift { } } - /// Creates a unique key for an enum by combining name and namespace - private func enumKey(name: String, namespace: [String]?) -> String { - if let namespace = namespace, !namespace.isEmpty { - return "\(namespace.joined(separator: ".")).\(name)" - } else { - return name + struct NamespaceResolution { + let namespace: [String]? + let isValid: Bool + } + + /// Resolves and validates namespace from both @JS attribute and computed (nested) namespace + /// Returns the effective namespace and whether validation succeeded + private func resolveNamespace( + from jsAttribute: AttributeSyntax, + for node: some SyntaxProtocol, + declarationType: String + ) -> NamespaceResolution { + let attributeNamespace = extractNamespace(from: jsAttribute) + let computedNamespace = computeNamespace(for: node) + + if computedNamespace != nil && attributeNamespace != nil { + diagnose( + node: jsAttribute, + message: "Nested \(declarationType)s cannot specify their own namespace", + hint: + "Remove the namespace from @JS attribute - nested \(declarationType)s inherit namespace from parent" + ) + return NamespaceResolution(namespace: nil, isValid: false) } + + return NamespaceResolution(namespace: computedNamespace ?? attributeNamespace, isValid: true) } enum State { case topLevel case classBody(name: String, key: String) case enumBody(name: String, key: String) + case protocolBody(name: String, key: String) } struct StateStack { @@ -386,6 +412,42 @@ public class ExportSwift { return nil } + /// Shared parameter parsing logic used by functions, initializers, and protocol methods + private func parseParameters( + from parameterClause: FunctionParameterClauseSyntax, + allowDefaults: Bool = true + ) -> [Parameter] { + var parameters: [Parameter] = [] + + for param in parameterClause.parameters { + let resolvedType = self.parent.lookupType(for: param.type) + + if let type = resolvedType, case .optional(let wrappedType) = type, wrappedType.isOptional { + diagnoseNestedOptional(node: param.type, type: param.type.trimmedDescription) + continue + } + + guard let type = resolvedType else { + diagnoseUnsupportedType(node: param.type, type: param.type.trimmedDescription) + continue + } + + let name = param.secondName?.text ?? param.firstName.text + let label = param.firstName.text + + let defaultValue: DefaultValue? + if allowDefaults { + defaultValue = extractDefaultValue(from: param.defaultValue, type: type) + } else { + defaultValue = nil + } + + parameters.append(Parameter(label: label, name: name, type: type, defaultValue: defaultValue)) + } + + return parameters + } + override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { guard node.attributes.hasJSAttribute() else { return .skipChildren @@ -427,6 +489,9 @@ public class ExportSwift { } } return .skipChildren + case .protocolBody(_, _): + // Protocol methods are handled in visitProtocolMethod during protocol parsing + return .skipChildren } } @@ -470,27 +535,7 @@ public class ExportSwift { ) } - var parameters: [Parameter] = [] - for param in node.signature.parameterClause.parameters { - let resolvedType = self.parent.lookupType(for: param.type) - - if let type = resolvedType, case .optional(let wrappedType) = type, wrappedType.isOptional { - diagnoseNestedOptional(node: param.type, type: param.type.trimmedDescription) - continue - } - - guard let type = resolvedType else { - diagnoseUnsupportedType(node: param.type, type: param.type.trimmedDescription) - continue - } - - let name = param.secondName?.text ?? param.firstName.text - let label = param.firstName.text - - let defaultValue = extractDefaultValue(from: param.defaultValue, type: type) - - parameters.append(Parameter(label: label, name: name, type: type, defaultValue: defaultValue)) - } + let parameters = parseParameters(from: node.signature.parameterClause, allowDefaults: true) let returnType: BridgeType if let returnClause = node.signature.returnClause { let resolvedType = self.parent.lookupType(for: returnClause.type) @@ -529,6 +574,8 @@ public class ExportSwift { let isNamespaceEnum = exportedEnumByName[enumKey]?.cases.isEmpty ?? true staticContext = isNamespaceEnum ? .namespaceEnum : .enumName(enumName) + case .protocolBody(_, _): + return nil } let classNameForABI: String? @@ -638,19 +685,7 @@ public class ExportSwift { ) } - var parameters: [Parameter] = [] - for param in node.signature.parameterClause.parameters { - guard let type = self.parent.lookupType(for: param.type) else { - diagnoseUnsupportedType(node: param.type, type: param.type.trimmedDescription) - continue - } - let name = param.secondName?.text ?? param.firstName.text - let label = param.firstName.text - - let defaultValue = extractDefaultValue(from: param.defaultValue, type: type) - - parameters.append(Parameter(label: label, name: name, type: type, defaultValue: defaultValue)) - } + let parameters = parseParameters(from: node.signature.parameterClause, allowDefaults: true) guard let effects = collectEffects(signature: node.signature) else { return .skipChildren @@ -677,7 +712,7 @@ public class ExportSwift { let attributeNamespace = extractNamespace(from: jsAttribute) if attributeNamespace != nil { diagnose( - node: node.attributes.firstJSAttribute!, + node: jsAttribute, message: "Namespace parameter within @JS attribute is not supported for property declarations", hint: "Remove the namespace from @JS attribute. If you need dedicated namespace, consider using a nested enum or class instead." @@ -714,6 +749,9 @@ public class ExportSwift { case .topLevel: diagnose(node: node, message: "@JS var must be inside a @JS class or enum") return .skipChildren + case .protocolBody(_, _): + diagnose(node: node, message: "Properties are not supported in protocols") + return .skipChildren } // Process each binding (variable declaration) @@ -785,20 +823,10 @@ public class ExportSwift { return .skipChildren } - let attributeNamespace = extractNamespace(from: jsAttribute) - let computedNamespace = computeNamespace(for: node) - - if computedNamespace != nil && attributeNamespace != nil { - diagnose( - node: jsAttribute, - message: "Nested classes cannot specify their own namespace", - hint: "Remove the namespace from @JS attribute - nested classes inherit namespace from parent" - ) + let namespaceResult = resolveNamespace(from: jsAttribute, for: node, declarationType: "class") + guard namespaceResult.isValid else { return .skipChildren } - - let effectiveNamespace = computedNamespace ?? attributeNamespace - let swiftCallName = ExportSwift.computeSwiftCallName(for: node, itemName: name) let explicitAccessControl = computeExplicitAtLeastInternalAccessControl( for: node, @@ -811,9 +839,9 @@ public class ExportSwift { constructor: nil, methods: [], properties: [], - namespace: effectiveNamespace + namespace: namespaceResult.namespace ) - let uniqueKey = classKey(name: name, namespace: effectiveNamespace) + let uniqueKey = makeKey(name: name, namespace: namespaceResult.namespace) stateStack.push(state: .classBody(name: name, key: uniqueKey)) exportedClassByName[uniqueKey] = exportedClass @@ -829,10 +857,6 @@ public class ExportSwift { } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - guard node.attributes.hasJSAttribute() else { - return .skipChildren - } - guard let jsAttribute = node.attributes.firstJSAttribute else { return .skipChildren } @@ -844,19 +868,10 @@ public class ExportSwift { return Constants.supportedRawTypes.contains(typeName) }?.type.trimmedDescription - let attributeNamespace = extractNamespace(from: jsAttribute) - let computedNamespace = computeNamespace(for: node) - - if computedNamespace != nil && attributeNamespace != nil { - diagnose( - node: jsAttribute, - message: "Nested enums cannot specify their own namespace", - hint: "Remove the namespace from @JS attribute - nested enums inherit namespace from parent" - ) + let namespaceResult = resolveNamespace(from: jsAttribute, for: node, declarationType: "enum") + guard namespaceResult.isValid else { return .skipChildren } - - let effectiveNamespace = computedNamespace ?? attributeNamespace let emitStyle = extractEnumStyle(from: jsAttribute) ?? .const let swiftCallName = ExportSwift.computeSwiftCallName(for: node, itemName: name) let explicitAccessControl = computeExplicitAtLeastInternalAccessControl( @@ -871,13 +886,13 @@ public class ExportSwift { explicitAccessControl: explicitAccessControl, cases: [], // Will be populated in visit(EnumCaseDeclSyntax) rawType: SwiftEnumRawType(rawType), - namespace: effectiveNamespace, + namespace: namespaceResult.namespace, emitStyle: emitStyle, staticMethods: [], staticProperties: [] ) - let enumUniqueKey = enumKey(name: name, namespace: effectiveNamespace) + let enumUniqueKey = makeKey(name: name, namespace: namespaceResult.namespace) exportedEnumByName[enumUniqueKey] = exportedEnum exportedEnumNames.append(enumUniqueKey) @@ -966,6 +981,100 @@ public class ExportSwift { stateStack.pop() } + override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { + guard let jsAttribute = node.attributes.firstJSAttribute else { + return .skipChildren + } + + let name = node.name.text + + let namespaceResult = resolveNamespace(from: jsAttribute, for: node, declarationType: "protocol") + guard namespaceResult.isValid else { + return .skipChildren + } + _ = computeExplicitAtLeastInternalAccessControl( + for: node, + message: "Protocol visibility must be at least internal" + ) + + var methods: [ExportedFunction] = [] + for member in node.memberBlock.members { + if let funcDecl = member.decl.as(FunctionDeclSyntax.self) { + if let exportedFunction = visitProtocolMethod( + node: funcDecl, + protocolName: name, + namespace: namespaceResult.namespace + ) { + methods.append(exportedFunction) + } + } + } + + let exportedProtocol = ExportedProtocol( + name: name, + methods: methods, + namespace: namespaceResult.namespace + ) + + let protocolUniqueKey = makeKey(name: name, namespace: namespaceResult.namespace) + exportedProtocolByName[protocolUniqueKey] = exportedProtocol + exportedProtocolNames.append(protocolUniqueKey) + + parent.exportedProtocolNameByKey[protocolUniqueKey] = name + + return .skipChildren + } + + private func visitProtocolMethod( + node: FunctionDeclSyntax, + protocolName: String, + namespace: [String]? + ) -> ExportedFunction? { + let name = node.name.text + + let parameters = parseParameters(from: node.signature.parameterClause, allowDefaults: false) + + let returnType: BridgeType + if let returnClause = node.signature.returnClause { + let resolvedType = self.parent.lookupType(for: returnClause.type) + + if let type = resolvedType, case .optional(let wrappedType) = type, wrappedType.isOptional { + diagnoseNestedOptional(node: returnClause.type, type: returnClause.type.trimmedDescription) + return nil + } + + guard let type = resolvedType else { + diagnoseUnsupportedType(node: returnClause.type, type: returnClause.type.trimmedDescription) + return nil + } + returnType = type + } else { + returnType = .void + } + + let abiName = ABINameGenerator.generateABIName( + baseName: name, + namespace: namespace, + staticContext: nil, + operation: nil, + className: protocolName + ) + + guard let effects = collectEffects(signature: node.signature) else { + return nil + } + + return ExportedFunction( + name: name, + abiName: abiName, + parameters: parameters, + returnType: returnType, + effects: effects, + namespace: namespace, + staticContext: nil + ) + } + override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind { guard case .enumBody(_, let enumKey) = stateStack.current else { return .visitChildren @@ -1095,6 +1204,12 @@ public class ExportSwift { collector.exportedEnumByName[$0]! } ) + exportedProtocols.append( + contentsOf: collector.exportedProtocolNames.map { + collector.exportedProtocolByName[$0]! + } + ) + return collector.errors } @@ -1163,10 +1278,20 @@ public class ExportSwift { return primitiveType } + let protocolKey = typeName + if let protocolName = exportedProtocolNameByKey[protocolKey] { + return .swiftProtocol(protocolName) + } + guard let typeDecl = typeDeclResolver.resolve(type) else { return nil } + if typeDecl.is(ProtocolDeclSyntax.self) { + let swiftCallName = ExportSwift.computeSwiftCallName(for: typeDecl, itemName: typeDecl.name.text) + return .swiftProtocol(swiftCallName) + } + if let enumDecl = typeDecl.as(EnumDeclSyntax.self) { let swiftCallName = ExportSwift.computeSwiftCallName(for: enumDecl, itemName: enumDecl.name.text) let rawTypeString = enumDecl.inheritanceClause?.inheritedTypes.first { inheritedType in @@ -1220,11 +1345,18 @@ public class ExportSwift { func renderSwiftGlue() throws -> String? { var decls: [DeclSyntax] = [] - guard exportedFunctions.count > 0 || exportedClasses.count > 0 || exportedEnums.count > 0 else { + guard + exportedFunctions.count > 0 || exportedClasses.count > 0 || exportedEnums.count > 0 + || exportedProtocols.count > 0 + else { return nil } decls.append(Self.prelude) + for proto in exportedProtocols { + decls.append(try renderProtocolWrapper(protocol: proto)) + } + for enumDef in exportedEnums { switch enumDef.enumType { case .simple: @@ -1343,7 +1475,26 @@ public class ExportSwift { if returnType == .void { return CodeBlockItemSyntax(item: .init(ExpressionStmtSyntax(expression: callExpr))) } else { - return CodeBlockItemSyntax(item: .init(DeclSyntax("let ret = \(raw: callExpr)"))) + switch returnType { + case .swiftProtocol(let protocolName): + let wrapperName = "Any\(protocolName)" + return CodeBlockItemSyntax( + item: .init(DeclSyntax("let ret = \(raw: callExpr) as! \(raw: wrapperName)")) + ) + case .optional(let wrappedType): + if case .swiftProtocol(let protocolName) = wrappedType { + let wrapperName = "Any\(protocolName)" + return CodeBlockItemSyntax( + item: .init( + DeclSyntax("let ret = (\(raw: callExpr)).flatMap { $0 as? \(raw: wrapperName) }") + ) + ) + } else { + return CodeBlockItemSyntax(item: .init(DeclSyntax("let ret = \(raw: callExpr)"))) + } + default: + return CodeBlockItemSyntax(item: .init(DeclSyntax("let ret = \(raw: callExpr)"))) + } } } @@ -1356,7 +1507,20 @@ public class ExportSwift { if returnType == .void { append("\(raw: name)") } else { - append("let ret = \(raw: name)") + switch returnType { + case .swiftProtocol(let protocolName): + let wrapperName = "Any\(protocolName)" + append("let ret = \(raw: name) as! \(raw: wrapperName)") + case .optional(let wrappedType): + if case .swiftProtocol(let protocolName) = wrappedType { + let wrapperName = "Any\(protocolName)" + append("let ret = \(raw: name).flatMap { $0 as? \(raw: wrapperName) }") + } else { + append("let ret = \(raw: name)") + } + default: + append("let ret = \(raw: name)") + } } } @@ -1374,7 +1538,20 @@ public class ExportSwift { if returnType == .void { append("\(raw: selfExpr).\(raw: propertyName)") } else { - append("let ret = \(raw: selfExpr).\(raw: propertyName)") + switch returnType { + case .swiftProtocol(let protocolName): + let wrapperName = "Any\(protocolName)" + append("let ret = \(raw: selfExpr).\(raw: propertyName) as! \(raw: wrapperName)") + case .optional(let wrappedType): + if case .swiftProtocol(let protocolName) = wrappedType { + let wrapperName = "Any\(protocolName)" + append("let ret = \(raw: selfExpr).\(raw: propertyName).flatMap { $0 as? \(raw: wrapperName) }") + } else { + append("let ret = \(raw: selfExpr).\(raw: propertyName)") + } + default: + append("let ret = \(raw: selfExpr).\(raw: propertyName)") + } } } @@ -1930,6 +2107,90 @@ public class ExportSwift { } """ } + + /// Generates an AnyProtocol wrapper struct for a protocol + /// + /// Creates a struct that wraps a JSObject and implements protocol methods + /// by calling `@_extern(wasm)` functions that forward to JavaScript + func renderProtocolWrapper(protocol proto: ExportedProtocol) throws -> DeclSyntax { + let wrapperName = "Any\(proto.name)" + let protocolName = proto.name + + var methodDecls: [DeclSyntax] = [] + + for method in proto.methods { + var swiftParams: [String] = [] + for param in method.parameters { + let label = param.label ?? param.name + if label == param.name { + swiftParams.append("\(param.name): \(param.type.swiftType)") + } else { + swiftParams.append("\(label) \(param.name): \(param.type.swiftType)") + } + } + + var externParams: [String] = ["this: Int32"] + for param in method.parameters { + let loweringInfo = try param.type.loweringParameterInfo() + assert( + loweringInfo.loweredParameters.count == 1, + "Protocol parameters must lower to a single WASM type" + ) + let (_, wasmType) = loweringInfo.loweredParameters[0] + externParams.append("\(param.name): \(wasmType.swiftType)") + } + + var callArgs: [String] = ["this: Int32(bitPattern: jsObject.id)"] + for param in method.parameters { + callArgs.append("\(param.name): \(param.name).bridgeJSLowerParameter()") + } + + let returnTypeStr: String + let externReturnType: String + let callCode: DeclSyntax + + if method.returnType == .void { + returnTypeStr = "" + externReturnType = "" + callCode = """ + _extern_\(raw: method.name)(\(raw: callArgs.joined(separator: ", "))) + """ + } else { + returnTypeStr = " -> \(method.returnType.swiftType)" + let liftingInfo = try method.returnType.liftingReturnInfo() + if let abiType = liftingInfo.valueToLift { + externReturnType = " -> \(abiType.swiftType)" + } else { + externReturnType = "" + } + callCode = """ + let ret = _extern_\(raw: method.name)(\(raw: callArgs.joined(separator: ", "))) + return \(raw: method.returnType.swiftType).bridgeJSLiftReturn(ret) + """ + } + let methodImplementation: DeclSyntax = """ + func \(raw: method.name)(\(raw: swiftParams.joined(separator: ", ")))\(raw: returnTypeStr) { + @_extern(wasm, module: "\(raw: moduleName)", name: "\(raw: method.abiName)") + func _extern_\(raw: method.name)(\(raw: externParams.joined(separator: ", ")))\(raw: externReturnType) + \(raw: callCode) + } + """ + + methodDecls.append(methodImplementation) + } + + return """ + struct \(raw: wrapperName): \(raw: protocolName), _BridgedSwiftProtocolWrapper { + let jsObject: JSObject + + \(raw: methodDecls.map { $0.description }.joined(separator: "\n\n")) + + static func bridgeJSLiftParameter(_ value: Int32) -> Self { + return \(raw: wrapperName)(jsObject: JSObject(id: UInt32(bitPattern: value))) + } + } + """ + } } fileprivate enum Constants { @@ -1994,6 +2255,7 @@ extension BridgeType { case .jsObject(nil): return "JSObject" case .jsObject(let name?): return name case .swiftHeapObject(let name): return name + case .swiftProtocol(let name): return "Any\(name)" case .void: return "Void" case .optional(let wrappedType): return "Optional<\(wrappedType.swiftType)>" case .caseEnum(let name): return name @@ -2029,6 +2291,7 @@ extension BridgeType { case .string: return .string case .jsObject: return .jsObject case .swiftHeapObject: return .swiftHeapObject + case .swiftProtocol: return .jsObject case .void: return .void case .optional(let wrappedType): var optionalParams: [(name: String, type: WasmCoreType)] = [("isSome", .i32)] @@ -2081,6 +2344,7 @@ extension BridgeType { case .string: return .string case .jsObject: return .jsObject case .swiftHeapObject: return .swiftHeapObject + case .swiftProtocol: return .jsObject case .void: return .void case .optional: return .optional case .caseEnum: return .caseEnum diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index abacddd0..64e89d66 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -435,6 +435,8 @@ extension BridgeType { case .void: return .void case .swiftHeapObject: throw BridgeJSCoreError("swiftHeapObject is not supported in imported signatures") + case .swiftProtocol: + throw BridgeJSCoreError("swiftProtocol is not supported in imported signatures") case .caseEnum, .rawValueEnum, .associatedValueEnum, .namespaceEnum: throw BridgeJSCoreError("Enum types are not yet supported in TypeScript imports") case .optional: @@ -465,6 +467,8 @@ extension BridgeType { case .void: return .void case .swiftHeapObject: throw BridgeJSCoreError("swiftHeapObject is not supported in imported signatures") + case .swiftProtocol: + throw BridgeJSCoreError("swiftProtocol is not supported in imported signatures") case .caseEnum, .rawValueEnum, .associatedValueEnum, .namespaceEnum: throw BridgeJSCoreError("Enum types are not yet supported in TypeScript imports") case .optional: diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 087cf17d..88f2af3d 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -240,7 +240,9 @@ struct BridgeJSLink { enumPropertyPrinter.write("},") if !property.isReadonly { - let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false)) + let setterThunkBuilder = ExportedThunkBuilder( + effects: Effects(isAsync: false, isThrows: false) + ) try setterThunkBuilder.lowerParameter( param: Parameter(label: "value", name: "value", type: property.type) ) @@ -300,6 +302,29 @@ struct BridgeJSLink { data.importObjectBuilders.append(importObjectBuilder) } + for skeleton in exportedSkeletons { + if !skeleton.protocols.isEmpty { + let importObjectBuilder: ImportObjectBuilder + if let existingBuilder = data.importObjectBuilders.first(where: { $0.moduleName == skeleton.moduleName } + ) { + importObjectBuilder = existingBuilder + } else { + importObjectBuilder = ImportObjectBuilder(moduleName: skeleton.moduleName) + data.importObjectBuilders.append(importObjectBuilder) + } + + for proto in skeleton.protocols { + for method in proto.methods { + try renderProtocolMethod( + importObjectBuilder: importObjectBuilder, + protocol: proto, + method: method + ) + } + } + } + } + return data } @@ -572,6 +597,22 @@ struct BridgeJSLink { """ let printer = CodeFragmentPrinter(header: header) printer.nextLine() + + for skeleton in exportedSkeletons { + for proto in skeleton.protocols { + printer.write("export interface \(proto.name) {") + printer.indent { + for method in proto.methods { + printer.write( + "\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));" + ) + } + } + printer.write("}") + printer.nextLine() + } + } + printer.write(lines: data.topLevelDtsEnumLines) // Generate Object types for const-style enums @@ -1468,7 +1509,9 @@ extension BridgeJSLink { if !property.isReadonly { // Generate setter - let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false)) + let setterThunkBuilder = ExportedThunkBuilder( + effects: Effects(isAsync: false, isThrows: false) + ) try setterThunkBuilder.lowerParameter( param: Parameter(label: "value", name: "value", type: property.type) ) @@ -1539,7 +1582,9 @@ extension BridgeJSLink { if !property.isReadonly { // Generate setter - let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false)) + let setterThunkBuilder = ExportedThunkBuilder( + effects: Effects(isAsync: false, isThrows: false) + ) try setterThunkBuilder.lowerParameter( param: Parameter(label: "value", name: "value", type: property.type) ) @@ -1704,7 +1749,9 @@ extension BridgeJSLink { // Generate static property setter if not readonly if !property.isReadonly { - let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false)) + let setterThunkBuilder = ExportedThunkBuilder( + effects: Effects(isAsync: false, isThrows: false) + ) try setterThunkBuilder.lowerParameter( param: Parameter(label: "value", name: "value", type: property.type) ) @@ -1752,7 +1799,9 @@ extension BridgeJSLink { // Generate instance property setter if not readonly if !property.isReadonly { - let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false)) + let setterThunkBuilder = ExportedThunkBuilder( + effects: Effects(isAsync: false, isThrows: false) + ) setterThunkBuilder.lowerSelf() try setterThunkBuilder.lowerParameter( param: Parameter(label: "value", name: "value", type: property.type) @@ -2367,6 +2416,25 @@ extension BridgeJSLink { ) return (funcLines, []) } + + func renderProtocolMethod( + importObjectBuilder: ImportObjectBuilder, + protocol: ExportedProtocol, + method: ExportedFunction + ) throws { + let thunkBuilder = ImportedThunkBuilder() + thunkBuilder.liftSelf() + for param in method.parameters { + try thunkBuilder.liftParameter(param: param) + } + let returnExpr = try thunkBuilder.callMethod(name: method.name, returnType: method.returnType) + let funcLines = thunkBuilder.renderFunction( + name: method.abiName, + returnExpr: returnExpr, + returnType: method.returnType + ) + importObjectBuilder.assignToImportObject(name: method.abiName, function: funcLines) + } } struct BridgeJSLinkError: Error { @@ -2402,6 +2470,8 @@ extension BridgeType { return "\(name)Tag" case .namespaceEnum(let name): return name + case .swiftProtocol(let name): + return name } } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index 9d0d9162..ef431432 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -297,6 +297,25 @@ struct IntrinsicJSFragment: Sendable { switch wrappedType { case .swiftHeapObject: return ["+\(isSomeVar)", "\(isSomeVar) ? \(value).pointer : 0"] + case .swiftProtocol: + return [ + "+\(isSomeVar)", + "\(isSomeVar) ? \(JSGlueVariableScope.reservedSwift).memory.retain(\(value)) : 0", + ] + case .jsObject: + let idVar = scope.variable("id") + printer.write("let \(idVar);") + printer.write("if (\(isSomeVar)) {") + printer.indent { + printer.write("\(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(value));") + } + printer.write("}") + cleanupCode.write("if (\(idVar) !== undefined) {") + cleanupCode.indent { + cleanupCode.write("\(JSGlueVariableScope.reservedSwift).memory.release(\(idVar));") + } + cleanupCode.write("}") + return ["+\(isSomeVar)", "\(isSomeVar) ? \(idVar) : 0"] default: return ["+\(isSomeVar)", "\(isSomeVar) ? \(value) : 0"] } @@ -326,6 +345,9 @@ struct IntrinsicJSFragment: Sendable { case .string: printer.write("const \(resultVar) = \(JSGlueVariableScope.reservedStorageToReturnString);") printer.write("\(JSGlueVariableScope.reservedStorageToReturnString) = undefined;") + case .jsObject, .swiftProtocol: + printer.write("const \(resultVar) = \(JSGlueVariableScope.reservedStorageToReturnString);") + printer.write("\(JSGlueVariableScope.reservedStorageToReturnString) = undefined;") case .swiftHeapObject(let className): let pointerVar = scope.variable("pointer") printer.write( @@ -397,6 +419,7 @@ struct IntrinsicJSFragment: Sendable { case .jsObject: return .jsObjectLowerParameter case .swiftHeapObject: return .swiftHeapObjectLowerParameter + case .swiftProtocol: return .jsObjectLowerParameter case .void: return .void case .optional(let wrappedType): return try .optionalLowerParameter(wrappedType: wrappedType) @@ -422,6 +445,7 @@ struct IntrinsicJSFragment: Sendable { case .string: return .stringLiftReturn case .jsObject: return .jsObjectLiftReturn case .swiftHeapObject(let name): return .swiftHeapObjectLiftReturn(name) + case .swiftProtocol: return .jsObjectLiftReturn case .void: return .void case .optional(let wrappedType): return .optionalLiftReturn(wrappedType: wrappedType) case .caseEnum: return .identity @@ -455,6 +479,7 @@ struct IntrinsicJSFragment: Sendable { message: "Swift heap objects are not supported to be passed as parameters to imported JS functions: \(name)" ) + case .swiftProtocol: return .jsObjectLiftParameter case .void: throw BridgeJSLinkError( message: "Void can't appear in parameters of imported JS functions" @@ -494,6 +519,7 @@ struct IntrinsicJSFragment: Sendable { throw BridgeJSLinkError( message: "Swift heap objects are not supported to be returned from imported JS functions" ) + case .swiftProtocol: return .jsObjectLowerReturn case .void: return .void case .optional(let wrappedType): throw BridgeJSLinkError( diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 84595782..1a414b95 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -74,6 +74,7 @@ public enum BridgeType: Codable, Equatable, Sendable { case rawValueEnum(String, SwiftEnumRawType) case associatedValueEnum(String) case namespaceEnum(String) + case swiftProtocol(String) } public enum WasmCoreType: String, Codable, Sendable { @@ -269,6 +270,18 @@ public enum EnumType: String, Codable, Sendable { // MARK: - Exported Skeleton +public struct ExportedProtocol: Codable, Equatable { + public let name: String + public let methods: [ExportedFunction] + public let namespace: [String]? + + public init(name: String, methods: [ExportedFunction], namespace: [String]? = nil) { + self.name = name + self.methods = methods + self.namespace = namespace + } +} + public struct ExportedFunction: Codable, Equatable, Sendable { public var name: String public var abiName: String @@ -407,12 +420,20 @@ public struct ExportedSkeleton: Codable { public let functions: [ExportedFunction] public let classes: [ExportedClass] public let enums: [ExportedEnum] + public let protocols: [ExportedProtocol] - public init(moduleName: String, functions: [ExportedFunction], classes: [ExportedClass], enums: [ExportedEnum]) { + public init( + moduleName: String, + functions: [ExportedFunction], + classes: [ExportedClass], + enums: [ExportedEnum], + protocols: [ExportedProtocol] = [] + ) { self.moduleName = moduleName self.functions = functions self.classes = classes self.enums = enums + self.protocols = protocols } } @@ -514,6 +535,9 @@ extension BridgeType { return nil case .namespaceEnum: return nil + case .swiftProtocol: + // Protocols pass JSObject IDs as Int32 + return .i32 } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Protocol.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Protocol.swift new file mode 100644 index 00000000..f0213369 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Protocol.swift @@ -0,0 +1,41 @@ +import JavaScriptKit + +@JS protocol MyViewControllerDelegate { + func onSomethingHappened() + func onValueChanged(_ value: String) + func onCountUpdated(count: Int) -> Bool + func onLabelUpdated(_ prefix: String, _ suffix: String) + func isCountEven() -> Bool +} + +@JS class MyViewController { + @JS + var delegate: MyViewControllerDelegate + + @JS + var secondDelegate: MyViewControllerDelegate? + + @JS init(delegate: MyViewControllerDelegate) { + self.delegate = delegate + } + + @JS func triggerEvent() { + delegate.onSomethingHappened() + } + + @JS func updateValue(_ value: String) { + delegate.onValueChanged(value) + } + + @JS func updateCount(_ count: Int) -> Bool { + return delegate.onCountUpdated(count: count) + } + + @JS func updateLabel(_ prefix: String, _ suffix: String) { + delegate.onLabelUpdated(prefix, suffix) + } + + @JS func checkEvenCount() -> Bool { + return delegate.isCountEven() + } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.Export.d.ts new file mode 100644 index 00000000..0f5d0fb2 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.Export.d.ts @@ -0,0 +1,44 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export interface MyViewControllerDelegate { + onSomethingHappened(): void; + onValueChanged(value: string): void; + onCountUpdated(count: number): boolean; + onLabelUpdated(prefix: string, suffix: string): void; + isCountEven(): boolean; +} + +/// Represents a Swift heap object like a class instance or an actor instance. +export interface SwiftHeapObject { + /// Release the heap object. + /// + /// Note: Calling this method will release the heap object and it will no longer be accessible. + release(): void; +} +export interface MyViewController extends SwiftHeapObject { + triggerEvent(): void; + updateValue(value: string): void; + updateCount(count: number): boolean; + updateLabel(prefix: string, suffix: string): void; + checkEvenCount(): boolean; + delegate: MyViewControllerDelegate; + secondDelegate: MyViewControllerDelegate | null; +} +export type Exports = { + MyViewController: { + new(delegate: MyViewControllerDelegate): MyViewController; + } +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.Export.js new file mode 100644 index 00000000..142c1cf0 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.Export.js @@ -0,0 +1,286 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let tmpRetTag; + let tmpRetStrings = []; + let tmpRetInts = []; + let tmpRetF32s = []; + let tmpRetF64s = []; + let tmpParamInts = []; + let tmpParamF32s = []; + let tmpParamF64s = []; + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + const bjs = {}; + importObject["bjs"] = bjs; + const imports = options.getImports(importsContext); + bjs["swift_js_return_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + return swift.memory.retain(textDecoder.decode(bytes)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_tag"] = function(tag) { + tmpRetTag = tag; + } + bjs["swift_js_push_int"] = function(v) { + tmpRetInts.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + tmpRetF32s.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + tmpRetF64s.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + const value = textDecoder.decode(bytes); + tmpRetStrings.push(value); + } + bjs["swift_js_pop_param_int32"] = function() { + return tmpParamInts.pop(); + } + bjs["swift_js_pop_param_f32"] = function() { + return tmpParamF32s.pop(); + } + bjs["swift_js_pop_param_f64"] = function() { + return tmpParamF64s.pop(); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_MyViewController_wrap"] = function(pointer) { + const obj = MyViewController.__construct(pointer); + return swift.memory.retain(obj); + }; + const TestModule = importObject["TestModule"] = importObject["TestModule"] || {}; + TestModule["bjs_MyViewControllerDelegate_onSomethingHappened"] = function bjs_MyViewControllerDelegate_onSomethingHappened(self) { + try { + swift.memory.getObject(self).onSomethingHappened(); + } catch (error) { + setException(error); + } + } + TestModule["bjs_MyViewControllerDelegate_onValueChanged"] = function bjs_MyViewControllerDelegate_onValueChanged(self, value) { + try { + const valueObject = swift.memory.getObject(value); + swift.memory.release(value); + swift.memory.getObject(self).onValueChanged(valueObject); + } catch (error) { + setException(error); + } + } + TestModule["bjs_MyViewControllerDelegate_onCountUpdated"] = function bjs_MyViewControllerDelegate_onCountUpdated(self, count) { + try { + let ret = swift.memory.getObject(self).onCountUpdated(count); + return ret ? 1 : 0; + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_MyViewControllerDelegate_onLabelUpdated"] = function bjs_MyViewControllerDelegate_onLabelUpdated(self, prefix, suffix) { + try { + const prefixObject = swift.memory.getObject(prefix); + swift.memory.release(prefix); + const suffixObject = swift.memory.getObject(suffix); + swift.memory.release(suffix); + swift.memory.getObject(self).onLabelUpdated(prefixObject, suffixObject); + } catch (error) { + setException(error); + } + } + TestModule["bjs_MyViewControllerDelegate_isCountEven"] = function bjs_MyViewControllerDelegate_isCountEven(self) { + try { + let ret = swift.memory.getObject(self).isCountEven(); + return ret ? 1 : 0; + } catch (error) { + setException(error); + return 0 + } + } + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype) { + const obj = Object.create(prototype); + obj.pointer = pointer; + obj.hasReleased = false; + obj.deinit = deinit; + obj.registry = new FinalizationRegistry((pointer) => { + deinit(pointer); + }); + obj.registry.register(this, obj.pointer); + return obj; + } + + release() { + this.registry.unregister(this); + this.deinit(this.pointer); + } + } + class MyViewController extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_MyViewController_deinit, MyViewController.prototype); + } + + constructor(delegate) { + const ret = instance.exports.bjs_MyViewController_init(swift.memory.retain(delegate)); + return MyViewController.__construct(ret); + } + triggerEvent() { + instance.exports.bjs_MyViewController_triggerEvent(this.pointer); + } + updateValue(value) { + const valueBytes = textEncoder.encode(value); + const valueId = swift.memory.retain(valueBytes); + instance.exports.bjs_MyViewController_updateValue(this.pointer, valueId, valueBytes.length); + swift.memory.release(valueId); + } + updateCount(count) { + const ret = instance.exports.bjs_MyViewController_updateCount(this.pointer, count); + return ret !== 0; + } + updateLabel(prefix, suffix) { + const prefixBytes = textEncoder.encode(prefix); + const prefixId = swift.memory.retain(prefixBytes); + const suffixBytes = textEncoder.encode(suffix); + const suffixId = swift.memory.retain(suffixBytes); + instance.exports.bjs_MyViewController_updateLabel(this.pointer, prefixId, prefixBytes.length, suffixId, suffixBytes.length); + swift.memory.release(prefixId); + swift.memory.release(suffixId); + } + checkEvenCount() { + const ret = instance.exports.bjs_MyViewController_checkEvenCount(this.pointer); + return ret !== 0; + } + get delegate() { + const ret = instance.exports.bjs_MyViewController_delegate_get(this.pointer); + const ret1 = swift.memory.getObject(ret); + swift.memory.release(ret); + return ret1; + } + set delegate(value) { + instance.exports.bjs_MyViewController_delegate_set(this.pointer, swift.memory.retain(value)); + } + get secondDelegate() { + instance.exports.bjs_MyViewController_secondDelegate_get(this.pointer); + const optResult = tmpRetString; + tmpRetString = undefined; + return optResult; + } + set secondDelegate(value) { + const isSome = value != null; + instance.exports.bjs_MyViewController_secondDelegate_set(this.pointer, +isSome, isSome ? swift.memory.retain(value) : 0); + } + } + return { + MyViewController, + }; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Async.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Async.json index b9f86357..41696d63 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Async.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Async.json @@ -174,5 +174,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json index edfd4e57..05d71b5f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/DefaultParameters.json @@ -642,5 +642,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumAssociatedValue.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumAssociatedValue.json index 6a9cf04c..c75e7d85 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumAssociatedValue.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumAssociatedValue.json @@ -782,5 +782,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json index 1184b4b0..825b4105 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumCase.json @@ -306,5 +306,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json index 673cfccc..0b94c1c5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumNamespace.json @@ -396,5 +396,8 @@ "functions" : [ ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json index 334c8f05..b5da1726 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/EnumRawType.json @@ -1476,5 +1476,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json index 87684f13..dc613452 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Namespaces.json @@ -172,5 +172,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Optionals.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Optionals.json index aef4e890..104b11d5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Optionals.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Optionals.json @@ -689,5 +689,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json index 10638ebf..e54d0ad5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json @@ -59,5 +59,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json index b1f89765..99c5aff0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json @@ -75,5 +75,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PropertyTypes.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PropertyTypes.json index 8de3a12d..94641103 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PropertyTypes.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PropertyTypes.json @@ -350,5 +350,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Protocol.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Protocol.json new file mode 100644 index 00000000..77327c45 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Protocol.json @@ -0,0 +1,305 @@ +{ + "classes" : [ + { + "constructor" : { + "abiName" : "bjs_MyViewController_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "delegate", + "name" : "delegate", + "type" : { + "swiftProtocol" : { + "_0" : "MyViewControllerDelegate" + } + } + } + ] + }, + "methods" : [ + { + "abiName" : "bjs_MyViewController_triggerEvent", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "triggerEvent", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_MyViewController_updateValue", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "updateValue", + "parameters" : [ + { + "label" : "_", + "name" : "value", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_MyViewController_updateCount", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "updateCount", + "parameters" : [ + { + "label" : "_", + "name" : "count", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "bool" : { + + } + } + }, + { + "abiName" : "bjs_MyViewController_updateLabel", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "updateLabel", + "parameters" : [ + { + "label" : "_", + "name" : "prefix", + "type" : { + "string" : { + + } + } + }, + { + "label" : "_", + "name" : "suffix", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_MyViewController_checkEvenCount", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "checkEvenCount", + "parameters" : [ + + ], + "returnType" : { + "bool" : { + + } + } + } + ], + "name" : "MyViewController", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "delegate", + "type" : { + "swiftProtocol" : { + "_0" : "MyViewControllerDelegate" + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "secondDelegate", + "type" : { + "optional" : { + "_0" : { + "swiftProtocol" : { + "_0" : "MyViewControllerDelegate" + } + } + } + } + } + ], + "swiftCallName" : "MyViewController" + } + ], + "enums" : [ + + ], + "functions" : [ + + ], + "moduleName" : "TestModule", + "protocols" : [ + { + "methods" : [ + { + "abiName" : "bjs_MyViewControllerDelegate_onSomethingHappened", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "onSomethingHappened", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_MyViewControllerDelegate_onValueChanged", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "onValueChanged", + "parameters" : [ + { + "label" : "_", + "name" : "value", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_MyViewControllerDelegate_onCountUpdated", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "onCountUpdated", + "parameters" : [ + { + "label" : "count", + "name" : "count", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "bool" : { + + } + } + }, + { + "abiName" : "bjs_MyViewControllerDelegate_onLabelUpdated", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "onLabelUpdated", + "parameters" : [ + { + "label" : "_", + "name" : "prefix", + "type" : { + "string" : { + + } + } + }, + { + "label" : "_", + "name" : "suffix", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_MyViewControllerDelegate_isCountEven", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "isCountEven", + "parameters" : [ + + ], + "returnType" : { + "bool" : { + + } + } + } + ], + "name" : "MyViewControllerDelegate" + } + ] +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Protocol.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Protocol.swift new file mode 100644 index 00000000..6ac515c7 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Protocol.swift @@ -0,0 +1,174 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(BridgeJS) import JavaScriptKit + +struct AnyMyViewControllerDelegate: MyViewControllerDelegate, _BridgedSwiftProtocolWrapper { + let jsObject: JSObject + + func onSomethingHappened() { + @_extern(wasm, module: "TestModule", name: "bjs_MyViewControllerDelegate_onSomethingHappened") + func _extern_onSomethingHappened(this: Int32) + _extern_onSomethingHappened(this: Int32(bitPattern: jsObject.id)) + } + + func onValueChanged(_ value: String) { + @_extern(wasm, module: "TestModule", name: "bjs_MyViewControllerDelegate_onValueChanged") + func _extern_onValueChanged(this: Int32, value: Int32) + _extern_onValueChanged(this: Int32(bitPattern: jsObject.id), value: value.bridgeJSLowerParameter()) + } + + func onCountUpdated(count: Int) -> Bool { + @_extern(wasm, module: "TestModule", name: "bjs_MyViewControllerDelegate_onCountUpdated") + func _extern_onCountUpdated(this: Int32, count: Int32) -> Int32 + let ret = _extern_onCountUpdated(this: Int32(bitPattern: jsObject.id), count: count.bridgeJSLowerParameter()) + return Bool.bridgeJSLiftReturn(ret) + } + + func onLabelUpdated(_ prefix: String, _ suffix: String) { + @_extern(wasm, module: "TestModule", name: "bjs_MyViewControllerDelegate_onLabelUpdated") + func _extern_onLabelUpdated(this: Int32, prefix: Int32, suffix: Int32) + _extern_onLabelUpdated(this: Int32(bitPattern: jsObject.id), prefix: prefix.bridgeJSLowerParameter(), suffix: suffix.bridgeJSLowerParameter()) + } + + func isCountEven() -> Bool { + @_extern(wasm, module: "TestModule", name: "bjs_MyViewControllerDelegate_isCountEven") + func _extern_isCountEven(this: Int32) -> Int32 + let ret = _extern_isCountEven(this: Int32(bitPattern: jsObject.id)) + return Bool.bridgeJSLiftReturn(ret) + } + + static func bridgeJSLiftParameter(_ value: Int32) -> Self { + return AnyMyViewControllerDelegate(jsObject: JSObject(id: UInt32(bitPattern: value))) + } +} + +@_expose(wasm, "bjs_MyViewController_init") +@_cdecl("bjs_MyViewController_init") +public func _bjs_MyViewController_init(delegate: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = MyViewController(delegate: AnyMyViewControllerDelegate.bridgeJSLiftParameter(delegate)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_triggerEvent") +@_cdecl("bjs_MyViewController_triggerEvent") +public func _bjs_MyViewController_triggerEvent(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + MyViewController.bridgeJSLiftParameter(_self).triggerEvent() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_updateValue") +@_cdecl("bjs_MyViewController_updateValue") +public func _bjs_MyViewController_updateValue(_self: UnsafeMutableRawPointer, valueBytes: Int32, valueLength: Int32) -> Void { + #if arch(wasm32) + MyViewController.bridgeJSLiftParameter(_self).updateValue(_: String.bridgeJSLiftParameter(valueBytes, valueLength)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_updateCount") +@_cdecl("bjs_MyViewController_updateCount") +public func _bjs_MyViewController_updateCount(_self: UnsafeMutableRawPointer, count: Int32) -> Int32 { + #if arch(wasm32) + let ret = MyViewController.bridgeJSLiftParameter(_self).updateCount(_: Int.bridgeJSLiftParameter(count)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_updateLabel") +@_cdecl("bjs_MyViewController_updateLabel") +public func _bjs_MyViewController_updateLabel(_self: UnsafeMutableRawPointer, prefixBytes: Int32, prefixLength: Int32, suffixBytes: Int32, suffixLength: Int32) -> Void { + #if arch(wasm32) + MyViewController.bridgeJSLiftParameter(_self).updateLabel(_: String.bridgeJSLiftParameter(prefixBytes, prefixLength), _: String.bridgeJSLiftParameter(suffixBytes, suffixLength)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_checkEvenCount") +@_cdecl("bjs_MyViewController_checkEvenCount") +public func _bjs_MyViewController_checkEvenCount(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = MyViewController.bridgeJSLiftParameter(_self).checkEvenCount() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_delegate_get") +@_cdecl("bjs_MyViewController_delegate_get") +public func _bjs_MyViewController_delegate_get(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = MyViewController.bridgeJSLiftParameter(_self).delegate as! AnyMyViewControllerDelegate + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_delegate_set") +@_cdecl("bjs_MyViewController_delegate_set") +public func _bjs_MyViewController_delegate_set(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + MyViewController.bridgeJSLiftParameter(_self).delegate = AnyMyViewControllerDelegate.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_secondDelegate_get") +@_cdecl("bjs_MyViewController_secondDelegate_get") +public func _bjs_MyViewController_secondDelegate_get(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = MyViewController.bridgeJSLiftParameter(_self).secondDelegate.flatMap { + $0 as? AnyMyViewControllerDelegate + } + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_secondDelegate_set") +@_cdecl("bjs_MyViewController_secondDelegate_set") +public func _bjs_MyViewController_secondDelegate_set(_self: UnsafeMutableRawPointer, valueIsSome: Int32, valueValue: Int32) -> Void { + #if arch(wasm32) + MyViewController.bridgeJSLiftParameter(_self).secondDelegate = Optional.bridgeJSLiftParameter(valueIsSome, valueValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_MyViewController_deinit") +@_cdecl("bjs_MyViewController_deinit") +public func _bjs_MyViewController_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension MyViewController: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + #if arch(wasm32) + @_extern(wasm, module: "TestModule", name: "bjs_MyViewController_wrap") + func _bjs_MyViewController_wrap(_: UnsafeMutableRawPointer) -> Int32 + #else + func _bjs_MyViewController_wrap(_: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + return .object(JSObject(id: UInt32(bitPattern: _bjs_MyViewController_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StaticFunctions.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StaticFunctions.json index 8a73c1b8..cbd0fa2b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StaticFunctions.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StaticFunctions.json @@ -322,5 +322,8 @@ "functions" : [ ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StaticProperties.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StaticProperties.json index 4808e518..63de88b9 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StaticProperties.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StaticProperties.json @@ -326,5 +326,8 @@ "functions" : [ ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json index 55972aa1..cf6b0025 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json @@ -57,5 +57,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json index 5d727c85..f34a04f9 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json @@ -24,5 +24,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json index 59b7f9ef..fc99462b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json @@ -132,5 +132,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json index 51d548e6..d8533915 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json @@ -24,5 +24,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json index f3f7abaa..df3f5041 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json @@ -24,5 +24,8 @@ } } ], - "moduleName" : "TestModule" + "moduleName" : "TestModule", + "protocols" : [ + + ] } \ No newline at end of file diff --git a/Sources/JavaScriptKit/BridgeJSInstrincics.swift b/Sources/JavaScriptKit/BridgeJSInstrincics.swift index d8ddf8fd..4742aa62 100644 --- a/Sources/JavaScriptKit/BridgeJSInstrincics.swift +++ b/Sources/JavaScriptKit/BridgeJSInstrincics.swift @@ -301,6 +301,21 @@ extension _JSBridgedClass { @_spi(BridgeJS) public consuming func bridgeJSLowerReturn() -> Int32 { jsObject.bridgeJSLowerReturn() } } +/// A protocol that Swift protocol wrappers exposed from JavaScript must conform to. +/// +/// The conformance is automatically synthesized by the BridgeJS code generator. +public protocol _BridgedSwiftProtocolWrapper { + var jsObject: JSObject { get } + static func bridgeJSLiftParameter(_ value: Int32) -> Self +} + +extension _BridgedSwiftProtocolWrapper { + // MARK: ExportSwift + @_spi(BridgeJS) public consuming func bridgeJSLowerReturn() -> Int32 { + jsObject.bridgeJSLowerReturn() + } +} + /// 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. @@ -603,6 +618,56 @@ extension Optional where Wrapped == JSObject { } } +extension Optional where Wrapped: _BridgedSwiftProtocolWrapper { + // MARK: ImportTS + + @available( + *, + unavailable, + message: "Optional protocol types are not supported to be passed to imported JS functions" + ) + @_spi(BridgeJS) public func bridgeJSLowerParameter() -> Void {} + + @available( + *, + unavailable, + message: "Optional protocol types are not supported to be passed to imported JS functions" + ) + @_spi(BridgeJS) public static func bridgeJSLiftReturn(_ isSome: Int32, _ objectId: Int32) -> Wrapped? { + return nil + } + + // MARK: ExportSwift + + @_spi(BridgeJS) public static func bridgeJSLiftParameter(_ isSome: Int32, _ objectId: Int32) -> Wrapped? { + if isSome == 0 { + return nil + } else { + return Wrapped.bridgeJSLiftParameter(objectId) + } + } + + @_spi(BridgeJS) public consuming func bridgeJSLowerReturn() -> Void { + #if arch(wasm32) + @_extern(wasm, module: "bjs", name: "swift_js_return_optional_object") + func _swift_js_return_optional_object(_ isSome: Int32, _ objectId: Int32) + #else + /// Write an optional protocol wrapper to reserved storage to be returned to JavaScript + func _swift_js_return_optional_object(_ isSome: Int32, _ objectId: Int32) { + _onlyAvailableOnWasm() + } + #endif + + switch consume self { + case .none: + _swift_js_return_optional_object(0, 0) + case .some(let wrapper): + let retainedId = wrapper.bridgeJSLowerReturn() + _swift_js_return_optional_object(1, retainedId) + } + } +} + /// Optional support for Swift heap objects extension Optional where Wrapped: _BridgedSwiftHeapObject { // MARK: ImportTS diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md index 05be58fa..dbdfba78 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md @@ -66,6 +66,7 @@ This command will: - - - +- - - - diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md new file mode 100644 index 00000000..802ff3ae --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md @@ -0,0 +1,209 @@ +# Exporting Swift Protocols + +Learn how to expose Swift protocols to JavaScript as TypeScript interfaces. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +BridgeJS allows you to export Swift protocols as TypeScript interfaces. JavaScript objects implementing these interfaces can be passed to Swift code, enabling protocol-oriented design across the Swift-JavaScript boundary. + +When you mark a protocol with `@JS`, BridgeJS generates: + +- A TypeScript interface with the protocol's method signatures +- A Swift wrapper struct (`Any{ProtocolName}`) that conforms to the protocol and bridges calls to JavaScript objects + +## Example: Counter Protocol + +Mark a Swift protocol with `@JS` to expose it: + +```swift +import JavaScriptKit + +@JS protocol Counter { + func increment(by amount: Int) + func reset() + func getValue() -> Int +} + +@JS class CounterManager { + var delegate: Counter + + @JS init(delegate: Counter) { + self.delegate = delegate + } + + @JS func incrementTwice() { + delegate.increment(by: 1) + delegate.increment(by: 1) + } + + @JS func getCurrentValue() -> Int { + return delegate.getValue() + } +} +``` + +In JavaScript: + +```javascript +import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; +const { exports } = await init({}); + +// Implement the Counter protocol +const counterImpl = { + count: 0, + increment(amount) { + this.count += amount; + }, + reset() { + this.count = 0; + }, + getValue() { + return this.count; + } +}; + +// Pass the implementation to Swift +const manager = new exports.CounterManager(counterImpl); +manager.incrementTwice(); +console.log(manager.getCurrentValue()); // 2 +``` + +The generated TypeScript interface: + +```typescript +export interface Counter { + increment(amount: number): void; + reset(): void; + getValue(): number; +} + +export type Exports = { + CounterManager: { + new(delegate: Counter): CounterManager; + } +} +``` + +## Generated Wrapper + +BridgeJS generates a Swift wrapper struct for each `@JS` protocol. This wrapper holds a `JSObject` reference and forwards protocol method calls to the JavaScript implementation: + +```swift +struct AnyCounter: Counter, _BridgedSwiftProtocolWrapper { + let jsObject: JSObject + + func increment(by amount: Int) { + @_extern(wasm, module: "TestModule", name: "bjs_Counter_increment") + func _extern_increment(this: Int32, amount: Int32) + _extern_increment( + this: Int32(bitPattern: jsObject.id), + amount: amount.bridgeJSLowerParameter() + ) + } + + func reset() { + @_extern(wasm, module: "TestModule", name: "bjs_Counter_reset") + func _extern_reset(this: Int32) + _extern_reset(this: Int32(bitPattern: jsObject.id)) + } + + func getValue() -> Int { + @_extern(wasm, module: "TestModule", name: "bjs_Counter_getValue") + func _extern_getValue(this: Int32) -> Int32 + let ret = _extern_getValue(this: Int32(bitPattern: jsObject.id)) + return Int.bridgeJSLiftReturn(ret) + } + + static func bridgeJSLiftParameter(_ value: Int32) -> Self { + return AnyCounter(jsObject: JSObject(id: UInt32(bitPattern: value))) + } +} +``` + +## Swift Implementation + +You can also implement protocols in Swift and use them from JavaScript: + +```swift +@JS protocol Counter { + func increment(by amount: Int) + func reset() + func getValue() -> Int +} + +final class SwiftCounter: Counter { + private var count = 0 + + func increment(by amount: Int) { + count += amount + } + + func reset() { + count = 0 + } + + func getValue() -> Int { + return count + } +} + +@JS func createCounter() -> Counter { + return SwiftCounter() +} +``` + +From JavaScript: + +```javascript +const counter = exports.createCounter(); +counter.increment(5); +counter.increment(3); +console.log(counter.getValue()); // 8 +counter.reset(); +console.log(counter.getValue()); // 0 +``` + +## How It Works + +When you pass a JavaScript object implementing a protocol to Swift: + +1. **JavaScript Side**: The object is stored in JavaScriptKit's memory heap and its ID is passed as an `Int32` to Swift +2. **Swift Side**: BridgeJS creates an `Any{ProtocolName}` wrapper that holds a `JSObject` reference +3. **Method Calls**: Protocol method calls are forwarded through WASM to the JavaScript implementation +4. **Memory Management**: The `JSObject` reference keeps the JavaScript object alive using JavaScriptKit's retain/release system. When the Swift wrapper is deallocated, the JavaScript object is automatically released. + +## Supported Features + +| Swift Feature | Status | +|:--------------|:-------| +| Method requirements: `func method()` | ✅ | +| Method requirements with parameters | ✅ | +| Method requirements with return values | ✅ | +| Throwing method requirements: `func method() throws(JSException)` | ✅ | +| Async method requirements: `func method() async` | ✅ | +| Optional protocol methods | ❌ | +| Property requirements: `var property: Type { get }` | ❌ | +| Property requirements: `var property: Type { get set }` | ❌ | +| Associated types | ❌ | +| Protocol inheritance | ❌ | +| Protocol composition: `Protocol1 & Protocol2` | ❌ | +| Generics | ❌ | + +### Type Support for Protocol Method Parameters and Return Types + +Protocol method parameters and return values have more limited type support compared to regular exported Swift functions and classes. + +**Supported Types:** +- Primitives: `Bool`, `Int`, `Float`, `Double` +- `String` +- `JSObject` + +**Not Supported:** +- `@JS class` types +- `@JS enum` types (case, raw value, or associated value) +- `@JS protocol` types +- Optional types: `Int?`, `String?`, etc. + +> Note: For regular `@JS func` and `@JS class` exports (not within protocols), all these types including optionals, enums, and classes are fully supported. See and for more information. diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 3336801d..b6255a4e 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -857,6 +857,87 @@ enum APIOptionalResult { "const:\(StaticPropertyHolder.staticConstant),var:\(StaticPropertyHolder.staticVariable),computed:\(StaticPropertyHolder.computedProperty),readonly:\(StaticPropertyHolder.readOnlyComputed)" } +// MARK: - Protocol Tests + +@JS protocol Counter { + func increment(by amount: Int) + func getValue() -> Int + func setLabelElements(_ labelPrefix: String, _ labelSuffix: String) + func getLabel() -> String + func isEven() -> Bool +} + +@JS class CounterManager { + + @JS var counter: Counter + @JS var backupCounter: Counter? + + @JS init(counter: Counter) { + self.counter = counter + self.backupCounter = nil + } + + @JS func incrementByAmount(_ amount: Int) { + counter.increment(by: amount) + } + + @JS func setCounterLabel(_ prefix: String, _ suffix: String) { + counter.setLabelElements(prefix, suffix) + } + + @JS func isCounterEven() -> Bool { + return counter.isEven() + } + + @JS func getCounterLabel() -> String { + return counter.getLabel() + } + + @JS func getCurrentValue() -> Int { + return counter.getValue() + } + + @JS func incrementBoth() { + counter.increment(by: 1) + backupCounter?.increment(by: 1) + } + + @JS func getBackupValue() -> Int? { + return backupCounter?.getValue() + } + + @JS func hasBackup() -> Bool { + return backupCounter != nil + } +} + +@JS class SwiftCounter: Counter { + private var value: Int = 0 + private var label: String = "" + + @JS init() {} + + @JS func increment(by amount: Int) { + value += amount + } + + @JS func getValue() -> Int { + return value + } + + @JS func setLabelElements(_ labelPrefix: String, _ labelSuffix: String) { + self.label = labelPrefix + labelSuffix + } + + @JS func getLabel() -> String { + return label + } + + @JS func isEven() -> Bool { + return value % 2 == 0 + } +} + class ExportAPITests: XCTestCase { func testAll() { var hasDeinitGreeter = false diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift index 0eff4e0b..1e22c2b0 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift @@ -6,6 +6,47 @@ @_spi(BridgeJS) import JavaScriptKit +struct AnyCounter: Counter, _BridgedSwiftProtocolWrapper { + let jsObject: JSObject + + func increment(by amount: Int) { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_Counter_increment") + func _extern_increment(this: Int32, amount: Int32) + _extern_increment(this: Int32(bitPattern: jsObject.id), amount: amount.bridgeJSLowerParameter()) + } + + func getValue() -> Int { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_Counter_getValue") + func _extern_getValue(this: Int32) -> Int32 + let ret = _extern_getValue(this: Int32(bitPattern: jsObject.id)) + return Int.bridgeJSLiftReturn(ret) + } + + func setLabelElements(_ labelPrefix: String, _ labelSuffix: String) { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_Counter_setLabelElements") + func _extern_setLabelElements(this: Int32, labelPrefix: Int32, labelSuffix: Int32) + _extern_setLabelElements(this: Int32(bitPattern: jsObject.id), labelPrefix: labelPrefix.bridgeJSLowerParameter(), labelSuffix: labelSuffix.bridgeJSLowerParameter()) + } + + func getLabel() -> String { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_Counter_getLabel") + func _extern_getLabel(this: Int32) -> Int32 + let ret = _extern_getLabel(this: Int32(bitPattern: jsObject.id)) + return String.bridgeJSLiftReturn(ret) + } + + func isEven() -> Bool { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_Counter_isEven") + func _extern_isEven(this: Int32) -> Int32 + let ret = _extern_isEven(this: Int32(bitPattern: jsObject.id)) + return Bool.bridgeJSLiftReturn(ret) + } + + static func bridgeJSLiftParameter(_ value: Int32) -> Self { + return AnyCounter(jsObject: JSObject(id: UInt32(bitPattern: value))) + } +} + extension Direction: _BridgedSwiftCaseEnum { @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerParameter() -> Int32 { return bridgeJSRawValue @@ -3242,4 +3283,248 @@ extension StaticPropertyHolder: ConvertibleToJSValue, _BridgedSwiftHeapObject { #endif return .object(JSObject(id: UInt32(bitPattern: _bjs_StaticPropertyHolder_wrap(Unmanaged.passRetained(self).toOpaque())))) } +} + +@_expose(wasm, "bjs_CounterManager_init") +@_cdecl("bjs_CounterManager_init") +public func _bjs_CounterManager_init(counter: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = CounterManager(counter: AnyCounter.bridgeJSLiftParameter(counter)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_incrementByAmount") +@_cdecl("bjs_CounterManager_incrementByAmount") +public func _bjs_CounterManager_incrementByAmount(_self: UnsafeMutableRawPointer, amount: Int32) -> Void { + #if arch(wasm32) + CounterManager.bridgeJSLiftParameter(_self).incrementByAmount(_: Int.bridgeJSLiftParameter(amount)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_setCounterLabel") +@_cdecl("bjs_CounterManager_setCounterLabel") +public func _bjs_CounterManager_setCounterLabel(_self: UnsafeMutableRawPointer, prefixBytes: Int32, prefixLength: Int32, suffixBytes: Int32, suffixLength: Int32) -> Void { + #if arch(wasm32) + CounterManager.bridgeJSLiftParameter(_self).setCounterLabel(_: String.bridgeJSLiftParameter(prefixBytes, prefixLength), _: String.bridgeJSLiftParameter(suffixBytes, suffixLength)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_isCounterEven") +@_cdecl("bjs_CounterManager_isCounterEven") +public func _bjs_CounterManager_isCounterEven(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = CounterManager.bridgeJSLiftParameter(_self).isCounterEven() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_getCounterLabel") +@_cdecl("bjs_CounterManager_getCounterLabel") +public func _bjs_CounterManager_getCounterLabel(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = CounterManager.bridgeJSLiftParameter(_self).getCounterLabel() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_getCurrentValue") +@_cdecl("bjs_CounterManager_getCurrentValue") +public func _bjs_CounterManager_getCurrentValue(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = CounterManager.bridgeJSLiftParameter(_self).getCurrentValue() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_incrementBoth") +@_cdecl("bjs_CounterManager_incrementBoth") +public func _bjs_CounterManager_incrementBoth(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + CounterManager.bridgeJSLiftParameter(_self).incrementBoth() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_getBackupValue") +@_cdecl("bjs_CounterManager_getBackupValue") +public func _bjs_CounterManager_getBackupValue(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = CounterManager.bridgeJSLiftParameter(_self).getBackupValue() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_hasBackup") +@_cdecl("bjs_CounterManager_hasBackup") +public func _bjs_CounterManager_hasBackup(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = CounterManager.bridgeJSLiftParameter(_self).hasBackup() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_counter_get") +@_cdecl("bjs_CounterManager_counter_get") +public func _bjs_CounterManager_counter_get(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = CounterManager.bridgeJSLiftParameter(_self).counter as! AnyCounter + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_counter_set") +@_cdecl("bjs_CounterManager_counter_set") +public func _bjs_CounterManager_counter_set(_self: UnsafeMutableRawPointer, value: Int32) -> Void { + #if arch(wasm32) + CounterManager.bridgeJSLiftParameter(_self).counter = AnyCounter.bridgeJSLiftParameter(value) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_backupCounter_get") +@_cdecl("bjs_CounterManager_backupCounter_get") +public func _bjs_CounterManager_backupCounter_get(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = CounterManager.bridgeJSLiftParameter(_self).backupCounter.flatMap { + $0 as? AnyCounter + } + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_backupCounter_set") +@_cdecl("bjs_CounterManager_backupCounter_set") +public func _bjs_CounterManager_backupCounter_set(_self: UnsafeMutableRawPointer, valueIsSome: Int32, valueValue: Int32) -> Void { + #if arch(wasm32) + CounterManager.bridgeJSLiftParameter(_self).backupCounter = Optional.bridgeJSLiftParameter(valueIsSome, valueValue) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_CounterManager_deinit") +@_cdecl("bjs_CounterManager_deinit") +public func _bjs_CounterManager_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension CounterManager: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_CounterManager_wrap") + func _bjs_CounterManager_wrap(_: UnsafeMutableRawPointer) -> Int32 + #else + func _bjs_CounterManager_wrap(_: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + return .object(JSObject(id: UInt32(bitPattern: _bjs_CounterManager_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + +@_expose(wasm, "bjs_SwiftCounter_init") +@_cdecl("bjs_SwiftCounter_init") +public func _bjs_SwiftCounter_init() -> UnsafeMutableRawPointer { + #if arch(wasm32) + let ret = SwiftCounter() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SwiftCounter_increment") +@_cdecl("bjs_SwiftCounter_increment") +public func _bjs_SwiftCounter_increment(_self: UnsafeMutableRawPointer, amount: Int32) -> Void { + #if arch(wasm32) + SwiftCounter.bridgeJSLiftParameter(_self).increment(by: Int.bridgeJSLiftParameter(amount)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SwiftCounter_getValue") +@_cdecl("bjs_SwiftCounter_getValue") +public func _bjs_SwiftCounter_getValue(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = SwiftCounter.bridgeJSLiftParameter(_self).getValue() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SwiftCounter_setLabelElements") +@_cdecl("bjs_SwiftCounter_setLabelElements") +public func _bjs_SwiftCounter_setLabelElements(_self: UnsafeMutableRawPointer, labelPrefixBytes: Int32, labelPrefixLength: Int32, labelSuffixBytes: Int32, labelSuffixLength: Int32) -> Void { + #if arch(wasm32) + SwiftCounter.bridgeJSLiftParameter(_self).setLabelElements(_: String.bridgeJSLiftParameter(labelPrefixBytes, labelPrefixLength), _: String.bridgeJSLiftParameter(labelSuffixBytes, labelSuffixLength)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SwiftCounter_getLabel") +@_cdecl("bjs_SwiftCounter_getLabel") +public func _bjs_SwiftCounter_getLabel(_self: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let ret = SwiftCounter.bridgeJSLiftParameter(_self).getLabel() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SwiftCounter_isEven") +@_cdecl("bjs_SwiftCounter_isEven") +public func _bjs_SwiftCounter_isEven(_self: UnsafeMutableRawPointer) -> Int32 { + #if arch(wasm32) + let ret = SwiftCounter.bridgeJSLiftParameter(_self).isEven() + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_SwiftCounter_deinit") +@_cdecl("bjs_SwiftCounter_deinit") +public func _bjs_SwiftCounter_deinit(pointer: UnsafeMutableRawPointer) { + Unmanaged.fromOpaque(pointer).release() +} + +extension SwiftCounter: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_SwiftCounter_wrap") + func _bjs_SwiftCounter_wrap(_: UnsafeMutableRawPointer) -> Int32 + #else + func _bjs_SwiftCounter_wrap(_: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + return .object(JSObject(id: UInt32(bitPattern: _bjs_SwiftCounter_wrap(Unmanaged.passRetained(self).toOpaque())))) + } } \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json index 9af2ee95..a6be6754 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.ExportSwift.json @@ -1162,6 +1162,352 @@ } ], "swiftCallName" : "StaticPropertyHolder" + }, + { + "constructor" : { + "abiName" : "bjs_CounterManager_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "counter", + "name" : "counter", + "type" : { + "swiftProtocol" : { + "_0" : "Counter" + } + } + } + ] + }, + "methods" : [ + { + "abiName" : "bjs_CounterManager_incrementByAmount", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "incrementByAmount", + "parameters" : [ + { + "label" : "_", + "name" : "amount", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_CounterManager_setCounterLabel", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "setCounterLabel", + "parameters" : [ + { + "label" : "_", + "name" : "prefix", + "type" : { + "string" : { + + } + } + }, + { + "label" : "_", + "name" : "suffix", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_CounterManager_isCounterEven", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "isCounterEven", + "parameters" : [ + + ], + "returnType" : { + "bool" : { + + } + } + }, + { + "abiName" : "bjs_CounterManager_getCounterLabel", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getCounterLabel", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_CounterManager_getCurrentValue", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getCurrentValue", + "parameters" : [ + + ], + "returnType" : { + "int" : { + + } + } + }, + { + "abiName" : "bjs_CounterManager_incrementBoth", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "incrementBoth", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_CounterManager_getBackupValue", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getBackupValue", + "parameters" : [ + + ], + "returnType" : { + "optional" : { + "_0" : { + "int" : { + + } + } + } + } + }, + { + "abiName" : "bjs_CounterManager_hasBackup", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "hasBackup", + "parameters" : [ + + ], + "returnType" : { + "bool" : { + + } + } + } + ], + "name" : "CounterManager", + "properties" : [ + { + "isReadonly" : false, + "isStatic" : false, + "name" : "counter", + "type" : { + "swiftProtocol" : { + "_0" : "Counter" + } + } + }, + { + "isReadonly" : false, + "isStatic" : false, + "name" : "backupCounter", + "type" : { + "optional" : { + "_0" : { + "swiftProtocol" : { + "_0" : "Counter" + } + } + } + } + } + ], + "swiftCallName" : "CounterManager" + }, + { + "constructor" : { + "abiName" : "bjs_SwiftCounter_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + + ] + }, + "methods" : [ + { + "abiName" : "bjs_SwiftCounter_increment", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "increment", + "parameters" : [ + { + "label" : "by", + "name" : "amount", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_SwiftCounter_getValue", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getValue", + "parameters" : [ + + ], + "returnType" : { + "int" : { + + } + } + }, + { + "abiName" : "bjs_SwiftCounter_setLabelElements", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "setLabelElements", + "parameters" : [ + { + "label" : "_", + "name" : "labelPrefix", + "type" : { + "string" : { + + } + } + }, + { + "label" : "_", + "name" : "labelSuffix", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_SwiftCounter_getLabel", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getLabel", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_SwiftCounter_isEven", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "isEven", + "parameters" : [ + + ], + "returnType" : { + "bool" : { + + } + } + } + ], + "name" : "SwiftCounter", + "properties" : [ + + ], + "swiftCallName" : "SwiftCounter" } ], "enums" : [ @@ -5435,5 +5781,122 @@ } } ], - "moduleName" : "BridgeJSRuntimeTests" + "moduleName" : "BridgeJSRuntimeTests", + "protocols" : [ + { + "methods" : [ + { + "abiName" : "bjs_Counter_increment", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "increment", + "parameters" : [ + { + "label" : "by", + "name" : "amount", + "type" : { + "int" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_Counter_getValue", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getValue", + "parameters" : [ + + ], + "returnType" : { + "int" : { + + } + } + }, + { + "abiName" : "bjs_Counter_setLabelElements", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "setLabelElements", + "parameters" : [ + { + "label" : "_", + "name" : "labelPrefix", + "type" : { + "string" : { + + } + } + }, + { + "label" : "_", + "name" : "labelSuffix", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_Counter_getLabel", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "getLabel", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_Counter_isEven", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "isEven", + "parameters" : [ + + ], + "returnType" : { + "bool" : { + + } + } + } + ], + "name" : "Counter" + } + ] } \ No newline at end of file diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 7f561027..e2d0c980 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -683,6 +683,7 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(exports.testComplexInit(), "Hello, DefaultGreeter!"); const customGreeter = new exports.Greeter("CustomName"); assert.equal(exports.testComplexInit(customGreeter), "Hello, CustomName!"); + customGreeter.release(); const cd1 = new exports.ConstructorDefaults(); @@ -704,6 +705,8 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { const cd5 = new exports.ConstructorDefaults("Test", 99, false, exports.Status.Loading); assert.equal(cd5.describe(), "Test:99:false:loading:nil"); cd5.release(); + + testProtocolSupport(exports); } /** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports */ @@ -802,4 +805,104 @@ function setupTestGlobals(global) { sym: Symbol("s"), bi: BigInt(3) }; -} \ No newline at end of file +} + +/** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports */ +function testProtocolSupport(exports) { + let counterValue = 0; + let counterLabel = ""; + const jsCounter = { + increment(amount) { counterValue += amount; }, + getValue() { return counterValue; }, + setLabelElements(labelPrefix, labelSuffix) { counterLabel = labelPrefix + labelSuffix; }, + getLabel() { return counterLabel; }, + isEven() { return counterValue % 2 === 0; } + }; + + const manager = new exports.CounterManager(jsCounter); + manager.incrementByAmount(4); + assert.equal(manager.getCurrentValue(), 4); + + manager.setCounterLabel("Test", "Label"); + assert.equal(manager.getCounterLabel(), "TestLabel"); + assert.equal(jsCounter.getLabel(), "TestLabel"); + + assert.equal(manager.isCounterEven(), true); + manager.incrementByAmount(1); + assert.equal(manager.isCounterEven(), false); + assert.equal(jsCounter.isEven(), false); + + jsCounter.increment(3); + assert.equal(jsCounter.getValue(), 8); + manager.release(); + + const swiftCounter = new exports.SwiftCounter(); + const swiftManager = new exports.CounterManager(swiftCounter); + + swiftManager.incrementByAmount(10); + assert.equal(swiftManager.getCurrentValue(), 10); + + swiftManager.setCounterLabel("Swift", "Label"); + assert.equal(swiftManager.getCounterLabel(), "SwiftLabel"); + + swiftCounter.increment(5); + assert.equal(swiftCounter.getValue(), 15); + swiftManager.release(); + swiftCounter.release(); + + let optionalCounterValue = 100; + let optionalCounterLabel = "optional"; + const optionalCounter = { + increment(amount) { optionalCounterValue += amount; }, + getValue() { return optionalCounterValue; }, + setLabelElements(labelPrefix, labelSuffix) { optionalCounterLabel = labelPrefix + labelSuffix; }, + getLabel() { return optionalCounterLabel; }, + isEven() { return optionalCounterValue % 2 === 0; } + }; + + let mainCounterValue = 0; + let mainCounterLabel = "main"; + const mainCounter = { + increment(amount) { mainCounterValue += amount; }, + getValue() { return mainCounterValue; }, + setLabelElements(labelPrefix, labelSuffix) { mainCounterLabel = labelPrefix + labelSuffix; }, + getLabel() { return mainCounterLabel; }, + isEven() { return mainCounterValue % 2 === 0; } + }; + + const managerWithOptional = new exports.CounterManager(mainCounter); + + assert.equal(managerWithOptional.backupCounter, null); + assert.equal(managerWithOptional.hasBackup(), false); + assert.equal(managerWithOptional.getBackupValue(), null); + + managerWithOptional.backupCounter = optionalCounter; + assert.notEqual(managerWithOptional.backupCounter, null); + assert.equal(managerWithOptional.hasBackup(), true); + + managerWithOptional.incrementBoth(); + assert.equal(managerWithOptional.getCurrentValue(), 1); + assert.equal(managerWithOptional.getBackupValue(), 101); + + managerWithOptional.incrementBoth(); + assert.equal(managerWithOptional.getCurrentValue(), 2); + assert.equal(managerWithOptional.getBackupValue(), 102); + + managerWithOptional.backupCounter = null; + assert.equal(managerWithOptional.backupCounter, null); + assert.equal(managerWithOptional.hasBackup(), false); + + managerWithOptional.incrementBoth(); + assert.equal(managerWithOptional.getCurrentValue(), 3); + assert.equal(managerWithOptional.getBackupValue(), null); + + const swiftBackupCounter = new exports.SwiftCounter(); + swiftBackupCounter.increment(1); + managerWithOptional.backupCounter = swiftBackupCounter; + + assert.equal(managerWithOptional.hasBackup(), true); + assert.equal(managerWithOptional.getBackupValue(), 1); + + managerWithOptional.release(); + swiftBackupCounter.release(); +}