Skip to content

Commit e492e45

Browse files
committed
add caching
1 parent 2770318 commit e492e45

File tree

8 files changed

+187
-7
lines changed

8 files changed

+187
-7
lines changed

Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
114114
outputSwiftDirectory.appending(path: "\(sourceModule.name)Module+SwiftJava.swift")
115115
]
116116

117+
// Append any JNI cache files
118+
if configuration?.mode == .jni {
119+
outputSwiftFiles += [
120+
outputSwiftDirectory.appending(path: "\(sourceModule.name)+JNICaches.swift")
121+
]
122+
}
123+
117124
// If the module uses 'Data' type, the thunk file is emitted as if 'Data' is declared
118125
// in that module. Declare the thunk file as the output.
119126
// FIXME: Make this conditional.

Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.ArrayList;
2323
import java.util.List;
2424
import java.util.Optional;
25+
import java.util.OptionalInt;
2526
import java.util.concurrent.TimeUnit;
2627

2728
@BenchmarkMode(Mode.AverageTime)
@@ -39,7 +40,7 @@ public static class BenchmarkState {
3940
@Setup(Level.Trial)
4041
public void beforeAll() {
4142
arena = new ConfinedSwiftMemorySession();
42-
vehicle = Vehicle.motorbike("Yamaha", 900, arena);
43+
vehicle = Vehicle.motorbike("Yamaha", 900, OptionalInt.empty(), arena);
4344
}
4445

4546
@TearDown(Level.Trial)
@@ -49,7 +50,7 @@ public void afterAll() {
4950
}
5051

5152
@Benchmark
52-
public Vehicle.Motorbike java_copy(BenchmarkState state, Blackhole bh) {
53+
public Vehicle.Motorbike getAssociatedValues(BenchmarkState state, Blackhole bh) {
5354
Vehicle.Motorbike motorbike = state.vehicle.getAsMotorbike().orElseThrow();
5455
bh.consume(motorbike.arg0());
5556
bh.consume(motorbike.horsePower());

Sources/JExtractSwiftLib/Convenience/String+Extensions.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ extension String {
2424
return "\(f.uppercased())\(String(dropFirst()))"
2525
}
2626

27+
var firstCharacterLowercased: String {
28+
guard let f = first else {
29+
return self
30+
}
31+
32+
return "\(f.lowercased())\(String(dropFirst()))"
33+
}
34+
2735
/// Returns whether the string is of the format `isX`
2836
var hasJavaBooleanNamingConvention: Bool {
2937
guard self.hasPrefix("is"), self.count > 2 else {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
enum JNICaching {
16+
static func cacheName(for enumCase: ImportedEnumCase) -> String {
17+
"\(enumCase.enumType.nominalTypeDecl.name.firstCharacterLowercased)\(enumCase.name.firstCharacterUppercased)Cache"
18+
}
19+
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,14 +259,31 @@ extension JNISwift2JavaGenerator {
259259

260260
// Print record
261261
printer.printBraceBlock("public record \(caseName)(\(members.joined(separator: ", "))) implements Case") { printer in
262+
printer.printBraceBlock("static class $JNI") { printer in
263+
printer.print("private static native void $nativeInit();")
264+
}
265+
266+
// Used to ensure static initializer has been calling to trigger caching.
267+
printer.print("static void $ensureInitialized() {}")
268+
269+
printer.printBraceBlock("static") { printer in
270+
printer.print("$JNI.$nativeInit();")
271+
}
272+
262273
let nativeParameters = zip(translatedCase.translatedValues, translatedCase.parameterConversions).flatMap { value, conversion in
263274
["\(conversion.native.javaType) \(value.parameter.name)"]
264275
}
265276

266277
printer.print("record $NativeParameters(\(nativeParameters.joined(separator: ", "))) {}")
267278
}
268279

269-
self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction)
280+
self.printJavaBindingWrapperMethod(
281+
&printer,
282+
translatedCase.getAsCaseFunction,
283+
prefix: { printer in
284+
printer.print("\(caseName).$ensureInitialized();")
285+
}
286+
)
270287
printer.println()
271288
}
272289
}
@@ -335,7 +352,11 @@ extension JNISwift2JavaGenerator {
335352
printJavaBindingWrapperMethod(&printer, translatedDecl)
336353
}
337354

338-
private func printJavaBindingWrapperMethod(_ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl) {
355+
private func printJavaBindingWrapperMethod(
356+
_ printer: inout CodePrinter,
357+
_ translatedDecl: TranslatedFunctionDecl,
358+
prefix: (inout CodePrinter) -> Void = { _ in }
359+
) {
339360
var modifiers = ["public"]
340361
if translatedDecl.isStatic {
341362
modifiers.append("static")
@@ -358,6 +379,7 @@ extension JNISwift2JavaGenerator {
358379
printer.printBraceBlock(
359380
"\(annotationsStr)\(modifiersStr) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)"
360381
) { printer in
382+
prefix(&printer)
361383
printDowncall(&printer, translatedDecl)
362384
}
363385

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ extension JNISwift2JavaGenerator {
5656
self.expectedOutputSwiftFiles.remove(moduleFilename)
5757
}
5858

59+
try self.writeJNICacheSource(&printer)
60+
5961
for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) {
6062
let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava"
6163
let filename = "\(fileNameBase).swift"
@@ -80,6 +82,34 @@ extension JNISwift2JavaGenerator {
8082
}
8183
}
8284

85+
private func writeJNICacheSource(_ printer: inout CodePrinter) throws {
86+
printer.print("import JavaKit")
87+
printer.println()
88+
printer.printBraceBlock("enum JNICaches") { printer in
89+
let enumCases = self.analysis.importedTypes.values.filter { $0.swiftNominal.kind == .enum }.flatMap(\.cases)
90+
for enumCase in enumCases {
91+
printer.print("static var \(JNICaching.cacheName(for: enumCase)): _JNICache!")
92+
}
93+
printer.println()
94+
printer.printBraceBlock("func cleanup()") { printer in
95+
for enumCase in enumCases {
96+
printer.print("JNICaches.\(JNICaching.cacheName(for: enumCase)) = nil")
97+
}
98+
}
99+
}
100+
101+
let fileName = "\(self.swiftModuleName)+JNICaches.swift"
102+
103+
if let outputFile = try printer.writeContents(
104+
outputDirectory: self.swiftOutputDirectory,
105+
javaPackagePath: nil,
106+
filename: fileName
107+
) {
108+
print("[swift-java] Generated: \(fileName.bold) (at \(outputFile))")
109+
self.expectedOutputSwiftFiles.remove(fileName)
110+
}
111+
}
112+
83113
private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws {
84114
printHeader(&printer)
85115

@@ -156,12 +186,32 @@ extension JNISwift2JavaGenerator {
156186
printSwiftFunctionThunk(&printer, enumCase.caseFunction)
157187
printer.println()
158188

189+
// Print enum case native init
190+
printEnumNativeInit(&printer, translatedCase)
191+
159192
// Print getAsCase method
160193
if !translatedCase.translatedValues.isEmpty {
161194
printEnumGetAsCaseThunk(&printer, translatedCase)
162195
}
163196
}
164197

198+
private func printEnumNativeInit(_ printer: inout CodePrinter, _ enumCase: TranslatedEnumCase) {
199+
printCDecl(
200+
&printer,
201+
javaMethodName: "$nativeInit",
202+
parentName: "\(enumCase.original.enumType.nominalTypeDecl.name)$\(enumCase.name)$$JNI",
203+
parameters: [],
204+
resultType: .void
205+
) { printer in
206+
// Setup caching
207+
let nativeParametersClassName = "\(javaPackagePath)/\(enumCase.enumName)$\(enumCase.name)$$NativeParameters"
208+
let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType))
209+
let methods = #"[.init(name: "<init>", signature: "\#(methodSignature.mangledName)")]"#
210+
211+
printer.print(#"JNICaches.\#(JNICaching.cacheName(for: enumCase.original)) = _JNICache(environment: environment, className: "\#(nativeParametersClassName)", methods: \#(methods))"#)
212+
}
213+
}
214+
165215
private func printEnumGetAsCaseThunk(
166216
_ printer: inout CodePrinter,
167217
_ enumCase: TranslatedEnumCase
@@ -176,14 +226,15 @@ extension JNISwift2JavaGenerator {
176226
}
177227
let caseNamesWithLet = caseNames.map { "let \($0)" }
178228
let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType))
179-
// TODO: Caching of class and static method ID.
180229
printer.print(
181230
"""
182231
guard case .\(enumCase.original.name)(\(caseNamesWithLet.joined(separator: ", "))) = \(selfPointer).pointee else {
183232
fatalError("Expected enum case '\(enumCase.original.name)', but was '\\(\(selfPointer).pointee)'!")
184233
}
185-
let class$ = environment.interface.FindClass(environment, "\(javaPackagePath)/\(enumCase.enumName)$\(enumCase.name)$$NativeParameters")!
186-
let constructorID$ = environment.interface.GetMethodID(environment, class$, "<init>", "\(methodSignature.mangledName)")!
234+
let cache$ = JNICaches.\(JNICaching.cacheName(for: enumCase.original))!
235+
let class$ = cache$.javaClass
236+
let method$ = _JNICache.Method(name: "<init>", signature: "\(methodSignature.mangledName)")
237+
let constructorID$ = cache$[method$]
187238
"""
188239
)
189240
let upcallArguments = zip(enumCase.parameterConversions, caseNames).map { conversion, caseName in

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
7272
return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift"))
7373
})
7474
self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift")
75+
self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)+JNICaches.swift")
7576

7677
// FIXME: Can we avoid this?
7778
self.expectedOutputSwiftFiles.insert("Data+SwiftJava.swift")
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 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+
/// A cache used to hold references for JNI method and classes.
16+
///
17+
/// This type is used internally in by the outputted JExtract wrappers
18+
/// to improve performance of any JNI lookups.
19+
///
20+
/// - Warning: This type is inherently thread-unsafe.
21+
/// We assume that there exists **at most** one of these
22+
/// per Java `class`, and it must only be initialized and modified in the
23+
/// static initializer of that Java class.
24+
public final class _JNICache: @unchecked Sendable {
25+
public struct Method: Hashable {
26+
public let name: String
27+
public let signature: String
28+
29+
public init(name: String, signature: String) {
30+
self.name = name
31+
self.signature = signature
32+
}
33+
}
34+
35+
var _class: jclass?
36+
let environment: JNIEnvironment
37+
let methods: [Method: jmethodID]
38+
39+
public var javaClass: jclass {
40+
self._class!
41+
}
42+
43+
public init(environment: UnsafeMutablePointer<JNIEnv?>!, className: String, methods: [Method]) {
44+
guard let clazz = environment.interface.FindClass(environment, className) else {
45+
fatalError("Class \(className) could not be found!")
46+
}
47+
self.environment = environment
48+
self._class = environment.interface.NewGlobalRef(environment, clazz)!
49+
self.methods = methods.reduce(into: [:]) { (result, method) in
50+
if let methodID = environment.interface.GetMethodID(environment, clazz, method.name, method.signature) {
51+
result[method] = methodID
52+
} else {
53+
fatalError("Method \(method.signature) with signature \(method.signature) not found in class \(className)")
54+
}
55+
}
56+
}
57+
58+
59+
public subscript(_ method: Method) -> jmethodID? {
60+
methods[method]
61+
}
62+
63+
func cleanup() {
64+
environment.interface.DeleteGlobalRef(environment, self._class)
65+
}
66+
67+
deinit {
68+
cleanup()
69+
self._class = nil
70+
}
71+
}

0 commit comments

Comments
 (0)