diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift new file mode 100644 index 00000000..00d1f4b0 --- /dev/null +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +public func emptyClosure(closure: () -> ()) { + closure() +} + +public func closureWithInt(input: Int64, closure: (Int64) -> Int64) -> Int64 { + return closure(input) +} + +public func closureMultipleArguments( + input1: Int64, + input2: Int64, + closure: (Int64, Int64) -> Int64 +) -> Int64 { + return closure(input1, input2) +} + + diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java new file mode 100644 index 00000000..b8389d41 --- /dev/null +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +public class ClosuresTest { + @Test + void emptyClosure() { + AtomicBoolean closureCalled = new AtomicBoolean(false); + MySwiftLibrary.emptyClosure(() -> { + closureCalled.set(true); + }); + assertTrue(closureCalled.get()); + } + + @Test + void closureWithInt() { + long result = MySwiftLibrary.closureWithInt(10, (value) -> value * 2); + assertEquals(20, result); + } + + @Test + void closureMultipleArguments() { + long result = MySwiftLibrary.closureMultipleArguments(5, 10, (a, b) -> a + b); + assertEquals(15, result); + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index 9da3ae5b..f9a67419 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -17,21 +17,22 @@ import JavaTypes extension JavaType { var jniTypeSignature: String { switch self { - case .boolean: "Z" - case .byte: "B" - case .char: "C" - case .short: "S" - case .int: "I" - case .long: "J" - case .float: "F" - case .double: "D" + case .boolean: return "Z" + case .byte: return "B" + case .char: return "C" + case .short: return "S" + case .int: return "I" + case .long: return "J" + case .float: return "F" + case .double: return "D" case .class(let package, let name): + let nameWithInnerClasses = name.replacingOccurrences(of: ".", with: "$") if let package { - "L\(package.replacingOccurrences(of: ".", with: "/"))/\(name);" + return "L\(package.replacingOccurrences(of: ".", with: "/"))/\(nameWithInnerClasses);" } else { - "L\(name);" + return "L\(nameWithInnerClasses);" } - case .array(let javaType): "[\(javaType.jniTypeSignature)" + case .array(let javaType): return "[\(javaType.jniTypeSignature)" case .void: fatalError("There is no type signature for 'void'") } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 9c3d14b8..e0e5cb70 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -76,12 +76,12 @@ extension JNISwift2JavaGenerator { for decl in analysis.importedGlobalFuncs { self.logger.trace("Print global function: \(decl)") - printFunctionBinding(&printer, decl) + printFunctionDowncallMethods(&printer, decl) printer.println() } for decl in analysis.importedGlobalVariables { - printFunctionBinding(&printer, decl) + printFunctionDowncallMethods(&printer, decl) printer.println() } } @@ -117,17 +117,17 @@ extension JNISwift2JavaGenerator { printer.println() for initializer in decl.initializers { - printFunctionBinding(&printer, initializer) + printFunctionDowncallMethods(&printer, initializer) printer.println() } for method in decl.methods { - printFunctionBinding(&printer, method) + printFunctionDowncallMethods(&printer, method) printer.println() } for variable in decl.variables { - printFunctionBinding(&printer, variable) + printFunctionDowncallMethods(&printer, variable) printer.println() } @@ -175,12 +175,67 @@ extension JNISwift2JavaGenerator { } } - private func printFunctionBinding(_ printer: inout CodePrinter, _ decl: ImportedFunc) { - guard let translatedDecl = translatedDecl(for: decl) else { + private func printFunctionDowncallMethods( + _ printer: inout CodePrinter, + _ decl: ImportedFunc + ) { + guard translatedDecl(for: decl) != nil else { // Failed to translate. Skip. return } + printer.printSeparator(decl.displayName) + + printJavaBindingWrapperHelperClass(&printer, decl) + + printJavaBindingWrapperMethod(&printer, decl) + } + + /// Print the helper type container for a user-facing Java API. + /// + /// * User-facing functional interfaces. + private func printJavaBindingWrapperHelperClass( + _ printer: inout CodePrinter, + _ decl: ImportedFunc + ) { + let translated = self.translatedDecl(for: decl)! + if translated.functionTypes.isEmpty { + return + } + + printer.printBraceBlock( + """ + public static class \(translated.name) + """ + ) { printer in + for functionType in translated.functionTypes { + printJavaBindingWrapperFunctionTypeHelper(&printer, functionType) + } + } + } + + /// Print "wrapper" functional interface representing a Swift closure type. + func printJavaBindingWrapperFunctionTypeHelper( + _ printer: inout CodePrinter, + _ functionType: TranslatedFunctionType + ) { + let apiParams = functionType.parameters.map(\.parameter.asParameter) + + printer.print( + """ + @FunctionalInterface + public interface \(functionType.name) { + \(functionType.result.javaType) apply(\(apiParams.joined(separator: ", "))); + } + """ + ) + } + + private func printJavaBindingWrapperMethod(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + guard let translatedDecl = translatedDecl(for: decl) else { + fatalError("Decl was not translated, \(decl)") + } + var modifiers = ["public"] if decl.isStatic || decl.isInitializer || !decl.hasParent { modifiers.append("static") @@ -215,7 +270,7 @@ extension JNISwift2JavaGenerator { if let selfParameter = nativeSignature.selfParameter { parameters.append(selfParameter) } - let renderedParameters = parameters.map { "\($0.javaParameter.type) \($0.javaParameter.name)"}.joined(separator: ", ") + let renderedParameters = parameters.map { "\($0.javaType) \($0.name)"}.joined(separator: ", ") printer.print("private static native \(resultType) \(translatedDecl.nativeFunctionName)(\(renderedParameters));") } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index e0253624..e704739c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -24,7 +24,7 @@ extension JNISwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { - let translation = JavaTranslation(swiftModuleName: swiftModuleName) + let translation = JavaTranslation(swiftModuleName: swiftModuleName, javaPackage: self.javaPackage) translated = try translation.translate(decl) } catch { self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") @@ -37,17 +37,10 @@ extension JNISwift2JavaGenerator { struct JavaTranslation { let swiftModuleName: String + let javaPackage: String func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { - let nativeTranslation = NativeJavaTranslation() - - // Swift -> Java - let translatedFunctionSignature = try translate(functionSignature: decl.functionSignature) - // Java -> Java (native) - let nativeFunctionSignature = try nativeTranslation.translate( - functionSignature: decl.functionSignature, - translatedFunctionSignature: translatedFunctionSignature - ) + let nativeTranslation = NativeJavaTranslation(javaPackage: self.javaPackage) // Types with no parent will be outputted inside a "module" class. let parentName = decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName @@ -59,19 +52,81 @@ extension JNISwift2JavaGenerator { case .function, .initializer: decl.name } + // Swift -> Java + let translatedFunctionSignature = try translate( + functionSignature: decl.functionSignature, + methodName: javaName, + parentName: parentName + ) + // Java -> Java (native) + let nativeFunctionSignature = try nativeTranslation.translate( + functionSignature: decl.functionSignature, + translatedFunctionSignature: translatedFunctionSignature, + methodName: javaName, + parentName: parentName + ) + + // Closures. + var funcTypes: [TranslatedFunctionType] = [] + for (idx, param) in decl.functionSignature.parameters.enumerated() { + let parameterName = param.parameterName ?? "_\(idx)" + + switch param.type { + case .function(let funcTy): + let translatedClosure = try translateFunctionType( + name: parameterName, + swiftType: funcTy, + parentName: parentName + ) + funcTypes.append(translatedClosure) + default: + break + } + } + return TranslatedFunctionDecl( name: javaName, nativeFunctionName: "$\(javaName)", parentName: parentName, + functionTypes: funcTypes, translatedFunctionSignature: translatedFunctionSignature, nativeFunctionSignature: nativeFunctionSignature ) } - func translate(functionSignature: SwiftFunctionSignature) throws -> TranslatedFunctionSignature { + /// Translate Swift closure type to Java functional interface. + func translateFunctionType( + name: String, + swiftType: SwiftFunctionType, + parentName: String + ) throws -> TranslatedFunctionType { + var translatedParams: [TranslatedParameter] = [] + + for (i, param) in swiftType.parameters.enumerated() { + let paramName = param.parameterName ?? "_\(i)" + translatedParams.append( + try translateParameter(swiftType: param.type, parameterName: paramName, methodName: name, parentName: parentName) + ) + } + + let transltedResult = try translate(swiftResult: SwiftResult(convention: .direct, type: swiftType.resultType)) + + return TranslatedFunctionType( + name: name, + parameters: translatedParams, + result: transltedResult, + swiftType: swiftType + ) + } + + func translate( + functionSignature: SwiftFunctionSignature, + methodName: String, + parentName: String + ) throws -> TranslatedFunctionSignature { let parameters = try functionSignature.parameters.enumerated().map { idx, param in let parameterName = param.parameterName ?? "arg\(idx))" - return try translateParameter(swiftType: param.type, parameterName: parameterName) + return try translateParameter(swiftType: param.type, parameterName: parameterName, methodName: methodName, parentName: parentName) } // 'self' @@ -79,7 +134,9 @@ extension JNISwift2JavaGenerator { if case .instance(let swiftSelf) = functionSignature.selfParameter { selfParameter = try self.translateParameter( swiftType: swiftSelf.type, - parameterName: swiftSelf.parameterName ?? "self" + parameterName: swiftSelf.parameterName ?? "self", + methodName: methodName, + parentName: parentName ) } else { selfParameter = nil @@ -92,7 +149,12 @@ extension JNISwift2JavaGenerator { ) } - func translateParameter(swiftType: SwiftType, parameterName: String) throws -> TranslatedParameter { + func translateParameter( + swiftType: SwiftType, + parameterName: String, + methodName: String, + parentName: String + ) throws -> TranslatedParameter { switch swiftType { case .nominal(let nominalType): if let knownType = nominalType.nominalTypeDecl.knownTypeKind { @@ -121,7 +183,16 @@ extension JNISwift2JavaGenerator { conversion: .placeholder ) - case .metatype, .optional, .tuple, .function, .existential, .opaque: + case .function: + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)") + ), + conversion: .placeholder + ) + + case .metatype, .optional, .tuple, .existential, .opaque: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -168,6 +239,9 @@ extension JNISwift2JavaGenerator { /// The name of the Java parent scope this function is declared in let parentName: String + /// Functional interfaces required for the Java method. + let functionTypes: [TranslatedFunctionType] + /// Function signature of the Java function the user will call let translatedFunctionSignature: TranslatedFunctionSignature @@ -220,6 +294,16 @@ extension JNISwift2JavaGenerator { let conversion: JavaNativeConversionStep } + /// Represent a Swift closure type in the user facing Java API. + /// + /// Closures are translated to named functional interfaces in Java. + struct TranslatedFunctionType { + var name: String + var parameters: [TranslatedParameter] + var result: TranslatedResult + var swiftType: SwiftFunctionType + } + /// Describes how to convert values between Java types and the native Java function enum JavaNativeConversionStep { /// The value being converted diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 461c7301..cae6010d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -17,14 +17,25 @@ import JavaTypes extension JNISwift2JavaGenerator { struct NativeJavaTranslation { + let javaPackage: String + /// Translates a Swift function into the native JNI method signature. func translate( functionSignature: SwiftFunctionSignature, - translatedFunctionSignature: TranslatedFunctionSignature + translatedFunctionSignature: TranslatedFunctionSignature, + methodName: String, + parentName: String ) throws -> NativeFunctionSignature { - let parameters = try zip(translatedFunctionSignature.parameters, functionSignature.parameters).map { translatedParameter, swiftParameter in + let parameters = try zip(translatedFunctionSignature.parameters, functionSignature.parameters).map { + translatedParameter, + swiftParameter in let parameterName = translatedParameter.parameter.name - return try translate(swiftParameter: swiftParameter, parameterName: parameterName) + return try translate( + swiftParameter: swiftParameter, + parameterName: parameterName, + methodName: methodName, + parentName: parentName + ) } // Lower the self parameter. @@ -32,7 +43,9 @@ extension JNISwift2JavaGenerator { case .instance(let selfParameter): try translate( swiftParameter: selfParameter, - parameterName: selfParameter.parameterName ?? "self" + parameterName: selfParameter.parameterName ?? "self", + methodName: methodName, + parentName: parentName ) case nil, .initializer(_), .staticMethod(_): nil @@ -47,7 +60,9 @@ extension JNISwift2JavaGenerator { func translate( swiftParameter: SwiftParameter, - parameterName: String + parameterName: String, + methodName: String, + parentName: String, ) throws -> NativeParameter { switch swiftParameter.type { case .nominal(let nominalType): @@ -57,28 +72,111 @@ extension JNISwift2JavaGenerator { } return NativeParameter( - javaParameter: JavaParameter(name: parameterName, type: javaType), + name: parameterName, + javaType: javaType, conversion: .initFromJNI(.placeholder, swiftType: swiftParameter.type) ) } case .tuple([]): return NativeParameter( - javaParameter: JavaParameter(name: parameterName, type: .void), + name: parameterName, + javaType: .void, conversion: .placeholder ) - case .metatype, .optional, .tuple, .function, .existential, .opaque: + case .function(let fn): + var parameters = [NativeParameter]() + for (i, parameter) in fn.parameters.enumerated() { + let parameterName = parameter.parameterName ?? "_\(i)" + let closureParameter = try translateClosureParameter( + parameter.type, + parameterName: parameterName + ) + parameters.append(closureParameter) + } + + let result = try translateClosureResult(fn.resultType) + + return NativeParameter( + name: parameterName, + javaType: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)"), + conversion: .closureLowering( + parameters: parameters, + result: result + ) + ) + + case .metatype, .optional, .tuple, .existential, .opaque: throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) } // Classes are passed as the pointer. return NativeParameter( - javaParameter: JavaParameter(name: parameterName, type: .long), + name: parameterName, + javaType: .long, conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: swiftParameter.type)) ) } + func translateClosureResult( + _ type: SwiftType + ) throws -> NativeResult { + switch type { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + + // Only support primitives for now. + return NativeResult( + javaType: javaType, + conversion: .initFromJNI(.placeholder, swiftType: type) + ) + } + + // Custom types are not supported yet. + throw JavaTranslationError.unsupportedSwiftType(type) + + case .tuple([]): + return NativeResult( + javaType: .void, + conversion: .placeholder + ) + + case .function, .metatype, .optional, .tuple, .existential, .opaque: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + func translateClosureParameter( + _ type: SwiftType, + parameterName: String + ) throws -> NativeParameter { + switch type { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + + // Only support primitives for now. + return NativeParameter( + name: parameterName, + javaType: javaType, + conversion: .getJValue(.placeholder) + ) + } + + // Custom types are not supported yet. + throw JavaTranslationError.unsupportedSwiftType(type) + + case .function, .metatype, .optional, .tuple, .existential, .opaque: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + func translate( swiftResult: SwiftResult ) throws -> NativeResult { @@ -122,10 +220,11 @@ extension JNISwift2JavaGenerator { } struct NativeParameter { - let javaParameter: JavaParameter + let name: String + let javaType: JavaType var jniType: JNIType { - javaParameter.type.jniType + javaType.jniType } /// Represents how to convert the JNI parameter to a Swift parameter @@ -145,6 +244,9 @@ extension JNISwift2JavaGenerator { /// `value.getJNIValue(in:)` indirect case getJNIValue(NativeSwiftConversionStep) + /// `value.getJValue(in:)` + indirect case getJValue(NativeSwiftConversionStep) + /// `SwiftType(from: value, in: environment!)` indirect case initFromJNI(NativeSwiftConversionStep, swiftType: SwiftType) @@ -152,12 +254,13 @@ extension JNISwift2JavaGenerator { indirect case extractSwiftValue(NativeSwiftConversionStep, swiftType: SwiftType) /// Allocate memory for a Swift value and outputs the pointer - indirect case allocateSwiftValue(name: String, swiftType: SwiftType) + case allocateSwiftValue(name: String, swiftType: SwiftType) /// The thing to which the pointer typed, which is the `pointee` property /// of the `Unsafe(Mutable)Pointer` types in Swift. indirect case pointee(NativeSwiftConversionStep) + indirect case closureLowering(parameters: [NativeParameter], result: NativeResult) /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { @@ -171,6 +274,10 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(inner).getJNIValue(in: environment!)" + case .getJValue(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner).getJValue(in: environment!)" + case .initFromJNI(let inner, let swiftType): let inner = inner.render(&printer, placeholder) return "\(swiftType)(fromJNI: \(inner), in: environment!)" @@ -203,6 +310,44 @@ extension JNISwift2JavaGenerator { case .pointee(let inner): let inner = inner.render(&printer, placeholder) return "\(inner).pointee" + + case .closureLowering(let parameters, let nativeResult): + var printer = CodePrinter() + + let methodSignature = MethodSignature( + resultType: nativeResult.javaType, + parameterTypes: parameters.map(\.javaType) + ) + + let closureParameters = !parameters.isEmpty ? "\(parameters.map(\.name).joined(separator: ", ")) in" : "" + printer.print("{ \(closureParameters)") + printer.indent() + + let arguments = parameters.map { + $0.conversion.render(&printer, $0.name) + } + + printer.print( + """ + let class$ = environment!.interface.GetObjectClass(environment, \(placeholder)) + let methodID$ = environment!.interface.GetMethodID(environment, class$, "apply", "\(methodSignature.mangledName)")! + let arguments$: [jvalue] = [\(arguments.joined(separator: ", "))] + """ + ) + + let upcall = "environment!.interface.\(nativeResult.javaType.jniType.callMethodAName)(environment, \(placeholder), methodID$, arguments$)" + let result = nativeResult.conversion.render(&printer, upcall) + + if nativeResult.javaType.isVoid { + printer.print(result) + } else { + printer.print("return \(result)") + } + + printer.outdent() + printer.print("}") + + return printer.finalize() } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index d8b6b275..20551465 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -135,7 +135,7 @@ extension JNISwift2JavaGenerator { &printer, javaMethodName: translatedDecl.nativeFunctionName, parentName: translatedDecl.parentName, - parameters: parameters.map(\.javaParameter), + parameters: parameters.map { JavaParameter(name: $0.name, type: $0.javaType) }, resultType: nativeSignature.result.javaType.jniType ) { printer in self.printFunctionDowncall(&printer, decl) @@ -156,7 +156,7 @@ extension JNISwift2JavaGenerator { // Regular parameters. var arguments = [String]() for parameter in nativeSignature.parameters { - let lowered = parameter.conversion.render(&printer, parameter.javaParameter.name) + let lowered = parameter.conversion.render(&printer, parameter.name) arguments.append(lowered) } @@ -273,6 +273,7 @@ extension JNISwift2JavaGenerator { // Generated by swift-java import JavaKit + import JavaRuntime """ ) diff --git a/Sources/JExtractSwiftLib/JNI/JNIType.swift b/Sources/JExtractSwiftLib/JNI/JNIType.swift index d152942a..feb8a545 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIType.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIType.swift @@ -40,6 +40,22 @@ enum JNIType { case jfloatArray case jdoubleArray case jobjectArray + + var callMethodAName: String { + switch self { + case .jboolean: "CallBooleanMethodA" + case .jbyte: "CallByteMethodA" + case .jchar: "CallCharMethodA" + case .jshort: "CallShortMethodA" + case .jint: "CallIntMethodA" + case .jlong: "CallLongMethodA" + case .jfloat: "CallFloatMethodA" + case .jdouble: "CallDoubleMethodA" + case .void: "CallVoidMethodA" + case .jobject, .jstring, .jclass, .jthrowable: "CallObjectMethodA" + case .jbooleanArray, .jbyteArray, .jcharArray, .jshortArray, .jintArray, .jlongArray, .jfloatArray, .jdoubleArray, .jobjectArray: "CallObjectMethodA" + } + } } extension JavaType { diff --git a/Sources/JavaKit/JavaEnvironment.swift b/Sources/JavaKit/JavaEnvironment.swift index d74146ab..4895b32c 100644 --- a/Sources/JavaKit/JavaEnvironment.swift +++ b/Sources/JavaKit/JavaEnvironment.swift @@ -15,5 +15,5 @@ import JavaRuntime extension UnsafeMutablePointer { - var interface: JNINativeInterface_ { self.pointee!.pointee } + public var interface: JNINativeInterface_ { self.pointee!.pointee } } diff --git a/Sources/JavaKit/JavaValue.swift b/Sources/JavaKit/JavaValue.swift index 1bc156a0..0f208144 100644 --- a/Sources/JavaKit/JavaValue.swift +++ b/Sources/JavaKit/JavaValue.swift @@ -137,7 +137,7 @@ extension JavaValue where Self: ~Copyable { } /// Convert to a jvalue within the given JNI environment. - consuming func getJValue(in environment: JNIEnvironment) -> jvalue { + public consuming func getJValue(in environment: JNIEnvironment) -> jvalue { var result = jvalue() Self.assignJNIType(&result[keyPath: Self.jvalueKeyPath], to: getJNIValue(in: environment)) return result diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift new file mode 100644 index 00000000..47d7e35d --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 JExtractSwiftLib +import Testing + +@Suite +struct JNIClosureTests { + let source = + """ + public func emptyClosure(closure: () -> ()) {} + public func closureWithArgumentsAndReturn(closure: (Int64, Bool) -> Int64) {} + """ + + @Test + func emptyClosure_javaBindings() throws { + try assertOutput(input: source, .jni, .java, expectedChunks: [ + """ + public static class emptyClosure { + @FunctionalInterface + public interface closure { + void apply(); + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func emptyClosure(closure: () -> ()) + * } + */ + public static void emptyClosure(com.example.swift.SwiftModule.emptyClosure.closure closure) { + SwiftModule.$emptyClosure(closure); + } + """, + """ + private static native void $emptyClosure(com.example.swift.SwiftModule.emptyClosure.closure closure); + """ + ]) + } + + @Test + func emptyClosure_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024emptyClosure__Lcom_example_swift_SwiftModule_00024emptyClosure_00024closure_2") + func Java_com_example_swift_SwiftModule__00024emptyClosure__Lcom_example_swift_SwiftModule_00024emptyClosure_00024closure_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject) { + SwiftModule.emptyClosure(closure: { + let class$ = environment!.interface.GetObjectClass(environment, closure) + let methodID$ = environment!.interface.GetMethodID(environment, class$, "apply", "()V")! + let arguments$: [jvalue] = [] + environment!.interface.CallVoidMethodA(environment, closure, methodID$, arguments$) + } + ) + } + """ + ] + ) + } + + @Test + func closureWithArgumentsAndReturn_javaBindings() throws { + try assertOutput(input: source, .jni, .java, expectedChunks: [ + """ + public static class closureWithArgumentsAndReturn { + @FunctionalInterface + public interface closure { + long apply(long _0, boolean _1); + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func closureWithArgumentsAndReturn(closure: (Int64, Bool) -> Int64) + * } + */ + public static void closureWithArgumentsAndReturn(com.example.swift.SwiftModule.closureWithArgumentsAndReturn.closure closure) { + SwiftModule.$closureWithArgumentsAndReturn(closure); + } + """, + """ + private static native void $closureWithArgumentsAndReturn(com.example.swift.SwiftModule.closureWithArgumentsAndReturn.closure closure); + """ + ]) + } + + @Test + func closureWithArgumentsAndReturn_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024closureWithArgumentsAndReturn__Lcom_example_swift_SwiftModule_00024closureWithArgumentsAndReturn_00024closure_2") + func Java_com_example_swift_SwiftModule__00024closureWithArgumentsAndReturn__Lcom_example_swift_SwiftModule_00024closureWithArgumentsAndReturn_00024closure_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject) { + SwiftModule.closureWithArgumentsAndReturn(closure: { _0, _1 in + let class$ = environment!.interface.GetObjectClass(environment, closure) + let methodID$ = environment!.interface.GetMethodID(environment, class$, "apply", "(JZ)J")! + let arguments$: [jvalue] = [_0.getJValue(in: environment!), _1.getJValue(in: environment!)] + return Int64(fromJNI: environment!.interface.CallLongMethodA(environment, closure, methodID$, arguments$), in: environment!) + } + ) + } + """ + ] + ) + } +}