From 092cea2780a7e6fdf521538a953f7e3b202f3f34 Mon Sep 17 00:00:00 2001 From: Hokila <1647434+Hokila@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:12:48 +0800 Subject: [PATCH] jextract: deduplicate protocol default JNI thunks --- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 65 ++++++++++++++----- .../Asserts/TextAssertions.swift | 12 ++++ .../JNI/JNIProtocolTests.swift | 37 +++++++++++ 3 files changed, 99 insertions(+), 15 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 229512e24..a74236cee 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -389,6 +389,7 @@ extension JNISwift2JavaGenerator { /// function (including requirements inherited from refined protocols). private func printExistentialBoxDispatchThunks(_ printer: inout SwiftPrinter, _ type: ExtractedNominalType) { let boxParentName = SwiftQualifiedTypeName(type.swiftNominal.javaExistentialBoxName) + var emittedCDeclSymbols: Set = [] for method in self.allProtocolRequirementMethods(of: type) { guard var translated = try? self.javaTranslator.translate(method) else { @@ -397,6 +398,14 @@ extension JNISwift2JavaGenerator { } translated.parentName = boxParentName + let cName = cDeclSymbolName(for: translated) + // A protocol requirement and its default implementation can be represented + // by distinct extracted declarations while mapping to the same JNI symbol. + // The existential box only needs one dispatch thunk for that symbol. + guard emittedCDeclSymbols.insert(cName).inserted else { + continue + } + printCDecl(&printer, translated) { printer in self.printFunctionDowncall(&printer, method) } @@ -838,12 +847,15 @@ extension JNISwift2JavaGenerator { ) } - private func printCDecl( - _ printer: inout SwiftPrinter, - _ translatedDecl: TranslatedFunctionDecl, - _ body: (inout SwiftPrinter) -> Void, - ) { - let nativeSignature = translatedDecl.nativeFunctionSignature + private func cDeclSymbolName(for translatedDecl: TranslatedFunctionDecl) -> String { + cDeclSymbolName( + javaMethodName: translatedDecl.nativeFunctionName, + parentName: translatedDecl.parentName, + parameters: nativeParameters(for: translatedDecl.nativeFunctionSignature) + ) + } + + private func nativeParameters(for nativeSignature: NativeFunctionSignature) -> [JavaParameter] { var parameters = nativeSignature.parameters.flatMap(\.parameters) if let selfParameter = nativeSignature.selfParameter { @@ -854,6 +866,17 @@ extension JNISwift2JavaGenerator { } parameters += nativeSignature.result.outParameters + return parameters + } + + private func printCDecl( + _ printer: inout SwiftPrinter, + _ translatedDecl: TranslatedFunctionDecl, + _ body: (inout SwiftPrinter) -> Void, + ) { + let nativeSignature = translatedDecl.nativeFunctionSignature + let parameters = nativeParameters(for: nativeSignature) + printCDecl( &printer, javaMethodName: translatedDecl.nativeFunctionName, @@ -873,15 +896,10 @@ extension JNISwift2JavaGenerator { resultType: JavaType, _ body: (inout SwiftPrinter) -> Void, ) { - let jniSignature = parameters.reduce(into: "") { signature, parameter in - signature += parameter.type.jniTypeSignature - } - - let cName = String.jniSymbolName( - package: self.javaPackage, - parent: parentName, - method: javaMethodName, - signature: jniSignature, + let cName = cDeclSymbolName( + javaMethodName: javaMethodName, + parentName: parentName, + parameters: parameters ) self.generatedCDeclSymbolNames.append(cName) @@ -914,6 +932,23 @@ extension JNISwift2JavaGenerator { } } + private func cDeclSymbolName( + javaMethodName: String, + parentName: SwiftQualifiedTypeName, + parameters: [JavaParameter], + ) -> String { + let jniSignature = parameters.reduce(into: "") { signature, parameter in + signature += parameter.type.jniTypeSignature + } + + return String.jniSymbolName( + package: self.javaPackage, + parent: parentName, + method: javaMethodName, + signature: jniSignature, + ) + } + private func printJNICache(_ printer: inout SwiftPrinter, _ type: ExtractedNominalType) { let cacheName = JNICaching.cacheName(for: type) let jniClassName = "\(javaPackagePath)/\(type.effectiveJavaTypeName.jniEscapedName)" diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index 4471b1776..f5788067e 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -45,6 +45,7 @@ func assertOutput( moduleJavaPackages: [String: String] = [:], expectedChunks: [String], notExpectedChunks: [String] = [], + expectedOccurrences: [String: Int] = [:], fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, @@ -112,6 +113,7 @@ func assertOutput( dump: dump, expectedChunks: expectedChunks, notExpectedChunks: notExpectedChunks, + expectedOccurrences: expectedOccurrences, detectChunkByInitialLines: _detectChunkByInitialLines, fileID: fileID, filePath: filePath, @@ -126,6 +128,7 @@ func assertOutput( dump: Bool = false, expectedChunks: [String], notExpectedChunks: [String] = [], + expectedOccurrences: [String: Int] = [:], detectChunkByInitialLines _detectChunkByInitialLines: Int = 4, fileID: String = #fileID, filePath: String = #filePath, @@ -149,6 +152,15 @@ func assertOutput( ) } + for (expectedChunk, expectedCount) in expectedOccurrences { + let actualCount = output.components(separatedBy: expectedChunk).count - 1 + #expect( + actualCount == expectedCount, + "Expected '\(expectedChunk)' to occur \(expectedCount) time(s), but found \(actualCount).", + sourceLocation: sourceLocation + ) + } + let gotLines = output.split(separator: "\n").filter { l in l.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).count > 0 } diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index fc1e971bf..995f7190c 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -82,6 +82,18 @@ struct JNIProtocolTests { public func makeChild() -> any ChildProtocol """ + let protocolDefaultImplementationSource = """ + public protocol Test { + public func action() + } + + public extension Test { + public func action() {} + } + + public func makeTest() -> any Test + """ + @Test func generatesJavaInterface() throws { try assertOutput( @@ -542,6 +554,31 @@ struct JNIProtocolTests { ) } + @Test + func existentialBoxDispatchThunkWithDefaultImplementationIsUnique() throws { + var config = config + config.enableJavaCallbacks = false + + try assertOutput( + input: protocolDefaultImplementationSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_TestBox__00024action_1__JJ") + public func Java_com_example_swift_TestBox__00024action_1__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong, selfTypePointer: jlong) { + ... + } + """ + ], + expectedOccurrences: [ + "@_cdecl(\"Java_com_example_swift_TestBox__00024action_1__JJ\")": 1 + ] + ) + } + @Test func existentialBoxPropertyAccessors_swift() throws { try assertOutput(