diff --git a/Samples/SwiftAndJavaJarFFMSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftAndJavaJarFFMSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift index 0de06d630..94743a062 100644 --- a/Samples/SwiftAndJavaJarFFMSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftAndJavaJarFFMSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -60,6 +60,10 @@ public func globalCallMeLongConsumer(run: (Int64) -> Void) { run(1) } +public func globalCallMeDoubleConsumer(run: (Double) -> Void) { + run(1.0) +} + // ==== Internal helpers func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { diff --git a/Samples/SwiftAndJavaJarFFMSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftAndJavaJarFFMSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java index 2a07bd45b..0bd104b7a 100644 --- a/Samples/SwiftAndJavaJarFFMSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftAndJavaJarFFMSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -87,4 +87,9 @@ void call_globalCallMeIntConsumer_noThrow() { void call_globalCallMeLongConsumer_noThrow() { MySwiftLibrary.globalCallMeLongConsumer((long a) -> { }); } + + @Test + void call_globalCallMeDoubleConsumer_noThrow() { + MySwiftLibrary.globalCallMeDoubleConsumer((double a) -> { }); + } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index a3237d144..9cc936a98 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -72,6 +72,10 @@ public func globalCallMeLongConsumer(run: (Int64) -> Void) { run(1) } +public func globalCallMeDoubleConsumer(run: (Double) -> Void) { + run(1.0) +} + public func globalReceiveRawBuffer(buf: UnsafeRawBufferPointer) -> Int { buf.count } 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 b9a5d949b..98fcd5b82 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -199,4 +199,9 @@ void call_globalCallMeIntConsumer_noThrow() { void call_globalCallMeLongConsumer_noThrow() { MySwiftLibrary.globalCallMeLongConsumer((long a) -> { }); } + + @Test + void call_globalCallMeDoubleConsumer_noThrow() { + MySwiftLibrary.globalCallMeDoubleConsumer((double a) -> { }); + } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift index f5254bb54..3e74074a8 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift @@ -45,6 +45,10 @@ public func globalCallMeLongConsumer(run: (Int64) -> Void) { run(1) } +public func globalCallMeDoubleConsumer(run: (Double) -> Void) { + run(1.0) +} + public func closureMultipleArguments( input1: Int64, input2: Int64, diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java index afa0ee19c..26e75004b 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java @@ -81,4 +81,9 @@ void globalCallMeIntConsumer() { void globalCallMeLongConsumer() { MySwiftLibrary.globalCallMeLongConsumer((long a) -> {}); } + + @Test + void globalCallMeDoubleConsumer() { + MySwiftLibrary.globalCallMeDoubleConsumer((double a) -> {}); + } } diff --git a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift index 9fb67eda5..80a97539a 100644 --- a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift +++ b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift @@ -67,6 +67,10 @@ public func globalCallMeLongConsumer(run: (Int64) -> Void) { run(1) } +public func globalCallMeDoubleConsumer(run: (Double) -> Void) { + run(1.0) +} + public func globalReceiveRawBuffer(buf: UnsafeRawBufferPointer) -> Int { buf.count } diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift index 316c1878d..d46ab9d1b 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift @@ -60,6 +60,11 @@ extension JavaType { .class(package: "java.util.function", name: "LongConsumer") } + /// The description of the type java.util.function.DoubleConsumer. + static var javaUtilFunctionDoubleConsumer: JavaType { + .class(package: "java.util.function", name: "DoubleConsumer") + } + /// The description of the type java.lang.Class. static var javaLangClass: JavaType { .class(package: "java.lang", name: "Class") diff --git a/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift b/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift index 9b4daf286..48547c8a4 100644 --- a/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift +++ b/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -71,6 +71,13 @@ struct KnownJavaFunctionalInterface: Sendable { result: .void ) + static let doubleConsumer = KnownJavaFunctionalInterface( + JavaType.javaUtilFunctionDoubleConsumer, + method: "accept", + parameters: [.double], + result: .void + ) + static let all: [KnownJavaFunctionalInterface] = [ .runnable, .booleanSupplier, @@ -79,6 +86,7 @@ struct KnownJavaFunctionalInterface: Sendable { .doubleSupplier, .intConsumer, .longConsumer, + .doubleConsumer, ] static func find(parameters: [JavaType], result: JavaType) -> KnownJavaFunctionalInterface? { @@ -131,6 +139,8 @@ struct KnownJavaFunctionalInterface: Sendable { intConsumer case _ where parameter.isInt64: longConsumer + case _ where parameter.isDouble: + doubleConsumer default: nil } diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 768000c4a..72abaea74 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -41,6 +41,7 @@ final class FuncCallbackImportTests { public func callMeIntConsumer(callback: (Int32) -> Void) public func callMeLongConsumer(callback: (Int64) -> Void) + public func callMeDoubleConsumer(callback: (Double) -> Void) public func callMeMore(callback: (UnsafeRawPointer, Float) -> Int, fn: () -> ()) public func withBuffer(body: (UnsafeRawBufferPointer) -> Int) @@ -431,6 +432,49 @@ final class FuncCallbackImportTests { ) } + @Test("Import: public func callMeDoubleConsumerFunc(callback: (Double) -> Void)") + func func_callMeDoubleConsumerFunc_callback() throws { + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = makeSwiftJavaAnalyzer(config: config) + st.log.logLevel = .error + + try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) + + let funcDecl = st.extractedGlobalFuncs.first { $0.name == "callMeDoubleConsumer" }! + + let generator = FFMSwift2JavaGenerator( + config: config, + translator: st, + javaPackage: "com.example.swift", + swiftOutputDirectory: "/fake", + javaOutputDirectory: "/fake" + ) + + let output = JavaPrinter.toString { printer in + generator.printFunctionDowncallMethods(&printer, funcDecl) + } + + assertOutput( + output, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func callMeDoubleConsumer(callback: (Double) -> Void) + * } + */ + public static void callMeDoubleConsumer(java.util.function.DoubleConsumer callback) { + try(var arena$ = Arena.ofConfined()) { + swiftjava___FakeModule_callMeDoubleConsumer_callback.call(callMeDoubleConsumer.$toUpcallStub(callback, arena$)); + } + } + """ + ] + ) + } + @Test("Import: public func callMeMore(callback: (UnsafeRawPointer, Float) -> Int, fn: () -> ())") func func_callMeMoreFunc_callback() throws { var config = Configuration() diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift index ec304b562..9de44b536 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -27,6 +27,7 @@ struct JNIClosureTests { public func closureIntConsumer(closure: (Int32) -> Void) {} public func closureLongConsumer(closure: (Int64) -> Void) {} + public func closureDoubleConsumer(closure: (Double) -> Void) {} public func closureWithArgumentsAndReturn(closure: (Int64, Bool) -> Int64) {} """ @@ -206,6 +207,31 @@ struct JNIClosureTests { ) } + @Test + func closureDoubleConsumer_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func closureDoubleConsumer(closure: (Double) -> Void) + * } + */ + public static void closureDoubleConsumer(java.util.function.DoubleConsumer closure) { + SwiftModule.$closureDoubleConsumer(closure); + } + """, + """ + private static native void $closureDoubleConsumer(java.util.function.DoubleConsumer closure); + """, + ] + ) + } + @Test func emptyClosure_swiftThunks() throws { try assertOutput( @@ -381,6 +407,31 @@ struct JNIClosureTests { ) } + @Test + func closureDoubleConsumer_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024closureDoubleConsumer__Ljava_util_function_DoubleConsumer_2") + public func Java_com_example_swift_SwiftModule__00024closureDoubleConsumer__Ljava_util_function_DoubleConsumer_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject?) { + SwiftModule.closureDoubleConsumer(closure: { + let class$ = environment.interface.GetObjectClass(environment, closure) + let methodID$ = environment.interface.GetMethodID(environment, class$, "accept", "(D)V")! + environment.interface.DeleteLocalRef(environment, class$) + let arguments$: [jvalue] = [_0.getJValue(in: environment)] + environment.interface.CallVoidMethodA(environment, closure, methodID$, arguments$) + } + ) + } + """ + ] + ) + } + @Test func closureWithArgumentsAndReturn_javaBindings() throws { try assertOutput(