diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index e98d29b3..3619890b 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -60,7 +60,6 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { self.swiftDecl.signatureString } - var parentType: SwiftType? { guard let selfParameter = functionSignature.selfParameter else { return nil diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift new file mode 100644 index 00000000..56d75218 --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -0,0 +1,253 @@ +//===----------------------------------------------------------------------===// +// +// 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 JavaTypes + +package class JNISwift2JavaGenerator: Swift2JavaGenerator { + let analysis: AnalysisResult + let swiftModuleName: String + let javaPackage: String + let logger: Logger + let swiftOutputDirectory: String + let javaOutputDirectory: String + + var javaPackagePath: String { + javaPackage.replacingOccurrences(of: ".", with: "/") + } + + var thunkNameRegistry = ThunkNameRegistry() + + package init( + translator: Swift2JavaTranslator, + javaPackage: String, + swiftOutputDirectory: String, + javaOutputDirectory: String + ) { + self.logger = Logger(label: "jni-generator", logLevel: translator.log.logLevel) + self.analysis = translator.result + self.swiftModuleName = translator.swiftModuleName + self.javaPackage = javaPackage + self.swiftOutputDirectory = swiftOutputDirectory + self.javaOutputDirectory = javaOutputDirectory + } + + func generate() throws { + try writeSwiftThunkSources() + try writeExportedJavaSources() + } +} + +extension JNISwift2JavaGenerator { + func writeExportedJavaSources() throws { + var printer = CodePrinter() + try writeExportedJavaSources(&printer) + } + + package func writeExportedJavaSources(_ printer: inout CodePrinter) throws { + let filename = "\(self.swiftModuleName).java" + logger.trace("Printing module class: \(filename)") + printModule(&printer) + + if let outputFile = try printer.writeContents( + outputDirectory: javaOutputDirectory, + javaPackagePath: javaPackagePath, + filename: filename + ) { + logger.info("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))") + } + } + + func writeSwiftThunkSources() throws { + var printer = CodePrinter() + try writeSwiftThunkSources(&printer) + } + + package func writeSwiftThunkSources(_ printer: inout CodePrinter) throws { + let moduleFilenameBase = "\(self.swiftModuleName)Module+SwiftJava" + let moduleFilename = "\(moduleFilenameBase).swift" + + do { + logger.trace("Printing swift module class: \(moduleFilename)") + + try printGlobalSwiftThunkSources(&printer) + + if let outputFile = try printer.writeContents( + outputDirectory: self.swiftOutputDirectory, + javaPackagePath: javaPackagePath, + filename: moduleFilename + ) { + print("[swift-java] Generated: \(moduleFilenameBase.bold).swift (at \(outputFile)") + } + } catch { + logger.warning("Failed to write to Swift thunks: \(moduleFilename)") + } + } +} + +extension JNISwift2JavaGenerator { + private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { + printer.print( + """ + // Generated by swift-java + + import JavaKit + + """) + + for decl in analysis.importedGlobalFuncs { + printSwiftFunctionThunk(&printer, decl) + printer.println() + } + } + + private func printSwiftFunctionThunk(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + // TODO: Replace swiftModuleName with class name if non-global + let cName = "Java_" + self.javaPackage.replacingOccurrences(of: ".", with: "_") + "_\(swiftModuleName)_" + decl.name + let thunkName = thunkNameRegistry.functionThunkName(decl: decl) + let translatedParameters = decl.functionSignature.parameters.enumerated().map { idx, param in + (param.parameterName ?? "arg\(idx)", param.type.javaType) + } + + let thunkParameters = [ + "environment: UnsafeMutablePointer!", + "thisClass: jclass" + ] + translatedParameters.map { "\($0.0): \($0.1.jniTypeName)"} + let swiftReturnType = decl.functionSignature.result.type + + let thunkReturnType = !swiftReturnType.isVoid ? " -> \(swiftReturnType.javaType.jniTypeName)" : "" + + printer.printBraceBlock( + """ + @_cdecl("\(cName)") + func \(thunkName)(\(thunkParameters.joined(separator: ", ")))\(thunkReturnType) + """ + ) { printer in + let downcallParameters = zip(decl.functionSignature.parameters, translatedParameters).map { originalParam, translatedParam in + let label = originalParam.argumentLabel ?? originalParam.parameterName ?? "" + return "\(label)\(!label.isEmpty ? ": " : "")\(originalParam.type)(fromJNI: \(translatedParam.0), in: environment!)" + } + let functionDowncall = "\(swiftModuleName).\(decl.name)(\(downcallParameters.joined(separator: ", ")))" + + if swiftReturnType.isVoid { + printer.print(functionDowncall) + } else { + printer.print( + """ + let result = \(functionDowncall) + return result.getJNIValue(in: environment)") + """ + ) + } + } + } +} + +extension JNISwift2JavaGenerator { + private func printModule(_ printer: inout CodePrinter) { + printHeader(&printer) + printPackage(&printer) + + printModuleClass(&printer) { printer in + for decl in analysis.importedGlobalFuncs { + self.logger.trace("Print global function: \(decl)") + printFunctionBinding(&printer, decl) + printer.println() + } + } + } + + private func printHeader(_ printer: inout CodePrinter) { + printer.print( + """ + // Generated by jextract-swift + // Swift module: \(swiftModuleName) + + """ + ) + } + + private func printPackage(_ printer: inout CodePrinter) { + printer.print( + """ + package \(javaPackage); + + """ + ) + } + + private func printModuleClass(_ printer: inout CodePrinter, body: (inout CodePrinter) -> Void) { + printer.printBraceBlock("public final class \(swiftModuleName)") { printer in + body(&printer) + } + } + + private func printFunctionBinding(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + let returnType = decl.functionSignature.result.type.javaType + let params = decl.functionSignature.parameters.enumerated().map { idx, param in + "\(param.type.javaType) \(param.parameterName ?? "arg\(idx))")" + } + + printer.print( + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * \(decl.signatureString) + * } + */ + """ + ) + printer.print("public static native \(returnType) \(decl.name)(\(params.joined(separator: ", ")));") + } +} + +extension SwiftType { + var javaType: JavaType { + switch self { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownStandardLibraryType { + guard let javaType = knownType.javaType else { + fatalError("unsupported known type: \(knownType)") + } + return javaType + } + + fatalError("unsupported nominal type: \(nominalType)") + + case .tuple([]): + return .void + + case .metatype, .optional, .tuple, .function: + fatalError("unsupported type: \(self)") + } + } +} + +extension SwiftStandardLibraryTypeKind { + var javaType: JavaType? { + switch self { + case .bool: .boolean + case .int: .long // TODO: Handle 32-bit or 64-bit + case .int8: .byte + case .uint16: .char + case .int16: .short + case .int32: .int + case .int64: .long + case .float: .float + case .double: .double + case .void: .void + case .uint, .uint8, .uint32, .uint64, .unsafeRawPointer, .unsafeMutableRawPointer, .unsafePointer, .unsafeMutablePointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .string: nil + } + } +} diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 8adb7670..9b1b06bc 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -97,6 +97,16 @@ public struct SwiftToJava { javaOutputDirectory: outputJavaDirectory ) + try generator.generate() + + case .jni: + let generator = JNISwift2JavaGenerator( + translator: translator, + javaPackage: config.javaPackage ?? "", + swiftOutputDirectory: outputSwiftDirectory, + javaOutputDirectory: outputJavaDirectory + ) + try generator.generate() } diff --git a/Sources/JavaKitConfigurationShared/GenerationMode.swift b/Sources/JavaKitConfigurationShared/GenerationMode.swift index ff1d081d..ea30a436 100644 --- a/Sources/JavaKitConfigurationShared/GenerationMode.swift +++ b/Sources/JavaKitConfigurationShared/GenerationMode.swift @@ -15,4 +15,7 @@ public enum GenerationMode: String, Codable { /// Foreign Value and Memory API case ffm + + /// Java Native Interface + case jni } diff --git a/Sources/JavaTypes/JavaType+JNI.swift b/Sources/JavaTypes/JavaType+JNI.swift index 41a93d25..08361bac 100644 --- a/Sources/JavaTypes/JavaType+JNI.swift +++ b/Sources/JavaTypes/JavaType+JNI.swift @@ -14,7 +14,7 @@ extension JavaType { /// Map this Java type to the appropriate JNI type name. - var jniTypeName: String { + package var jniTypeName: String { switch self { case .boolean: "jboolean" case .float: "jfloat" diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index 3ea03d98..b3a09a22 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -14,6 +14,7 @@ import JExtractSwiftLib import Testing +import JavaKitConfigurationShared import struct Foundation.CharacterSet enum RenderKind { @@ -23,9 +24,10 @@ enum RenderKind { func assertOutput( dump: Bool = false, - _ translator: Swift2JavaTranslator, input: String, + _ mode: GenerationMode, _ renderKind: RenderKind, + swiftModuleName: String = "SwiftModule", detectChunkByInitialLines: Int = 4, expectedChunks: [String], fileID: String = #fileID, @@ -33,22 +35,42 @@ func assertOutput( line: Int = #line, column: Int = #column ) throws { - try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input) + let translator = Swift2JavaTranslator(swiftModuleName: swiftModuleName) - let generator = FFMSwift2JavaGenerator( - translator: translator, - javaPackage: "com.example.swift", - swiftOutputDirectory: "/fake", - javaOutputDirectory: "/fake" - ) + try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input) let output: String var printer: CodePrinter = CodePrinter(mode: .accumulateAll) - switch renderKind { - case .swift: - try generator.writeSwiftThunkSources(printer: &printer) - case .java: - try generator.writeExportedJavaSources(printer: &printer) + switch mode { + case .ffm: + let generator = FFMSwift2JavaGenerator( + translator: translator, + javaPackage: "com.example.swift", + swiftOutputDirectory: "/fake", + javaOutputDirectory: "/fake" + ) + + switch renderKind { + case .swift: + try generator.writeSwiftThunkSources(printer: &printer) + case .java: + try generator.writeExportedJavaSources(printer: &printer) + } + + case .jni: + let generator = JNISwift2JavaGenerator( + translator: translator, + javaPackage: "com.example.swift", + swiftOutputDirectory: "/fake", + javaOutputDirectory: "/fake" + ) + + switch renderKind { + case .swift: + try generator.writeSwiftThunkSources(&printer) + case .java: + try generator.writeExportedJavaSources(&printer) + } } output = printer.finalize() diff --git a/Tests/JExtractSwiftTests/ClassPrintingTests.swift b/Tests/JExtractSwiftTests/ClassPrintingTests.swift index f0c59456..6c0d8d0a 100644 --- a/Tests/JExtractSwiftTests/ClassPrintingTests.swift +++ b/Tests/JExtractSwiftTests/ClassPrintingTests.swift @@ -41,11 +41,7 @@ struct ClassPrintingTests { @Test("Import: class layout") func class_layout() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) - - try assertOutput(st, input: class_interfaceFile, .java, expectedChunks: [ + try assertOutput(input: class_interfaceFile, .ffm, .java, swiftModuleName: "__FakeModule", expectedChunks: [ """ public static final SwiftAnyType TYPE_METADATA = new SwiftAnyType(SwiftKit.swiftjava.getType("__FakeModule", "MySwiftClass")); diff --git a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift new file mode 100644 index 00000000..c5df98a1 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// 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 JNIModuleTests { + let globalMethodWithPrimitives = """ + public func helloWorld() + public func takeIntegers(i1: Int8, i2: Int16, i3: Int32, i4: Int) -> UInt16 + public func otherPrimitives(b: Bool, f: Float, d: Double) + """ + + @Test + func generatesModuleJavaClass() throws { + let input = "public func helloWorld()" + + try assertOutput(input: input, .jni, .java, expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule + + package com.example.swift; + + public final class SwiftModule { + """ + ]) + } + + @Test + func globalMethodWithPrimitives_javaBindings() throws { + try assertOutput( + input: globalMethodWithPrimitives, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func helloWorld() + * } + */ + public static native void helloWorld(); + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func takeIntegers(i1: Int8, i2: Int16, i3: Int32, i4: Int) -> UInt16 + * } + */ + public static native char takeIntegers(byte i1, short i2, int i3, long i4); + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func otherPrimitives(b: Bool, f: Float, d: Double) + * } + */ + public static native void otherPrimitives(boolean b, float f, double d); + """ + ] + ) + } + + @Test + func globalMethodWithPrimitives_swiftThunks() throws { + try assertOutput( + input: globalMethodWithPrimitives, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule_helloWorld") + func swiftjava_SwiftModule_helloWorld(environment: UnsafeMutablePointer!, thisClass: jclass) { + SwiftModule.helloWorld() + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule_takeIntegers") + func swiftjava_SwiftModule_takeIntegers_i1_i2_i3_i4(environment: UnsafeMutablePointer!, thisClass: jclass, i1: jbyte, i2: jshort, i3: jint, i4: jlong) -> jchar { + let result = SwiftModule.takeIntegers(i1: Int8(fromJNI: i1, in: environment!), i2: Int16(fromJNI: i2, in: environment!), i3: Int32(fromJNI: i3, in: environment!), i4: Int(fromJNI: i4, in: environment!)) + return result.getJNIValue(in: environment)") + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule_otherPrimitives") + func swiftjava_SwiftModule_otherPrimitives_b_f_d(environment: UnsafeMutablePointer!, thisClass: jclass, b: jboolean, f: jfloat, d: jdouble) { + SwiftModule.otherPrimitives(b: Bool(fromJNI: b, in: environment!), f: Float(fromJNI: f, in: environment!), d: Double(fromJNI: d, in: environment!)) + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/MethodThunkTests.swift b/Tests/JExtractSwiftTests/MethodThunkTests.swift index b91c0d8f..dab550b3 100644 --- a/Tests/JExtractSwiftTests/MethodThunkTests.swift +++ b/Tests/JExtractSwiftTests/MethodThunkTests.swift @@ -32,13 +32,9 @@ final class MethodThunkTests { @Test("Thunk overloads: globalFunc(a: Int32, b: Int64) & globalFunc(i32: Int32, l: Int64)") func thunk_overloads() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "FakeModule" - ) - st.log.logLevel = .error - try assertOutput( - st, input: input, .swift, + input: input, .ffm, .swift, + swiftModuleName: "FakeModule", detectChunkByInitialLines: 1, expectedChunks: [ diff --git a/Tests/JExtractSwiftTests/StringPassingTests.swift b/Tests/JExtractSwiftTests/StringPassingTests.swift index 92ce8ea6..99dfab5c 100644 --- a/Tests/JExtractSwiftTests/StringPassingTests.swift +++ b/Tests/JExtractSwiftTests/StringPassingTests.swift @@ -25,13 +25,9 @@ final class StringPassingTests { @Test("Import: public func writeString(string: String) -> Int") func method_helloWorld() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) - st.log.logLevel = .trace - try assertOutput( - st, input: class_interfaceFile, .java, + input: class_interfaceFile, .ffm, .java, + swiftModuleName: "__FakeModule", expectedChunks: [ """ /** diff --git a/Tests/JExtractSwiftTests/VariableImportTests.swift b/Tests/JExtractSwiftTests/VariableImportTests.swift index 562b92ae..33f41e20 100644 --- a/Tests/JExtractSwiftTests/VariableImportTests.swift +++ b/Tests/JExtractSwiftTests/VariableImportTests.swift @@ -35,13 +35,9 @@ final class VariableImportTests { @Test("Import: var counter: Int") func variable_int() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "FakeModule" - ) - st.log.logLevel = .error - try assertOutput( - st, input: class_interfaceFile, .java, + input: class_interfaceFile, .ffm, .java, + swiftModuleName: "FakeModule", detectChunkByInitialLines: 8, expectedChunks: [ """