diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift new file mode 100644 index 000000000..ed55d0398 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public class ConcreteProtocolAB: ProtocolA, ProtocolB { + public let constantA: Int64 + public let constantB: Int64 + public var mutable: Int64 = 0 + + public func name() -> String { + return "ConcreteProtocolAB" + } + + public init(constantA: Int64, constantB: Int64) { + self.constantA = constantA + self.constantB = constantB + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift new file mode 100644 index 000000000..d5281b81e --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public protocol ProtocolA { + var constantA: Int64 { get } + var mutable: Int64 { get set } + + func name() -> String +} + +public func takeProtocol(_ proto1: any ProtocolA, _ proto2: some ProtocolA) -> Int64 { + return proto1.constantA + proto2.constantA +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift new file mode 100644 index 000000000..70d075c25 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public protocol ProtocolB { + var constantB: Int64 { get } +} + +public func takeCombinedProtocol(_ proto: some ProtocolA & ProtocolB) -> Int64 { + return proto.constantA + proto.constantB +} + +public func takeGenericProtocol(_ proto1: First, _ proto2: Second) -> Int64 { + return proto1.constantA + proto2.constantB +} + +public func takeCombinedGenericProtocol(_ proto: T) -> Int64 { + return proto.constantA + proto.constantB +} diff --git a/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java similarity index 100% rename from Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java rename to Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java index 6be85c75d..5bf059515 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.JNISwiftInstance; import org.swift.swiftkit.core.SwiftArena; import java.util.Optional; diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java new file mode 100644 index 000000000..c095a42a4 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class ProtocolTest { + @Test + void takeProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ConcreteProtocolAB proto1 = ConcreteProtocolAB.init(10, 5, arena); + ConcreteProtocolAB proto2 = ConcreteProtocolAB.init(20, 1, arena); + assertEquals(30, MySwiftLibrary.takeProtocol(proto1, proto2)); + } + } + + @Test + void takeCombinedProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ConcreteProtocolAB proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals(15, MySwiftLibrary.takeCombinedProtocol(proto1)); + } + } + + @Test + void takeGenericProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ConcreteProtocolAB proto1 = ConcreteProtocolAB.init(10, 5, arena); + ConcreteProtocolAB proto2 = ConcreteProtocolAB.init(20, 1, arena); + assertEquals(11, MySwiftLibrary.takeGenericProtocol(proto1, proto2)); + } + } + + @Test + void takeCombinedGenericProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ConcreteProtocolAB proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals(15, MySwiftLibrary.takeCombinedGenericProtocol(proto1)); + } + } + + @Test + void protocolVariables() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolA proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals(10, proto1.getConstantA()); + assertEquals(0, proto1.getMutable()); + proto1.setMutable(3); + assertEquals(3, proto1.getMutable()); + } + } + + @Test + void protocolMethod() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolA proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals("ConcreteProtocolAB", proto1.name()); + } + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift index 3719d99d6..8a58f42ab 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift @@ -95,7 +95,11 @@ extension DeclModifierSyntax { } extension WithModifiersSyntax { - var isPublic: Bool { + func isPublic(in type: NominalTypeDeclSyntaxNode?) -> Bool { + if let type, case .protocolDecl(let protocolDecl) = Syntax(type).as(SyntaxEnum.self) { + return protocolDecl.isPublic(in: nil) + } + return self.modifiers.contains { modifier in modifier.isPublic } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index d2a71defd..193b545f9 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -68,7 +68,7 @@ extension CType { case .optional(let wrapped) where wrapped.isPointer: try self.init(cdeclType: wrapped) - case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential: + case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite: throw CDeclToCLoweringError.invalidCDeclType(cdeclType) } } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 0a61708e6..5dfa5b71e 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -344,6 +344,9 @@ struct CdeclLowering { case .optional(let wrapped): return try lowerOptionalParameter(wrapped, convention: convention, parameterName: parameterName, genericParameters: genericParameters, genericRequirements: genericRequirements) + + case .composite: + throw LoweringError.unhandledType(type) } } @@ -412,7 +415,7 @@ struct CdeclLowering { } throw LoweringError.unhandledType(.optional(wrappedType)) - case .function, .metatype, .optional: + case .function, .metatype, .optional, .composite: throw LoweringError.unhandledType(.optional(wrappedType)) } } @@ -513,7 +516,7 @@ struct CdeclLowering { // Custom types are not supported yet. throw LoweringError.unhandledType(type) - case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque: + case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite: // TODO: Implement throw LoweringError.unhandledType(type) } @@ -667,7 +670,7 @@ struct CdeclLowering { conversion: .tupleExplode(conversions, name: outParameterName) ) - case .genericParameter, .function, .optional, .existential, .opaque: + case .genericParameter, .function, .optional, .existential, .opaque, .composite: throw LoweringError.unhandledType(type) } } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 84a902755..188f5e8bb 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -396,9 +396,12 @@ extension FFMSwift2JavaGenerator { // Indirect return receivers. for outParameter in translatedSignature.result.outParameters { - let memoryLayout = renderMemoryLayoutValue(for: outParameter.type) + guard case .concrete(let type) = outParameter.type else { + continue + } + let memoryLayout = renderMemoryLayoutValue(for: type) - let arena = if let className = outParameter.type.className, + let arena = if let className = type.className, analysis.importedTypes[className] != nil { // Use passed-in 'SwiftArena' for 'SwiftValue'. "swiftArena$" diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 00fa60e74..438393cb3 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -472,6 +472,9 @@ extension FFMSwift2JavaGenerator { genericParameters: genericParameters, genericRequirements: genericRequirements ) + + case .composite: + throw JavaTranslationError.unhandledType(swiftType) } } @@ -691,7 +694,7 @@ extension FFMSwift2JavaGenerator { // TODO: Implement. throw JavaTranslationError.unhandledType(swiftType) - case .genericParameter, .optional, .function, .existential, .opaque: + case .genericParameter, .optional, .function, .existential, .opaque, .composite: throw JavaTranslationError.unhandledType(swiftType) } diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index 5655af006..7a071d9a5 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -34,9 +34,13 @@ package class ImportedNominalType: ImportedDecl { package var methods: [ImportedFunc] = [] package var variables: [ImportedFunc] = [] package var cases: [ImportedEnumCase] = [] + var inheritedTypes: [SwiftType] - init(swiftNominal: SwiftNominalTypeDeclaration) { + init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { self.swiftNominal = swiftNominal + self.inheritedTypes = swiftNominal.inheritanceTypes?.compactMap { + try? SwiftType($0.type, lookupContext: lookupContext) + } ?? [] } var swiftType: SwiftType { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 9351252e5..258b537e0 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -22,6 +22,7 @@ extension JNISwift2JavaGenerator { "org.swift.swiftkit.core.*", "org.swift.swiftkit.core.util.*", "java.util.*", + "java.util.concurrent.atomic.AtomicBoolean", // NonNull, Unsigned and friends "org.swift.swiftkit.core.annotations.*", @@ -101,6 +102,35 @@ extension JNISwift2JavaGenerator { printPackage(&printer) printImports(&printer) + switch decl.swiftNominal.kind { + case .actor, .class, .enum, .struct: + printConcreteType(&printer, decl) + case .protocol: + printProtocol(&printer, decl) + } + } + + private func printProtocol(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + let extends = ["JNISwiftInstance"] + printer.printBraceBlock("public interface \(decl.swiftNominal.name) extends \(extends.joined(separator: ", "))") { printer in + for initializer in decl.initializers { + printFunctionDowncallMethods(&printer, initializer, signaturesOnly: true) + printer.println() + } + + for method in decl.methods { + printFunctionDowncallMethods(&printer, method, signaturesOnly: true) + printer.println() + } + + for variable in decl.variables { + printFunctionDowncallMethods(&printer, variable, signaturesOnly: true) + printer.println() + } + } + } + + private func printConcreteType(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { printNominal(&printer, decl) { printer in printer.print( """ @@ -117,8 +147,18 @@ extension JNISwift2JavaGenerator { printer.print( """ + /** + * The designated constructor of any imported Swift types. + * + * @param selfPointer a pointer to the memory containing the value + * @param swiftArena the arena this object belongs to. When the arena goes out of scope, this value is destroyed. + */ private \(decl.swiftNominal.name)(long selfPointer, SwiftArena swiftArena) { - super(selfPointer, swiftArena); + SwiftObjects.requireNonZero(selfPointer, "selfPointer"); + this.selfPointer = selfPointer; + + // Only register once we have fully initialized the object since this will need the object pointer. + swiftArena.register(this); } /** @@ -136,6 +176,25 @@ extension JNISwift2JavaGenerator { """ ) + printer.print( + """ + /** Pointer to the "self". */ + private final long selfPointer; + + /** Used to track additional state of the underlying object, e.g. if it was explicitly destroyed. */ + private final AtomicBoolean $state$destroyed = new AtomicBoolean(false); + + public long $memoryAddress() { + return this.selfPointer; + } + + @Override + public AtomicBoolean $statusDestroyedFlag() { + return $state$destroyed; + } + """ + ) + printer.println() if decl.swiftNominal.kind == .enum { @@ -158,6 +217,8 @@ extension JNISwift2JavaGenerator { printer.println() } + printTypeMetadataAddressFunction(&printer, decl) + printer.println() printDestroyFunction(&printer, decl) } } @@ -194,7 +255,13 @@ extension JNISwift2JavaGenerator { if decl.swiftNominal.isSendable { printer.print("@ThreadSafe // Sendable") } - printer.printBraceBlock("public final class \(decl.swiftNominal.name) extends JNISwiftInstance") { printer in + var implements = ["JNISwiftInstance"] + implements += decl.inheritedTypes + .compactMap(\.asNominalTypeDeclaration) + .filter { $0.kind == .protocol } + .map(\.name) + let implementsClause = implements.joined(separator: ", ") + printer.printBraceBlock("public final class \(decl.swiftNominal.name) implements \(implementsClause)") { printer in body(&printer) } } @@ -279,14 +346,15 @@ extension JNISwift2JavaGenerator { printer.print("record $NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") } - self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction) + self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, signaturesOnly: false) printer.println() } } private func printFunctionDowncallMethods( _ printer: inout CodePrinter, - _ decl: ImportedFunc + _ decl: ImportedFunc, + signaturesOnly: Bool = false ) { guard translatedDecl(for: decl) != nil else { // Failed to translate. Skip. @@ -297,7 +365,7 @@ extension JNISwift2JavaGenerator { printJavaBindingWrapperHelperClass(&printer, decl) - printJavaBindingWrapperMethod(&printer, decl) + printJavaBindingWrapperMethod(&printer, decl, signaturesOnly: signaturesOnly) } /// Print the helper type container for a user-facing Java API. @@ -340,17 +408,18 @@ extension JNISwift2JavaGenerator { ) } - private func printJavaBindingWrapperMethod(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + private func printJavaBindingWrapperMethod(_ printer: inout CodePrinter, _ decl: ImportedFunc, signaturesOnly: Bool) { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") } - printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl) + printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, signaturesOnly: signaturesOnly) } private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, - importedFunc: ImportedFunc? = nil + importedFunc: ImportedFunc? = nil, + signaturesOnly: Bool ) { var modifiers = ["public"] if translatedDecl.isStatic { @@ -362,6 +431,19 @@ extension JNISwift2JavaGenerator { var parameters = translatedDecl.translatedFunctionSignature.parameters.map { $0.parameter.renderParameter() } let throwsClause = translatedDecl.isThrowing ? " throws Exception" : "" + let generics = translatedDecl.translatedFunctionSignature.parameters.reduce(into: [(String, [JavaType])]()) { generics, parameter in + guard case .generic(let name, let extends) = parameter.parameter.type else { + return + } + generics.append((name, extends)) + } + .map { "\($0) extends \($1.compactMap(\.className).joined(separator: " & "))" } + .joined(separator: ", ") + + if !generics.isEmpty { + modifiers.append("<" + generics + ">") + } + var annotationsStr = translatedSignature.annotations.map({ $0.render() }).joined(separator: "\n") if !annotationsStr.isEmpty { annotationsStr += "\n" } @@ -372,9 +454,7 @@ extension JNISwift2JavaGenerator { if let importedFunc { printDeclDocumentation(&printer, importedFunc) } - printer.printBraceBlock( - "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)" - ) { printer in + printer.printBraceBlock("\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)") { printer in let globalArenaName = "SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA" let arguments = translatedDecl.translatedFunctionSignature.parameters.map(\.parameter.name) + [globalArenaName] let call = "\(translatedDecl.name)(\(arguments.joined(separator: ", ")))" @@ -393,13 +473,17 @@ extension JNISwift2JavaGenerator { if let importedFunc { printDeclDocumentation(&printer, importedFunc) } - printer.printBraceBlock( - "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)" - ) { printer in - printDowncall(&printer, translatedDecl) + let signature = "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)" + if signaturesOnly { + printer.print("\(signature);") + } else { + printer.printBraceBlock(signature) { printer in + printDowncall(&printer, translatedDecl) + } + + printNativeFunction(&printer, translatedDecl) } - printNativeFunction(&printer, translatedDecl) } private func printNativeFunction(_ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl) { @@ -470,13 +554,33 @@ extension JNISwift2JavaGenerator { ) } + private func printTypeMetadataAddressFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + printer.print("private static native long $typeMetadataAddressDowncall();") + + let funcName = "$typeMetadataAddress" + printer.print("@Override") + printer.printBraceBlock("public long $typeMetadataAddress()") { printer in + printer.print( + """ + long self$ = this.$memoryAddress(); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("\(type.swiftNominal.name).\(funcName)", + "this", this, + "self", self$); + } + return \(type.swiftNominal.name).$typeMetadataAddressDowncall(); + """ + ) + } + } + /// Prints the destroy function for a `JNISwiftInstance` private func printDestroyFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) { printer.print("private static native void $destroy(long selfPointer);") let funcName = "$createDestroyFunction" printer.print("@Override") - printer.printBraceBlock("protected Runnable \(funcName)()") { printer in + printer.printBraceBlock("public Runnable \(funcName)()") { printer in printer.print( """ long self$ = this.$memoryAddress(); diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 073883a2c..a0178d3c7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -29,7 +29,8 @@ extension JNISwift2JavaGenerator { config: config, swiftModuleName: swiftModuleName, javaPackage: self.javaPackage, - javaClassLookupTable: self.javaClassLookupTable + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) ) translated = try translation.translate(decl) } catch { @@ -54,7 +55,8 @@ extension JNISwift2JavaGenerator { config: config, swiftModuleName: swiftModuleName, javaPackage: self.javaPackage, - javaClassLookupTable: self.javaClassLookupTable + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) ) translated = try translation.translate(enumCase: decl) } catch { @@ -71,12 +73,14 @@ extension JNISwift2JavaGenerator { let swiftModuleName: String let javaPackage: String let javaClassLookupTable: JavaClassLookupTable + var knownTypes: SwiftKnownTypes func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { let nativeTranslation = NativeJavaTranslation( config: self.config, javaPackage: self.javaPackage, - javaClassLookupTable: self.javaClassLookupTable + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: self.knownTypes ) let methodName = "" // TODO: Used for closures, replace with better name? @@ -85,7 +89,9 @@ extension JNISwift2JavaGenerator { let translatedValues = try self.translateParameters( enumCase.parameters.map { ($0.name, $0.type) }, methodName: methodName, - parentName: parentName + parentName: parentName, + genericParameters: [], + genericRequirements: [] ) let conversions = try enumCase.parameters.enumerated().map { idx, parameter in @@ -160,7 +166,8 @@ extension JNISwift2JavaGenerator { let nativeTranslation = NativeJavaTranslation( config: self.config, javaPackage: self.javaPackage, - javaClassLookupTable: self.javaClassLookupTable + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: self.knownTypes ) // Types with no parent will be outputted inside a "module" class. @@ -228,7 +235,15 @@ extension JNISwift2JavaGenerator { for (i, param) in swiftType.parameters.enumerated() { let paramName = param.parameterName ?? "_\(i)" translatedParams.append( - try translateParameter(swiftType: param.type, parameterName: paramName, methodName: name, parentName: parentName) + try translateParameter( + swiftType: param.type, + parameterName: paramName, + methodName: name, + parentName: parentName, + genericParameters: [], + genericRequirements: [], + parameterPosition: nil + ) ) } @@ -250,11 +265,19 @@ extension JNISwift2JavaGenerator { let parameters = try translateParameters( functionSignature.parameters.map { ($0.parameterName, $0.type )}, methodName: methodName, - parentName: parentName + parentName: parentName, + genericParameters: functionSignature.genericParameters, + genericRequirements: functionSignature.genericRequirements ) // 'self' - let selfParameter = try self.translateSelfParameter(functionSignature.selfParameter, methodName: methodName, parentName: parentName) + let selfParameter = try self.translateSelfParameter( + functionSignature.selfParameter, + methodName: methodName, + parentName: parentName, + genericParameters: functionSignature.genericParameters, + genericRequirements: functionSignature.genericRequirements + ) let resultType = try translate(swiftResult: functionSignature.result) @@ -268,22 +291,43 @@ extension JNISwift2JavaGenerator { func translateParameters( _ parameters: [(name: String?, type: SwiftType)], methodName: String, - parentName: String + parentName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] ) throws -> [TranslatedParameter] { - try parameters.enumerated().map { idx, param in + try parameters.enumerated().map { + idx, + param in let parameterName = param.name ?? "arg\(idx)" - return try translateParameter(swiftType: param.type, parameterName: parameterName, methodName: methodName, parentName: parentName) + return try translateParameter( + swiftType: param.type, + parameterName: parameterName, + methodName: methodName, + parentName: parentName, + genericParameters: genericParameters, + genericRequirements: genericRequirements, + parameterPosition: idx + ) } } - func translateSelfParameter(_ selfParameter: SwiftSelfParameter?, methodName: String, parentName: String) throws -> TranslatedParameter? { + func translateSelfParameter( + _ selfParameter: SwiftSelfParameter?, + methodName: String, + parentName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> TranslatedParameter? { // 'self' if case .instance(let swiftSelf) = selfParameter { return try self.translateParameter( swiftType: swiftSelf.type, parameterName: swiftSelf.parameterName ?? "self", methodName: methodName, - parentName: parentName + parentName: parentName, + genericParameters: genericParameters, + genericRequirements: genericRequirements, + parameterPosition: nil ) } else { return nil @@ -294,7 +338,10 @@ extension JNISwift2JavaGenerator { swiftType: SwiftType, parameterName: String, methodName: String, - parentName: String + parentName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement], + parameterPosition: Int? ) throws -> TranslatedParameter { // If the result type should cause any annotations on the method, include them here. @@ -354,7 +401,7 @@ extension JNISwift2JavaGenerator { return TranslatedParameter( parameter: JavaParameter( name: parameterName, - type: .class(package: nil, name: nominalTypeName), + type: .concrete(.class(package: nil, name: nominalTypeName)), annotations: parameterAnnotations ), conversion: .valueMemoryAddress(.placeholder) @@ -382,7 +429,29 @@ extension JNISwift2JavaGenerator { parameterName: parameterName ) - case .metatype, .tuple, .existential, .opaque, .genericParameter: + case .opaque(let proto), .existential(let proto): + guard let parameterPosition else { + fatalError("Cannot extract opaque or existential type that is not a parameter: \(proto)") + } + + return try translateProtocolParameter( + protocolType: proto, + parameterName: parameterName, + javaGenericName: "$T\(parameterPosition)" + ) + + case .genericParameter(let generic): + if let concreteTy = swiftType.typeIn(genericParameters: genericParameters, genericRequirements: genericRequirements) { + return try translateProtocolParameter( + protocolType: concreteTy, + parameterName: parameterName, + javaGenericName: generic.name + ) + } + + throw JavaTranslationError.unsupportedSwiftType(swiftType) + + case .metatype, .tuple, .composite: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -401,6 +470,50 @@ extension JNISwift2JavaGenerator { } } + func translateProtocolParameter( + protocolType: SwiftType, + parameterName: String, + javaGenericName: String + ) throws -> TranslatedParameter { + switch protocolType { + case .nominal: + return try translateProtocolParameter(protocolTypes: [protocolType], parameterName: parameterName, javaGenericName: javaGenericName) + + case .composite(let types): + return try translateProtocolParameter(protocolTypes: types, parameterName: parameterName, javaGenericName: javaGenericName) + + default: + throw JavaTranslationError.unsupportedSwiftType(protocolType) + } + } + + private func translateProtocolParameter( + protocolTypes: [SwiftType], + parameterName: String, + javaGenericName: String + ) throws -> TranslatedParameter { + let javaProtocolTypes = try protocolTypes.map { + switch $0 { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + return JavaType.class(package: nil, name: nominalTypeName) + + default: + throw JavaTranslationError.unsupportedSwiftType($0) + } + } + + // We assume this is a JExtract class. + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .generic(name: javaGenericName, extends: javaProtocolTypes), + annotations: [] + ), + conversion: .commaSeparated([.valueMemoryAddress(.placeholder), .typeMetadataAddress(.placeholder)]) + ) + } + func translateOptionalParameter( wrappedType swiftType: SwiftType, parameterName: String @@ -519,7 +632,7 @@ extension JNISwift2JavaGenerator { case .optional(let wrapped): return try translateOptionalResult(wrappedType: wrapped, resultName: resultName) - case .metatype, .tuple, .function, .existential, .opaque, .genericParameter: + case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -743,6 +856,9 @@ extension JNISwift2JavaGenerator { /// `value.$memoryAddress()` indirect case valueMemoryAddress(JavaNativeConversionStep) + /// `value.$typeMetadataAddress()` + indirect case typeMetadataAddress(JavaNativeConversionStep) + /// Call `new \(Type)(\(placeholder), swiftArena$)` indirect case constructSwiftValue(JavaNativeConversionStep, JavaType) @@ -816,6 +932,10 @@ extension JNISwift2JavaGenerator { case .valueMemoryAddress: return "\(placeholder).$memoryAddress()" + case .typeMetadataAddress(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner).$typeMetadataAddress()" + case .constructSwiftValue(let inner, let javaType): let inner = inner.render(&printer, placeholder) return "new \(javaType.className!)(\(inner), swiftArena$)" @@ -922,6 +1042,9 @@ extension JNISwift2JavaGenerator { case .valueMemoryAddress(let inner): return inner.requiresSwiftArena + case .typeMetadataAddress(let inner): + return inner.requiresSwiftArena + case .commaSeparated(let list): return list.contains(where: { $0.requiresSwiftArena }) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 03273e684..bdbfe2f18 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -21,6 +21,7 @@ extension JNISwift2JavaGenerator { let config: Configuration let javaPackage: String let javaClassLookupTable: JavaClassLookupTable + var knownTypes: SwiftKnownTypes /// Translates a Swift function into the native JNI method signature. func translate( @@ -33,22 +34,26 @@ extension JNISwift2JavaGenerator { translatedParameter, swiftParameter in let parameterName = translatedParameter.parameter.name - return try translate( - swiftParameter: swiftParameter, + return try translateParameter( + type: swiftParameter.type, parameterName: parameterName, methodName: methodName, - parentName: parentName + parentName: parentName, + genericParameters: functionSignature.genericParameters, + genericRequirements: functionSignature.genericRequirements ) } // Lower the self parameter. let nativeSelf: NativeParameter? = switch functionSignature.selfParameter { case .instance(let selfParameter): - try translate( - swiftParameter: selfParameter, + try translateParameter( + type: selfParameter.type, parameterName: selfParameter.parameterName ?? "self", methodName: methodName, - parentName: parentName + parentName: parentName, + genericParameters: functionSignature.genericParameters, + genericRequirements: functionSignature.genericRequirements ) case nil, .initializer(_), .staticMethod(_): nil @@ -65,26 +70,32 @@ extension JNISwift2JavaGenerator { _ parameters: [SwiftParameter], translatedParameters: [TranslatedParameter], methodName: String, - parentName: String + parentName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] ) throws -> [NativeParameter] { try zip(translatedParameters, parameters).map { translatedParameter, swiftParameter in let parameterName = translatedParameter.parameter.name - return try translate( - swiftParameter: swiftParameter, + return try translateParameter( + type: swiftParameter.type, parameterName: parameterName, methodName: methodName, - parentName: parentName + parentName: parentName, + genericParameters: genericParameters, + genericRequirements: genericRequirements ) } } - func translate( - swiftParameter: SwiftParameter, + func translateParameter( + type: SwiftType, parameterName: String, methodName: String, - parentName: String + parentName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] ) throws -> NativeParameter { - switch swiftParameter.type { + switch type { case .nominal(let nominalType): let nominalTypeName = nominalType.nominalTypeDecl.name @@ -92,7 +103,7 @@ extension JNISwift2JavaGenerator { switch knownType { case .optional: guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { - throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) + throw JavaTranslationError.unsupportedSwiftType(type) } return try translateOptionalParameter( wrappedType: genericArgs[0], @@ -102,14 +113,14 @@ extension JNISwift2JavaGenerator { default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { - throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) + throw JavaTranslationError.unsupportedSwiftType(type) } return NativeParameter( parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .initFromJNI(.placeholder, swiftType: swiftParameter.type) + conversion: .initFromJNI(.placeholder, swiftType: type) ) } @@ -117,7 +128,7 @@ extension JNISwift2JavaGenerator { if nominalType.isJavaKitWrapper { guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { - throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftParameter.type) + throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(type) } return NativeParameter( @@ -140,7 +151,7 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: .long) ], - conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: swiftParameter.type)) + conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: type)) ) case .tuple([]): @@ -180,11 +191,68 @@ extension JNISwift2JavaGenerator { parameterName: parameterName ) - case .metatype, .tuple, .existential, .opaque, .genericParameter: - throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) + case .opaque(let proto), .existential(let proto): + return try translateProtocolParameter( + protocolType: proto, + parameterName: parameterName + ) + + case .genericParameter: + if let concreteTy = type.typeIn(genericParameters: genericParameters, genericRequirements: genericRequirements) { + return try translateProtocolParameter( + protocolType: concreteTy, + parameterName: parameterName + ) + } + + throw JavaTranslationError.unsupportedSwiftType(type) + + case .metatype, .tuple, .composite: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + func translateProtocolParameter( + protocolType: SwiftType, + parameterName: String + ) throws -> NativeParameter { + switch protocolType { + case .nominal(let nominalType): + let protocolName = nominalType.nominalTypeDecl.qualifiedName + return try translateProtocolParameter(protocolNames: [protocolName], parameterName: parameterName) + + case .composite(let types): + let protocolNames = try types.map { + guard let nominalTypeName = $0.asNominalType?.nominalTypeDecl.qualifiedName else { + throw JavaTranslationError.unsupportedSwiftType($0) + } + return nominalTypeName + } + + return try translateProtocolParameter(protocolNames: protocolNames, parameterName: parameterName) + + default: + throw JavaTranslationError.unsupportedSwiftType(protocolType) } } + private func translateProtocolParameter( + protocolNames: [String], + parameterName: String + ) throws -> NativeParameter { + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: .long), + JavaParameter(name: "\(parameterName)_typeMetadataAddress", type: .long) + ], + conversion: .extractSwiftProtocolValue( + .placeholder, + typeMetadataVariableName: .combinedName(component: "typeMetadataAddress"), + protocolNames: protocolNames + ) + ) + } + func translateOptionalParameter( wrappedType swiftType: SwiftType, parameterName: String @@ -351,7 +419,7 @@ extension JNISwift2JavaGenerator { outParameters: [] ) - case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter: + case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -380,7 +448,7 @@ extension JNISwift2JavaGenerator { // Custom types are not supported yet. throw JavaTranslationError.unsupportedSwiftType(type) - case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter: + case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -432,7 +500,7 @@ extension JNISwift2JavaGenerator { case .optional(let wrapped): return try translateOptionalResult(wrappedType: wrapped, resultName: resultName) - case .metatype, .tuple, .function, .existential, .opaque, .genericParameter: + case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite: throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } } @@ -480,6 +548,12 @@ extension JNISwift2JavaGenerator { /// `SwiftType(from: value, in: environment!)` indirect case initFromJNI(NativeSwiftConversionStep, swiftType: SwiftType) + indirect case extractSwiftProtocolValue( + NativeSwiftConversionStep, + typeMetadataVariableName: NativeSwiftConversionStep, + protocolNames: [String] + ) + /// Extracts a swift type at a pointer given by a long. indirect case extractSwiftValue( NativeSwiftConversionStep, @@ -540,6 +614,35 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(swiftType)(fromJNI: \(inner), in: environment!)" + case .extractSwiftProtocolValue(let inner, let typeMetadataVariableName, let protocolNames): + let inner = inner.render(&printer, placeholder) + let typeMetadataVariableName = typeMetadataVariableName.render(&printer, placeholder) + let existentialName = "\(inner)Existential$" + + let compositeProtocolName = "(\(protocolNames.joined(separator: " & ")))" + + // TODO: Remove the _openExistential when we decide to only support language mode v6+ + printer.print( + """ + guard let \(inner)TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: \(typeMetadataVariableName), in: environment!))) else { + fatalError("\(typeMetadataVariableName) memory address was null") + } + let \(inner)DynamicType$: Any.Type = unsafeBitCast(\(inner)TypeMetadataPointer$, to: Any.Type.self) + guard let \(inner)RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: \(inner), in: environment!))) else { + fatalError("\(inner) memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let \(existentialName) = \(inner)RawPointer$.load(as: \(inner)DynamicType$) as! any \(compositeProtocolName) + #else + func \(inner)DoLoad(_ ty: Ty.Type) -> any \(compositeProtocolName) { + \(inner)RawPointer$.load(as: ty) as! any \(compositeProtocolName) + } + let \(existentialName) = _openExistential(\(inner)DynamicType$, do: \(inner)DoLoad) + #endif + """ + ) + return existentialName + case .extractSwiftValue(let inner, let swiftType, let allowNil): let inner = inner.render(&printer, placeholder) let pointerName = "\(inner)$" @@ -584,7 +687,14 @@ extension JNISwift2JavaGenerator { let methodSignature = MethodSignature( resultType: nativeResult.javaType, - parameterTypes: parameters.flatMap { $0.parameters.map(\.type) } + parameterTypes: parameters.flatMap { + $0.parameters.map { parameter in + guard case .concrete(let type) = parameter.type else { + fatalError("Closures do not support Java generics") + } + return type + } + } ) let names = parameters.flatMap { $0.parameters.map(\.name) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 329b7dfd4..3848ceac9 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -109,6 +109,15 @@ extension JNISwift2JavaGenerator { printJNICache(&printer, type) printer.println() + switch type.swiftNominal.kind { + case .actor, .class, .enum, .struct: + printConcreteTypeThunks(&printer, type) + case .protocol: + printProtocolThunks(&printer, type) + } + } + + private func printConcreteTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { for initializer in type.initializers { printSwiftFunctionThunk(&printer, initializer) printer.println() @@ -134,9 +143,16 @@ extension JNISwift2JavaGenerator { printer.println() } + printTypeMetadataAddressThunk(&printer, type) + printer.println() printDestroyFunctionThunk(&printer, type) } + private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let protocolName = type.swiftNominal.name + } + + private func printEnumDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) { let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) printCDecl( @@ -415,6 +431,23 @@ extension JNISwift2JavaGenerator { ) } + private func printTypeMetadataAddressThunk(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + printCDecl( + &printer, + javaMethodName: "$typeMetadataAddressDowncall", + parentName: type.swiftNominal.name, + parameters: [], + resultType: .long + ) { printer in + printer.print( + """ + let metadataPointer = unsafeBitCast(\(type.swiftNominal.qualifiedName).self, to: UnsafeRawPointer.self) + return Int64(Int(bitPattern: metadataPointer)).getJNIValue(in: environment) + """ + ) + } + } + /// Prints the implementation of the destroy function. private func printDestroyFunctionThunk(_ printer: inout CodePrinter, _ type: ImportedNominalType) { let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index f9b99f6b5..a677bcde8 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -28,6 +28,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { let javaPackage: String let swiftOutputDirectory: String let javaOutputDirectory: String + let lookupContext: SwiftTypeLookupContext let javaClassLookupTable: JavaClassLookupTable @@ -61,6 +62,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { self.swiftOutputDirectory = swiftOutputDirectory self.javaOutputDirectory = javaOutputDirectory self.javaClassLookupTable = javaClassLookupTable + self.lookupContext = translator.lookupContext // If we are forced to write empty files, construct the expected outputs if translator.config.writeEmptyFiles ?? false { diff --git a/Sources/JExtractSwiftLib/JavaParameter.swift b/Sources/JExtractSwiftLib/JavaParameter.swift index a12b13b28..43f5a2b49 100644 --- a/Sources/JExtractSwiftLib/JavaParameter.swift +++ b/Sources/JExtractSwiftLib/JavaParameter.swift @@ -16,17 +16,56 @@ import JavaTypes /// Represent a parameter in Java code. struct JavaParameter { - let name: String - let type: JavaType + enum ParameterType: CustomStringConvertible { + case concrete(JavaType) + case generic(name: String, extends: [JavaType]) + + var jniTypeSignature: String { + switch self { + case .concrete(let type): + return type.jniTypeSignature + case .generic(_, let extends): + guard !extends.isEmpty else { + return "Ljava/lang/Object;" + } + + // Generics only use the first type for JNI + return extends.first!.jniTypeSignature + } + } + + var jniTypeName: String { + switch self { + case .concrete(let type): type.jniTypeName + case .generic: "jobject?" + } + } + + var description: String { + switch self { + case .concrete(let type): type.description + case .generic(let name, _): name + } + } + } + var name: String + var type: ParameterType + /// Parameter annotations are used in parameter declarations like this: `@Annotation int example` let annotations: [JavaAnnotation] - init(name: String, type: JavaType, annotations: [JavaAnnotation] = []) { + init(name: String, type: ParameterType, annotations: [JavaAnnotation] = []) { self.name = name self.type = type self.annotations = annotations } + init(name: String, type: JavaType, annotations: [JavaAnnotation] = []) { + self.name = name + self.type = .concrete(type) + self.annotations = annotations + } + func renderParameter() -> String { if annotations.isEmpty { return "\(type) \(name)" diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift index 02049ed4b..ccbbd2745 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift @@ -146,6 +146,8 @@ extension Swift2JavaTranslator { return check(ty) case .existential(let ty), .opaque(let ty): return check(ty) + case .composite(let types): + return types.contains(where: check) case .genericParameter: return false } @@ -200,7 +202,7 @@ extension Swift2JavaTranslator { _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, parent: ImportedNominalType? ) -> ImportedNominalType? { - if !nominalNode.shouldExtract(config: config, log: log) { + if !nominalNode.shouldExtract(config: config, log: log, in: parent) { return nil } @@ -225,7 +227,7 @@ extension Swift2JavaTranslator { guard swiftNominalDecl.moduleName == self.swiftModuleName else { return nil } - guard swiftNominalDecl.syntax!.shouldExtract(config: config, log: log) else { + guard swiftNominalDecl.syntax!.shouldExtract(config: config, log: log, in: nil) else { return nil } @@ -239,7 +241,7 @@ extension Swift2JavaTranslator { return alreadyImported } - let importedNominal = ImportedNominalType(swiftNominal: nominal) + let importedNominal = try? ImportedNominalType(swiftNominal: nominal, lookupContext: lookupContext) importedTypes[fullName] = importedNominal return importedNominal diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 9b183cb6b..13185a5c6 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -105,7 +105,7 @@ final class Swift2JavaVisitor { } func visit(functionDecl node: FunctionDeclSyntax, in typeContext: ImportedNominalType?) { - guard node.shouldExtract(config: config, log: log) else { + guard node.shouldExtract(config: config, log: log, in: typeContext) else { return } @@ -183,7 +183,7 @@ final class Swift2JavaVisitor { } func visit(variableDecl node: VariableDeclSyntax, in typeContext: ImportedNominalType?) { - guard node.shouldExtract(config: config, log: log) else { + guard node.shouldExtract(config: config, log: log, in: typeContext) else { return } @@ -237,7 +237,7 @@ final class Swift2JavaVisitor { self.log.info("Initializer must be within a current type; \(node)") return } - guard node.shouldExtract(config: config, log: log) else { + guard node.shouldExtract(config: config, log: log, in: typeContext) else { return } @@ -293,10 +293,10 @@ final class Swift2JavaVisitor { } extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { - func shouldExtract(config: Configuration, log: Logger) -> Bool { + func shouldExtract(config: Configuration, log: Logger, in parent: ImportedNominalType?) -> Bool { let meetsRequiredAccessLevel: Bool = switch config.effectiveMinimumInputAccessLevelMode { - case .public: self.isPublic + case .public: self.isPublic(in: parent?.swiftNominal.syntax) case .package: self.isAtLeastPackage case .internal: self.isAtLeastInternal } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift index 335979a44..763a5da23 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -93,6 +93,10 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { return firstInheritanceType.type }() + var inheritanceTypes: InheritedTypeListSyntax? { + self.syntax?.inheritanceClause?.inheritedTypes + } + /// Returns true if this type conforms to `Sendable` and therefore is "threadsafe". lazy var isSendable: Bool = { // Check if Sendable is in the inheritance list diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+RepresentativeConcreteeType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift similarity index 72% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftType+RepresentativeConcreteeType.swift rename to Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift index ce668485d..1a658513d 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+RepresentativeConcreteeType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift @@ -27,6 +27,38 @@ extension SwiftType { genericRequirements: genericRequirements ) } + + /// Returns the protocol type if this is a generic parameter in the list + func typeIn( + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) -> SwiftType? { + switch self { + case .genericParameter(let genericParam): + if genericParameters.contains(genericParam) { + let types: [SwiftType] = genericRequirements.compactMap { + guard case .inherits(let left, let right) = $0, left == self else { + return nil + } + return right + } + + if types.isEmpty { + // TODO: Any?? + return nil + } else if types.count == 1 { + return types.first! + } else { + return .composite(types) + } + } + + return nil + + default: + return nil + } + } } private func representativeConcreteType( diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index 58bb65c39..81afe6370 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -37,6 +37,9 @@ enum SwiftType: Equatable { /// `some ` indirect case opaque(SwiftType) + /// `type1` & `type2` + indirect case composite([SwiftType]) + static var void: Self { return .tuple([]) } @@ -45,7 +48,7 @@ enum SwiftType: Equatable { switch self { case .nominal(let nominal): nominal case .tuple(let elements): elements.count == 1 ? elements[0].asNominalType : nil - case .genericParameter, .function, .metatype, .optional, .existential, .opaque: nil + case .genericParameter, .function, .metatype, .optional, .existential, .opaque, .composite: nil } } @@ -88,7 +91,7 @@ enum SwiftType: Equatable { return nominal.nominalTypeDecl.isReferenceType case .metatype, .function: return true - case .genericParameter, .optional, .tuple, .existential, .opaque: + case .genericParameter, .optional, .tuple, .existential, .opaque, .composite: return false } } @@ -123,7 +126,7 @@ extension SwiftType: CustomStringConvertible { /// requires parentheses. private var postfixRequiresParentheses: Bool { switch self { - case .function, .existential, .opaque: true + case .function, .existential, .opaque, .composite: true case .genericParameter, .metatype, .nominal, .optional, .tuple: false } } @@ -147,6 +150,8 @@ extension SwiftType: CustomStringConvertible { return "any \(constraintType)" case .opaque(let constraintType): return "some \(constraintType)" + case .composite(let types): + return types.map(\.description).joined(separator: " & ") } } } @@ -208,7 +213,7 @@ extension SwiftNominalType { extension SwiftType { init(_ type: TypeSyntax, lookupContext: SwiftTypeLookupContext) throws { switch type.as(TypeSyntaxEnum.self) { - case .arrayType, .classRestrictionType, .compositionType, + case .arrayType, .classRestrictionType, .dictionaryType, .missingType, .namedOpaqueReturnType, .packElementType, .packExpansionType, .suppressedType: throw TypeTranslationError.unimplementedType(type) @@ -311,6 +316,13 @@ extension SwiftType { } else { self = .opaque(try SwiftType(someOrAntType.constraint, lookupContext: lookupContext)) } + + case .compositionType(let compositeType): + let types = try compositeType.elements.map { + try SwiftType($0.type, lookupContext: lookupContext) + } + + self = .composite(types) } } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 8d1931496..5884270a7 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -60,10 +60,17 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Async functions `func async` and properties: `var { get async {} }` | ❌ | ❌ | | Arrays: `[UInt8]`, `[T]` | ❌ | ❌ | | Dictionaries: `[String: Int]`, `[K:V]` | ❌ | ❌ | -| Generic functions | ❌ | ❌ | -| `Foundation.Data`, `any Foundation.DataProtocol` | ✅ | ❌ | +| Generic parameters in functions: `func f(x: T)` | ❌ | ✅ | +| Generic return values in functions: `func f() -> T` | ❌ | ❌ | | Tuples: `(Int, String)`, `(A, B, C)` | ❌ | ❌ | -| Protocols: `protocol`, existential parameters `any Collection` | ❌ | ❌ | +| Protocols: `protocol` | ❌ | ✅ | +| Protocols: `protocol` with associated types | ❌ | ❌ | +| Existential parameters `f(x: any SomeProtocol) ` | ❌ | ✅ | +| Existential parameters `f(x: any (A & B)) ` | ❌ | ✅ | +| Existential return types `f() -> any Collection ` | ❌ | ❌ | +| Foundation Data and DataProtocol: `f(x: any DataProtocol) -> Data` | ✅ | ❌ | +| Opaque parameters: `func take(worker: some Builder) -> some Builder` | ❌ | ✅ | +| Opaque return types: `func get() -> some Builder` | ❌ | ❌ | | Optional parameters: `func f(i: Int?, class: MyClass?)` | ✅ | ✅ | | Optional return types: `func f() -> Int?`, `func g() -> MyClass?` | ❌ | ✅ | | Primitive types: `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Float`, `Double` | ✅ | ✅ | @@ -90,7 +97,6 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Result builders | ❌ | ❌ | | Automatic Reference Counting of class types / lifetime safety | ✅ | ✅ | | Value semantic types (e.g. struct copying) | ❌ | ❌ | -| Opaque types: `func get() -> some Builder`, func take(worker: some Worker) | ❌ | ❌ | | Swift concurrency: `func async`, `actor`, `distribued actor` | ❌ | ❌ | | | | | | | | | @@ -266,5 +272,47 @@ try (var arena = SwiftArena.ofConfined()) { } ``` +### Protocols +> Note: Protocols are currently only supported in JNI mode. +> +> With the exception of `any DataProtocol` which is handled as `Foundation.Data` in the FFM mode. +Swift `protocol` types are imported as Java `interface`s. For now, we require that all +concrete types of an interface wrap a Swift instance. In the future, we will add support +for providing Java-based implementations of interfaces, that you can pass to Java functions. + +Consider the following Swift protocol: +```swift +protocol Named { + var name: String { get } + + func describe() -> String +} +``` +will be exported as +```java +interface Named extends JNISwiftInstance { + public String getName(); + + public String describe(); +} +``` + +#### Parameters +Any opaque, existential or generic parameters are imported as Java generics. +This means that the following function: +```swift +func f(x: S, y: any C, z: some D) +``` +will be exported as +```java + void f(S x, T1 y, T2 z) +``` +On the Java side, only SwiftInstance implementing types may be passed; +so this isn't a way for compatibility with just any arbitrary Java interfaces, +but specifically, for allowing passing concrete binding types generated by jextract from Swift types +which conform a to a given Swift protocol. + +#### Returning protocol types +Protocols are not yet supported as return types. diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java index 95f1e5a0f..4e3b8378f 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java @@ -14,32 +14,7 @@ package org.swift.swiftkit.core; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; - -public abstract class JNISwiftInstance extends SwiftInstance { - // Pointer to the "self". - protected final long selfPointer; - - /** - * The designated constructor of any imported Swift types. - * - * @param selfPointer a pointer to the memory containing the value - * @param arena the arena this object belongs to. When the arena goes out of scope, this value is destroyed. - */ - protected JNISwiftInstance(long selfPointer, SwiftArena arena) { - SwiftObjects.requireNonZero(selfPointer, "selfPointer"); - this.selfPointer = selfPointer; - - // Only register once we have fully initialized the object since this will need the object pointer. - arena.register(this); - } - - @Override - public long $memoryAddress() { - return this.selfPointer; - } - +public interface JNISwiftInstance extends SwiftInstance { /** * Creates a function that will be called when the value should be destroyed. * This will be code-generated to call a native method to do deinitialization and deallocation. @@ -52,10 +27,12 @@ protected JNISwiftInstance(long selfPointer, SwiftArena arena) { * * @return a function that is called when the value should be destroyed. */ - protected abstract Runnable $createDestroyFunction(); + Runnable $createDestroyFunction(); + + long $typeMetadataAddress(); @Override - public SwiftInstanceCleanup $createCleanup() { + default SwiftInstanceCleanup $createCleanup() { var statusDestroyedFlag = $statusDestroyedFlag(); Runnable markAsDestroyed = () -> statusDestroyedFlag.set(true); diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstance.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstance.java index 44955cc6a..3ab07ffb4 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstance.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstance.java @@ -16,12 +16,7 @@ import java.util.concurrent.atomic.AtomicBoolean; -public abstract class SwiftInstance { - - // TODO: make this a flagset integer and/or use a field updater - /** Used to track additional state of the underlying object, e.g. if it was explicitly destroyed. */ - private final AtomicBoolean $state$destroyed = new AtomicBoolean(false); - +public interface SwiftInstance { /** * Pointer to the {@code self} of the underlying Swift object or value. * @@ -29,14 +24,14 @@ public abstract class SwiftInstance { * is kept alive using some means (e.g. a class remains retained), as * this function does not ensure safety of the address in any way. */ - public abstract long $memoryAddress(); + long $memoryAddress(); /** * Called when the arena has decided the value should be destroyed. *

* Warning: The cleanup action must not capture {@code this}. */ - public abstract SwiftInstanceCleanup $createCleanup(); + SwiftInstanceCleanup $createCleanup(); /** * Exposes a boolean value which can be used to indicate if the object was destroyed. @@ -45,10 +40,7 @@ public abstract class SwiftInstance { * form a strong reference to the {@code SwiftInstance} which could prevent the cleanup from running, * if using an GC managed instance (e.g. using an {@code AutoSwiftMemorySession}. */ - public final AtomicBoolean $statusDestroyedFlag() { - return this.$state$destroyed; - } - + AtomicBoolean $statusDestroyedFlag(); /** * Ensures that this instance has not been destroyed. *

@@ -56,8 +48,8 @@ public abstract class SwiftInstance { * to be thrown. This check should be performed before accessing {@code $memorySegment} to prevent * use-after-free errors. */ - protected final void $ensureAlive() { - if (this.$state$destroyed.get()) { + default void $ensureAlive() { + if (this.$statusDestroyedFlag().get()) { throw new IllegalStateException("Attempted to call method on already destroyed instance of " + getClass().getSimpleName() + "!"); } } diff --git a/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java b/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java index b414962f5..120934215 100644 --- a/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java +++ b/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java @@ -18,6 +18,8 @@ import org.swift.swiftkit.core.JNISwiftInstance; import org.swift.swiftkit.core.SwiftArena; +import java.util.concurrent.atomic.AtomicBoolean; + public class AutoArenaTest { @Test @@ -45,13 +47,30 @@ public void cleaner_releases_native_resource() { } } - private static class FakeSwiftInstance extends JNISwiftInstance { + private static class FakeSwiftInstance implements JNISwiftInstance { + AtomicBoolean $state$destroyed = new AtomicBoolean(false); + public FakeSwiftInstance(SwiftArena arena) { - super(1, arena); + arena.register(this); } - protected Runnable $createDestroyFunction() { + public Runnable $createDestroyFunction() { return () -> {}; } + + @Override + public long $typeMetadataAddress() { + return 0; + } + + @Override + public long $memoryAddress() { + return 0; + } + + @Override + public AtomicBoolean $statusDestroyedFlag() { + return $state$destroyed; + } } } diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstance.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstance.java index 1236bad2a..70324c942 100644 --- a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstance.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstance.java @@ -18,10 +18,15 @@ import org.swift.swiftkit.core.SwiftInstanceCleanup; import java.lang.foreign.MemorySegment; +import java.util.concurrent.atomic.AtomicBoolean; -public abstract class FFMSwiftInstance extends SwiftInstance { +public abstract class FFMSwiftInstance implements SwiftInstance { private final MemorySegment memorySegment; + // TODO: make this a flagset integer and/or use a field updater + /** Used to track additional state of the underlying object, e.g. if it was explicitly destroyed. */ + private final AtomicBoolean $state$destroyed = new AtomicBoolean(false); + /** * The designated constructor of any imported Swift types. * @@ -52,6 +57,16 @@ protected FFMSwiftInstance(MemorySegment segment, AllocatingSwiftArena arena) { */ public abstract SwiftAnyType $swiftType(); + /** + * Exposes a boolean value which can be used to indicate if the object was destroyed. + *

+ * This is exposing the object, rather than performing the action because we don't want to accidentally + * form a strong reference to the {@code SwiftInstance} which could prevent the cleanup from running, + * if using an GC managed instance (e.g. using an {@code AutoSwiftMemorySession}. + */ + public final AtomicBoolean $statusDestroyedFlag() { + return this.$state$destroyed; + } @Override public SwiftInstanceCleanup $createCleanup() { @@ -65,7 +80,6 @@ protected FFMSwiftInstance(MemorySegment segment, AllocatingSwiftArena arena) { ); } - /** * Returns `true` if this swift instance is a reference type, i.e. a `class` or (`distributed`) `actor`. * diff --git a/SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/AutoArenaTest.java b/SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/AutoArenaTest.java index b95f6c458..18c0a5af0 100644 --- a/SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/AutoArenaTest.java +++ b/SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/AutoArenaTest.java @@ -18,6 +18,7 @@ import java.lang.foreign.GroupLayout; import java.lang.foreign.MemorySegment; +import java.util.concurrent.atomic.AtomicBoolean; public class AutoArenaTest { diff --git a/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift index 30bb7433d..a7527aa8d 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift @@ -55,9 +55,12 @@ struct JNIClassTests { import org.swift.swiftkit.core.*; import org.swift.swiftkit.core.util.*; + import java.util.*; + import java.util.concurrent.atomic.AtomicBoolean; + import org.swift.swiftkit.core.annotations.*; """, """ - public final class MyClass extends JNISwiftInstance { + public final class MyClass implements JNISwiftInstance { static final String LIB_NAME = "SwiftModule"; @SuppressWarnings("unused") @@ -68,9 +71,21 @@ struct JNIClassTests { } """, """ - private MyClass(long selfPointer, SwiftArena swiftArena) { - super(selfPointer, swiftArena); - } + private final long selfPointer; + """, + """ + public long $memoryAddress() { + return this.selfPointer; + } + """, + """ + private MyClass(long selfPointer, SwiftArena swiftArena) { + SwiftObjects.requireNonZero(selfPointer, "selfPointer"); + this.selfPointer = selfPointer; + + // Only register once we have fully initialized the object since this will need the object pointer. + swiftArena.register(this); + } """, """ public static MyClass wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { @@ -92,7 +107,7 @@ struct JNIClassTests { expectedChunks: [ """ @Override - protected Runnable $createDestroyFunction() { + public Runnable $createDestroyFunction() { long self$ = this.$memoryAddress(); if (CallTraces.TRACE_DOWNCALLS) { CallTraces.traceDowncall("MyClass.$createDestroyFunction", diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift index 477650437..c6aaf923d 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -39,9 +39,12 @@ struct JNIEnumTests { import org.swift.swiftkit.core.*; import org.swift.swiftkit.core.util.*; + import java.util.*; + import java.util.concurrent.atomic.AtomicBoolean; + import org.swift.swiftkit.core.annotations.*; """, """ - public final class MyEnum extends JNISwiftInstance { + public final class MyEnum implements JNISwiftInstance { static final String LIB_NAME = "SwiftModule"; @SuppressWarnings("unused") @@ -53,7 +56,19 @@ struct JNIEnumTests { """, """ private MyEnum(long selfPointer, SwiftArena swiftArena) { - super(selfPointer, swiftArena); + SwiftObjects.requireNonZero(selfPointer, "selfPointer"); + this.selfPointer = selfPointer; + + // Only register once we have fully initialized the object since this will need the object pointer. + swiftArena.register(this); + } + """, + """ + private final long selfPointer; + """, + """ + public long $memoryAddress() { + return this.selfPointer; } """, """ @@ -66,7 +81,7 @@ struct JNIEnumTests { """, """ @Override - protected Runnable $createDestroyFunction() { + public Runnable $createDestroyFunction() { long self$ = this.$memoryAddress(); if (CallTraces.TRACE_DOWNCALLS) { CallTraces.traceDowncall("MyEnum.$createDestroyFunction", diff --git a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift index ad55f491e..27b0cdea7 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift @@ -46,6 +46,7 @@ struct JNIModuleTests { import org.swift.swiftkit.core.*; import org.swift.swiftkit.core.util.*; import java.util.*; + import java.util.concurrent.atomic.AtomicBoolean; import org.swift.swiftkit.core.annotations.*; public final class SwiftModule { diff --git a/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift b/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift index 548a2eacc..be2e0f6a9 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift @@ -127,12 +127,12 @@ struct JNIOptionalTests { result$ = innerResult$.getJNIValue(in: environment!) var flag$ = Int8(1) environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) - } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:649 + } else { result$ = String.jniPlaceholderValue var flag$ = Int8(0) environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) - } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:659 + } return result$ } """ @@ -190,12 +190,12 @@ struct JNIOptionalTests { result$ = _resultBits$.getJNIValue(in: environment!) var flag$ = Int8(1) environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) - } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:649 + } else { result$ = 0 var flag$ = Int8(0) environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) - } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:659 + } return result$ } """ @@ -243,7 +243,7 @@ struct JNIOptionalTests { func Java_com_example_swift_SwiftModule__00024optionalJavaKitClass__Ljava_lang_Long_2(environment: UnsafeMutablePointer!, thisClass: jclass, arg: jobject?) { SwiftModule.optionalJavaKitClass(arg.map { return JavaLong(javaThis: $0, environment: environment!) - } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:691 + } ) } """ diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift new file mode 100644 index 000000000..b5a0fcdbe --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -0,0 +1,246 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIProtocolTests { + let source = """ + public protocol SomeProtocol { + var x: Int64 { get set } + + public func method() {} + } + + public protocol B {} + + public class SomeClass: SomeProtocol {} + + public func takeProtocol(x: some SomeProtocol, y: any SomeProtocol) + public func takeGeneric(s: S) + public func takeComposite(x: any SomeProtocol & B) + """ + + @Test + func generatesJavaInterface() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule + + package com.example.swift; + + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + """, + """ + public interface SomeProtocol extends JNISwiftInstance { + ... + } + """, + """ + public long getX(); + """, + """ + public void setX(long newValue); + """, + """ + public void method(); + """ + ]) + } + + @Test + func generatesJavaClassWithExtends() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class SomeClass implements JNISwiftInstance, SomeProtocol { + """ + ]) + } + + @Test + func takeProtocol_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static <$T0 extends SomeProtocol, $T1 extends SomeProtocol> void takeProtocol($T0 x, $T1 y) { + SwiftModule.$takeProtocol(x.$memoryAddress(), x.$typeMetadataAddress(), y.$memoryAddress(), y.$typeMetadataAddress()); + } + """, + """ + private static native void $takeProtocol(long x, long x_typeMetadataAddress, long y, long y_typeMetadataAddress); + """ + ]) + } + + @Test + func takeProtocol_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeProtocol__JJJJ") + func Java_com_example_swift_SwiftModule__00024takeProtocol__JJJJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, x_typeMetadataAddress: jlong, y: jlong, y_typeMetadataAddress: jlong) { + guard let xTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: x_typeMetadataAddress, in: environment!))) else { + fatalError("x_typeMetadataAddress memory address was null") + } + let xDynamicType$: Any.Type = unsafeBitCast(xTypeMetadataPointer$, to: Any.Type.self) + guard let xRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: x, in: environment!))) else { + fatalError("x memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let xExistential$ = xRawPointer$.load(as: xDynamicType$) as! any (SomeProtocol) + #else + func xDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { + xRawPointer$.load(as: ty) as! any (SomeProtocol) + } + let xExistential$ = _openExistential(xDynamicType$, do: xDoLoad) + #endif + guard let yTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: y_typeMetadataAddress, in: environment!))) else { + fatalError("y_typeMetadataAddress memory address was null") + } + let yDynamicType$: Any.Type = unsafeBitCast(yTypeMetadataPointer$, to: Any.Type.self) + guard let yRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: y, in: environment!))) else { + fatalError("y memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let yExistential$ = yRawPointer$.load(as: yDynamicType$) as! any (SomeProtocol) + #else + func yDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { + yRawPointer$.load(as: ty) as! any (SomeProtocol) + } + let yExistential$ = _openExistential(yDynamicType$, do: yDoLoad) + #endif + SwiftModule.takeProtocol(x: xExistential$, y: yExistential$) + } + """ + ] + ) + } + + @Test + func takeGeneric_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static void takeGeneric(S s) { + SwiftModule.$takeGeneric(s.$memoryAddress(), s.$typeMetadataAddress()); + } + """, + """ + private static native void $takeGeneric(long s, long s_typeMetadataAddress); + """ + ]) + } + + @Test + func takeGeneric_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeGeneric__JJ") + func Java_com_example_swift_SwiftModule__00024takeGeneric__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, s: jlong, s_typeMetadataAddress: jlong) { + guard let sTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: s_typeMetadataAddress, in: environment!))) else { + fatalError("s_typeMetadataAddress memory address was null") + } + let sDynamicType$: Any.Type = unsafeBitCast(sTypeMetadataPointer$, to: Any.Type.self) + guard let sRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: s, in: environment!))) else { + fatalError("s memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let sExistential$ = sRawPointer$.load(as: sDynamicType$) as! any (SomeProtocol) + #else + func sDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { + sRawPointer$.load(as: ty) as! any (SomeProtocol) + } + let sExistential$ = _openExistential(sDynamicType$, do: sDoLoad) + #endif + SwiftModule.takeGeneric(s: sExistential$) + } + """ + ] + ) + } + + @Test + func takeComposite_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static <$T0 extends SomeProtocol & B> void takeComposite($T0 x) { + SwiftModule.$takeComposite(x.$memoryAddress(), x.$typeMetadataAddress()); + } + """, + """ + private static native void $takeComposite(long x, long x_typeMetadataAddress); + """ + ]) + } + + @Test + func takeComposite_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeComposite__JJ") + func Java_com_example_swift_SwiftModule__00024takeComposite__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, x_typeMetadataAddress: jlong) { + guard let xTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: x_typeMetadataAddress, in: environment!))) else { + fatalError("x_typeMetadataAddress memory address was null") + } + let xDynamicType$: Any.Type = unsafeBitCast(xTypeMetadataPointer$, to: Any.Type.self) + guard let xRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: x, in: environment!))) else { + fatalError("x memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let xExistential$ = xRawPointer$.load(as: xDynamicType$) as! any (SomeProtocol & B) + #else + func xDoLoad(_ ty: Ty.Type) -> any (SomeProtocol & B) { + xRawPointer$.load(as: ty) as! any (SomeProtocol & B) + } + let xExistential$ = _openExistential(xDynamicType$, do: xDoLoad) + #endif + SwiftModule.takeComposite(x: xExistential$) + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift b/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift index 7f54fa7e1..a7c689aaf 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift @@ -47,7 +47,7 @@ struct JNIStructTests { """,]) try assertOutput(input: source, .jni, .java, expectedChunks: [ """ - public final class MyStruct extends JNISwiftInstance { + public final class MyStruct implements JNISwiftInstance { static final String LIB_NAME = "SwiftModule"; @SuppressWarnings("unused") @@ -58,9 +58,13 @@ struct JNIStructTests { } """, """ - private MyStruct(long selfPointer, SwiftArena swiftArena) { - super(selfPointer, swiftArena); - } + private MyStruct(long selfPointer, SwiftArena swiftArena) { + SwiftObjects.requireNonZero(selfPointer, "selfPointer"); + this.selfPointer = selfPointer; + + // Only register once we have fully initialized the object since this will need the object pointer. + swiftArena.register(this); + } """, """ public static MyStruct wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { @@ -80,7 +84,7 @@ struct JNIStructTests { expectedChunks: [ """ @Override - protected Runnable $createDestroyFunction() { + public Runnable $createDestroyFunction() { long self$ = this.$memoryAddress(); if (CallTraces.TRACE_DOWNCALLS) { CallTraces.traceDowncall("MyStruct.$createDestroyFunction", diff --git a/Tests/JExtractSwiftTests/SendableTests.swift b/Tests/JExtractSwiftTests/SendableTests.swift index d6d0d2d6e..5116fc030 100644 --- a/Tests/JExtractSwiftTests/SendableTests.swift +++ b/Tests/JExtractSwiftTests/SendableTests.swift @@ -46,11 +46,11 @@ final class SendableTests { expectedChunks: [ """ @ThreadSafe // Sendable - public final class SendableStruct extends JNISwiftInstance { + public final class SendableStruct implements JNISwiftInstance { static final String LIB_NAME = "SwiftModule"; """, ] ) } -} \ No newline at end of file +}