Skip to content

Commit b9b723c

Browse files
committed
JavaKit: Rework @ImplementsJava to be more like @implements language feature
The `@ImplementsJava` macro had an odd formulation where it was used similarly to `@JavaMethod`, but was actually interpreted by the outer `@JavaClass` macro. Completely rework the way `@ImplementsJava` is used to make it more like the `@implements` feature introduced in SE-0436, and rename it to `@JavaImplements` to more closely match the canonical spelling for `@implementation` with a language and make use of the `@Java...` as a common prefix. In this new design, one places `@JavaImplements` on an extension of the Swift type and marks the implementing methods with `@JavaMethod`. For example: @JavaImplements("org.swift.javakit.HelloSwift") extension Hello { @JavaMethod func reportStatistics(_ meaning: String, _ numbers: [Double]) -> String { let average = numbers.isEmpty ? 0.0 : numbers.reduce(0.0) { $0 + $1 } / Double(numbers.count) return "Average of \(meaning) is \(average)" } } This also means that we can generate most of the `@JavaClass` definition for a given Java class, and leave the native methods to be implemented by an `@JavaImplements` extension. Update the JavaKitSampleApp to use the Java compilation plugin to compile the Java files in the target. Automatically feed those into the Java2Swift plugin to generate the Swift representations of the Java classes, skipping the native methods (since they'll be implemented here). The user guide / tutorial is a bit behind the implementation here as I smooth out more corners.
1 parent a6efd5d commit b9b723c

File tree

18 files changed

+416
-296
lines changed

18 files changed

+416
-296
lines changed

Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin {
9393
outputDirectory.appending(path: "\(swiftName).swift")
9494
}
9595

96+
// Find the Java .class files generated from prior plugins.
97+
let compiledClassFiles = sourceModule.pluginGeneratedResources.filter { url in
98+
url.pathExtension == "class"
99+
}
100+
101+
if let firstClassFile = compiledClassFiles.first {
102+
// Keep stripping off parts of the path until we hit the "Java" part.
103+
// That's where the class path starts.
104+
var classpath = firstClassFile
105+
while classpath.lastPathComponent != "Java" {
106+
classpath.deleteLastPathComponent()
107+
}
108+
arguments += [ "--classpath", classpath.path() ]
109+
}
110+
96111
return [
97112
.buildCommand(
98113
displayName: "Wrapping \(config.classes.count) Java classes target \(sourceModule.name) in Swift",

Samples/JavaKitSampleApp/Package.swift

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,42 @@
44
import CompilerPluginSupport
55
import PackageDescription
66

7+
import class Foundation.FileManager
8+
import class Foundation.ProcessInfo
9+
10+
// Note: the JAVA_HOME environment variable must be set to point to where
11+
// Java is installed, e.g.,
12+
// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home.
13+
func findJavaHome() -> String {
14+
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
15+
return home
16+
}
17+
18+
// This is a workaround for envs (some IDEs) which have trouble with
19+
// picking up env variables during the build process
20+
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
21+
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
22+
if let lastChar = home.last, lastChar.isNewline {
23+
return String(home.dropLast())
24+
}
25+
26+
return home
27+
}
28+
29+
fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
30+
}
31+
let javaHome = findJavaHome()
32+
33+
let javaIncludePath = "\(javaHome)/include"
34+
#if os(Linux)
35+
let javaPlatformIncludePath = "\(javaIncludePath)/linux"
36+
#elseif os(macOS)
37+
let javaPlatformIncludePath = "\(javaIncludePath)/darwin"
38+
#else
39+
// TODO: Handle windows as well
40+
#error("Currently only macOS and Linux platforms are supported, this may change in the future.")
41+
#endif
42+
743
let package = Package(
844
name: "JavaKitSampleApp",
945
platforms: [
@@ -34,11 +70,12 @@ let package = Package(
3470
.product(name: "JavaKitJar", package: "swift-java"),
3571
],
3672
swiftSettings: [
37-
.swiftLanguageMode(.v5)
73+
.swiftLanguageMode(.v5),
74+
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
3875
],
3976
plugins: [
77+
.plugin(name: "JavaCompilerPlugin", package: "swift-java"),
4078
.plugin(name: "Java2SwiftPlugin", package: "swift-java"),
41-
.plugin(name: "JavaCompilerPlugin", package: "swift-java")
4279
]
4380
),
4481
]
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"classes" : {
3-
"java.util.ArrayList" : "ArrayList"
3+
"java.util.ArrayList" : "ArrayList",
4+
"com.example.swift.HelloSwift" : "HelloSwift",
5+
"com.example.swift.HelloSubclass" : "HelloSubclass",
6+
"com.example.swift.JavaKitSampleMain" : "JavaKitSampleMain"
47
}
58
}

Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,9 @@ enum SwiftWrappedError: Error {
1818
case message(String)
1919
}
2020

21-
@JavaClass("com.example.swift.HelloSwift")
22-
struct HelloSwift {
21+
@JavaImplements("com.example.swift.HelloSwift")
22+
extension HelloSwift {
2323
@JavaMethod
24-
init(environment: JNIEnvironment)
25-
26-
@JavaMethod
27-
func sayHelloBack(_ i: Int32) -> Double
28-
29-
@JavaMethod
30-
func greet(_ name: String)
31-
32-
@JavaMethod
33-
func doublesToStrings(doubles: [Double]) -> [String]
34-
35-
@JavaMethod
36-
func throwMessage(message: String) throws
37-
38-
@JavaField
39-
var value: Double
40-
41-
@JavaField
42-
var name: String
43-
44-
@ImplementsJava
4524
func sayHello(i: Int32, _ j: Int32) -> Int32 {
4625
print("Hello from Swift!")
4726
let answer = self.sayHelloBack(i + j)
@@ -65,7 +44,7 @@ struct HelloSwift {
6544
self.name = "a 🗑️-collected language"
6645
_ = self.sayHelloBack(42)
6746

68-
let strings = doublesToStrings(doubles: [3.14159, 2.71828])
47+
let strings = doublesToStrings([3.14159, 2.71828])
6948
print("Converting doubles to strings: \(strings)")
7049

7150
// Try downcasting
@@ -83,42 +62,24 @@ struct HelloSwift {
8362
assert(!newHello.is(HelloSubclass.self))
8463

8564
// Create a new instance.
86-
let helloSubFromSwift = HelloSubclass(greeting: "Hello from Swift", environment: javaEnvironment)
65+
let helloSubFromSwift = HelloSubclass("Hello from Swift", environment: javaEnvironment)
8766
helloSubFromSwift.greetMe()
8867

8968
do {
90-
try throwMessage(message: "I am an error")
69+
try throwMessage("I am an error")
9170
} catch {
9271
print("Caught Java error: \(error)")
9372
}
9473

9574
return i * j
9675
}
9776

98-
@ImplementsJava
77+
@JavaMethod
9978
func throwMessageFromSwift(message: String) throws -> String {
10079
throw SwiftWrappedError.message(message)
10180
}
10281
}
10382

104-
extension JavaClass<HelloSwift> {
105-
@JavaStaticField
106-
var initialValue: Double
107-
}
108-
109-
@JavaClass("com.example.swift.HelloSubclass", extends: HelloSwift.self)
110-
struct HelloSubclass {
111-
@JavaField
112-
var greeting: String
113-
114-
@JavaMethod
115-
func greetMe()
116-
117-
@JavaMethod
118-
init(greeting: String, environment: JNIEnvironment)
119-
}
120-
121-
12283
func removeLast(arrayList: ArrayList<JavaClass<HelloSwift>>) {
12384
if let lastObject = arrayList.getLast() {
12485
_ = arrayList.remove(lastObject)

Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public HelloSubclass(String greeting) {
2121
this.greeting = greeting;
2222
}
2323

24-
private void greetMe() {
24+
public void greetMe() {
2525
super.greet(greeting);
2626
}
2727
}

Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
package com.example.swift;
1616

1717
public class HelloSwift {
18-
private double value;
19-
private static double initialValue = 3.14159;
20-
private String name = "Java";
18+
public double value;
19+
public static double initialValue = 3.14159;
20+
public String name = "Java";
2121

2222
static {
2323
System.loadLibrary("JavaKitExample");
@@ -31,7 +31,7 @@ public HelloSwift() {
3131
public native String throwMessageFromSwift(String message) throws Exception;
3232

3333
// To be called back by the native code
34-
private double sayHelloBack(int i) {
34+
public double sayHelloBack(int i) {
3535
System.out.println("And hello back from " + name + "! You passed me " + i);
3636
return value;
3737
}
@@ -40,7 +40,7 @@ public void greet(String name) {
4040
System.out.println("Salutations, " + name);
4141
}
4242

43-
String[] doublesToStrings(double[] doubles) {
43+
public String[] doublesToStrings(double[] doubles) {
4444
int size = doubles.length;
4545
String[] strings = new String[size];
4646

Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
* For the Swift implementation refer to
2020
*/
2121
public class JavaKitSampleMain {
22-
2322
public static void main(String[] args) {
2423
int result = new HelloSubclass("Swift").sayHello(17, 25);
2524
System.out.println("sayHello(17, 25) = " + result);

Sources/Java2SwiftLib/JavaTranslator.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,10 @@ extension JavaTranslator {
268268
members.append(
269269
contentsOf: javaClass.getConstructors().compactMap {
270270
$0.flatMap { constructor in
271+
if constructor.isNative {
272+
return nil
273+
}
274+
271275
do {
272276
return try translateConstructor(constructor)
273277
} catch {
@@ -283,6 +287,11 @@ extension JavaTranslator {
283287
members.append(
284288
contentsOf: javaClass.getMethods().compactMap {
285289
$0.flatMap { method in
290+
if method.isNative {
291+
return nil
292+
}
293+
294+
286295
// Save the static methods; they need to go on an extension of
287296
// JavaClass.
288297
if method.isStatic {

Sources/JavaKit/Macros.swift

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
named(`as`)
4242
)
4343
@attached(extension, conformances: AnyJavaObject)
44-
@attached(peer)
4544
public macro JavaClass(
4645
_ fullClassName: String,
4746
extends: (any AnyJavaObject.Type)? = nil,
@@ -143,23 +142,27 @@ public macro JavaMethod() = #externalMacro(module: "JavaKitMacros", type: "JavaM
143142
@attached(body)
144143
public macro JavaStaticMethod() = #externalMacro(module: "JavaKitMacros", type: "JavaMethodMacro")
145144

146-
/// Macro that exposes the given Swift method as a native method in Java.
147-
///
148-
/// The macro must be used within a struct type marked with `@JavaClass`, and there
149-
/// must be a corresponding Java method declared as `native` for it to be called from
150-
/// Java. For example, given this Swift method:
151-
///
152-
/// ```swift
153-
/// @ImplementsJava
154-
/// func sayHello(i: Int32, _ j: Int32) -> Int32 {
155-
/// // swift implementation
156-
/// }
145+
/// Macro that marks extensions to specify that all of the @JavaMethod
146+
/// methods are implementations of Java methods spelled as `native`.
157147
///
158-
/// inside a struct with `@JavaClass("com.example.swift.HelloSwift")`, the
159-
/// corresponding `HelloSwift` Java class should have:
148+
/// For example, given a Java native method such as the following in
149+
/// a Java class `org.swift.example.Hello`:
160150
///
161151
/// ```java
162152
/// public native int sayHello(int i, int j);
163153
/// ```
154+
///
155+
/// Assuming that the Java class with imported into Swift as `Hello`, t
156+
/// the method can be implemented in Swift with the following:
157+
///
158+
/// ```swift
159+
/// @JavaImplements
160+
/// extension Hello {
161+
/// @JavaMethod
162+
/// func sayHello(i: Int32, _ j: Int32) -> Int32 {
163+
/// // swift implementation
164+
/// }
165+
/// }
166+
/// ```
164167
@attached(peer)
165-
public macro ImplementsJava() = #externalMacro(module: "JavaKitMacros", type: "ImplementsJavaMacro")
168+
public macro JavaImplements(_ fullClassName: String) = #externalMacro(module: "JavaKitMacros", type: "JavaImplementsMacro")
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
import SwiftSyntax
16+
import SwiftSyntaxMacros
17+
18+
/// The mode of code generation being used for macros.
19+
enum GenerationMode {
20+
/// This macro is describing a Java class in Swift.
21+
case importFromJava
22+
23+
/// This macro is describing a Swift type that will be represented by
24+
/// a generated Java class.
25+
case exportToJava
26+
27+
/// This macro is describing an extension that is implementing the native
28+
/// methods of a Java class.
29+
case JavaImplements
30+
31+
/// Determine the mode for Java class generation based on an attribute.
32+
init?(attribute: AttributeSyntax) {
33+
switch attribute.attributeName.trimmedDescription {
34+
case "JavaClass", "JavaInterface":
35+
self = .importFromJava
36+
37+
case "ExportToJavaClass":
38+
self = .exportToJava
39+
40+
case "JavaImplements":
41+
self = .JavaImplements
42+
43+
default:
44+
return nil
45+
}
46+
}
47+
48+
/// Determine the mode for Java class generation based on the the macro
49+
/// expansion context.
50+
init?(expansionContext: some MacroExpansionContext) {
51+
for lexicalContext in expansionContext.lexicalContext {
52+
// FIXME: swift-syntax probably needs an AttributedSyntax node for us
53+
// to look at. For now, we can look at just structs and extensions.
54+
let attributes: AttributeListSyntax
55+
if let structSyntax = lexicalContext.as(StructDeclSyntax.self) {
56+
attributes = structSyntax.attributes
57+
} else if let extSyntax = lexicalContext.as(ExtensionDeclSyntax.self) {
58+
attributes = extSyntax.attributes
59+
} else {
60+
continue
61+
}
62+
63+
// Look for the first attribute that is associated with a mode, and
64+
// return that.
65+
for attribute in attributes {
66+
if case .attribute(let attribute) = attribute,
67+
let mode = GenerationMode(attribute: attribute) {
68+
self = mode
69+
return
70+
}
71+
}
72+
}
73+
74+
return nil
75+
}
76+
}

0 commit comments

Comments
 (0)