Skip to content

Commit 02f6a1d

Browse files
committed
add support for JavaKit parameters
1 parent c7adc8e commit 02f6a1d

File tree

15 files changed

+272
-77
lines changed

15 files changed

+272
-77
lines changed

Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import Foundation
1616
import PackagePlugin
1717

18+
fileprivate let SwiftJavaConfigFileName = "swift-java.config"
19+
1820
@main
1921
struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
2022

@@ -51,12 +53,55 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
5153
log("Skipping jextract step, no 'javaPackage' configuration in \(getSwiftJavaConfigPath(target: target) ?? "")")
5254
return []
5355
}
54-
56+
5557
// We use the the usual maven-style structure of "src/[generated|main|test]/java/..."
5658
// that is common in JVM ecosystem
5759
let outputJavaDirectory = context.outputJavaDirectory
5860
let outputSwiftDirectory = context.outputSwiftDirectory
5961

62+
/// Find the manifest files from other swift-java executions in any targets
63+
/// this target depends on.
64+
var dependentConfigFiles: [(String, URL)] = []
65+
func searchForConfigFiles(in target: any Target) {
66+
// log("Search for config files in target: \(target.name)")
67+
let dependencyURL = URL(filePath: target.directory.string)
68+
69+
// Look for a config file within this target.
70+
let dependencyConfigURL = dependencyURL
71+
.appending(path: SwiftJavaConfigFileName)
72+
let dependencyConfigString = dependencyConfigURL
73+
.path(percentEncoded: false)
74+
75+
if FileManager.default.fileExists(atPath: dependencyConfigString) {
76+
dependentConfigFiles.append((target.name, dependencyConfigURL))
77+
}
78+
}
79+
80+
// Process direct dependencies of this target.
81+
for dependency in target.dependencies {
82+
switch dependency {
83+
case .target(let target):
84+
// log("Dependency target: \(target.name)")
85+
searchForConfigFiles(in: target)
86+
87+
case .product(let product):
88+
// log("Dependency product: \(product.name)")
89+
for target in product.targets {
90+
// log("Dependency product: \(product.name), target: \(target.name)")
91+
searchForConfigFiles(in: target)
92+
}
93+
94+
@unknown default:
95+
break
96+
}
97+
}
98+
99+
// Process indirect target dependencies.
100+
for dependency in target.recursiveTargetDependencies {
101+
// log("Recursive dependency target: \(dependency.name)")
102+
searchForConfigFiles(in: dependency)
103+
}
104+
60105
var arguments: [String] = [
61106
/*subcommand=*/"jextract",
62107
"--swift-module", sourceModule.name,
@@ -71,6 +116,16 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
71116
// as it depends on the contents of the input files. Therefore we have to implement this as a prebuild plugin.
72117
// We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc.
73118
]
119+
120+
let dependentConfigFilesArguments = dependentConfigFiles.flatMap { moduleAndConfigFile in
121+
let (moduleName, configFile) = moduleAndConfigFile
122+
return [
123+
"--depends-on",
124+
"\(configFile.path(percentEncoded: false))"
125+
]
126+
}
127+
arguments += dependentConfigFilesArguments
128+
74129
if !javaPackage.isEmpty {
75130
arguments += ["--java-package", javaPackage]
76131
}

Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import JavaKit
16+
1517
public class MySwiftClass {
1618
public let x: Int64
1719
public let y: Int64
@@ -84,4 +86,8 @@ public class MySwiftClass {
8486
public func copy() -> MySwiftClass {
8587
return MySwiftClass(x: self.x, y: self.y)
8688
}
89+
90+
public func addXWithJavaLong(_ other: JavaLong) -> Int64 {
91+
return self.x + other.longValue()
92+
}
8793
}

Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,13 @@ void copy() {
139139
assertNotEquals(c1.$memoryAddress(), c2.$memoryAddress());
140140
}
141141
}
142+
143+
@Test
144+
void addXWithJavaLong() {
145+
try (var arena = new ConfinedSwiftMemorySession()) {
146+
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
147+
Long javaLong = new Long(50);
148+
assertEquals(70, c1.addXWithJavaLong(javaLong));
149+
}
150+
}
142151
}

Sources/JExtractSwiftLib/Convenience/String+Extensions.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import JavaTypes
16+
1517
extension String {
1618

1719
var firstCharacterUppercased: String {
@@ -31,4 +33,41 @@ extension String {
3133
let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2)
3234
return self[thirdCharacterIndex].isUppercase
3335
}
36+
37+
/// Returns a version of the string correctly escaped for a JNI
38+
var escapedJNIIdentifier: String {
39+
self.map {
40+
if $0 == "_" {
41+
return "_1"
42+
} else if $0 == "/" {
43+
return "_"
44+
} else if $0 == ";" {
45+
return "_2"
46+
} else if $0 == "[" {
47+
return "_3"
48+
} else if $0.isASCII && ($0.isLetter || $0.isNumber) {
49+
return String($0)
50+
} else if let utf16 = $0.utf16.first {
51+
// Escape any non-alphanumeric to their UTF16 hex encoding
52+
let utf16Hex = String(format: "%04x", utf16)
53+
return "_0\(utf16Hex)"
54+
} else {
55+
fatalError("Invalid JNI character: \($0)")
56+
}
57+
}
58+
.joined()
59+
}
60+
61+
/// Looks up self as a JavaKit wrapped class name and converts it
62+
/// into a `JavaType.class` if it exists in `lookupTable`.
63+
func parseJavaClassFromJavaKitName(in lookupTable: [String: String]) -> JavaType? {
64+
guard let canonicalJavaName = lookupTable[self] else {
65+
return nil
66+
}
67+
let nameParts = canonicalJavaName.components(separatedBy: ".")
68+
let javaPackageName = nameParts.dropLast().joined(separator: ".")
69+
let javaClassName = nameParts.last!
70+
71+
return .class(package: javaPackageName, name: javaClassName)
72+
}
3473
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ extension JNISwift2JavaGenerator {
2424

2525
let translated: TranslatedFunctionDecl?
2626
do {
27-
let translation = JavaTranslation(swiftModuleName: swiftModuleName, javaPackage: self.javaPackage)
27+
let translation = JavaTranslation(
28+
swiftModuleName: swiftModuleName,
29+
javaPackage: self.javaPackage,
30+
javaClassLookupTable: self.javaClassLookupTable
31+
)
2832
translated = try translation.translate(decl)
2933
} catch {
3034
self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)")
@@ -38,9 +42,13 @@ extension JNISwift2JavaGenerator {
3842
struct JavaTranslation {
3943
let swiftModuleName: String
4044
let javaPackage: String
45+
let javaClassLookupTable: [String: String]
4146

4247
func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl {
43-
let nativeTranslation = NativeJavaTranslation(javaPackage: self.javaPackage)
48+
let nativeTranslation = NativeJavaTranslation(
49+
javaPackage: self.javaPackage,
50+
javaClassLookupTable: self.javaClassLookupTable
51+
)
4452

4553
// Types with no parent will be outputted inside a "module" class.
4654
let parentName = decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName
@@ -157,6 +165,8 @@ extension JNISwift2JavaGenerator {
157165
) throws -> TranslatedParameter {
158166
switch swiftType {
159167
case .nominal(let nominalType):
168+
let nominalTypeName = nominalType.nominalTypeDecl.name
169+
160170
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
161171
guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else {
162172
throw JavaTranslationError.unsupportedSwiftType(swiftType)
@@ -168,11 +178,25 @@ extension JNISwift2JavaGenerator {
168178
)
169179
}
170180

171-
// For now, we assume this is a JExtract class.
181+
if nominalType.isJavaKitWrapper {
182+
guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else {
183+
throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType)
184+
}
185+
186+
return TranslatedParameter(
187+
parameter: JavaParameter(
188+
name: parameterName,
189+
type: javaType
190+
),
191+
conversion: .placeholder
192+
)
193+
}
194+
195+
// We assume this is a JExtract class.
172196
return TranslatedParameter(
173197
parameter: JavaParameter(
174198
name: parameterName,
175-
type: .class(package: nil, name: nominalType.nominalTypeDecl.name)
199+
type: .class(package: nil, name: nominalTypeName)
176200
),
177201
conversion: .valueMemoryAddress(.placeholder)
178202
)
@@ -213,7 +237,11 @@ extension JNISwift2JavaGenerator {
213237
)
214238
}
215239

216-
// For now, we assume this is a JExtract class.
240+
if nominalType.isJavaKitWrapper {
241+
throw JavaTranslationError.unsupportedSwiftType(swiftResult.type)
242+
}
243+
244+
// We assume this is a JExtract class.
217245
let javaType = JavaType.class(package: nil, name: nominalType.nominalTypeDecl.name)
218246
return TranslatedResult(
219247
javaType: javaType,
@@ -350,5 +378,9 @@ extension JNISwift2JavaGenerator {
350378

351379
enum JavaTranslationError: Error {
352380
case unsupportedSwiftType(SwiftType)
381+
382+
/// The user has not supplied a mapping from `SwiftType` to
383+
/// a java class.
384+
case wrappedJavaClassTranslationNotProvided(SwiftType)
353385
}
354386
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ extension JNISwift2JavaGenerator {
1818

1919
struct NativeJavaTranslation {
2020
let javaPackage: String
21+
let javaClassLookupTable: [String: String]
2122

2223
/// Translates a Swift function into the native JNI method signature.
2324
func translate(
@@ -66,6 +67,8 @@ extension JNISwift2JavaGenerator {
6667
) throws -> NativeParameter {
6768
switch swiftParameter.type {
6869
case .nominal(let nominalType):
70+
let nominalTypeName = nominalType.nominalTypeDecl.name
71+
6972
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
7073
guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else {
7174
throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type)
@@ -78,6 +81,25 @@ extension JNISwift2JavaGenerator {
7881
)
7982
}
8083

84+
if nominalType.isJavaKitWrapper {
85+
guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else {
86+
throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftParameter.type)
87+
}
88+
89+
return NativeParameter(
90+
name: parameterName,
91+
javaType: javaType,
92+
conversion: .initializeJavaKitWrapper(wrapperName: nominalTypeName)
93+
)
94+
}
95+
96+
// JExtract classes are passed as the pointer.
97+
return NativeParameter(
98+
name: parameterName,
99+
javaType: .long,
100+
conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: swiftParameter.type))
101+
)
102+
81103
case .tuple([]):
82104
return NativeParameter(
83105
name: parameterName,
@@ -110,13 +132,6 @@ extension JNISwift2JavaGenerator {
110132
case .metatype, .optional, .tuple, .existential, .opaque:
111133
throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type)
112134
}
113-
114-
// Classes are passed as the pointer.
115-
return NativeParameter(
116-
name: parameterName,
117-
javaType: .long,
118-
conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: swiftParameter.type))
119-
)
120135
}
121136

122137
func translateClosureResult(
@@ -193,6 +208,15 @@ extension JNISwift2JavaGenerator {
193208
)
194209
}
195210

211+
if nominalType.isJavaKitWrapper {
212+
throw JavaTranslationError.unsupportedSwiftType(swiftResult.type)
213+
}
214+
215+
return NativeResult(
216+
javaType: .long,
217+
conversion: .getJNIValue(.allocateSwiftValue(name: "result", swiftType: swiftResult.type))
218+
)
219+
196220
case .tuple([]):
197221
return NativeResult(
198222
javaType: .void,
@@ -203,13 +227,7 @@ extension JNISwift2JavaGenerator {
203227
throw JavaTranslationError.unsupportedSwiftType(swiftResult.type)
204228
}
205229

206-
// TODO: Handle other classes, for example from JavaKit macros.
207-
// for now we assume all passed in classes are JExtract generated
208-
// so we pass the pointer.
209-
return NativeResult(
210-
javaType: .long,
211-
conversion: .getJNIValue(.allocateSwiftValue(name: "result", swiftType: swiftResult.type))
212-
)
230+
213231
}
214232
}
215233

@@ -262,6 +280,8 @@ extension JNISwift2JavaGenerator {
262280

263281
indirect case closureLowering(parameters: [NativeParameter], result: NativeResult)
264282

283+
case initializeJavaKitWrapper(wrapperName: String)
284+
265285
/// Returns the conversion string applied to the placeholder.
266286
func render(_ printer: inout CodePrinter, _ placeholder: String) -> String {
267287
// NOTE: 'printer' is used if the conversion wants to cause side-effects.
@@ -348,6 +368,9 @@ extension JNISwift2JavaGenerator {
348368
printer.print("}")
349369

350370
return printer.finalize()
371+
372+
case .initializeJavaKitWrapper(let wrapperName):
373+
return "\(wrapperName)(javaThis: \(placeholder), environment: environment!)"
351374
}
352375
}
353376
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -328,29 +328,3 @@ extension JNISwift2JavaGenerator {
328328
return newSelfParamName
329329
}
330330
}
331-
332-
extension String {
333-
/// Returns a version of the string correctly escaped for a JNI
334-
var escapedJNIIdentifier: String {
335-
self.map {
336-
if $0 == "_" {
337-
return "_1"
338-
} else if $0 == "/" {
339-
return "_"
340-
} else if $0 == ";" {
341-
return "_2"
342-
} else if $0 == "[" {
343-
return "_3"
344-
} else if $0.isASCII && ($0.isLetter || $0.isNumber) {
345-
return String($0)
346-
} else if let utf16 = $0.utf16.first {
347-
// Escape any non-alphanumeric to their UTF16 hex encoding
348-
let utf16Hex = String(format: "%04x", utf16)
349-
return "_0\(utf16Hex)"
350-
} else {
351-
fatalError("Invalid JNI character: \($0)")
352-
}
353-
}
354-
.joined()
355-
}
356-
}

0 commit comments

Comments
 (0)