diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index 83c5f0d9..2778cebe 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -15,6 +15,8 @@ import Foundation import PackagePlugin +fileprivate let SwiftJavaConfigFileName = "swift-java.config" + @main struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { @@ -51,12 +53,14 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { log("Skipping jextract step, no 'javaPackage' configuration in \(getSwiftJavaConfigPath(target: target) ?? "")") return [] } - + // We use the the usual maven-style structure of "src/[generated|main|test]/java/..." // that is common in JVM ecosystem let outputJavaDirectory = context.outputJavaDirectory let outputSwiftDirectory = context.outputSwiftDirectory + let dependentConfigFiles = searchForDependentConfigFiles(in: target) + var arguments: [String] = [ /*subcommand=*/"jextract", "--swift-module", sourceModule.name, @@ -71,6 +75,16 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { // as it depends on the contents of the input files. Therefore we have to implement this as a prebuild plugin. // We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc. ] + + let dependentConfigFilesArguments = dependentConfigFiles.flatMap { moduleAndConfigFile in + let (moduleName, configFile) = moduleAndConfigFile + return [ + "--depends-on", + "\(configFile.path(percentEncoded: false))" + ] + } + arguments += dependentConfigFilesArguments + if !javaPackage.isEmpty { arguments += ["--java-package", javaPackage] } @@ -117,5 +131,53 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { ) ] } + + /// Find the manifest files from other swift-java executions in any targets + /// this target depends on. + func searchForDependentConfigFiles(in target: any Target) -> [(String, URL)] { + var dependentConfigFiles = [(String, URL)]() + + func _searchForConfigFiles(in target: any Target) { + // log("Search for config files in target: \(target.name)") + let dependencyURL = URL(filePath: target.directory.string) + + // Look for a config file within this target. + let dependencyConfigURL = dependencyURL + .appending(path: SwiftJavaConfigFileName) + let dependencyConfigString = dependencyConfigURL + .path(percentEncoded: false) + + if FileManager.default.fileExists(atPath: dependencyConfigString) { + dependentConfigFiles.append((target.name, dependencyConfigURL)) + } + } + + // Process direct dependencies of this target. + for dependency in target.dependencies { + switch dependency { + case .target(let target): + // log("Dependency target: \(target.name)") + _searchForConfigFiles(in: target) + + case .product(let product): + // log("Dependency product: \(product.name)") + for target in product.targets { + // log("Dependency product: \(product.name), target: \(target.name)") + _searchForConfigFiles(in: target) + } + + @unknown default: + break + } + } + + // Process indirect target dependencies. + for dependency in target.recursiveTargetDependencies { + // log("Recursive dependency target: \(dependency.name)") + _searchForConfigFiles(in: dependency) + } + + return dependentConfigFiles + } } diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index b4447f4f..9a78c3c2 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import JavaKit + public class MySwiftClass { public let x: Int64 public let y: Int64 @@ -84,4 +86,8 @@ public class MySwiftClass { public func copy() -> MySwiftClass { return MySwiftClass(x: self.x, y: self.y) } + + public func addXWithJavaLong(_ other: JavaLong) -> Int64 { + return self.x + other.longValue() + } } diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index 4425d6b2..f034b904 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -139,4 +139,13 @@ void copy() { assertNotEquals(c1.$memoryAddress(), c2.$memoryAddress()); } } + + @Test + void addXWithJavaLong() { + try (var arena = new ConfinedSwiftMemorySession()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + Long javaLong = 50L; + assertEquals(70, c1.addXWithJavaLong(javaLong)); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift index 1851e154..0f5cfeac 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import JavaTypes + extension String { var firstCharacterUppercased: String { @@ -31,4 +33,41 @@ extension String { let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2) return self[thirdCharacterIndex].isUppercase } + + /// Returns a version of the string correctly escaped for a JNI + var escapedJNIIdentifier: String { + self.map { + if $0 == "_" { + return "_1" + } else if $0 == "/" { + return "_" + } else if $0 == ";" { + return "_2" + } else if $0 == "[" { + return "_3" + } else if $0.isASCII && ($0.isLetter || $0.isNumber) { + return String($0) + } else if let utf16 = $0.utf16.first { + // Escape any non-alphanumeric to their UTF16 hex encoding + let utf16Hex = String(format: "%04x", utf16) + return "_0\(utf16Hex)" + } else { + fatalError("Invalid JNI character: \($0)") + } + } + .joined() + } + + /// Looks up self as a JavaKit wrapped class name and converts it + /// into a `JavaType.class` if it exists in `lookupTable`. + func parseJavaClassFromJavaKitName(in lookupTable: [String: String]) -> JavaType? { + guard let canonicalJavaName = lookupTable[self] else { + return nil + } + let nameParts = canonicalJavaName.components(separatedBy: ".") + let javaPackageName = nameParts.dropLast().joined(separator: ".") + let javaClassName = nameParts.last! + + return .class(package: javaPackageName, name: javaClassName) + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index e704739c..57ea15b2 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -24,7 +24,11 @@ extension JNISwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { - let translation = JavaTranslation(swiftModuleName: swiftModuleName, javaPackage: self.javaPackage) + let translation = JavaTranslation( + swiftModuleName: swiftModuleName, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable + ) translated = try translation.translate(decl) } catch { self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") @@ -38,9 +42,13 @@ extension JNISwift2JavaGenerator { struct JavaTranslation { let swiftModuleName: String let javaPackage: String + let javaClassLookupTable: JavaClassLookupTable func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { - let nativeTranslation = NativeJavaTranslation(javaPackage: self.javaPackage) + let nativeTranslation = NativeJavaTranslation( + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable + ) // Types with no parent will be outputted inside a "module" class. let parentName = decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName @@ -157,6 +165,8 @@ extension JNISwift2JavaGenerator { ) throws -> TranslatedParameter { switch swiftType { case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -168,11 +178,25 @@ extension JNISwift2JavaGenerator { ) } - // For now, we assume this is a JExtract class. + if nominalType.isJavaKitWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) + } + + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: javaType + ), + conversion: .placeholder + ) + } + + // We assume this is a JExtract class. return TranslatedParameter( parameter: JavaParameter( name: parameterName, - type: .class(package: nil, name: nominalType.nominalTypeDecl.name) + type: .class(package: nil, name: nominalTypeName) ), conversion: .valueMemoryAddress(.placeholder) ) @@ -213,7 +237,11 @@ extension JNISwift2JavaGenerator { ) } - // For now, we assume this is a JExtract class. + if nominalType.isJavaKitWrapper { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + + // We assume this is a JExtract class. let javaType = JavaType.class(package: nil, name: nominalType.nominalTypeDecl.name) return TranslatedResult( javaType: javaType, @@ -350,5 +378,9 @@ extension JNISwift2JavaGenerator { enum JavaTranslationError: Error { case unsupportedSwiftType(SwiftType) + + /// The user has not supplied a mapping from `SwiftType` to + /// a java class. + case wrappedJavaClassTranslationNotProvided(SwiftType) } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index cae6010d..5ccefadb 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -18,6 +18,7 @@ extension JNISwift2JavaGenerator { struct NativeJavaTranslation { let javaPackage: String + let javaClassLookupTable: JavaClassLookupTable /// Translates a Swift function into the native JNI method signature. func translate( @@ -62,10 +63,12 @@ extension JNISwift2JavaGenerator { swiftParameter: SwiftParameter, parameterName: String, methodName: String, - parentName: String, + parentName: String ) throws -> NativeParameter { switch swiftParameter.type { case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) @@ -78,6 +81,25 @@ extension JNISwift2JavaGenerator { ) } + if nominalType.isJavaKitWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftParameter.type) + } + + return NativeParameter( + name: parameterName, + javaType: javaType, + conversion: .initializeJavaKitWrapper(wrapperName: nominalTypeName) + ) + } + + // JExtract classes are passed as the pointer. + return NativeParameter( + name: parameterName, + javaType: .long, + conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: swiftParameter.type)) + ) + case .tuple([]): return NativeParameter( name: parameterName, @@ -110,13 +132,6 @@ extension JNISwift2JavaGenerator { case .metatype, .optional, .tuple, .existential, .opaque: throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) } - - // Classes are passed as the pointer. - return NativeParameter( - name: parameterName, - javaType: .long, - conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: swiftParameter.type)) - ) } func translateClosureResult( @@ -193,6 +208,15 @@ extension JNISwift2JavaGenerator { ) } + if nominalType.isJavaKitWrapper { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + + return NativeResult( + javaType: .long, + conversion: .getJNIValue(.allocateSwiftValue(name: "result", swiftType: swiftResult.type)) + ) + case .tuple([]): return NativeResult( javaType: .void, @@ -203,13 +227,7 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } - // TODO: Handle other classes, for example from JavaKit macros. - // for now we assume all passed in classes are JExtract generated - // so we pass the pointer. - return NativeResult( - javaType: .long, - conversion: .getJNIValue(.allocateSwiftValue(name: "result", swiftType: swiftResult.type)) - ) + } } @@ -262,6 +280,8 @@ extension JNISwift2JavaGenerator { indirect case closureLowering(parameters: [NativeParameter], result: NativeResult) + case initializeJavaKitWrapper(wrapperName: String) + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -348,6 +368,9 @@ extension JNISwift2JavaGenerator { printer.print("}") return printer.finalize() + + case .initializeJavaKitWrapper(let wrapperName): + return "\(wrapperName)(javaThis: \(placeholder), environment: environment!)" } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 20551465..35d4dbef 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -328,29 +328,3 @@ extension JNISwift2JavaGenerator { return newSelfParamName } } - -extension String { - /// Returns a version of the string correctly escaped for a JNI - var escapedJNIIdentifier: String { - self.map { - if $0 == "_" { - return "_1" - } else if $0 == "/" { - return "_" - } else if $0 == ";" { - return "_2" - } else if $0 == "[" { - return "_3" - } else if $0.isASCII && ($0.isLetter || $0.isNumber) { - return String($0) - } else if let utf16 = $0.utf16.first { - // Escape any non-alphanumeric to their UTF16 hex encoding - let utf16Hex = String(format: "%04x", utf16) - return "_0\(utf16Hex)" - } else { - fatalError("Invalid JNI character: \($0)") - } - } - .joined() - } -} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 0cc523b4..79f546ac 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -14,6 +14,10 @@ import JavaTypes +/// A table that where keys are Swift class names and the values are +/// the fully qualified canoical names. +package typealias JavaClassLookupTable = [String: String] + package class JNISwift2JavaGenerator: Swift2JavaGenerator { let analysis: AnalysisResult let swiftModuleName: String @@ -22,6 +26,8 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { let swiftOutputDirectory: String let javaOutputDirectory: String + let javaClassLookupTable: JavaClassLookupTable + var javaPackagePath: String { javaPackage.replacingOccurrences(of: ".", with: "/") } @@ -39,7 +45,8 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { translator: Swift2JavaTranslator, javaPackage: String, swiftOutputDirectory: String, - javaOutputDirectory: String + javaOutputDirectory: String, + javaClassLookupTable: JavaClassLookupTable ) { self.logger = Logger(label: "jni-generator", logLevel: translator.log.logLevel) self.analysis = translator.result @@ -47,6 +54,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { self.javaPackage = javaPackage self.swiftOutputDirectory = swiftOutputDirectory self.javaOutputDirectory = javaOutputDirectory + self.javaClassLookupTable = javaClassLookupTable // If we are forced to write empty files, construct the expected outputs if translator.config.writeEmptyFiles ?? false { diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index dad25299..6473cea3 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -20,9 +20,11 @@ import JavaKitConfigurationShared // TODO: this should become SwiftJavaConfigura public struct SwiftToJava { let config: Configuration + let dependentConfigs: [Configuration] - public init(config: Configuration) { + public init(config: Configuration, dependentConfigs: [Configuration]) { self.config = config + self.dependentConfigs = dependentConfigs } public func run() throws { @@ -83,6 +85,14 @@ public struct SwiftToJava { fatalError("Missing --output-java directory!") } + let wrappedJavaClassesLookupTable: JavaClassLookupTable = dependentConfigs.compactMap(\.classes).reduce(into: [:]) { + for (canonicalName, javaClass) in $1 { + $0[javaClass] = canonicalName + } + } + + translator.dependenciesClasses = Array(wrappedJavaClassesLookupTable.keys) + try translator.analyze() switch config.mode { @@ -101,7 +111,8 @@ public struct SwiftToJava { translator: translator, javaPackage: config.javaPackage ?? "", swiftOutputDirectory: outputSwiftDirectory, - javaOutputDirectory: outputJavaDirectory + javaOutputDirectory: outputJavaDirectory, + javaClassLookupTable: wrappedJavaClassesLookupTable ) try generator.generate() diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift index 07023b09..d1141e5f 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift @@ -39,6 +39,9 @@ public final class Swift2JavaTranslator { var inputs: [Input] = [] + /// A list of used Swift class names that live in dependencies, e.g. `JavaInteger` + package var dependenciesClasses: [String] = [] + // ==== Output state package var importedGlobalVariables: [ImportedFunc] = [] @@ -111,9 +114,11 @@ extension Swift2JavaTranslator { } package func prepareForTranslation() { + let dependenciesSource = self.buildDependencyClassesSourceFile() + self.symbolTable = SwiftSymbolTable.setup( moduleName: self.swiftModuleName, - inputs.map({ $0.syntax }), + inputs.map({ $0.syntax }) + [dependenciesSource], log: self.log ) } @@ -167,6 +172,16 @@ extension Swift2JavaTranslator { } return false } + + /// Returns a source file that contains all the available dependency classes. + private func buildDependencyClassesSourceFile() -> SourceFileSyntax { + let contents = self.dependenciesClasses.map { + "public class \($0) {}" + } + .joined(separator: "\n") + + return SourceFileSyntax(stringLiteral: contents) + } } // ==== ---------------------------------------------------------------------------------------------------------------- diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index 531ab45c..d0f1f98d 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -171,6 +171,13 @@ extension SwiftNominalType: CustomStringConvertible { } } +extension SwiftNominalType { + // TODO: Better way to detect Java wrapped classes. + var isJavaKitWrapper: Bool { + nominalTypeDecl.name.hasPrefix("Java") + } +} + extension SwiftType { init(_ type: TypeSyntax, symbolTable: SwiftSymbolTable) throws { switch type.as(TypeSyntaxEnum.self) { diff --git a/Sources/JavaKitConfigurationShared/Configuration.swift b/Sources/JavaKitConfigurationShared/Configuration.swift index 876b577b..5c57082e 100644 --- a/Sources/JavaKitConfigurationShared/Configuration.swift +++ b/Sources/JavaKitConfigurationShared/Configuration.swift @@ -141,6 +141,26 @@ public func readConfiguration(configPath: URL, file: String = #fileID, line: UIn } } +/// Load all dependent configs configured with `--depends-on` and return a list of +/// `(SwiftModuleName, Configuration)` tuples. +public func loadDependentConfigs(dependsOn: [String]) throws -> [(String?, Configuration)] { + try dependsOn.map { dependentConfig in + let equalLoc = dependentConfig.firstIndex(of: "=") + + var swiftModuleName: String? = nil + if let equalLoc { + swiftModuleName = String(dependentConfig[.. [String] { let basePath: String = FileManager.default.currentDirectoryPath let pluginOutputsDir = URL(fileURLWithPath: basePath) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index dc2c7b0e..e7acb3eb 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -65,6 +65,8 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Protocols: `protocol`, existential parameters `any Collection` | ❌ | ❌ | | Optional types: `Int?`, `AnyObject?` | ❌ | ❌ | | Primitive types: `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Float`, `Double` | ✅ | ✅ | +| Parameters: JavaKit wrapped types `JavaLong`, `JavaInteger` | ❌ | ✅ | +| Return values: JavaKit wrapped types `JavaLong`, `JavaInteger` | ❌ | ❌ | | Unsigned primitive types: `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` | ❌ | ❌ | | String (with copying data) | ✅ | ✅ | | Variadic parameters: `T...` | ❌ | ❌ | diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index c7fe51a8..e4901bc2 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -60,6 +60,15 @@ extension SwiftJava { @Flag(help: "Some build systems require an output to be present when it was 'expected', even if empty. This is used by the JExtractSwiftPlugin build plugin, but otherwise should not be necessary.") var writeEmptyFiles: Bool = false + + @Option( + help: """ + A swift-java configuration file for a given Swift module name on which this module depends, + e.g., Sources/JavaKitJar/Java2Swift.config. There should be one of these options + for each Swift module that this module depends on (transitively) that contains wrapped Java sources. + """ + ) + var dependsOn: [String] = [] } } @@ -82,15 +91,20 @@ extension SwiftJava.JExtractCommand { print("[debug][swift-java] Running 'swift-java jextract' in mode: " + "\(self.mode)".bold) - try jextractSwift(config: config) + // Load all of the dependent configurations and associate them with Swift modules. + let dependentConfigs = try loadDependentConfigs(dependsOn: self.dependsOn) + print("[debug][swift-java] Dependent configs: \(dependentConfigs.count)") + + try jextractSwift(config: config, dependentConfigs: dependentConfigs.map(\.1)) } } extension SwiftJava.JExtractCommand { func jextractSwift( - config: Configuration + config: Configuration, + dependentConfigs: [Configuration] ) throws { - try SwiftToJava(config: config).run() + try SwiftToJava(config: config, dependentConfigs: dependentConfigs).run() } } diff --git a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift index 05ac7651..4c5bd97b 100644 --- a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift +++ b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift @@ -77,7 +77,12 @@ extension SwiftJava.WrapJavaCommand { searchDirs: classpathSearchDirs, config: config) // Load all of the dependent configurations and associate them with Swift modules. - let dependentConfigs = try self.loadDependentConfigs() + let dependentConfigs = try loadDependentConfigs(dependsOn: self.dependsOn).map { moduleName, config in + guard let moduleName else { + throw JavaToSwiftError.badConfigOption + } + return (moduleName, config) + } print("[debug][swift-java] Dependent configs: \(dependentConfigs.count)") // Include classpath entries which libs we depend on require... @@ -102,27 +107,6 @@ extension SwiftJava.WrapJavaCommand { } } -extension SwiftJava.WrapJavaCommand { - - /// Load all dependent configs configured with `--depends-on` and return a list of - /// `(SwiftModuleName, Configuration)` tuples. - func loadDependentConfigs() throws -> [(String, Configuration)] { - try dependsOn.map { dependentConfig in - guard let equalLoc = dependentConfig.firstIndex(of: "=") else { - throw JavaToSwiftError.badConfigOption(dependentConfig) - } - - let afterEqual = dependentConfig.index(after: equalLoc) - let swiftModuleName = String(dependentConfig[..=" } } diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index 47397c63..4b0162f3 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -29,6 +29,7 @@ func assertOutput( _ renderKind: RenderKind, swiftModuleName: String = "SwiftModule", detectChunkByInitialLines _detectChunkByInitialLines: Int = 4, + javaClassLookupTable: [String: String] = [:], expectedChunks: [String], fileID: String = #fileID, filePath: String = #filePath, @@ -38,6 +39,7 @@ func assertOutput( var config = Configuration() config.swiftModule = swiftModuleName let translator = Swift2JavaTranslator(config: config) + translator.dependenciesClasses = Array(javaClassLookupTable.keys) try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input) @@ -64,7 +66,8 @@ func assertOutput( translator: translator, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", - javaOutputDirectory: "/fake" + javaOutputDirectory: "/fake", + javaClassLookupTable: javaClassLookupTable ) switch renderKind { diff --git a/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift b/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift new file mode 100644 index 00000000..0283780a --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// 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 JNIJavaKitTests { + let source = + """ + public func function(javaLong: JavaLong, javaInteger: JavaInteger, int: Int64) {} + """ + + let classLookupTable = [ + "JavaLong": "java.lang.Long", + "JavaInteger": "java.lang.Integer" + ] + + @Test + func function_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func function(javaLong: JavaLong, javaInteger: JavaInteger, int: Int64) + * } + */ + public static void function(java.lang.Long javaLong, java.lang.Integer javaInteger, long int) { + SwiftModule.$function(javaLong, javaInteger, int); + } + """, + """ + private static native void $function(java.lang.Long javaLong, java.lang.Integer javaInteger, long int); + """ + ] + ) + } + + @Test + func function_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J") + func Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J(environment: UnsafeMutablePointer!, thisClass: jclass, javaLong: jobject, javaInteger: jobject, int: jlong) { + SwiftModule.function(javaLong: JavaLong(javaThis: javaLong, environment: environment!), javaInteger: JavaInteger(javaThis: javaInteger, environment: environment!), int: Int64(fromJNI: int, in: environment!)) + } + """ + ] + ) + } +}