diff --git a/Package.swift b/Package.swift index 5b34a2eb..e7f6353f 100644 --- a/Package.swift +++ b/Package.swift @@ -262,7 +262,8 @@ let package = Package( ], swiftSettings: [ .swiftLanguageMode(.v5), - .enableUpcomingFeature("BareSlashRegexLiterals") + .enableUpcomingFeature("BareSlashRegexLiterals"), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]), ] ), @@ -281,7 +282,8 @@ let package = Package( swiftSettings: [ .swiftLanguageMode(.v5), - .enableUpcomingFeature("BareSlashRegexLiterals") + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]), + .enableUpcomingFeature("BareSlashRegexLiterals"), ] ), @@ -296,7 +298,8 @@ let package = Package( "JavaTypes", ], swiftSettings: [ - .swiftLanguageMode(.v5) + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]), ] ), @@ -314,7 +317,8 @@ let package = Package( name: "JavaKitTests", dependencies: ["JavaKit", "JavaKitNetwork"], swiftSettings: [ - .swiftLanguageMode(.v5) + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) ] ), @@ -341,7 +345,8 @@ let package = Package( name: "Java2SwiftTests", dependencies: ["Java2SwiftLib"], swiftSettings: [ - .swiftLanguageMode(.v5) + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) ] ), diff --git a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift index 30be39a9..37ee1b3a 100644 --- a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift +++ b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift @@ -93,12 +93,44 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin { outputDirectory.appending(path: "\(swiftName).swift") } + // Find the Java .class files generated from prior plugins. + let compiledClassFiles = sourceModule.pluginGeneratedResources.filter { url in + url.pathExtension == "class" + } + + if let firstClassFile = compiledClassFiles.first { + // Keep stripping off parts of the path until we hit the "Java" part. + // That's where the class path starts. + var classpath = firstClassFile + while classpath.lastPathComponent != "Java" { + classpath.deleteLastPathComponent() + } + arguments += [ "--classpath", classpath.path() ] + + // For each of the class files, note that it can have Swift-native + // implementations. We figure this out based on the path. + for classFile in compiledClassFiles { + var classFile = classFile.deletingPathExtension() + var classNameComponents: [String] = [] + + while classFile.lastPathComponent != "Java" { + classNameComponents.append(classFile.lastPathComponent) + classFile.deleteLastPathComponent() + } + + let className = classNameComponents + .reversed() + .joined(separator: ".") + arguments += [ "--swift-native-implementation", className] + } + } + return [ .buildCommand( displayName: "Wrapping \(config.classes.count) Java classes target \(sourceModule.name) in Swift", executable: try context.tool(named: "Java2Swift").url, arguments: arguments, - inputFiles: [ configFile ], + inputFiles: [ configFile ] + compiledClassFiles, outputFiles: outputSwiftFiles ) ] diff --git a/Samples/JavaKitSampleApp/Package.swift b/Samples/JavaKitSampleApp/Package.swift index df2c65f1..1da05c29 100644 --- a/Samples/JavaKitSampleApp/Package.swift +++ b/Samples/JavaKitSampleApp/Package.swift @@ -4,6 +4,42 @@ import CompilerPluginSupport import PackageDescription +import class Foundation.FileManager +import class Foundation.ProcessInfo + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} +let javaHome = findJavaHome() + +let javaIncludePath = "\(javaHome)/include" +#if os(Linux) + let javaPlatformIncludePath = "\(javaIncludePath)/linux" +#elseif os(macOS) + let javaPlatformIncludePath = "\(javaIncludePath)/darwin" +#else + // TODO: Handle windows as well + #error("Currently only macOS and Linux platforms are supported, this may change in the future.") +#endif + let package = Package( name: "JavaKitSampleApp", platforms: [ @@ -34,11 +70,12 @@ let package = Package( .product(name: "JavaKitJar", package: "swift-java"), ], swiftSettings: [ - .swiftLanguageMode(.v5) + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) ], plugins: [ + .plugin(name: "JavaCompilerPlugin", package: "swift-java"), .plugin(name: "Java2SwiftPlugin", package: "swift-java"), - .plugin(name: "JavaCompilerPlugin", package: "swift-java") ] ), ] diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/Java2Swift.config b/Samples/JavaKitSampleApp/Sources/JavaKitExample/Java2Swift.config index ec19d82b..5ccb042b 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/Java2Swift.config +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/Java2Swift.config @@ -1,5 +1,8 @@ { "classes" : { - "java.util.ArrayList" : "ArrayList" + "java.util.ArrayList" : "ArrayList", + "com.example.swift.HelloSwift" : "HelloSwift", + "com.example.swift.HelloSubclass" : "HelloSubclass", + "com.example.swift.JavaKitSampleMain" : "JavaKitSampleMain" } } diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift index 090c4356..aaaff0bb 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift @@ -18,31 +18,10 @@ enum SwiftWrappedError: Error { case message(String) } -@JavaClass("com.example.swift.HelloSwift") -struct HelloSwift { +@JavaImplementation("com.example.swift.HelloSwift") +extension HelloSwift: HelloSwiftNativeMethods { @JavaMethod - init(environment: JNIEnvironment) - - @JavaMethod - func sayHelloBack(_ i: Int32) -> Double - - @JavaMethod - func greet(_ name: String) - - @JavaMethod - func doublesToStrings(doubles: [Double]) -> [String] - - @JavaMethod - func throwMessage(message: String) throws - - @JavaField - var value: Double - - @JavaField - var name: String - - @ImplementsJava - func sayHello(i: Int32, _ j: Int32) -> Int32 { + func sayHello(_ i: Int32, _ j: Int32) -> Int32 { print("Hello from Swift!") let answer = self.sayHelloBack(i + j) print("Swift got back \(answer) from Java") @@ -65,7 +44,7 @@ struct HelloSwift { self.name = "a 🗑️-collected language" _ = self.sayHelloBack(42) - let strings = doublesToStrings(doubles: [3.14159, 2.71828]) + let strings = doublesToStrings([3.14159, 2.71828]) print("Converting doubles to strings: \(strings)") // Try downcasting @@ -83,11 +62,11 @@ struct HelloSwift { assert(!newHello.is(HelloSubclass.self)) // Create a new instance. - let helloSubFromSwift = HelloSubclass(greeting: "Hello from Swift", environment: javaEnvironment) + let helloSubFromSwift = HelloSubclass("Hello from Swift", environment: javaEnvironment) helloSubFromSwift.greetMe() do { - try throwMessage(message: "I am an error") + try throwMessage("I am an error") } catch { print("Caught Java error: \(error)") } @@ -95,30 +74,12 @@ struct HelloSwift { return i * j } - @ImplementsJava - func throwMessageFromSwift(message: String) throws -> String { + @JavaMethod + func throwMessageFromSwift(_ message: String) throws -> String { throw SwiftWrappedError.message(message) } } -extension JavaClass { - @JavaStaticField - var initialValue: Double -} - -@JavaClass("com.example.swift.HelloSubclass", extends: HelloSwift.self) -struct HelloSubclass { - @JavaField - var greeting: String - - @JavaMethod - func greetMe() - - @JavaMethod - init(greeting: String, environment: JNIEnvironment) -} - - func removeLast(arrayList: ArrayList>) { if let lastObject = arrayList.getLast() { _ = arrayList.remove(lastObject) diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java index eb3cba32..2312d8f5 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java @@ -21,7 +21,7 @@ public HelloSubclass(String greeting) { this.greeting = greeting; } - private void greetMe() { + public void greetMe() { super.greet(greeting); } } diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java index b4c87f71..f988bbb6 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java @@ -15,9 +15,9 @@ package com.example.swift; public class HelloSwift { - private double value; - private static double initialValue = 3.14159; - private String name = "Java"; + public double value; + public static double initialValue = 3.14159; + public String name = "Java"; static { System.loadLibrary("JavaKitExample"); @@ -31,7 +31,7 @@ public HelloSwift() { public native String throwMessageFromSwift(String message) throws Exception; // To be called back by the native code - private double sayHelloBack(int i) { + public double sayHelloBack(int i) { System.out.println("And hello back from " + name + "! You passed me " + i); return value; } @@ -40,7 +40,7 @@ public void greet(String name) { System.out.println("Salutations, " + name); } - String[] doublesToStrings(double[] doubles) { + public String[] doublesToStrings(double[] doubles) { int size = doubles.length; String[] strings = new String[size]; diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java index 2b565608..9036b7a5 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java @@ -19,7 +19,6 @@ * For the Swift implementation refer to */ public class JavaKitSampleMain { - public static void main(String[] args) { int result = new HelloSubclass("Swift").sayHello(17, 25); System.out.println("sayHello(17, 25) = " + result); diff --git a/Sources/Java2Swift/JavaToSwift.swift b/Sources/Java2Swift/JavaToSwift.swift index ffb9a24f..1cc094c2 100644 --- a/Sources/Java2Swift/JavaToSwift.swift +++ b/Sources/Java2Swift/JavaToSwift.swift @@ -48,6 +48,11 @@ struct JavaToSwift: ParsableCommand { ) var classpath: [String] = [] + @Option( + help: "The names of Java classes whose declared native methods will be implemented in Swift." + ) + var swiftNativeImplementation: [String] = [] + @Option(name: .shortAndLong, help: "The directory in which to output the generated Swift files or the Java2Swift configuration file.") var outputDirectory: String? = nil @@ -181,6 +186,10 @@ struct JavaToSwift: ParsableCommand { environment: environment ) + // Keep track of all of the Java classes that will have + // Swift-native implementations. + translator.swiftNativeImplementations = Set(swiftNativeImplementation) + // Note all of the dependent configurations. for (swiftModuleName, dependentConfig) in dependentConfigs { translator.addConfiguration( diff --git a/Sources/Java2SwiftLib/JavaTranslator.swift b/Sources/Java2SwiftLib/JavaTranslator.swift index b7eec617..a5af9baa 100644 --- a/Sources/Java2SwiftLib/JavaTranslator.swift +++ b/Sources/Java2SwiftLib/JavaTranslator.swift @@ -39,6 +39,10 @@ package class JavaTranslator { /// import declarations. package var importedSwiftModules: Set = JavaTranslator.defaultImportedSwiftModules + /// The canonical names of Java classes whose declared 'native' + /// methods will be implemented in Swift. + package var swiftNativeImplementations: Set = [] + package init( swiftModuleName: String, environment: JNIEnvironment, @@ -229,9 +233,16 @@ extension JavaTranslator { interfacesStr = ", \(prefix): \(interfaces.joined(separator: ", "))" } + // The top-level declarations we will be returning. + var topLevelDecls: [DeclSyntax] = [] + // Members var members: [DeclSyntax] = [] - + + // Members that are native and will instead go into a NativeMethods + // protocol. + var nativeMembers: [DeclSyntax] = [] + // Fields var staticFields: [Field] = [] var enumConstants: [Field] = [] @@ -269,7 +280,21 @@ extension JavaTranslator { contentsOf: javaClass.getConstructors().compactMap { $0.flatMap { constructor in do { - return try translateConstructor(constructor) + let implementedInSwift = constructor.isNative && + constructor.getDeclaringClass()!.equals(javaClass.as(JavaObject.self)!) && + swiftNativeImplementations.contains(javaClass.getCanonicalName()) + + let translated = try translateConstructor( + constructor, + implementedInSwift: implementedInSwift + ) + + if implementedInSwift { + nativeMembers.append(translated) + return nil + } + + return translated } catch { logUntranslated("Unable to translate '\(fullName)' constructor: \(error)") return nil @@ -282,7 +307,7 @@ extension JavaTranslator { var staticMethods: [Method] = [] members.append( contentsOf: javaClass.getMethods().compactMap { - $0.flatMap { method in + $0.flatMap { (method) -> DeclSyntax? in // Save the static methods; they need to go on an extension of // JavaClass. if method.isStatic { @@ -290,9 +315,23 @@ extension JavaTranslator { return nil } + let implementedInSwift = method.isNative && + method.getDeclaringClass()!.equals(javaClass.as(JavaObject.self)!) && + swiftNativeImplementations.contains(javaClass.getCanonicalName()) + // Translate the method if we can. do { - return try translateMethod(method) + let translated = try translateMethod( + method, + implementedInSwift: implementedInSwift + ) + + if implementedInSwift { + nativeMembers.append(translated) + return nil + } + + return translated } catch { logUntranslated("Unable to translate '\(fullName)' method '\(method.getName())': \(error)") return nil @@ -347,10 +386,8 @@ extension JavaTranslator { // Format the class declaration. classDecl = classDecl.formatted(using: format).cast(DeclSyntax.self) - - if staticMethods.isEmpty && staticFields.isEmpty { - return [classDecl] - } + + topLevelDecls.append(classDecl) // Translate static members. var staticMembers: [DeclSyntax] = [] @@ -372,7 +409,7 @@ extension JavaTranslator { // Translate each static method. do { return try translateMethod( - method, + method, implementedInSwift: /*FIXME:*/false, genericParameterClause: genericParameterClause, whereClause: staticMemberWhereClause ) @@ -383,46 +420,74 @@ extension JavaTranslator { } ) - if staticMembers.isEmpty { - return [classDecl] - } + if !staticMembers.isEmpty { + // Specify the specialization arguments when needed. + let extSpecialization: String + if genericParameterClause.isEmpty { + extSpecialization = "<\(swiftTypeName)>" + } else { + extSpecialization = "" + } - // Specify the specialization arguments when needed. - let extSpecialization: String - if genericParameterClause.isEmpty { - extSpecialization = "<\(swiftTypeName)>" - } else { - extSpecialization = "" + let extDecl: DeclSyntax = + """ + extension JavaClass\(raw: extSpecialization) { + \(raw: staticMembers.map { $0.description }.joined(separator: "\n\n")) + } + """ + + topLevelDecls.append( + extDecl.formatted(using: format).cast(DeclSyntax.self) + ) } - let extDecl = - (""" - extension JavaClass\(raw: extSpecialization) { - \(raw: staticMembers.map { $0.description }.joined(separator: "\n\n")) + if !nativeMembers.isEmpty { + let protocolDecl: DeclSyntax = + """ + /// Describes the Java `native` methods for ``\(raw: swiftTypeName)``. + /// + /// To implement all of the `native` methods for \(raw: swiftTypeName) in Swift, + /// extend \(raw: swiftTypeName) to conform to this protocol and mark + /// each implementation of the protocol requirement with + /// `@JavaMethod`. + protocol \(raw: swiftTypeName)NativeMethods { + \(raw: nativeMembers.map { $0.description }.joined(separator: "\n\n")) + } + """ + + topLevelDecls.append( + protocolDecl.formatted(using: format).cast(DeclSyntax.self) + ) } - """ as DeclSyntax).formatted(using: format).cast(DeclSyntax.self) - return [classDecl, extDecl] + return topLevelDecls } } // MARK: Method and constructor translation extension JavaTranslator { /// Translates the given Java constructor into a Swift declaration. - package func translateConstructor(_ javaConstructor: Constructor) throws -> DeclSyntax { + package func translateConstructor( + _ javaConstructor: Constructor, + implementedInSwift: Bool + ) throws -> DeclSyntax { let parameters = try translateParameters(javaConstructor.getParameters()) + ["environment: JNIEnvironment? = nil"] let parametersStr = parameters.map { $0.description }.joined(separator: ", ") let throwsStr = javaConstructor.throwsCheckedException ? "throws" : "" + let javaMethodAttribute = implementedInSwift + ? "" + : "@JavaMethod\n" + let accessModifier = implementedInSwift ? "" : "public " return """ - @JavaMethod - public init(\(raw: parametersStr))\(raw: throwsStr) + \(raw: javaMethodAttribute)\(raw: accessModifier)init(\(raw: parametersStr))\(raw: throwsStr) """ } /// Translates the given Java method into a Swift declaration. package func translateMethod( _ javaMethod: Method, + implementedInSwift: Bool, genericParameterClause: String = "", whereClause: String = "" ) throws -> DeclSyntax { @@ -442,10 +507,12 @@ extension JavaTranslator { let throwsStr = javaMethod.throwsCheckedException ? "throws" : "" let swiftMethodName = javaMethod.getName().escapedSwiftName - let methodAttribute: AttributeSyntax = javaMethod.isStatic ? "@JavaStaticMethod" : "@JavaMethod"; + let methodAttribute: AttributeSyntax = implementedInSwift + ? "" + : javaMethod.isStatic ? "@JavaStaticMethod\n" : "@JavaMethod\n"; + let accessModifier = implementedInSwift ? "" : "public " return """ - \(methodAttribute) - public func \(raw: swiftMethodName)\(raw: genericParameterClause)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause) + \(methodAttribute)\(raw: accessModifier)func \(raw: swiftMethodName)\(raw: genericParameterClause)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause) """ } diff --git a/Sources/JavaKit/JavaObject+MethodCalls.swift b/Sources/JavaKit/JavaObject+MethodCalls.swift index e9b97bc2..0a48b8bd 100644 --- a/Sources/JavaKit/JavaObject+MethodCalls.swift +++ b/Sources/JavaKit/JavaObject+MethodCalls.swift @@ -20,13 +20,9 @@ private func methodMangling( parameterTypes: repeat (each Param).Type, resultType: JavaType ) -> String { - let parameterTypesArray = [JavaType].init(unsafeUninitializedCapacity: countArgs(repeat each parameterTypes)) { - buffer, - initializedCount in - for parameterType in repeat each parameterTypes { - buffer[initializedCount] = parameterType.javaType - initializedCount += 1 - } + var parameterTypesArray: [JavaType] = [] + for parameterType in repeat each parameterTypes { + parameterTypesArray.append(parameterType.javaType) } return MethodSignature( resultType: resultType, diff --git a/Sources/JavaKit/Macros.swift b/Sources/JavaKit/Macros.swift index 347a3ebf..12ae0c26 100644 --- a/Sources/JavaKit/Macros.swift +++ b/Sources/JavaKit/Macros.swift @@ -41,7 +41,6 @@ named(`as`) ) @attached(extension, conformances: AnyJavaObject) -@attached(peer) public macro JavaClass( _ fullClassName: String, extends: (any AnyJavaObject.Type)? = nil, @@ -143,23 +142,27 @@ public macro JavaMethod() = #externalMacro(module: "JavaKitMacros", type: "JavaM @attached(body) public macro JavaStaticMethod() = #externalMacro(module: "JavaKitMacros", type: "JavaMethodMacro") -/// Macro that exposes the given Swift method as a native method in Java. -/// -/// The macro must be used within a struct type marked with `@JavaClass`, and there -/// must be a corresponding Java method declared as `native` for it to be called from -/// Java. For example, given this Swift method: -/// -/// ```swift -/// @ImplementsJava -/// func sayHello(i: Int32, _ j: Int32) -> Int32 { -/// // swift implementation -/// } +/// Macro that marks extensions to specify that all of the @JavaMethod +/// methods are implementations of Java methods spelled as `native`. /// -/// inside a struct with `@JavaClass("com.example.swift.HelloSwift")`, the -/// corresponding `HelloSwift` Java class should have: +/// For example, given a Java native method such as the following in +/// a Java class `org.swift.example.Hello`: /// /// ```java /// public native int sayHello(int i, int j); /// ``` +/// +/// Assuming that the Java class with imported into Swift as `Hello`, t +/// the method can be implemented in Swift with the following: +/// +/// ```swift +/// @JavaImplementation +/// extension Hello { +/// @JavaMethod +/// func sayHello(i: Int32, _ j: Int32) -> Int32 { +/// // swift implementation +/// } +/// } +/// ``` @attached(peer) -public macro ImplementsJava() = #externalMacro(module: "JavaKitMacros", type: "ImplementsJavaMacro") +public macro JavaImplementation(_ fullClassName: String) = #externalMacro(module: "JavaKitMacros", type: "JavaImplementationMacro") diff --git a/Sources/JavaKitMacros/GenerationMode.swift b/Sources/JavaKitMacros/GenerationMode.swift new file mode 100644 index 00000000..d000d209 --- /dev/null +++ b/Sources/JavaKitMacros/GenerationMode.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax +import SwiftSyntaxMacros + +/// The mode of code generation being used for macros. +enum GenerationMode { + /// This macro is describing a Java class in Swift. + case importFromJava + + /// This macro is describing a Swift type that will be represented by + /// a generated Java class. + case exportToJava + + /// This macro is describing an extension that is implementing the native + /// methods of a Java class. + case javaImplementation + + /// Determine the mode for Java class generation based on an attribute. + init?(attribute: AttributeSyntax) { + switch attribute.attributeName.trimmedDescription { + case "JavaClass", "JavaInterface": + self = .importFromJava + + case "ExportToJavaClass": + self = .exportToJava + + case "JavaImplementation": + self = .javaImplementation + + default: + return nil + } + } + + /// Determine the mode for Java class generation based on the the macro + /// expansion context. + init?(expansionContext: some MacroExpansionContext) { + for lexicalContext in expansionContext.lexicalContext { + // FIXME: swift-syntax probably needs an AttributedSyntax node for us + // to look at. For now, we can look at just structs and extensions. + let attributes: AttributeListSyntax + if let structSyntax = lexicalContext.as(StructDeclSyntax.self) { + attributes = structSyntax.attributes + } else if let extSyntax = lexicalContext.as(ExtensionDeclSyntax.self) { + attributes = extSyntax.attributes + } else { + continue + } + + // Look for the first attribute that is associated with a mode, and + // return that. + for attribute in attributes { + if case .attribute(let attribute) = attribute, + let mode = GenerationMode(attribute: attribute) { + self = mode + return + } + } + } + + return nil + } +} diff --git a/Sources/JavaKitMacros/ImplementsJavaMacro.swift b/Sources/JavaKitMacros/ImplementsJavaMacro.swift index 2d6d7159..b3057ae4 100644 --- a/Sources/JavaKitMacros/ImplementsJavaMacro.swift +++ b/Sources/JavaKitMacros/ImplementsJavaMacro.swift @@ -16,14 +16,152 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -enum ImplementsJavaMacro {} +enum JavaImplementationMacro {} -extension ImplementsJavaMacro: PeerMacro { +extension JavaImplementationMacro: PeerMacro { static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { - [] + guard let extensionDecl = declaration.as(ExtensionDeclSyntax.self) else { + throw MacroErrors.javaImplementationRequiresExtension + } + + // Dig out the Java class name. + guard case .argumentList(let arguments) = node.arguments, + let wrapperTypeNameExpr = arguments.first?.expression, + let stringLiteral = wrapperTypeNameExpr.as(StringLiteralExprSyntax.self), + stringLiteral.segments.count == 1, + case let .stringSegment(classNameSegment)? = stringLiteral.segments.first + else { + throw MacroErrors.classNameNotStringLiteral + } + + // Check that the class name is fully-qualified, as it should be. + let className = classNameSegment.content.text + if className.firstIndex(of: ".") == nil { + throw MacroErrors.classNameNotFullyQualified(className) + } + + var exposedMembers: [DeclSyntax] = [] + for memberItem in extensionDecl.memberBlock.members { + let memberDecl = memberItem.decl + + guard let attributes = memberDecl.asProtocol(WithAttributesSyntax.self)?.attributes, + attributes.contains(where: { + guard case .attribute(let attribute) = $0 else { + return false + } + return attribute.attributeName.trimmedDescription == "JavaMethod" + }), + let memberFunc = memberDecl.as(FunctionDeclSyntax.self) + else { + continue + } + + let isStatic = memberFunc.modifiers.contains { modifier in + modifier.name.text == "static" + } + + // Static functions exposed to Java must have an "environment" parameter. + // We remove it from the signature of the native C function we expose. + var parametersClause = memberFunc.signature.parameterClause + let environmentIndex = parametersClause.parameters.indexOfParameter(named: "environment") + if isStatic { + guard let environmentIndex else { + throw MacroErrors.missingEnvironment + } + + parametersClause.parameters.remove(at: environmentIndex) + } + + // Map the parameters. + let cParameters: [FunctionParameterSyntax] = + [ + "environment: UnsafeMutablePointer!", + isStatic ? "thisClass: jclass" : "thisObj: jobject", + ] + + parametersClause.parameters.map { param in + param.with(\.type, "\(param.type).JNIType") + .with(\.trailingComma, nil) + } + + // Map the arguments. + let swiftArguments: [ExprSyntax] = memberFunc.signature.parameterClause.parameters.map { param in + let label = + if let argumentName = param.argumentName { + "\(argumentName):" + } else { + "" + } + + // The "environment" is passed through directly. + if let environmentIndex, memberFunc.signature.parameterClause.parameters[environmentIndex] == param { + return "\(raw: label)\(param.secondName ?? param.firstName)" + } + + return "\(raw: label)\(param.type)(fromJNI: \(param.secondName ?? param.firstName), in: environment!)" + } + + // Map the return type, if there is one. + let returnType = memberFunc.signature.returnClause?.type.trimmed + let cReturnType = + returnType.map { + "-> \($0).JNIType" + } ?? "" + + let swiftName = memberFunc.name.text + let cName = "Java_" + className.replacingOccurrences(of: ".", with: "_") + "_" + swiftName + let innerBody: CodeBlockItemListSyntax + let isThrowing = memberFunc.signature.effectSpecifiers?.throwsClause != nil + let tryClause: String = isThrowing ? "try " : "" + let getJNIValue: String = + returnType != nil + ? "\n .getJNIValue(in: environment)" + : "" + let swiftTypeName = extensionDecl.extendedType.trimmedDescription + if isStatic { + innerBody = """ + return \(raw: tryClause)\(raw: swiftTypeName).\(raw: swiftName)(\(raw: swiftArguments.map { $0.description }.joined(separator: ", ")))\(raw: getJNIValue) + """ + } else { + innerBody = """ + let obj = \(raw: swiftTypeName)(javaThis: thisObj, environment: environment!) + return \(raw: tryClause)obj.\(raw: swiftName)(\(raw: swiftArguments.map { $0.description }.joined(separator: ", ")))\(raw: getJNIValue) + """ + } + + let body: CodeBlockItemListSyntax + if isThrowing { + let dummyReturn: StmtSyntax + if let returnType { + dummyReturn = "return \(returnType).jniPlaceholderValue" + } else { + dummyReturn = "return ()" + } + body = """ + do { + \(innerBody) + } catch let error { + environment.throwAsException(error) + \(dummyReturn) + } + """ + } else { + body = innerBody + } + + exposedMembers.append( + """ + @_cdecl(\(literal: cName)) + func \(context.makeUniqueName(swiftName))(\(raw: cParameters.map{ $0.description }.joined(separator: ", ")))\(raw: cReturnType) { + \(body) + } + """ + ) + } + + return exposedMembers } } diff --git a/Sources/JavaKitMacros/JavaClassMacro.swift b/Sources/JavaKitMacros/JavaClassMacro.swift index 3cee441d..2f0783f1 100644 --- a/Sources/JavaKitMacros/JavaClassMacro.swift +++ b/Sources/JavaKitMacros/JavaClassMacro.swift @@ -126,150 +126,3 @@ extension JavaClassMacro: ExtensionMacro { return [AnyJavaObjectConformance.as(ExtensionDeclSyntax.self)!] } } - -extension JavaClassMacro: PeerMacro { - package static func expansion( - of node: AttributeSyntax, - providingPeersOf declaration: some DeclSyntaxProtocol, - in context: some MacroExpansionContext - ) throws -> [DeclSyntax] { - // Dig out the Java class name. - guard case .argumentList(let arguments) = node.arguments, - let wrapperTypeNameExpr = arguments.first?.expression, - let stringLiteral = wrapperTypeNameExpr.as(StringLiteralExprSyntax.self), - stringLiteral.segments.count == 1, - case let .stringSegment(classNameSegment)? = stringLiteral.segments.first - else { - throw MacroErrors.classNameNotStringLiteral - } - - // Check that the class name is fully-qualified, as it should be. - let className = classNameSegment.content.text - if className.firstIndex(of: ".") == nil { - throw MacroErrors.classNameNotFullyQualified(className) - } - - guard let swiftStruct = declaration.as(StructDeclSyntax.self) else { - throw MacroErrors.javaClassNotOnType - } - - var exposedMembers: [DeclSyntax] = [] - for memberItem in swiftStruct.memberBlock.members { - let memberDecl = memberItem.decl - - guard let attributes = memberDecl.asProtocol(WithAttributesSyntax.self)?.attributes, - attributes.contains(where: { - guard case .attribute(let attribute) = $0 else { - return false - } - return attribute.attributeName.trimmedDescription == "ImplementsJava" - }), - let memberFunc = memberDecl.as(FunctionDeclSyntax.self) - else { - continue - } - - let isStatic = memberFunc.modifiers.contains { modifier in - modifier.name.text == "static" - } - - // Static functions exposed to Java must have an "environment" parameter. - // We remove it from the signature of the native C function we expose. - var parametersClause = memberFunc.signature.parameterClause - let environmentIndex = parametersClause.parameters.indexOfParameter(named: "environment") - if isStatic { - guard let environmentIndex else { - throw MacroErrors.missingEnvironment - } - - parametersClause.parameters.remove(at: environmentIndex) - } - - // Map the parameters. - let cParameters: [FunctionParameterSyntax] = - [ - "environment: UnsafeMutablePointer!", - isStatic ? "thisClass: jclass" : "thisObj: jobject", - ] - + parametersClause.parameters.map { param in - param.with(\.type, "\(param.type).JNIType") - .with(\.trailingComma, nil) - } - - // Map the arguments. - let swiftArguments: [ExprSyntax] = memberFunc.signature.parameterClause.parameters.map { param in - let label = - if let argumentName = param.argumentName { - "\(argumentName):" - } else { - "" - } - - // The "environment" is passed through directly. - if let environmentIndex, memberFunc.signature.parameterClause.parameters[environmentIndex] == param { - return "\(raw: label)\(param.secondName ?? param.firstName)" - } - - return "\(raw: label)\(param.type)(fromJNI: \(param.secondName ?? param.firstName), in: environment!)" - } - - // Map the return type, if there is one. - let returnType = memberFunc.signature.returnClause?.type.trimmed - let cReturnType = - returnType.map { - "-> \($0).JNIType" - } ?? "" - - let swiftName = memberFunc.name.text - let cName = "Java_" + className.replacingOccurrences(of: ".", with: "_") + "_" + swiftName - let innerBody: CodeBlockItemListSyntax - let isThrowing = memberFunc.signature.effectSpecifiers?.throwsClause != nil - let tryClause: String = isThrowing ? "try " : "" - let getJNIValue: String = - returnType != nil - ? "\n .getJNIValue(in: environment)" - : "" - if isStatic { - innerBody = """ - return \(raw: tryClause)\(raw: swiftStruct.name.text).\(raw: swiftName)(\(raw: swiftArguments.map { $0.description }.joined(separator: ", ")))\(raw: getJNIValue) - """ - } else { - innerBody = """ - let obj = \(raw: swiftStruct.name.text)(javaThis: thisObj, environment: environment!) - return \(raw: tryClause)obj.\(raw: swiftName)(\(raw: swiftArguments.map { $0.description }.joined(separator: ", ")))\(raw: getJNIValue) - """ - } - - let body: CodeBlockItemListSyntax - if isThrowing { - let dummyReturn: StmtSyntax - if let returnType { - dummyReturn = "return \(returnType).jniPlaceholderValue" - } else { - dummyReturn = "return ()" - } - body = """ - do { - \(innerBody) - } catch let error { - environment.throwAsException(error) - \(dummyReturn) - } - """ - } else { - body = innerBody - } - - exposedMembers.append( - """ - @_cdecl(\(literal: cName)) - func \(context.makeUniqueName(swiftName))(\(raw: cParameters.map{ $0.description }.joined(separator: ", ")))\(raw: cReturnType) { - \(body) - } - """ - ) - } - - return exposedMembers - } -} diff --git a/Sources/JavaKitMacros/JavaMethodMacro.swift b/Sources/JavaKitMacros/JavaMethodMacro.swift index c748f050..f525c985 100644 --- a/Sources/JavaKitMacros/JavaMethodMacro.swift +++ b/Sources/JavaKitMacros/JavaMethodMacro.swift @@ -25,6 +25,22 @@ extension JavaMethodMacro: BodyMacro { providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, in context: some MacroExpansionContext ) throws -> [CodeBlockItemSyntax] { + // @JavaMethod only provides an implementation when the method was + // imported from Java. + let mode = GenerationMode(expansionContext: context) + + // FIXME: nil is a workaround for the fact that extension JavaClass doesn't + // currently have the annotations we need. We should throw + // MacroErrors.macroOutOfContext(node.attributeName.trimmedDescription) + + switch mode { + case .javaImplementation, .exportToJava: + return declaration.body.map { Array($0.statements) } ?? [] + + case .importFromJava, nil: + break + } + // Handle initializers separately. if let initDecl = declaration.as(InitializerDeclSyntax.self) { return try bridgeInitializer(initDecl, in: context) diff --git a/Sources/JavaKitMacros/MacroErrors.swift b/Sources/JavaKitMacros/MacroErrors.swift index 7a9622c9..2fcbfa91 100644 --- a/Sources/JavaKitMacros/MacroErrors.swift +++ b/Sources/JavaKitMacros/MacroErrors.swift @@ -13,9 +13,12 @@ //===----------------------------------------------------------------------===// enum MacroErrors: Error { + case unrecognizedJavaClassMacro(String) + case javaImplementationRequiresExtension case classNameNotStringLiteral case classNameNotFullyQualified(String) case javaClassNotOnType case methodNotOnFunction case missingEnvironment + case macroOutOfContext(String) } diff --git a/Sources/JavaKitMacros/SwiftJNIMacrosPlugin.swift b/Sources/JavaKitMacros/SwiftJNIMacrosPlugin.swift index f8841ef0..255e61f5 100644 --- a/Sources/JavaKitMacros/SwiftJNIMacrosPlugin.swift +++ b/Sources/JavaKitMacros/SwiftJNIMacrosPlugin.swift @@ -18,7 +18,7 @@ import SwiftSyntaxMacros @main struct JavaKitMacrosPlugin: CompilerPlugin { var providingMacros: [Macro.Type] = [ - ImplementsJavaMacro.self, + JavaImplementationMacro.self, JavaClassMacro.self, JavaFieldMacro.self, JavaMethodMacro.self, diff --git a/Sources/JavaKitReflection/Constructor+Utilities.swift b/Sources/JavaKitReflection/Constructor+Utilities.swift index 4979abbc..0eed3edd 100644 --- a/Sources/JavaKitReflection/Constructor+Utilities.swift +++ b/Sources/JavaKitReflection/Constructor+Utilities.swift @@ -13,6 +13,11 @@ //===----------------------------------------------------------------------===// extension Constructor { + /// Whether this is a 'native' method. + public var isNative: Bool { + return (getModifiers() & 256) != 0 + } + /// Whether this executable throws any checked exception. public var throwsCheckedException: Bool { return self.as(Executable.self)!.throwsCheckedException diff --git a/Sources/JavaKitReflection/JavaClass+Reflection.swift b/Sources/JavaKitReflection/JavaClass+Reflection.swift index fcaf80cc..65c93e15 100644 --- a/Sources/JavaKitReflection/JavaClass+Reflection.swift +++ b/Sources/JavaKitReflection/JavaClass+Reflection.swift @@ -17,6 +17,9 @@ import JavaKit // TODO: We should be able to autogenerate this as an extension based on // knowing that JavaClass was defined elsewhere. extension JavaClass { + @JavaMethod + public func equals(_ arg0: JavaObject?) -> Bool + @JavaMethod public func getName() -> String diff --git a/Sources/JavaKitReflection/Method+Utilities.swift b/Sources/JavaKitReflection/Method+Utilities.swift index 8f981369..28556c0f 100644 --- a/Sources/JavaKitReflection/Method+Utilities.swift +++ b/Sources/JavaKitReflection/Method+Utilities.swift @@ -18,6 +18,11 @@ extension Method { return (getModifiers() & 0x08) != 0 } + /// Whether this is a 'native' method. + public var isNative: Bool { + return (getModifiers() & 256) != 0 + } + /// Whether this executable throws any checked exception. public var throwsCheckedException: Bool { return self.as(Executable.self)!.throwsCheckedException diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index 1c1dceaa..2f3bccb1 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -102,8 +102,8 @@ final class FunctionDescriptorTests { } @Test - func FunctionDescriptor_class_counter_get() async throws { - try await variableAccessorDescriptorTest("counter", .get) { output in + func FunctionDescriptor_class_counter_get() throws { + try variableAccessorDescriptorTest("counter", .get) { output in assertOutput( output, expected: @@ -117,8 +117,8 @@ final class FunctionDescriptorTests { } } @Test - func FunctionDescriptor_class_counter_set() async throws { - try await variableAccessorDescriptorTest("counter", .set) { output in + func FunctionDescriptor_class_counter_set() throws { + try variableAccessorDescriptorTest("counter", .set) { output in assertOutput( output, expected: @@ -195,4 +195,4 @@ extension FunctionDescriptorTests { try body(getOutput) } -} \ No newline at end of file +} diff --git a/Tests/JExtractSwiftTests/VariableImportTests.swift b/Tests/JExtractSwiftTests/VariableImportTests.swift index f59b8ff4..d776b42a 100644 --- a/Tests/JExtractSwiftTests/VariableImportTests.swift +++ b/Tests/JExtractSwiftTests/VariableImportTests.swift @@ -34,14 +34,14 @@ final class VariableImportTests { """ @Test("Import: var counter: Int") - func variable_() async throws { + func variable_() throws { let st = Swift2JavaTranslator( javaPackage: "com.example.swift", swiftModuleName: "__FakeModule" ) st.log.logLevel = .error - try await st.analyze(swiftInterfacePath: "/fake/Fake.swiftinterface", text: class_interfaceFile) + try st.analyze(swiftInterfacePath: "/fake/Fake.swiftinterface", text: class_interfaceFile) let identifier = "counterInt" let varDecl: ImportedVariable? = @@ -168,4 +168,4 @@ final class VariableImportTests { """ ) } -} \ No newline at end of file +} diff --git a/Tests/Java2SwiftTests/Java2SwiftTests.swift b/Tests/Java2SwiftTests/Java2SwiftTests.swift index e4ded5ab..a2e3cb34 100644 --- a/Tests/Java2SwiftTests/Java2SwiftTests.swift +++ b/Tests/Java2SwiftTests/Java2SwiftTests.swift @@ -51,7 +51,7 @@ class Java2SwiftTests: XCTestCase { ) } - func testEnum() async throws { + func testEnum() throws { try assertTranslatedClass( JavaMonth.self, swiftTypeName: "Month", diff --git a/Tests/JavaKitTests/BasicRuntimeTests.swift b/Tests/JavaKitTests/BasicRuntimeTests.swift index ad379a92..0e5fed0b 100644 --- a/Tests/JavaKitTests/BasicRuntimeTests.swift +++ b/Tests/JavaKitTests/BasicRuntimeTests.swift @@ -24,11 +24,7 @@ var jvm: JavaVirtualMachine { } class BasicRuntimeTests: XCTestCase { - func testJavaObjectManagement() async throws { - if isLinux { - throw XCTSkip("Attempts to refcount a null pointer on Linux") - } - + func testJavaObjectManagement() throws { let environment = try jvm.environment() let sneakyJavaThis: jobject do { @@ -54,11 +50,7 @@ class BasicRuntimeTests: XCTestCase { XCTAssert(url.javaHolder === urlAgain.javaHolder) } - func testJavaExceptionsInSwift() async throws { - if isLinux { - throw XCTSkip("Attempts to refcount a null pointer on Linux") - } - + func testJavaExceptionsInSwift() throws { let environment = try jvm.environment() do { @@ -68,18 +60,14 @@ class BasicRuntimeTests: XCTestCase { } } - func testStaticMethods() async throws { - if isLinux { - throw XCTSkip("Attempts to refcount a null pointer on Linux") - } - + func testStaticMethods() throws { let environment = try jvm.environment() let urlConnectionClass = try JavaClass(in: environment) XCTAssert(urlConnectionClass.getDefaultAllowUserInteraction() == false) } - func testClassInstanceLookup() async throws { + func testClassInstanceLookup() throws { let environment = try jvm.environment() do { @@ -92,12 +80,3 @@ class BasicRuntimeTests: XCTestCase { @JavaClass("org.swift.javakit.Nonexistent") struct Nonexistent { } - -/// Whether we're running on Linux. -var isLinux: Bool { - #if os(Linux) - return true - #else - return false - #endif -} diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 1e75b93d..dccdd27e 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -213,6 +213,69 @@ do { Note that we are passing the Jar file in the `classPath` argument when initializing the `JavaVirtualMachine` instance. Otherwise, the program will fail with an error because it cannot find the Java class `com.gazman.quadratic_sieve.primes.SieveOfEratosthenes`. +### Up and downcasting + +All Java classes available in Swift provide `is` and `as` methods to check whether an object dynamically matches another type. The `is` operation is the equivalent of Java's `instanceof` and Swift's `is` operator, and will checkin whether a given object is of the specified type, e.g., + +```swift +if myObject.is(URL.self) { + // myObject is a Java URL. +} +``` + +Often, one also wants to cast to that type. The `as` method returns an optional of the specified type, so it works well with `if let`: + +```swift +if let url = myObject.as(URL.self) { + // okay, url is a Java URL +} +``` + +> *Note*: The Swift `is`, `as?`, and `as!` operators do *not* work correctly with the Swift projections of Java types. Use the `is` and `as` methods consistently. + +### Implementing Java `native` methods in Swift + +JavaKit supports implementing Java `native` methods in Swift using JNI with the `@JavaImplementation` macro. In Java, the method must be declared as `native`, e.g., + +```java +package org.swift.javakit.example; + +public class HelloSwift { + static { + System.loadLibrary("HelloSwiftLib"); + } + + public native String reportStatistics(String meaning, double[] numbers); +} +``` + +On the Swift side, the Java class needs to be exposed to Swift through `Java2Swift.config`, e.g.,: + +```swift +{ + "classes" : { + "org.swift.javakit.example.HelloSwift" : "Hello", + } +} +``` + +Implementations of `native` methods are written in an extension of the Swift type that has been marked with `@JavaImplementation`. 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: + +```swift +@JavaImplementation("org.swift.javakit.HelloSwift") +extension Hello: HelloNativeMethods { + @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)" + } +} +``` + +Java native methods that throw any checked exception should be marked as `throws` in Swift. Swift will translate any thrown error into a Java exception. + +The Swift implementations of Java `native` constructors and static methods require an additional Swift parameter `environment: JNIEnvironment? = nil`, which will receive the JNI environment in which the function is being executed. In case of nil, the `JavaVirtualMachine.shared().environment()` value will be used. + ## Using Java libraries from Swift This section describes how Java libraries and mapped into Swift and their use from Swift. @@ -324,27 +387,7 @@ A number of JavaKit modules provide Swift projections of Java classes and interf | `java.lang.Throwable` | `Throwable` | `JavaKit` | | `java.net.URL` | `URL` | `JavaKitNetwork` | -The `Java2Swift` tool can translate any other Java classes into Swift projections. The easiest way to use `Java2Swift` is with the SwiftPM plugin described above. More information about using this tool directly are provided later in this document. - -### Up and downcasting - -All `AnyJavaObject` instances provide `is` and `as` methods to check whether an object dynamically matches another type. The `is` operation is the equivalent of Java's `instanceof` and Swift's `is` operator, and will checkin whether a given object is of the specified type, e.g., - -```swift -if myObject.is(URL.self) { - // myObject is a Java URL. -} -``` - -Often, one also wants to cast to that type. The `as` method returns an optional of the specified type, so it works well with `if let`: - -```swift -if let url = myObject.as(URL.self) { - // okay, url is a Java URL -} -``` - -> *Note*: The Swift `is`, `as?`, and `as!` operators do *not* work correctly with the Swift projections of Java types. Use the `is` and `as` methods consistently. +The `Java2Swift` tool can translate any other Java classes into Swift projections. The easiest way to use `Java2Swift` is with the SwiftPM plugin described above. More information about using this tool directly are provided later in this document ### Class objects and static methods @@ -362,57 +405,6 @@ extension JavaClass { Java interfaces are similar to classes, and are projected into Swift in much the same way, but with the macro `JavaInterface`. The `JavaInterface` macro takes the Java interface name as well as any Java interfaces that this interface extends. As an example, here is the Swift projection of the [`java.util.Enumeration`](https://docs.oracle.com/javase/8/docs/api/java/util/Enumeration.html) generic interface: -```swift -@JavaInterface("java.util.Enumeration") -public struct Enumeration { - @JavaMethod - public func hasMoreElements() -> Bool - - @JavaMethod - public func nextElement() -> JavaObject? -} -``` - -## Implementing Java `native` methods in Swift - -JavaKit supports implementing Java `native` methods in Swift using JNI. In Java, the method must be declared as `native`, e.g., - -```java -package org.swift.javakit; - -public class HelloSwift { - static { - System.loadLibrary("HelloSwiftLib"); - } - - public native String reportStatistics(String meaning, double[] numbers); -} -``` - -On the Swift side, the Java class needs to have been exposed to Swift: - -```swift -@JavaClass("org.swift.javakit.HelloSwift") -struct HelloSwift { ... } -``` - -Implementations of `native` methods can be written within the Swift type or an extension thereof, and should be marked with `@ImplementsJava`. For example: - -```swift -@JavaClass("org.swift.javakit.HelloSwift") -extension HelloSwift { - @ImplementsJava - 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)" - } -} -``` - -Java native methods that throw any checked exception should be marked as `throws` in Swift. Swift will translate any thrown error into a Java exception. - -The Swift implementations of Java `native` constructors and static methods require an additional Swift parameter `environment: JNIEnvironment? = nil`, which will receive the JNI environment in which the function is being executed. In case of nil, the `JavaVirtualMachine.shared().environment()` value will be used. - ## Translating Java classes with `Java2Swift` The `Java2Swift` is a Swift program that uses Java's runtime reflection facilities to translate the requested Java classes into their Swift projections. The output is a number of Swift source files, each of which corresponds to a @@ -540,9 +532,9 @@ Now, in the `HelloSwift` Swift library, define a `struct` that provides the `mai ```swift import JavaKit -@JavaClass("org.swift.javakit.HelloSwiftMain") +@JavaImplementation("org.swift.javakit.HelloSwiftMain") struct HelloSwiftMain { - @ImplementsJava + @JavaStaticMethod static func main(arguments: [String], environment: JNIEnvironment? = nil) { print("Command line arguments are: \(arguments)") } @@ -574,7 +566,7 @@ struct HelloSwiftMain: ParsableCommand { @Option(name: .shortAndLong, help: "Enable verbose output") var verbose: Bool = false - @ImplementsJava + @JavaImplementation static func main(arguments: [String], environment: JNIEnvironment? = nil) { let command = Self.parseOrExit(arguments) command.run(environment: environment)