diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index e0decfb3e..478b24c34 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -81,9 +81,9 @@ void call_globalStringIdentity_empty() { void call_globalCallMeRunnable() { CountDownLatch countDownLatch = new CountDownLatch(3); - MySwiftLibrary.globalCallMeRunnable(new MySwiftLibrary.globalCallMeRunnable.run() { + MySwiftLibrary.globalCallMeRunnable(new java.lang.Runnable() { @Override - public void apply() { + public void run() { countDownLatch.countDown(); } }); diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 5093dad61..37ce8ce9c 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -255,6 +255,17 @@ extension FFMSwift2JavaGenerator { } printFunctionDescriptorDefinition(&printer, cResultType, cParams) + if cParameterTypes.isEmpty && cResultType.isVoid { + printer.print( + """ + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(java.lang.Runnable.class, "run", DESC); + private static MemorySegment toUpcallStub(java.lang.Runnable fi, Arena arena) { + return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); + } + """ + ) + return + } printer.print( """ private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); @@ -296,6 +307,16 @@ extension FFMSwift2JavaGenerator { ) { let cdeclDescriptor = "\(bindingDescriptorName).$\(functionType.name)" if functionType.isCompatibleWithC { + if !functionType.swiftType.isEscaping && functionType.parameters.isEmpty && functionType.swiftType.resultType.isVoid { + printer.print( + """ + private static MemorySegment $toUpcallStub(java.lang.Runnable fi, Arena arena) { + return \(bindingDescriptorName).$\(functionType.name).toUpcallStub(fi, arena); + } + """ + ) + return + } // If the user-facing functional interface is C ABI compatible, just extend // the lowered function pointer parameter interface. printer.print( diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 0b4f52695..4afcdd508 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -489,13 +489,11 @@ extension FFMSwift2JavaGenerator { genericRequirements: genericRequirements ) - case .function: - return TranslatedParameter( - parameter: JavaParameter( - name: parameterName, - type: JavaType.class(package: nil, name: "\(methodName).\(parameterName)") - ), - conversion: .call(.placeholder, function: "\(methodName).$toUpcallStub", withArena: true) + case .function(let functionType): + return translateFunctionParameter( + functionType: functionType, + parameterName: parameterName, + methodName: methodName ) case .existential, .opaque, .genericParameter: @@ -568,6 +566,35 @@ extension FFMSwift2JavaGenerator { ) } + // Extract Java known functional interface type or null if not exists + func extractKnownJavaFunctionalInterfaceType( + functionType: SwiftFunctionType, + ) -> JavaType? { + if functionType.parameters.isEmpty && functionType.resultType.isVoid { + JavaType.javaLangRunnable + } else { + nil + } + } + + /// Translate a Swift Function parameter to the Java interface. + func translateFunctionParameter( + functionType: SwiftFunctionType, + parameterName: String, + methodName: String, + ) -> TranslatedParameter { + let parameterType = + extractKnownJavaFunctionalInterfaceType(functionType: functionType) ?? JavaType.class(package: nil, name: "\(methodName).\(parameterName)") + + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: parameterType + ), + conversion: .call(.placeholder, function: "\(methodName).$toUpcallStub", withArena: true) + ) + } + /// Translate an Optional Swift API parameter to the user-facing Java API parameter. func translateOptionalParameter( wrappedType swiftType: SwiftType, diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 1e30455a5..81db978da 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -516,17 +516,14 @@ extension JNISwift2JavaGenerator { conversion: .placeholder, ) - case .function: - return TranslatedParameter( - parameter: JavaParameter( - name: parameterName, - type: .class( - package: javaPackage, - name: String.javaQualifiedName(parentName.fullName, methodName, parameterName) - ), - annotations: parameterAnnotations, - ), - conversion: .placeholder, + case .function(let functionType): + return translateFunctionParameter( + swiftType: swiftType, + functionType: functionType, + parameterName: parameterName, + methodName: methodName, + parentName: parentName, + parameterAnnotations: parameterAnnotations ) case .opaque(let proto), .existential(let proto): @@ -699,6 +696,52 @@ extension JNISwift2JavaGenerator { nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType)) } + func extractKnownJavaFunctionalInterfaceType( + swiftType: SwiftType, + functionType: SwiftFunctionType, + parameterName: String, + parameterAnnotations: [JavaAnnotation] + ) -> TranslatedParameter? { + if !functionType.isEscaping && functionType.parameters.isEmpty && functionType.resultType.isVoid { + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: JavaType.javaLangRunnable, + annotations: parameterAnnotations, + ), + conversion: .placeholder, + ) + } + return nil + } + + func translateFunctionParameter( + swiftType: SwiftType, + functionType: SwiftFunctionType, + parameterName: String, + methodName: String, + parentName: SwiftQualifiedTypeName, + parameterAnnotations: [JavaAnnotation] + ) -> TranslatedParameter { + extractKnownJavaFunctionalInterfaceType( + swiftType: swiftType, + functionType: functionType, + parameterName: parameterName, + parameterAnnotations: parameterAnnotations + ) + ?? TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .class( + package: javaPackage, + name: String.javaQualifiedName(parentName.fullName, methodName, parameterName) + ), + annotations: parameterAnnotations, + ), + conversion: .placeholder, + ) + } + func translateProtocolParameter( protocolType: SwiftType, parameterName: String, diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 9f025bbdf..f3fa65512 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -265,87 +265,13 @@ extension JNISwift2JavaGenerator { conversionCheck: nil ) - case .function(let fn): - - // @Sendable is not supported yet as "environment" is later captured inside the closure. - if fn.isEscaping { - // For escaping closures we e need to create a Swift wrapper around - // the passed down Java functional interface because we must keep it - // alive with a global ref, that will remain around for as long as the - // escaping closure is. - // - // Prepare the name and shapes of the Java side functional interface - // and Swift side @JavaInterface wrapper we'll use to implement that - // interface (and keep the Java object reachable). - - // Name it: interface MyClass_myMethod_theClosure { ... } - let javaClosureInterfaceName = String.flatName( - prefix: "Java", - parent: parentName, - method: methodName, - parameter: parameterName, - ) - let javaBinaryName = String.javaBinaryName( - package: self.javaPackage, - parent: parentName, - method: methodName, - qualifier: parameterName, - ) - let generator = JavaInterfaceProtocolWrapperGenerator() - let syntheticFunction = try generator.generateSyntheticClosureFunction( - functionType: fn, - javaInterfaceName: javaClosureInterfaceName, - javaBinaryName: javaBinaryName, - ) - - return NativeParameter( - parameters: [ - JavaParameter( - name: parameterName, - type: .class( - package: javaPackage, - name: String.javaQualifiedName(parentName.fullName, methodName, parameterName) - ) - ) - ], - conversion: .escapingClosureLowering( - syntheticFunction: syntheticFunction - ), - indirectConversion: nil, - conversionCheck: nil, - syntheticClosure: syntheticFunction - ) - } - - // Non-escaping closures use the legacy translation - var parameters = [NativeParameter]() - for (i, parameter) in fn.parameters.enumerated() { - let closureParamName = parameter.parameterName ?? "_\(i)" - let closureParameter = try translateClosureParameter( - parameter.type, - parameterName: closureParamName - ) - parameters.append(closureParameter) - } - - let result = try translateClosureResult(fn.resultType) - - return NativeParameter( - parameters: [ - JavaParameter( - name: parameterName, - type: .class( - package: javaPackage, - name: String.javaQualifiedName(parentName.fullName, methodName, parameterName) - ) - ) - ], - conversion: .closureLowering( - parameters: parameters, - result: result - ), - indirectConversion: nil, - conversionCheck: nil + case .function(let functionType): + return try translateFunctionParameter( + swiftType: type, + functionType: functionType, + parameterName: parameterName, + methodName: methodName, + parentName: parentName ) case .opaque(let proto), .existential(let proto): @@ -436,6 +362,125 @@ extension JNISwift2JavaGenerator { ) } + func extractKnownJavaFunctionalInterfaceType( + functionType: SwiftFunctionType, + parameterName: String, + parameters: [JNISwift2JavaGenerator.NativeParameter], + result: JNISwift2JavaGenerator.NativeResult + ) -> NativeParameter? { + if !functionType.isEscaping && functionType.parameters.isEmpty && functionType.resultType.isVoid { + return NativeParameter( + parameters: [ + JavaParameter( + name: parameterName, + type: JavaType.javaLangRunnable + ) + ], + conversion: .closureLowering( + parameters: parameters, + result: result + ), + indirectConversion: nil, + conversionCheck: nil + ) + } + return nil + } + + func translateFunctionParameter( + swiftType: SwiftType, + functionType: SwiftFunctionType, + parameterName: String, + methodName: String, + parentName: SwiftQualifiedTypeName + ) throws -> NativeParameter { + // @Sendable is not supported yet as "environment" is later captured inside the closure. + if functionType.isEscaping { + // For escaping closures we e need to create a Swift wrapper around + // the passed down Java functional interface because we must keep it + // alive with a global ref, that will remain around for as long as the + // escaping closure is. + // + // Prepare the name and shapes of the Java side functional interface + // and Swift side @JavaInterface wrapper we'll use to implement that + // interface (and keep the Java object reachable). + + // Name it: interface MyClass_myMethod_theClosure { ... } + let javaClosureInterfaceName = String.flatName( + prefix: "Java", + parent: parentName, + method: methodName, + parameter: parameterName, + ) + let javaBinaryName = String.javaBinaryName( + package: self.javaPackage, + parent: parentName, + method: methodName, + qualifier: parameterName, + ) + let generator = JavaInterfaceProtocolWrapperGenerator() + let syntheticFunction = try generator.generateSyntheticClosureFunction( + functionType: functionType, + javaInterfaceName: javaClosureInterfaceName, + javaBinaryName: javaBinaryName, + ) + + return NativeParameter( + parameters: [ + JavaParameter( + name: parameterName, + type: .class( + package: javaPackage, + name: String.javaQualifiedName(parentName.fullName, methodName, parameterName) + ) + ) + ], + conversion: .escapingClosureLowering( + syntheticFunction: syntheticFunction + ), + indirectConversion: nil, + conversionCheck: nil, + syntheticClosure: syntheticFunction + ) + } + + // Non-escaping closures use the legacy translation + var parameters = [NativeParameter]() + for (i, parameter) in functionType.parameters.enumerated() { + let closureParamName = parameter.parameterName ?? "_\(i)" + let closureParameter = try translateClosureParameter( + parameter.type, + parameterName: closureParamName + ) + parameters.append(closureParameter) + } + + let result = try translateClosureResult(functionType.resultType) + return extractKnownJavaFunctionalInterfaceType( + functionType: functionType, + parameterName: parameterName, + parameters: parameters, + result: result + ) + ?? NativeParameter( + parameters: [ + JavaParameter( + name: parameterName, + type: .class( + package: javaPackage, + name: String.javaQualifiedName(parentName.fullName, methodName, parameterName) + ) + ) + ], + conversion: .closureLowering( + parameters: parameters, + result: result + ), + indirectConversion: nil, + conversionCheck: nil + ) + } + func translateProtocolParameter( protocolType: SwiftType, methodName: String, @@ -1644,10 +1689,17 @@ extension JNISwift2JavaGenerator { $0.conversion.render(&printer, $0.parameters.first!.name) } + let methodName = + if parameters.isEmpty && nativeResult.javaType.isVoid { + "run" + } else { + "apply" + } + printer.print( """ let class$ = environment.interface.GetObjectClass(environment, \(placeholder)) - let methodID$ = environment.interface.GetMethodID(environment, class$, "apply", "\(methodSignature.mangledName)")! + let methodID$ = environment.interface.GetMethodID(environment, class$, "\(methodName)", "\(methodSignature.mangledName)")! environment.interface.DeleteLocalRef(environment, class$) let arguments$: [jvalue] = [\(arguments.joined(separator: .comma))] """ diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 39bd94311..e3086dfba 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -100,16 +100,14 @@ final class FuncCallbackImportTests { void apply(); } private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid(); - private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); - private static MemorySegment toUpcallStub(Function fi, Arena arena) { + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(java.lang.Runnable.class, "run", DESC); + private static MemorySegment toUpcallStub(java.lang.Runnable fi, Arena arena) { return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); } } } public static class callMe { - @FunctionalInterface - public interface callback extends swiftjava___FakeModule_callMe_callback.$callback.Function {} - private static MemorySegment $toUpcallStub(callback fi, Arena arena) { + private static MemorySegment $toUpcallStub(java.lang.Runnable fi, Arena arena) { return swiftjava___FakeModule_callMe_callback.$callback.toUpcallStub(fi, arena); } } @@ -119,7 +117,7 @@ final class FuncCallbackImportTests { * public func callMe(callback: () -> Void) * } */ - public static void callMe(callMe.callback callback) { + public static void callMe(java.lang.Runnable callback) { try(var arena$ = Arena.ofConfined()) { swiftjava___FakeModule_callMe_callback.call(callMe.$toUpcallStub(callback, arena$)); } @@ -210,8 +208,8 @@ final class FuncCallbackImportTests { void apply(); } private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid(); - private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); - private static MemorySegment toUpcallStub(Function fi, Arena arena) { + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(java.lang.Runnable.class, "run", DESC); + private static MemorySegment toUpcallStub(java.lang.Runnable fi, Arena arena) { return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); } } @@ -222,9 +220,7 @@ final class FuncCallbackImportTests { private static MemorySegment $toUpcallStub(callback fi, Arena arena) { return swiftjava___FakeModule_callMeMore_callback_fn.$callback.toUpcallStub(fi, arena); } - @FunctionalInterface - public interface fn extends swiftjava___FakeModule_callMeMore_callback_fn.$fn.Function {} - private static MemorySegment $toUpcallStub(fn fi, Arena arena) { + private static MemorySegment $toUpcallStub(java.lang.Runnable fi, Arena arena) { return swiftjava___FakeModule_callMeMore_callback_fn.$fn.toUpcallStub(fi, arena); } } @@ -234,7 +230,7 @@ final class FuncCallbackImportTests { * public func callMeMore(callback: (UnsafeRawPointer, Float) -> Int, fn: () -> ()) * } */ - public static void callMeMore(callMeMore.callback callback, callMeMore.fn fn) { + public static void callMeMore(callMeMore.callback callback, java.lang.Runnable fn) { try(var arena$ = Arena.ofConfined()) { swiftjava___FakeModule_callMeMore_callback_fn.call(callMeMore.$toUpcallStub(callback, arena$), callMeMore.$toUpcallStub(fn, arena$)); } diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift index 65dd28997..451a30a11 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -46,12 +46,12 @@ struct JNIClosureTests { * public func emptyClosure(closure: () -> ()) * } */ - public static void emptyClosure(com.example.swift.SwiftModule.emptyClosure.closure closure) { + public static void emptyClosure(java.lang.Runnable closure) { SwiftModule.$emptyClosure(closure); } """, """ - private static native void $emptyClosure(com.example.swift.SwiftModule.emptyClosure.closure closure); + private static native void $emptyClosure(java.lang.Runnable closure); """, ] ) @@ -66,11 +66,11 @@ struct JNIClosureTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - @_cdecl("Java_com_example_swift_SwiftModule__00024emptyClosure__Lcom_example_swift_SwiftModule_00024emptyClosure_00024closure_2") - public func Java_com_example_swift_SwiftModule__00024emptyClosure__Lcom_example_swift_SwiftModule_00024emptyClosure_00024closure_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject?) { + @_cdecl("Java_com_example_swift_SwiftModule__00024emptyClosure__Ljava_lang_Runnable_2") + public func Java_com_example_swift_SwiftModule__00024emptyClosure__Ljava_lang_Runnable_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 methodID$ = environment.interface.GetMethodID(environment, class$, "run", "()V")! environment.interface.DeleteLocalRef(environment, class$) let arguments$: [jvalue] = [] environment.interface.CallVoidMethodA(environment, closure, methodID$, arguments$)