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 1/3] 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( From b8f1e3293229399fd75cdea80c8d8f86ac61b07b Mon Sep 17 00:00:00 2001 From: Hokila Jan <1647434+Hokila@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:11:27 +0800 Subject: [PATCH 2/3] Fix review comment - Move deduplication logic into allProtocolRequirementMethods so both Swift thunks and Java bindings are deduplicated. - Include apiKind in the deduplication key to avoid dropping property setters. - Remove redundant emission-level deduplication and dead helper code. - Fix invalid 'public' modifier in test extensions (thanks @ktoso). - Add test for Java-side existential box deduplication. --- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 16 ---------- .../JNI/JNISwift2JavaGenerator.swift | 14 ++++++++- .../JNI/JNIProtocolTests.swift | 29 +++++++++++++++++-- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index a74236cee..4f7f0bf79 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -389,7 +389,6 @@ 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 { @@ -398,14 +397,6 @@ 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) } @@ -847,13 +838,6 @@ extension JNISwift2JavaGenerator { ) } - 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) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 539d59baa..26281875f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -182,13 +182,25 @@ extension JNISwift2JavaGenerator { /// box's method bodies and per-requirement `@_cdecl` dispatch thunks, /// since the box must implement everything the protocol (directly or /// transitively) requires. + /// + /// A protocol requirement and its default implementation (defined in an + /// unconstrained extension) are extracted as distinct ``ExtractedFunc`` + /// instances that share the same signature. We keep only the first + /// occurrence so that downstream code never emits duplicate JNI thunks + /// or duplicate Java methods. func allProtocolRequirementMethods(of type: ExtractedNominalType) -> [ExtractedFunc] { var visited: Set = [] var queue: [ExtractedNominalType] = [type] var methods: [ExtractedFunc] = [] + var seenSignatures: Set = [] while let current = queue.popLast() { guard visited.insert(ObjectIdentifier(current)).inserted else { continue } - methods.append(contentsOf: self.supportedProtocolRequirements(of: current)) + for method in self.supportedProtocolRequirements(of: current) { + let key = "\(method.apiKind):\(method.signatureString)" + if seenSignatures.insert(key).inserted { + methods.append(method) + } + } queue.append(contentsOf: inheritedProtocols(of: current)) } return methods diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index 995f7190c..a5c7222f4 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -84,11 +84,11 @@ struct JNIProtocolTests { let protocolDefaultImplementationSource = """ public protocol Test { - public func action() + func action() } public extension Test { - public func action() {} + func action() {} } public func makeTest() -> any Test @@ -579,6 +579,31 @@ struct JNIProtocolTests { ) } + @Test + func existentialBoxJavaMethodWithDefaultImplementationIsUnique() throws { + var config = config + config.enableJavaCallbacks = false + + try assertOutput( + input: protocolDefaultImplementationSource, + config: config, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public void action() { + TestBox.$action(this.$memoryAddress(), this.$typeMetadataAddress()); + } + private static native void $action(long selfPointer, long selfTypePointer); + """ + ], + expectedOccurrences: [ + "private static native void $action(long selfPointer, long selfTypePointer);": 1 + ] + ) + } + @Test func existentialBoxPropertyAccessors_swift() throws { try assertOutput( From da06bd5934e843c6b76ed33f2b57295231cd437a Mon Sep 17 00:00:00 2001 From: Hokila Jan <1647434+Hokila@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:28:43 +0800 Subject: [PATCH 3/3] Fix protocol default implementation deduplication --- ...Generator+InterfaceWrapperGeneration.swift | 9 +- ...t2JavaGenerator+JavaBindingsPrinting.swift | 6 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 57 +++----- .../JNI/JNISwift2JavaGenerator.swift | 58 ++++++-- .../JNI/JNIProtocolTests.swift | 137 +++++++++++++++++- 5 files changed, 207 insertions(+), 60 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 236977e5e..d8613a74d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -35,7 +35,10 @@ extension JNISwift2JavaGenerator { do { let translator = JavaInterfaceProtocolWrapperGenerator() - wrappers[type] = try translator.generate(for: type) + wrappers[type] = try translator.generate( + for: type, + methods: uniqueProtocolRequirements(type.methods) + ) } catch { self.logger.warning("Failed to generate protocol wrapper for: '\(type.swiftNominal.qualifiedName)'; \(error)") } @@ -104,7 +107,7 @@ extension JNISwift2JavaGenerator { } struct JavaInterfaceProtocolWrapperGenerator { - func generate(for type: ExtractedNominalType) throws -> JavaInterfaceSwiftWrapper { + func generate(for type: ExtractedNominalType, methods: [ExtractedFunc]) throws -> JavaInterfaceSwiftWrapper { if !type.initializers.isEmpty || type.methods.contains(where: \.isStatic) || type.variables.contains(where: \.isStatic) @@ -112,7 +115,7 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.protocolStaticRequirementsNotSupported } - let functions = try type.methods.map { method in + let functions = try methods.map { method in try translate(function: method) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 98d1004c3..98399c1ac 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -173,7 +173,11 @@ extension JNISwift2JavaGenerator { let filename = "\(decl.effectiveJavaSimpleName).java" logger.debug("Printing contents: \(filename)") - resetForNewOutputFile(decl.initializers + decl.variables + decl.methods) + let methods = + decl.swiftNominal.kind == .protocol + ? self.supportedProtocolRequirements(of: decl) + : decl.initializers + decl.variables + decl.methods + resetForNewOutputFile(methods) printHeader(&printer) printPackage(&printer) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 4f7f0bf79..7fc54ea55 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -310,9 +310,11 @@ extension JNISwift2JavaGenerator { printHeader(&printer) printer.println() - self.currentJavaIdentifiers = JavaIdentifierFactory( - type.initializers + type.variables + type.methods - ) + let methods = + type.swiftNominal.kind == .protocol + ? self.allProtocolRequirementMethods(of: type) + : type.initializers + type.variables + type.methods + self.currentJavaIdentifiers = JavaIdentifierFactory(methods) switch type.swiftNominal.kind { case .actor, .class, .enum, .struct: @@ -838,8 +840,12 @@ extension JNISwift2JavaGenerator { ) } - - private func nativeParameters(for nativeSignature: NativeFunctionSignature) -> [JavaParameter] { + private func printCDecl( + _ printer: inout SwiftPrinter, + _ translatedDecl: TranslatedFunctionDecl, + _ body: (inout SwiftPrinter) -> Void, + ) { + let nativeSignature = translatedDecl.nativeFunctionSignature var parameters = nativeSignature.parameters.flatMap(\.parameters) if let selfParameter = nativeSignature.selfParameter { @@ -850,17 +856,6 @@ 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, @@ -880,10 +875,15 @@ extension JNISwift2JavaGenerator { resultType: JavaType, _ body: (inout SwiftPrinter) -> Void, ) { - let cName = cDeclSymbolName( - javaMethodName: javaMethodName, - parentName: parentName, - parameters: parameters + 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, ) self.generatedCDeclSymbolNames.append(cName) @@ -916,23 +916,6 @@ 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/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 26281875f..ce73036e9 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -171,9 +171,45 @@ extension JNISwift2JavaGenerator { /// (getters/setters), excluding statics and anything whose signature /// doesn't translate (e.g. referencing `Self`/associated types). func supportedProtocolRequirements(of type: ExtractedNominalType) -> [ExtractedFunc] { - (type.methods + type.variables).filter { requirement in - !requirement.isStatic && !requirement.isClass && self.translatedDecl(for: requirement) != nil + uniqueProtocolRequirements( + (type.methods + type.variables).filter { requirement in + !requirement.isStatic && !requirement.isClass + && (try? self.javaTranslator.translate(requirement)) != nil + } + ) + } + + /// Compare the callable signature rather than source text. A default + /// implementation may use different access modifiers, local parameter names, + /// default arguments, or a narrower throwing effect from the protocol + /// requirement it implements. + func uniqueProtocolRequirements(_ methods: [ExtractedFunc]) -> [ExtractedFunc] { + var unique: [ExtractedFunc] = [] + for method in methods { + let duplicate = unique.contains { existing in + existing.apiKind == method.apiKind && existing.name == method.name + && normalizedProtocolSignature(existing) == normalizedProtocolSignature(method) + } + if !duplicate { + unique.append(method) + } } + return unique + } + + private func normalizedProtocolSignature(_ method: ExtractedFunc) -> SwiftFunctionSignature { + var signature = method.functionSignature + signature.selfParameter = nil + signature.effectSpecifiers.removeAll { $0 == .throws } + signature.thrownTypedError = nil + signature.parameters = signature.parameters.map { parameter in + var parameter = parameter + parameter.parameterName = nil + parameter.hasDefaultValue = false + parameter.defaultValueExpression = nil + return parameter + } + return signature } /// All wrappable requirements for `type` (a protocol), including those @@ -183,26 +219,18 @@ extension JNISwift2JavaGenerator { /// since the box must implement everything the protocol (directly or /// transitively) requires. /// - /// A protocol requirement and its default implementation (defined in an - /// unconstrained extension) are extracted as distinct ``ExtractedFunc`` - /// instances that share the same signature. We keep only the first - /// occurrence so that downstream code never emits duplicate JNI thunks - /// or duplicate Java methods. + /// A protocol requirement and its default implementation are extracted as + /// distinct ``ExtractedFunc`` instances. Keep the first occurrence so the + /// box emits one JNI thunk and one Java method for each callable signature. func allProtocolRequirementMethods(of type: ExtractedNominalType) -> [ExtractedFunc] { var visited: Set = [] var queue: [ExtractedNominalType] = [type] var methods: [ExtractedFunc] = [] - var seenSignatures: Set = [] while let current = queue.popLast() { guard visited.insert(ObjectIdentifier(current)).inserted else { continue } - for method in self.supportedProtocolRequirements(of: current) { - let key = "\(method.apiKind):\(method.signatureString)" - if seenSignatures.insert(key).inserted { - methods.append(method) - } - } + methods.append(contentsOf: self.supportedProtocolRequirements(of: current)) queue.append(contentsOf: inheritedProtocols(of: current)) } - return methods + return uniqueProtocolRequirements(methods) } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index a5c7222f4..e3452f91b 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -94,6 +94,39 @@ struct JNIProtocolTests { public func makeTest() -> any Test """ + let protocolDefaultImplementationWithDifferentSpellingSource = """ + public protocol Test { + func action(_ value: Int64) + } + + public extension Test { + func action(_ otherValue: Int64) {} + } + + public func makeTest() -> any Test + """ + + let overloadedProtocolSource = """ + public protocol Overloaded { + func action(a: Int64) + func action(b: Int64) + } + + public func makeOverloaded() -> any Overloaded + """ + + let nonthrowingDefaultImplementationSource = """ + public protocol Test { + func work() throws + } + + public extension Test { + func work() {} + } + + public func makeTest() -> any Test + """ + @Test func generatesJavaInterface() throws { try assertOutput( @@ -567,14 +600,15 @@ struct JNIProtocolTests { 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) { + @_cdecl("Java_com_example_swift_TestBox__00024action__JJ") + public func Java_com_example_swift_TestBox__00024action__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong, selfTypePointer: jlong) { ... } """ ], expectedOccurrences: [ - "@_cdecl(\"Java_com_example_swift_TestBox__00024action_1__JJ\")": 1 + "@_cdecl(\"Java_com_example_swift_TestBox__00024action__JJ\")": 1, + "@_cdecl(\"Java_com_example_swift_TestBox__00024action": 1, ] ) } @@ -598,8 +632,103 @@ struct JNIProtocolTests { private static native void $action(long selfPointer, long selfTypePointer); """ ], + notExpectedChunks: ["action_("], + expectedOccurrences: [ + "private static native void $action(long selfPointer, long selfTypePointer);": 1, + "public void action();": 1, + "public void action() {": 1, + ] + ) + } + + @Test + func defaultImplementationWithDifferentSpellingIsUnique() throws { + var config = config + config.enableJavaCallbacks = false + + try assertOutput( + input: protocolDefaultImplementationWithDifferentSpellingSource, + config: config, + .jni, + .swift, + expectedChunks: [], + expectedOccurrences: [ + "@_cdecl(\"Java_com_example_swift_TestBox__00024action__JJJ\")": 1, + "@_cdecl(\"Java_com_example_swift_TestBox__00024action": 1, + ] + ) + + try assertOutput( + input: protocolDefaultImplementationWithDifferentSpellingSource, + config: config, + .jni, + .java, + expectedChunks: [], + notExpectedChunks: ["action_("], + expectedOccurrences: [ + "public void action(long": 2, + "private static native void $action(long": 1, + ] + ) + } + + @Test + func defaultImplementationCallbackWrapperIsUnique() throws { + try assertOutput( + input: protocolDefaultImplementationSource, + config: config, + .jni, + .swift, + expectedChunks: [], + expectedOccurrences: [ + "func action() {": 1 + ] + ) + } + + @Test + func distinctProtocolOverloadsArePreserved() throws { + var config = config + config.enableJavaCallbacks = false + + try assertOutput( + input: overloadedProtocolSource, + config: config, + .jni, + .java, + expectedChunks: [], + expectedOccurrences: [ + "public void actionA(long": 2, + "public void actionB(long": 2, + ] + ) + } + + @Test + func nonthrowingDefaultImplementationIsUnique() throws { + var config = config + config.enableJavaCallbacks = false + + try assertOutput( + input: nonthrowingDefaultImplementationSource, + config: config, + .jni, + .swift, + expectedChunks: [], + expectedOccurrences: [ + "@_cdecl(\"Java_com_example_swift_TestBox__00024work": 1 + ] + ) + + try assertOutput( + input: nonthrowingDefaultImplementationSource, + config: config, + .jni, + .java, + expectedChunks: [], expectedOccurrences: [ - "private static native void $action(long selfPointer, long selfTypePointer);": 1 + "private static native void $work(": 1, + "public void work() throws Exception;": 1, ] ) }