Skip to content

Commit e5c5753

Browse files
committed
Java2Swift: Produce a protocol to aid with native method implementation
When an imported Java class has native methods that we expect to implement from Swift, create a protocol names <Swift Type>NativeMethods that declares all of the methods that need to be implemented. From the user perspective, one should make the `@JavaImplements` extension conform to this protocol. Then, the compiler will ensure that the right Swift methods exist with the right signatures. Note that the user is still responsible for ensuring that the appropriate `@JavaImplements` and `@JavaMethod` annotations are present.
1 parent b9b723c commit e5c5753

File tree

6 files changed

+132
-45
lines changed

6 files changed

+132
-45
lines changed

Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,23 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin {
106106
classpath.deleteLastPathComponent()
107107
}
108108
arguments += [ "--classpath", classpath.path() ]
109+
110+
// For each of the class files, note that it can have Swift-native
111+
// implementations. We figure this out based on the path.
112+
for classFile in compiledClassFiles {
113+
var classFile = classFile.deletingPathExtension()
114+
var classNameComponents: [String] = []
115+
116+
while classFile.lastPathComponent != "Java" {
117+
classNameComponents.append(classFile.lastPathComponent)
118+
classFile.deleteLastPathComponent()
119+
}
120+
121+
let className = classNameComponents
122+
.reversed()
123+
.joined(separator: ".")
124+
arguments += [ "--swift-native-implementation", className]
125+
}
109126
}
110127

111128
return [

Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ enum SwiftWrappedError: Error {
1919
}
2020

2121
@JavaImplements("com.example.swift.HelloSwift")
22-
extension HelloSwift {
22+
extension HelloSwift: HelloSwiftNativeMethods {
2323
@JavaMethod
24-
func sayHello(i: Int32, _ j: Int32) -> Int32 {
24+
func sayHello(_ i: Int32, _ j: Int32) -> Int32 {
2525
print("Hello from Swift!")
2626
let answer = self.sayHelloBack(i + j)
2727
print("Swift got back \(answer) from Java")
@@ -75,7 +75,7 @@ extension HelloSwift {
7575
}
7676

7777
@JavaMethod
78-
func throwMessageFromSwift(message: String) throws -> String {
78+
func throwMessageFromSwift(_ message: String) throws -> String {
7979
throw SwiftWrappedError.message(message)
8080
}
8181
}

Sources/Java2Swift/JavaToSwift.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ struct JavaToSwift: ParsableCommand {
4848
)
4949
var classpath: [String] = []
5050

51+
@Option(
52+
help: "The names of Java classes whose declared native methods will be implemented in Swift."
53+
)
54+
var swiftNativeImplementation: [String] = []
55+
5156
@Option(name: .shortAndLong, help: "The directory in which to output the generated Swift files or the Java2Swift configuration file.")
5257
var outputDirectory: String? = nil
5358

@@ -181,6 +186,10 @@ struct JavaToSwift: ParsableCommand {
181186
environment: environment
182187
)
183188

189+
// Keep track of all of the Java classes that will have
190+
// Swift-native implementations.
191+
translator.swiftNativeImplementations = Set(swiftNativeImplementation)
192+
184193
// Note all of the dependent configurations.
185194
for (swiftModuleName, dependentConfig) in dependentConfigs {
186195
translator.addConfiguration(

Sources/Java2SwiftLib/JavaTranslator.swift

Lines changed: 97 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ package class JavaTranslator {
3939
/// import declarations.
4040
package var importedSwiftModules: Set<String> = JavaTranslator.defaultImportedSwiftModules
4141

42+
/// The canonical names of Java classes whose declared 'native'
43+
/// methods will be implemented in Swift.
44+
package var swiftNativeImplementations: Set<String> = []
45+
4246
package init(
4347
swiftModuleName: String,
4448
environment: JNIEnvironment,
@@ -229,9 +233,16 @@ extension JavaTranslator {
229233
interfacesStr = ", \(prefix): \(interfaces.joined(separator: ", "))"
230234
}
231235

236+
// The top-level declarations we will be returning.
237+
var topLevelDecls: [DeclSyntax] = []
238+
232239
// Members
233240
var members: [DeclSyntax] = []
234-
241+
242+
// Members that are native and will instead go into a NativeMethods
243+
// protocol.
244+
var nativeMembers: [DeclSyntax] = []
245+
235246
// Fields
236247
var staticFields: [Field] = []
237248
var enumConstants: [Field] = []
@@ -268,12 +279,22 @@ extension JavaTranslator {
268279
members.append(
269280
contentsOf: javaClass.getConstructors().compactMap {
270281
$0.flatMap { constructor in
271-
if constructor.isNative {
272-
return nil
273-
}
274-
275282
do {
276-
return try translateConstructor(constructor)
283+
let implementedInSwift = constructor.isNative &&
284+
constructor.getDeclaringClass()!.equals(javaClass.as(JavaObject.self)!) &&
285+
swiftNativeImplementations.contains(javaClass.getCanonicalName())
286+
287+
let translated = try translateConstructor(
288+
constructor,
289+
implementedInSwift: implementedInSwift
290+
)
291+
292+
if implementedInSwift {
293+
nativeMembers.append(translated)
294+
return nil
295+
}
296+
297+
return translated
277298
} catch {
278299
logUntranslated("Unable to translate '\(fullName)' constructor: \(error)")
279300
return nil
@@ -286,22 +307,31 @@ extension JavaTranslator {
286307
var staticMethods: [Method] = []
287308
members.append(
288309
contentsOf: javaClass.getMethods().compactMap {
289-
$0.flatMap { method in
290-
if method.isNative {
291-
return nil
292-
}
293-
294-
310+
$0.flatMap { (method) -> DeclSyntax? in
295311
// Save the static methods; they need to go on an extension of
296312
// JavaClass.
297313
if method.isStatic {
298314
staticMethods.append(method)
299315
return nil
300316
}
301317

318+
let implementedInSwift = method.isNative &&
319+
method.getDeclaringClass()!.equals(javaClass.as(JavaObject.self)!) &&
320+
swiftNativeImplementations.contains(javaClass.getCanonicalName())
321+
302322
// Translate the method if we can.
303323
do {
304-
return try translateMethod(method)
324+
let translated = try translateMethod(
325+
method,
326+
implementedInSwift: implementedInSwift
327+
)
328+
329+
if implementedInSwift {
330+
nativeMembers.append(translated)
331+
return nil
332+
}
333+
334+
return translated
305335
} catch {
306336
logUntranslated("Unable to translate '\(fullName)' method '\(method.getName())': \(error)")
307337
return nil
@@ -356,10 +386,8 @@ extension JavaTranslator {
356386

357387
// Format the class declaration.
358388
classDecl = classDecl.formatted(using: format).cast(DeclSyntax.self)
359-
360-
if staticMethods.isEmpty && staticFields.isEmpty {
361-
return [classDecl]
362-
}
389+
390+
topLevelDecls.append(classDecl)
363391

364392
// Translate static members.
365393
var staticMembers: [DeclSyntax] = []
@@ -381,7 +409,7 @@ extension JavaTranslator {
381409
// Translate each static method.
382410
do {
383411
return try translateMethod(
384-
method,
412+
method, implementedInSwift: /*FIXME:*/false,
385413
genericParameterClause: genericParameterClause,
386414
whereClause: staticMemberWhereClause
387415
)
@@ -392,46 +420,74 @@ extension JavaTranslator {
392420
}
393421
)
394422

395-
if staticMembers.isEmpty {
396-
return [classDecl]
397-
}
423+
if !staticMembers.isEmpty {
424+
// Specify the specialization arguments when needed.
425+
let extSpecialization: String
426+
if genericParameterClause.isEmpty {
427+
extSpecialization = "<\(swiftTypeName)>"
428+
} else {
429+
extSpecialization = ""
430+
}
398431

399-
// Specify the specialization arguments when needed.
400-
let extSpecialization: String
401-
if genericParameterClause.isEmpty {
402-
extSpecialization = "<\(swiftTypeName)>"
403-
} else {
404-
extSpecialization = ""
432+
let extDecl: DeclSyntax =
433+
"""
434+
extension JavaClass\(raw: extSpecialization) {
435+
\(raw: staticMembers.map { $0.description }.joined(separator: "\n\n"))
436+
}
437+
"""
438+
439+
topLevelDecls.append(
440+
extDecl.formatted(using: format).cast(DeclSyntax.self)
441+
)
405442
}
406443

407-
let extDecl =
408-
("""
409-
extension JavaClass\(raw: extSpecialization) {
410-
\(raw: staticMembers.map { $0.description }.joined(separator: "\n\n"))
444+
if !nativeMembers.isEmpty {
445+
let protocolDecl: DeclSyntax =
446+
"""
447+
/// Describes the Java `native` methods for \(raw: swiftTypeName).
448+
///
449+
/// To implement all of the `native` methods for \(raw: swiftTypeName) in Swift,
450+
/// extend \(raw: swiftTypeName) to conform to this protocol and mark
451+
/// each implementation of the protocol requirement with
452+
/// `@JavaMethod`.
453+
protocol \(raw: swiftTypeName)NativeMethods {
454+
\(raw: nativeMembers.map { $0.description }.joined(separator: "\n\n"))
455+
}
456+
"""
457+
458+
topLevelDecls.append(
459+
protocolDecl.formatted(using: format).cast(DeclSyntax.self)
460+
)
411461
}
412-
""" as DeclSyntax).formatted(using: format).cast(DeclSyntax.self)
413462

414-
return [classDecl, extDecl]
463+
return topLevelDecls
415464
}
416465
}
417466

418467
// MARK: Method and constructor translation
419468
extension JavaTranslator {
420469
/// Translates the given Java constructor into a Swift declaration.
421-
package func translateConstructor(_ javaConstructor: Constructor<some AnyJavaObject>) throws -> DeclSyntax {
470+
package func translateConstructor(
471+
_ javaConstructor: Constructor<some AnyJavaObject>,
472+
implementedInSwift: Bool
473+
) throws -> DeclSyntax {
422474
let parameters = try translateParameters(javaConstructor.getParameters()) + ["environment: JNIEnvironment? = nil"]
423475
let parametersStr = parameters.map { $0.description }.joined(separator: ", ")
424476
let throwsStr = javaConstructor.throwsCheckedException ? "throws" : ""
425477

478+
let javaMethodAttribute = implementedInSwift
479+
? ""
480+
: "@JavaMethod\n"
481+
let accessModifier = implementedInSwift ? "" : "public "
426482
return """
427-
@JavaMethod
428-
public init(\(raw: parametersStr))\(raw: throwsStr)
483+
\(raw: javaMethodAttribute)\(raw: accessModifier) init(\(raw: parametersStr))\(raw: throwsStr)
429484
"""
430485
}
431486

432487
/// Translates the given Java method into a Swift declaration.
433488
package func translateMethod(
434489
_ javaMethod: Method,
490+
implementedInSwift: Bool,
435491
genericParameterClause: String = "",
436492
whereClause: String = ""
437493
) throws -> DeclSyntax {
@@ -451,10 +507,12 @@ extension JavaTranslator {
451507

452508
let throwsStr = javaMethod.throwsCheckedException ? "throws" : ""
453509
let swiftMethodName = javaMethod.getName().escapedSwiftName
454-
let methodAttribute: AttributeSyntax = javaMethod.isStatic ? "@JavaStaticMethod" : "@JavaMethod";
510+
let methodAttribute: AttributeSyntax = implementedInSwift
511+
? ""
512+
: javaMethod.isStatic ? "@JavaStaticMethod\n" : "@JavaMethod\n";
513+
let accessModifier = implementedInSwift ? "" : "public "
455514
return """
456-
\(methodAttribute)
457-
public func \(raw: swiftMethodName)\(raw: genericParameterClause)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause)
515+
\(methodAttribute)\(raw: accessModifier)func \(raw: swiftMethodName)\(raw: genericParameterClause)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause)
458516
"""
459517
}
460518

Sources/JavaKitReflection/JavaClass+Reflection.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import JavaKit
1717
// TODO: We should be able to autogenerate this as an extension based on
1818
// knowing that JavaClass was defined elsewhere.
1919
extension JavaClass {
20+
@JavaMethod
21+
public func equals(_ arg0: JavaObject?) -> Bool
22+
2023
@JavaMethod
2124
public func getName() -> String
2225

USER_GUIDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ public class HelloSwift {
249249
}
250250
```
251251

252-
On the Swift side, the Java class needs to have been exposed to Swift through `Java2Swift.config`, e.g.,:
252+
On the Swift side, the Java class needs to be exposed to Swift through `Java2Swift.config`, e.g.,:
253253

254254
```swift
255255
{
@@ -259,11 +259,11 @@ On the Swift side, the Java class needs to have been exposed to Swift through `J
259259
}
260260
```
261261

262-
Implementations of `native` methods are written in an extension of the Swift type that has been marked with `@JavaImplements`. The methods themselves must be marked with `@JavaMethod`, indicating that they are available to Java as well. For example:
262+
Implementations of `native` methods are written in an extension of the Swift type that has been marked with `@JavaImplements`. The methods themselves must be marked with `@JavaMethod`, indicating that they are available to Java as well. To help ensure that the Swift code implements all of the `native` methods with the right signatures, JavaKit produces a protocol with the Swift type name suffixed by `NativeMethods`. Declare conformance to that protocol and implement its requirements, for example:
263263

264264
```swift
265265
@JavaImplements("org.swift.javakit.HelloSwift")
266-
extension Hello {
266+
extension Hello: HelloNativeMethods {
267267
@JavaMethod
268268
func reportStatistics(_ meaning: String, _ numbers: [Double]) -> String {
269269
let average = numbers.isEmpty ? 0.0 : numbers.reduce(0.0) { $0 + $1 } / Double(numbers.count)

0 commit comments

Comments
 (0)