Skip to content

Commit 660acb7

Browse files
authored
[jextract] Generate code for free functions with primitive types in JNI mode (#269)
* generate java code for primitive types and global functions * output swift thunk for JNI * add tests * remove unsigned integer support for now
1 parent 97770cf commit 660acb7

File tree

11 files changed

+419
-38
lines changed

11 files changed

+419
-38
lines changed

Sources/JExtractSwiftLib/ImportedDecls.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible {
6060
self.swiftDecl.signatureString
6161
}
6262

63-
6463
var parentType: SwiftType? {
6564
guard let selfParameter = functionSignature.selfParameter else {
6665
return nil
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import JavaTypes
16+
17+
package class JNISwift2JavaGenerator: Swift2JavaGenerator {
18+
let analysis: AnalysisResult
19+
let swiftModuleName: String
20+
let javaPackage: String
21+
let logger: Logger
22+
let swiftOutputDirectory: String
23+
let javaOutputDirectory: String
24+
25+
var javaPackagePath: String {
26+
javaPackage.replacingOccurrences(of: ".", with: "/")
27+
}
28+
29+
var thunkNameRegistry = ThunkNameRegistry()
30+
31+
package init(
32+
translator: Swift2JavaTranslator,
33+
javaPackage: String,
34+
swiftOutputDirectory: String,
35+
javaOutputDirectory: String
36+
) {
37+
self.logger = Logger(label: "jni-generator", logLevel: translator.log.logLevel)
38+
self.analysis = translator.result
39+
self.swiftModuleName = translator.swiftModuleName
40+
self.javaPackage = javaPackage
41+
self.swiftOutputDirectory = swiftOutputDirectory
42+
self.javaOutputDirectory = javaOutputDirectory
43+
}
44+
45+
func generate() throws {
46+
try writeSwiftThunkSources()
47+
try writeExportedJavaSources()
48+
}
49+
}
50+
51+
extension JNISwift2JavaGenerator {
52+
func writeExportedJavaSources() throws {
53+
var printer = CodePrinter()
54+
try writeExportedJavaSources(&printer)
55+
}
56+
57+
package func writeExportedJavaSources(_ printer: inout CodePrinter) throws {
58+
let filename = "\(self.swiftModuleName).java"
59+
logger.trace("Printing module class: \(filename)")
60+
printModule(&printer)
61+
62+
if let outputFile = try printer.writeContents(
63+
outputDirectory: javaOutputDirectory,
64+
javaPackagePath: javaPackagePath,
65+
filename: filename
66+
) {
67+
logger.info("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))")
68+
}
69+
}
70+
71+
func writeSwiftThunkSources() throws {
72+
var printer = CodePrinter()
73+
try writeSwiftThunkSources(&printer)
74+
}
75+
76+
package func writeSwiftThunkSources(_ printer: inout CodePrinter) throws {
77+
let moduleFilenameBase = "\(self.swiftModuleName)Module+SwiftJava"
78+
let moduleFilename = "\(moduleFilenameBase).swift"
79+
80+
do {
81+
logger.trace("Printing swift module class: \(moduleFilename)")
82+
83+
try printGlobalSwiftThunkSources(&printer)
84+
85+
if let outputFile = try printer.writeContents(
86+
outputDirectory: self.swiftOutputDirectory,
87+
javaPackagePath: javaPackagePath,
88+
filename: moduleFilename
89+
) {
90+
print("[swift-java] Generated: \(moduleFilenameBase.bold).swift (at \(outputFile)")
91+
}
92+
} catch {
93+
logger.warning("Failed to write to Swift thunks: \(moduleFilename)")
94+
}
95+
}
96+
}
97+
98+
extension JNISwift2JavaGenerator {
99+
private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws {
100+
printer.print(
101+
"""
102+
// Generated by swift-java
103+
104+
import JavaKit
105+
106+
""")
107+
108+
for decl in analysis.importedGlobalFuncs {
109+
printSwiftFunctionThunk(&printer, decl)
110+
printer.println()
111+
}
112+
}
113+
114+
private func printSwiftFunctionThunk(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
115+
// TODO: Replace swiftModuleName with class name if non-global
116+
let cName = "Java_" + self.javaPackage.replacingOccurrences(of: ".", with: "_") + "_\(swiftModuleName)_" + decl.name
117+
let thunkName = thunkNameRegistry.functionThunkName(decl: decl)
118+
let translatedParameters = decl.functionSignature.parameters.enumerated().map { idx, param in
119+
(param.parameterName ?? "arg\(idx)", param.type.javaType)
120+
}
121+
122+
let thunkParameters = [
123+
"environment: UnsafeMutablePointer<JNIEnv?>!",
124+
"thisClass: jclass"
125+
] + translatedParameters.map { "\($0.0): \($0.1.jniTypeName)"}
126+
let swiftReturnType = decl.functionSignature.result.type
127+
128+
let thunkReturnType = !swiftReturnType.isVoid ? " -> \(swiftReturnType.javaType.jniTypeName)" : ""
129+
130+
printer.printBraceBlock(
131+
"""
132+
@_cdecl("\(cName)")
133+
func \(thunkName)(\(thunkParameters.joined(separator: ", ")))\(thunkReturnType)
134+
"""
135+
) { printer in
136+
let downcallParameters = zip(decl.functionSignature.parameters, translatedParameters).map { originalParam, translatedParam in
137+
let label = originalParam.argumentLabel ?? originalParam.parameterName ?? ""
138+
return "\(label)\(!label.isEmpty ? ": " : "")\(originalParam.type)(fromJNI: \(translatedParam.0), in: environment!)"
139+
}
140+
let functionDowncall = "\(swiftModuleName).\(decl.name)(\(downcallParameters.joined(separator: ", ")))"
141+
142+
if swiftReturnType.isVoid {
143+
printer.print(functionDowncall)
144+
} else {
145+
printer.print(
146+
"""
147+
let result = \(functionDowncall)
148+
return result.getJNIValue(in: environment)")
149+
"""
150+
)
151+
}
152+
}
153+
}
154+
}
155+
156+
extension JNISwift2JavaGenerator {
157+
private func printModule(_ printer: inout CodePrinter) {
158+
printHeader(&printer)
159+
printPackage(&printer)
160+
161+
printModuleClass(&printer) { printer in
162+
for decl in analysis.importedGlobalFuncs {
163+
self.logger.trace("Print global function: \(decl)")
164+
printFunctionBinding(&printer, decl)
165+
printer.println()
166+
}
167+
}
168+
}
169+
170+
private func printHeader(_ printer: inout CodePrinter) {
171+
printer.print(
172+
"""
173+
// Generated by jextract-swift
174+
// Swift module: \(swiftModuleName)
175+
176+
"""
177+
)
178+
}
179+
180+
private func printPackage(_ printer: inout CodePrinter) {
181+
printer.print(
182+
"""
183+
package \(javaPackage);
184+
185+
"""
186+
)
187+
}
188+
189+
private func printModuleClass(_ printer: inout CodePrinter, body: (inout CodePrinter) -> Void) {
190+
printer.printBraceBlock("public final class \(swiftModuleName)") { printer in
191+
body(&printer)
192+
}
193+
}
194+
195+
private func printFunctionBinding(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
196+
let returnType = decl.functionSignature.result.type.javaType
197+
let params = decl.functionSignature.parameters.enumerated().map { idx, param in
198+
"\(param.type.javaType) \(param.parameterName ?? "arg\(idx))")"
199+
}
200+
201+
printer.print(
202+
"""
203+
/**
204+
* Downcall to Swift:
205+
* {@snippet lang=swift :
206+
* \(decl.signatureString)
207+
* }
208+
*/
209+
"""
210+
)
211+
printer.print("public static native \(returnType) \(decl.name)(\(params.joined(separator: ", ")));")
212+
}
213+
}
214+
215+
extension SwiftType {
216+
var javaType: JavaType {
217+
switch self {
218+
case .nominal(let nominalType):
219+
if let knownType = nominalType.nominalTypeDecl.knownStandardLibraryType {
220+
guard let javaType = knownType.javaType else {
221+
fatalError("unsupported known type: \(knownType)")
222+
}
223+
return javaType
224+
}
225+
226+
fatalError("unsupported nominal type: \(nominalType)")
227+
228+
case .tuple([]):
229+
return .void
230+
231+
case .metatype, .optional, .tuple, .function:
232+
fatalError("unsupported type: \(self)")
233+
}
234+
}
235+
}
236+
237+
extension SwiftStandardLibraryTypeKind {
238+
var javaType: JavaType? {
239+
switch self {
240+
case .bool: .boolean
241+
case .int: .long // TODO: Handle 32-bit or 64-bit
242+
case .int8: .byte
243+
case .uint16: .char
244+
case .int16: .short
245+
case .int32: .int
246+
case .int64: .long
247+
case .float: .float
248+
case .double: .double
249+
case .void: .void
250+
case .uint, .uint8, .uint32, .uint64, .unsafeRawPointer, .unsafeMutableRawPointer, .unsafePointer, .unsafeMutablePointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .string: nil
251+
}
252+
}
253+
}

Sources/JExtractSwiftLib/Swift2Java.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ public struct SwiftToJava {
9797
javaOutputDirectory: outputJavaDirectory
9898
)
9999

100+
try generator.generate()
101+
102+
case .jni:
103+
let generator = JNISwift2JavaGenerator(
104+
translator: translator,
105+
javaPackage: config.javaPackage ?? "",
106+
swiftOutputDirectory: outputSwiftDirectory,
107+
javaOutputDirectory: outputJavaDirectory
108+
)
109+
100110
try generator.generate()
101111
}
102112

Sources/JavaKitConfigurationShared/GenerationMode.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@
1515
public enum GenerationMode: String, Codable {
1616
/// Foreign Value and Memory API
1717
case ffm
18+
19+
/// Java Native Interface
20+
case jni
1821
}

Sources/JavaTypes/JavaType+JNI.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
extension JavaType {
1616
/// Map this Java type to the appropriate JNI type name.
17-
var jniTypeName: String {
17+
package var jniTypeName: String {
1818
switch self {
1919
case .boolean: "jboolean"
2020
case .float: "jfloat"

Tests/JExtractSwiftTests/Asserts/TextAssertions.swift

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import JExtractSwiftLib
1616
import Testing
17+
import JavaKitConfigurationShared
1718
import struct Foundation.CharacterSet
1819

1920
enum RenderKind {
@@ -23,32 +24,53 @@ enum RenderKind {
2324

2425
func assertOutput(
2526
dump: Bool = false,
26-
_ translator: Swift2JavaTranslator,
2727
input: String,
28+
_ mode: GenerationMode,
2829
_ renderKind: RenderKind,
30+
swiftModuleName: String = "SwiftModule",
2931
detectChunkByInitialLines: Int = 4,
3032
expectedChunks: [String],
3133
fileID: String = #fileID,
3234
filePath: String = #filePath,
3335
line: Int = #line,
3436
column: Int = #column
3537
) throws {
36-
try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input)
38+
let translator = Swift2JavaTranslator(swiftModuleName: swiftModuleName)
3739

38-
let generator = FFMSwift2JavaGenerator(
39-
translator: translator,
40-
javaPackage: "com.example.swift",
41-
swiftOutputDirectory: "/fake",
42-
javaOutputDirectory: "/fake"
43-
)
40+
try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input)
4441

4542
let output: String
4643
var printer: CodePrinter = CodePrinter(mode: .accumulateAll)
47-
switch renderKind {
48-
case .swift:
49-
try generator.writeSwiftThunkSources(printer: &printer)
50-
case .java:
51-
try generator.writeExportedJavaSources(printer: &printer)
44+
switch mode {
45+
case .ffm:
46+
let generator = FFMSwift2JavaGenerator(
47+
translator: translator,
48+
javaPackage: "com.example.swift",
49+
swiftOutputDirectory: "/fake",
50+
javaOutputDirectory: "/fake"
51+
)
52+
53+
switch renderKind {
54+
case .swift:
55+
try generator.writeSwiftThunkSources(printer: &printer)
56+
case .java:
57+
try generator.writeExportedJavaSources(printer: &printer)
58+
}
59+
60+
case .jni:
61+
let generator = JNISwift2JavaGenerator(
62+
translator: translator,
63+
javaPackage: "com.example.swift",
64+
swiftOutputDirectory: "/fake",
65+
javaOutputDirectory: "/fake"
66+
)
67+
68+
switch renderKind {
69+
case .swift:
70+
try generator.writeSwiftThunkSources(&printer)
71+
case .java:
72+
try generator.writeExportedJavaSources(&printer)
73+
}
5274
}
5375
output = printer.finalize()
5476

Tests/JExtractSwiftTests/ClassPrintingTests.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,7 @@ struct ClassPrintingTests {
4141

4242
@Test("Import: class layout")
4343
func class_layout() throws {
44-
let st = Swift2JavaTranslator(
45-
swiftModuleName: "__FakeModule"
46-
)
47-
48-
try assertOutput(st, input: class_interfaceFile, .java, expectedChunks: [
44+
try assertOutput(input: class_interfaceFile, .ffm, .java, swiftModuleName: "__FakeModule", expectedChunks: [
4945
"""
5046
public static final SwiftAnyType TYPE_METADATA =
5147
new SwiftAnyType(SwiftKit.swiftjava.getType("__FakeModule", "MySwiftClass"));

0 commit comments

Comments
 (0)