Skip to content

[jextract] Generate code for free functions with primitive types in JNI mode #269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Sources/JExtractSwiftLib/ImportedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible {
self.swiftDecl.signatureString
}


var parentType: SwiftType? {
guard let selfParameter = functionSignature.selfParameter else {
return nil
Expand Down
253 changes: 253 additions & 0 deletions Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift
Original file line number Diff line number Diff line change
@@ -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<JNIEnv?>!",
"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
}
}
}
10 changes: 10 additions & 0 deletions Sources/JExtractSwiftLib/Swift2Java.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/JavaKitConfigurationShared/GenerationMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@
public enum GenerationMode: String, Codable {
/// Foreign Value and Memory API
case ffm

/// Java Native Interface
case jni
}
2 changes: 1 addition & 1 deletion Sources/JavaTypes/JavaType+JNI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
48 changes: 35 additions & 13 deletions Tests/JExtractSwiftTests/Asserts/TextAssertions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import JExtractSwiftLib
import Testing
import JavaKitConfigurationShared
import struct Foundation.CharacterSet

enum RenderKind {
Expand All @@ -23,32 +24,53 @@ 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,
filePath: String = #filePath,
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()

Expand Down
6 changes: 1 addition & 5 deletions Tests/JExtractSwiftTests/ClassPrintingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Loading