From b9b723c5752b9a17c0db5ac9522c73a4eef9e5ce Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 09:09:29 -0700 Subject: [PATCH 01/13] 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. --- .../Java2SwiftPlugin/Java2SwiftPlugin.swift | 15 ++ Samples/JavaKitSampleApp/Package.swift | 41 ++++- .../Sources/JavaKitExample/Java2Swift.config | 5 +- .../JavaKitExample/JavaKitExample.swift | 51 +----- .../com/example/swift/HelloSubclass.java | 2 +- .../com/example/swift/HelloSwift.java | 10 +- .../com/example/swift/JavaKitSampleMain.java | 1 - Sources/Java2SwiftLib/JavaTranslator.swift | 9 ++ Sources/JavaKit/Macros.swift | 33 ++-- Sources/JavaKitMacros/GenerationMode.swift | 76 +++++++++ .../JavaKitMacros/ImplementsJavaMacro.swift | 144 ++++++++++++++++- Sources/JavaKitMacros/JavaClassMacro.swift | 147 ------------------ Sources/JavaKitMacros/JavaMethodMacro.swift | 16 ++ Sources/JavaKitMacros/MacroErrors.swift | 3 + .../JavaKitMacros/SwiftJNIMacrosPlugin.swift | 2 +- .../Constructor+Utilities.swift | 5 + .../JavaKitReflection/Method+Utilities.swift | 5 + USER_GUIDE.md | 147 +++++++++--------- 18 files changed, 416 insertions(+), 296 deletions(-) create mode 100644 Sources/JavaKitMacros/GenerationMode.swift diff --git a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift index 30be39a9..64267bd3 100644 --- a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift +++ b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift @@ -93,6 +93,21 @@ 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() ] + } + return [ .buildCommand( displayName: "Wrapping \(config.classes.count) Java classes target \(sourceModule.name) in Swift", 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..7898bf00 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift @@ -18,30 +18,9 @@ enum SwiftWrappedError: Error { case message(String) } -@JavaClass("com.example.swift.HelloSwift") -struct HelloSwift { +@JavaImplements("com.example.swift.HelloSwift") +extension HelloSwift { @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 { print("Hello from Swift!") let answer = self.sayHelloBack(i + j) @@ -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 + @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/Java2SwiftLib/JavaTranslator.swift b/Sources/Java2SwiftLib/JavaTranslator.swift index b7eec617..1ca1977e 100644 --- a/Sources/Java2SwiftLib/JavaTranslator.swift +++ b/Sources/Java2SwiftLib/JavaTranslator.swift @@ -268,6 +268,10 @@ extension JavaTranslator { members.append( contentsOf: javaClass.getConstructors().compactMap { $0.flatMap { constructor in + if constructor.isNative { + return nil + } + do { return try translateConstructor(constructor) } catch { @@ -283,6 +287,11 @@ extension JavaTranslator { members.append( contentsOf: javaClass.getMethods().compactMap { $0.flatMap { method in + if method.isNative { + return nil + } + + // Save the static methods; they need to go on an extension of // JavaClass. if method.isStatic { diff --git a/Sources/JavaKit/Macros.swift b/Sources/JavaKit/Macros.swift index 347a3ebf..58d50ac0 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 +/// @JavaImplements +/// extension Hello { +/// @JavaMethod +/// func sayHello(i: Int32, _ j: Int32) -> Int32 { +/// // swift implementation +/// } +/// } +/// ``` @attached(peer) -public macro ImplementsJava() = #externalMacro(module: "JavaKitMacros", type: "ImplementsJavaMacro") +public macro JavaImplements(_ fullClassName: String) = #externalMacro(module: "JavaKitMacros", type: "JavaImplementsMacro") diff --git a/Sources/JavaKitMacros/GenerationMode.swift b/Sources/JavaKitMacros/GenerationMode.swift new file mode 100644 index 00000000..970ca0ea --- /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 JavaImplements + + /// 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 "JavaImplements": + self = .JavaImplements + + 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..b6479070 100644 --- a/Sources/JavaKitMacros/ImplementsJavaMacro.swift +++ b/Sources/JavaKitMacros/ImplementsJavaMacro.swift @@ -16,14 +16,152 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -enum ImplementsJavaMacro {} +enum JavaImplementsMacro {} -extension ImplementsJavaMacro: PeerMacro { +extension JavaImplementsMacro: 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.JavaImplementsRequiresExtension + } + + // 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..065ca1f1 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 .JavaImplements, .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..5c9bf078 100644 --- a/Sources/JavaKitMacros/MacroErrors.swift +++ b/Sources/JavaKitMacros/MacroErrors.swift @@ -13,9 +13,12 @@ //===----------------------------------------------------------------------===// enum MacroErrors: Error { + case unrecognizedJavaClassMacro(String) + case JavaImplementsRequiresExtension 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..e37439cf 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, + JavaImplementsMacro.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/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/USER_GUIDE.md b/USER_GUIDE.md index 1e75b93d..87b88d18 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 `@JavaImplements` 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 have been 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 `@JavaImplements`. The methods themselves must be marked with `@JavaMethod`, indicating that they are available to Java as well. For example: + +```swift +@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)" + } +} +``` + +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,10 +532,15 @@ Now, in the `HelloSwift` Swift library, define a `struct` that provides the `mai ```swift import JavaKit -@JavaClass("org.swift.javakit.HelloSwiftMain") +@JavaImplements("org.swift.javakit.HelloSwiftMain") struct HelloSwiftMain { - @ImplementsJava +<<<<<<< HEAD + @JavaImplements static func main(arguments: [String], environment: JNIEnvironment? = nil) { +======= + @JavaStaticMethod + static func main(arguments: [String], environment: JNIEnvironment) { +>>>>>>> 148e53c (JavaKit: Rework `@JavaImplements` to be more like `@implements` language feature) print("Command line arguments are: \(arguments)") } } @@ -574,7 +571,7 @@ struct HelloSwiftMain: ParsableCommand { @Option(name: .shortAndLong, help: "Enable verbose output") var verbose: Bool = false - @ImplementsJava + @JavaImplements static func main(arguments: [String], environment: JNIEnvironment? = nil) { let command = Self.parseOrExit(arguments) command.run(environment: environment) From e5c5753a8e9b81b37feb68e01740b4dc3175cf91 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 11:35:35 -0700 Subject: [PATCH 02/13] 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 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. --- .../Java2SwiftPlugin/Java2SwiftPlugin.swift | 17 +++ .../JavaKitExample/JavaKitExample.swift | 6 +- Sources/Java2Swift/JavaToSwift.swift | 9 ++ Sources/Java2SwiftLib/JavaTranslator.swift | 136 +++++++++++++----- .../JavaClass+Reflection.swift | 3 + USER_GUIDE.md | 6 +- 6 files changed, 132 insertions(+), 45 deletions(-) diff --git a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift index 64267bd3..019b0077 100644 --- a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift +++ b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift @@ -106,6 +106,23 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin { 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 [ diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift index 7898bf00..07b13fd9 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift @@ -19,9 +19,9 @@ enum SwiftWrappedError: Error { } @JavaImplements("com.example.swift.HelloSwift") -extension HelloSwift { +extension HelloSwift: HelloSwiftNativeMethods { @JavaMethod - 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") @@ -75,7 +75,7 @@ extension HelloSwift { } @JavaMethod - func throwMessageFromSwift(message: String) throws -> String { + func throwMessageFromSwift(_ message: String) throws -> String { throw SwiftWrappedError.message(message) } } 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 1ca1977e..be26ecde 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] = [] @@ -268,12 +279,22 @@ extension JavaTranslator { members.append( contentsOf: javaClass.getConstructors().compactMap { $0.flatMap { constructor in - if constructor.isNative { - return nil - } - 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 @@ -286,12 +307,7 @@ extension JavaTranslator { var staticMethods: [Method] = [] members.append( contentsOf: javaClass.getMethods().compactMap { - $0.flatMap { method in - if method.isNative { - return nil - } - - + $0.flatMap { (method) -> DeclSyntax? in // Save the static methods; they need to go on an extension of // JavaClass. if method.isStatic { @@ -299,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 @@ -356,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] = [] @@ -381,7 +409,7 @@ extension JavaTranslator { // Translate each static method. do { return try translateMethod( - method, + method, implementedInSwift: /*FIXME:*/false, genericParameterClause: genericParameterClause, whereClause: staticMemberWhereClause ) @@ -392,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 { @@ -451,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/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/USER_GUIDE.md b/USER_GUIDE.md index 87b88d18..ef25cf29 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -249,7 +249,7 @@ public class HelloSwift { } ``` -On the Swift side, the Java class needs to have been exposed to Swift through `Java2Swift.config`, e.g.,: +On the Swift side, the Java class needs to be exposed to Swift through `Java2Swift.config`, e.g.,: ```swift { @@ -259,11 +259,11 @@ On the Swift side, the Java class needs to have been exposed to Swift through `J } ``` -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: +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: ```swift @JavaImplements("org.swift.javakit.HelloSwift") -extension Hello { +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) From 816dab0ed7f4d5f7568f924f379bfa5d1a62c101 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 11:41:22 -0700 Subject: [PATCH 03/13] Rename @JavaImplements to @JavaImplementation to actually match SE-0436 --- .../Sources/JavaKitExample/JavaKitExample.swift | 2 +- Sources/JavaKit/Macros.swift | 4 ++-- Sources/JavaKitMacros/GenerationMode.swift | 6 +++--- Sources/JavaKitMacros/ImplementsJavaMacro.swift | 6 +++--- Sources/JavaKitMacros/JavaMethodMacro.swift | 2 +- Sources/JavaKitMacros/MacroErrors.swift | 2 +- .../JavaKitMacros/SwiftJNIMacrosPlugin.swift | 2 +- USER_GUIDE.md | 17 ++++++----------- 8 files changed, 18 insertions(+), 23 deletions(-) diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift index 07b13fd9..aaaff0bb 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift @@ -18,7 +18,7 @@ enum SwiftWrappedError: Error { case message(String) } -@JavaImplements("com.example.swift.HelloSwift") +@JavaImplementation("com.example.swift.HelloSwift") extension HelloSwift: HelloSwiftNativeMethods { @JavaMethod func sayHello(_ i: Int32, _ j: Int32) -> Int32 { diff --git a/Sources/JavaKit/Macros.swift b/Sources/JavaKit/Macros.swift index 58d50ac0..12ae0c26 100644 --- a/Sources/JavaKit/Macros.swift +++ b/Sources/JavaKit/Macros.swift @@ -156,7 +156,7 @@ public macro JavaStaticMethod() = #externalMacro(module: "JavaKitMacros", type: /// the method can be implemented in Swift with the following: /// /// ```swift -/// @JavaImplements +/// @JavaImplementation /// extension Hello { /// @JavaMethod /// func sayHello(i: Int32, _ j: Int32) -> Int32 { @@ -165,4 +165,4 @@ public macro JavaStaticMethod() = #externalMacro(module: "JavaKitMacros", type: /// } /// ``` @attached(peer) -public macro JavaImplements(_ fullClassName: String) = #externalMacro(module: "JavaKitMacros", type: "JavaImplementsMacro") +public macro JavaImplementation(_ fullClassName: String) = #externalMacro(module: "JavaKitMacros", type: "JavaImplementationMacro") diff --git a/Sources/JavaKitMacros/GenerationMode.swift b/Sources/JavaKitMacros/GenerationMode.swift index 970ca0ea..d000d209 100644 --- a/Sources/JavaKitMacros/GenerationMode.swift +++ b/Sources/JavaKitMacros/GenerationMode.swift @@ -26,7 +26,7 @@ enum GenerationMode { /// This macro is describing an extension that is implementing the native /// methods of a Java class. - case JavaImplements + case javaImplementation /// Determine the mode for Java class generation based on an attribute. init?(attribute: AttributeSyntax) { @@ -37,8 +37,8 @@ enum GenerationMode { case "ExportToJavaClass": self = .exportToJava - case "JavaImplements": - self = .JavaImplements + case "JavaImplementation": + self = .javaImplementation default: return nil diff --git a/Sources/JavaKitMacros/ImplementsJavaMacro.swift b/Sources/JavaKitMacros/ImplementsJavaMacro.swift index b6479070..b3057ae4 100644 --- a/Sources/JavaKitMacros/ImplementsJavaMacro.swift +++ b/Sources/JavaKitMacros/ImplementsJavaMacro.swift @@ -16,16 +16,16 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -enum JavaImplementsMacro {} +enum JavaImplementationMacro {} -extension JavaImplementsMacro: 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.JavaImplementsRequiresExtension + throw MacroErrors.javaImplementationRequiresExtension } // Dig out the Java class name. diff --git a/Sources/JavaKitMacros/JavaMethodMacro.swift b/Sources/JavaKitMacros/JavaMethodMacro.swift index 065ca1f1..f525c985 100644 --- a/Sources/JavaKitMacros/JavaMethodMacro.swift +++ b/Sources/JavaKitMacros/JavaMethodMacro.swift @@ -34,7 +34,7 @@ extension JavaMethodMacro: BodyMacro { // MacroErrors.macroOutOfContext(node.attributeName.trimmedDescription) switch mode { - case .JavaImplements, .exportToJava: + case .javaImplementation, .exportToJava: return declaration.body.map { Array($0.statements) } ?? [] case .importFromJava, nil: diff --git a/Sources/JavaKitMacros/MacroErrors.swift b/Sources/JavaKitMacros/MacroErrors.swift index 5c9bf078..2fcbfa91 100644 --- a/Sources/JavaKitMacros/MacroErrors.swift +++ b/Sources/JavaKitMacros/MacroErrors.swift @@ -14,7 +14,7 @@ enum MacroErrors: Error { case unrecognizedJavaClassMacro(String) - case JavaImplementsRequiresExtension + case javaImplementationRequiresExtension case classNameNotStringLiteral case classNameNotFullyQualified(String) case javaClassNotOnType diff --git a/Sources/JavaKitMacros/SwiftJNIMacrosPlugin.swift b/Sources/JavaKitMacros/SwiftJNIMacrosPlugin.swift index e37439cf..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] = [ - JavaImplementsMacro.self, + JavaImplementationMacro.self, JavaClassMacro.self, JavaFieldMacro.self, JavaMethodMacro.self, diff --git a/USER_GUIDE.md b/USER_GUIDE.md index ef25cf29..dccdd27e 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -235,7 +235,7 @@ if let url = myObject.as(URL.self) { ### Implementing Java `native` methods in Swift -JavaKit supports implementing Java `native` methods in Swift using JNI with the `@JavaImplements` macro. In Java, the method must be declared as `native`, e.g., +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; @@ -259,10 +259,10 @@ On the Swift side, the Java class needs to be exposed to Swift through `Java2Swi } ``` -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: +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 -@JavaImplements("org.swift.javakit.HelloSwift") +@JavaImplementation("org.swift.javakit.HelloSwift") extension Hello: HelloNativeMethods { @JavaMethod func reportStatistics(_ meaning: String, _ numbers: [Double]) -> String { @@ -532,15 +532,10 @@ Now, in the `HelloSwift` Swift library, define a `struct` that provides the `mai ```swift import JavaKit -@JavaImplements("org.swift.javakit.HelloSwiftMain") +@JavaImplementation("org.swift.javakit.HelloSwiftMain") struct HelloSwiftMain { -<<<<<<< HEAD - @JavaImplements - static func main(arguments: [String], environment: JNIEnvironment? = nil) { -======= @JavaStaticMethod - static func main(arguments: [String], environment: JNIEnvironment) { ->>>>>>> 148e53c (JavaKit: Rework `@JavaImplements` to be more like `@implements` language feature) + static func main(arguments: [String], environment: JNIEnvironment? = nil) { print("Command line arguments are: \(arguments)") } } @@ -571,7 +566,7 @@ struct HelloSwiftMain: ParsableCommand { @Option(name: .shortAndLong, help: "Enable verbose output") var verbose: Bool = false - @JavaImplements + @JavaImplementation static func main(arguments: [String], environment: JNIEnvironment? = nil) { let command = Self.parseOrExit(arguments) command.run(environment: environment) From 44390696da5687d83632f4768a878bde48f98de3 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 11:56:38 -0700 Subject: [PATCH 04/13] Fix formatting the imported initializers --- Sources/Java2SwiftLib/JavaTranslator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Java2SwiftLib/JavaTranslator.swift b/Sources/Java2SwiftLib/JavaTranslator.swift index be26ecde..f82cd219 100644 --- a/Sources/Java2SwiftLib/JavaTranslator.swift +++ b/Sources/Java2SwiftLib/JavaTranslator.swift @@ -480,7 +480,7 @@ extension JavaTranslator { : "@JavaMethod\n" let accessModifier = implementedInSwift ? "" : "public " return """ - \(raw: javaMethodAttribute)\(raw: accessModifier) init(\(raw: parametersStr))\(raw: throwsStr) + \(raw: javaMethodAttribute)\(raw: accessModifier)init(\(raw: parametersStr))\(raw: throwsStr) """ } From 43965d9560d8cb025772ef5c9f35e0e82a9f7328 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 22:01:40 -0700 Subject: [PATCH 05/13] Drop unnecessary async --- .../FunctionDescriptorImportTests.swift | 10 +++++----- Tests/JExtractSwiftTests/VariableImportTests.swift | 6 +++--- Tests/Java2SwiftTests/Java2SwiftTests.swift | 2 +- Tests/JavaKitTests/BasicRuntimeTests.swift | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) 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..1010d24d 100644 --- a/Tests/JavaKitTests/BasicRuntimeTests.swift +++ b/Tests/JavaKitTests/BasicRuntimeTests.swift @@ -24,7 +24,7 @@ var jvm: JavaVirtualMachine { } class BasicRuntimeTests: XCTestCase { - func testJavaObjectManagement() async throws { + func testJavaObjectManagement() throws { if isLinux { throw XCTSkip("Attempts to refcount a null pointer on Linux") } @@ -54,7 +54,7 @@ class BasicRuntimeTests: XCTestCase { XCTAssert(url.javaHolder === urlAgain.javaHolder) } - func testJavaExceptionsInSwift() async throws { + func testJavaExceptionsInSwift() throws { if isLinux { throw XCTSkip("Attempts to refcount a null pointer on Linux") } @@ -68,7 +68,7 @@ class BasicRuntimeTests: XCTestCase { } } - func testStaticMethods() async throws { + func testStaticMethods() throws { if isLinux { throw XCTSkip("Attempts to refcount a null pointer on Linux") } @@ -79,7 +79,7 @@ class BasicRuntimeTests: XCTestCase { XCTAssert(urlConnectionClass.getDefaultAllowUserInteraction() == false) } - func testClassInstanceLookup() async throws { + func testClassInstanceLookup() throws { let environment = try jvm.environment() do { From d2a14640281fdddc11d0c31d7fd990cbaeeb488b Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 22:02:25 -0700 Subject: [PATCH 06/13] Improve comment --- Sources/Java2SwiftLib/JavaTranslator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Java2SwiftLib/JavaTranslator.swift b/Sources/Java2SwiftLib/JavaTranslator.swift index f82cd219..a5af9baa 100644 --- a/Sources/Java2SwiftLib/JavaTranslator.swift +++ b/Sources/Java2SwiftLib/JavaTranslator.swift @@ -444,7 +444,7 @@ extension JavaTranslator { if !nativeMembers.isEmpty { let protocolDecl: DeclSyntax = """ - /// Describes the Java `native` methods for \(raw: swiftTypeName). + /// 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 From e8269c7e0e84e726c2f2efb74f20fbcc6c4c271d Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 22:05:55 -0700 Subject: [PATCH 07/13] Make sure we have the Java include paths... everywhere --- Package.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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)"]) ] ), From a0d9c9e96a51aa15033235dd56d09b7312d4f0ae Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 22:30:23 -0700 Subject: [PATCH 08/13] Make sure that the Java2Swift plugin depends on the JavaCompiler's output --- Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift index 019b0077..37ee1b3a 100644 --- a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift +++ b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift @@ -130,7 +130,7 @@ struct Java2SwiftBuildToolPlugin: BuildToolPlugin { 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 ) ] From 5ee3126542437c3733797067a9a6a4ba347971f4 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 22:30:00 -0700 Subject: [PATCH 09/13] Temporarily disable test on Linux Something is very fishy in our JVM interactions within tests there --- Tests/Java2SwiftTests/Java2SwiftTests.swift | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Tests/Java2SwiftTests/Java2SwiftTests.swift b/Tests/Java2SwiftTests/Java2SwiftTests.swift index a2e3cb34..863fb8c0 100644 --- a/Tests/Java2SwiftTests/Java2SwiftTests.swift +++ b/Tests/Java2SwiftTests/Java2SwiftTests.swift @@ -30,6 +30,10 @@ public struct JavaMonth { class Java2SwiftTests: XCTestCase { func testJavaLangObjectMapping() throws { + if isLinux { + throw XCTSkip("Crashes for unexplained reasons on Linux") + } + try assertTranslatedClass( JavaObject.self, swiftTypeName: "MyJavaObject", @@ -52,6 +56,10 @@ class Java2SwiftTests: XCTestCase { } func testEnum() throws { + if isLinux { + throw XCTSkip("Crashes for unexplained reasons on Linux") + } + try assertTranslatedClass( JavaMonth.self, swiftTypeName: "Month", @@ -82,6 +90,10 @@ class Java2SwiftTests: XCTestCase { } func testGenericCollections() throws { + if isLinux { + throw XCTSkip("Crashes for unexplained reasons on Linux") + } + try assertTranslatedClass( MyArrayList.self, swiftTypeName: "JavaArrayList", @@ -99,6 +111,10 @@ class Java2SwiftTests: XCTestCase { } func testLinkedList() throws { + if isLinux { + throw XCTSkip("Crashes for unexplained reasons on Linux") + } + try assertTranslatedClass( MyLinkedList.self, swiftTypeName: "JavaLinkedList", @@ -167,3 +183,12 @@ func assertTranslatedClass( XCTFail("Expected chunk '\(expectedChunk)' not found in '\(swiftFileText)'", file: file, line: line) } } + +/// Whether we're running on Linux. +var isLinux: Bool { + #if os(Linux) + return true + #else + return false + #endif +} From 06737b33d5e78c018fc08121de481c215961e1ae Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 23:02:31 -0700 Subject: [PATCH 10/13] Revert "Temporarily disable test on Linux" This reverts commit 5ee3126542437c3733797067a9a6a4ba347971f4. --- Tests/Java2SwiftTests/Java2SwiftTests.swift | 25 --------------------- 1 file changed, 25 deletions(-) diff --git a/Tests/Java2SwiftTests/Java2SwiftTests.swift b/Tests/Java2SwiftTests/Java2SwiftTests.swift index 863fb8c0..a2e3cb34 100644 --- a/Tests/Java2SwiftTests/Java2SwiftTests.swift +++ b/Tests/Java2SwiftTests/Java2SwiftTests.swift @@ -30,10 +30,6 @@ public struct JavaMonth { class Java2SwiftTests: XCTestCase { func testJavaLangObjectMapping() throws { - if isLinux { - throw XCTSkip("Crashes for unexplained reasons on Linux") - } - try assertTranslatedClass( JavaObject.self, swiftTypeName: "MyJavaObject", @@ -56,10 +52,6 @@ class Java2SwiftTests: XCTestCase { } func testEnum() throws { - if isLinux { - throw XCTSkip("Crashes for unexplained reasons on Linux") - } - try assertTranslatedClass( JavaMonth.self, swiftTypeName: "Month", @@ -90,10 +82,6 @@ class Java2SwiftTests: XCTestCase { } func testGenericCollections() throws { - if isLinux { - throw XCTSkip("Crashes for unexplained reasons on Linux") - } - try assertTranslatedClass( MyArrayList.self, swiftTypeName: "JavaArrayList", @@ -111,10 +99,6 @@ class Java2SwiftTests: XCTestCase { } func testLinkedList() throws { - if isLinux { - throw XCTSkip("Crashes for unexplained reasons on Linux") - } - try assertTranslatedClass( MyLinkedList.self, swiftTypeName: "JavaLinkedList", @@ -183,12 +167,3 @@ func assertTranslatedClass( XCTFail("Expected chunk '\(expectedChunk)' not found in '\(swiftFileText)'", file: file, line: line) } } - -/// Whether we're running on Linux. -var isLinux: Bool { - #if os(Linux) - return true - #else - return false - #endif -} From d5dda789e0380e3a66743372e44c0aaa370ed0b4 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 23:02:42 -0700 Subject: [PATCH 11/13] Disable a different test on Linux (ugh) --- Tests/JavaKitTests/BasicRuntimeTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/JavaKitTests/BasicRuntimeTests.swift b/Tests/JavaKitTests/BasicRuntimeTests.swift index 1010d24d..267d5dce 100644 --- a/Tests/JavaKitTests/BasicRuntimeTests.swift +++ b/Tests/JavaKitTests/BasicRuntimeTests.swift @@ -80,6 +80,10 @@ class BasicRuntimeTests: XCTestCase { } func testClassInstanceLookup() throws { + if isLinux { + throw XCTSkip("Attempts to refcount a null pointer on Linux") + } + let environment = try jvm.environment() do { From e87c3bf56e81850cdb6b312812924fd84deac7e3 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 23:11:27 -0700 Subject: [PATCH 12/13] Simplify construction of parameter types array --- Sources/JavaKit/JavaObject+MethodCalls.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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, From f14a44dd63178faadbe662cbfdc5f976f3989c38 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 23 Oct 2024 23:21:06 -0700 Subject: [PATCH 13/13] Re-enable all tests on Linux --- Tests/JavaKitTests/BasicRuntimeTests.swift | 25 ---------------------- 1 file changed, 25 deletions(-) diff --git a/Tests/JavaKitTests/BasicRuntimeTests.swift b/Tests/JavaKitTests/BasicRuntimeTests.swift index 267d5dce..0e5fed0b 100644 --- a/Tests/JavaKitTests/BasicRuntimeTests.swift +++ b/Tests/JavaKitTests/BasicRuntimeTests.swift @@ -25,10 +25,6 @@ var jvm: JavaVirtualMachine { class BasicRuntimeTests: XCTestCase { func testJavaObjectManagement() throws { - if isLinux { - throw XCTSkip("Attempts to refcount a null pointer on Linux") - } - let environment = try jvm.environment() let sneakyJavaThis: jobject do { @@ -55,10 +51,6 @@ class BasicRuntimeTests: XCTestCase { } func testJavaExceptionsInSwift() throws { - if isLinux { - throw XCTSkip("Attempts to refcount a null pointer on Linux") - } - let environment = try jvm.environment() do { @@ -69,10 +61,6 @@ class BasicRuntimeTests: XCTestCase { } func testStaticMethods() throws { - if isLinux { - throw XCTSkip("Attempts to refcount a null pointer on Linux") - } - let environment = try jvm.environment() let urlConnectionClass = try JavaClass(in: environment) @@ -80,10 +68,6 @@ class BasicRuntimeTests: XCTestCase { } func testClassInstanceLookup() throws { - if isLinux { - throw XCTSkip("Attempts to refcount a null pointer on Linux") - } - let environment = try jvm.environment() do { @@ -96,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 -}