diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/ProfileService.swift b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/ProfileService.swift new file mode 100644 index 000000000..405ae4501 --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/ProfileService.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 KotlinSampleModels + +/// Uses a protocol declared in KotlinSampleModels, implemented in Kotlin. +public final class ProfileService: Sendable { + let client: any ProfileClient + + public init(client: any ProfileClient) { + self.client = client + } + + public func title(id: String) -> String { + client.displayName(for: id) + } + + public func name(id: String) -> String { + client.profile(id: id).name + } + + public func load(id: String) async throws -> String { + try await client.fetch(id: id).name + } + + /// Starts a fetch, cancels it, and reports whether it ended without a value. + public func fetchThenCancel(id: String) async -> Bool { + let client = self.client + let task = Task { try await client.fetch(id: id) } + try? await Task.sleep(for: .milliseconds(50)) + task.cancel() + return (try? await task.value) == nil + } +} diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleModels/ProfileClient.swift b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleModels/ProfileClient.swift new file mode 100644 index 000000000..8bc83ac4f --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleModels/ProfileClient.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +/// A protocol implemented in Kotlin and passed into Swift code in another module. +public struct Profile: Sendable { + public var name: String + + public init(name: String) { + self.name = name + } +} + +public enum ClientError: Error { + case offline +} + +public protocol ProfileClient: Sendable { + func displayName(for id: String) -> String + func profile(id: String) -> Profile + /// Implemented in Kotlin by returning a `CompletableFuture`, which Swift awaits without blocking. + func fetch(id: String) async throws -> Profile +} diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleModels/swift-java.config b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleModels/swift-java.config index 09fa44001..9333a5fb9 100644 --- a/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleModels/swift-java.config +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleModels/swift-java.config @@ -2,5 +2,6 @@ "javaPackage": "com.example.kotlin.models", "mode": "jni", "memoryManagementMode": "allowGlobalAutomatic", - "failOnSkippedDeclarations": true + "failOnSkippedDeclarations": true, + "enableJavaCallbacks": true } diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/ci-validate.sh b/Samples/SwiftJavaExtractJNIKotlinSampleApp/ci-validate.sh index de00ef16c..162dbf7d1 100755 --- a/Samples/SwiftJavaExtractJNIKotlinSampleApp/ci-validate.sh +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/ci-validate.sh @@ -10,6 +10,17 @@ else DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' fi +if [[ "$(uname)" == "Darwin" && -n "$GITHUB_ACTION" ]]; then + # WORKAROUND (as in SwiftJavaExtractJNISampleApp): with enableJavaCallbacks the jextract plugin runs + # Gradle in its own GRADLE_USER_HOME, which times out downloading the wrapper on GitHub's macOS runners. + ./gradlew -h + GRADLE_USER_HOME="$(pwd)/.build/plugins/outputs/swiftjavaextractjnikotlinsampleapp/KotlinSampleModels/destination/JExtractSwiftPlugin/gradle-user-home" + if [ -d "$HOME/.gradle" ] ; then + mkdir -p "$GRADLE_USER_HOME" + cp -r "$HOME/.gradle/"* "$GRADLE_USER_HOME/" || true + fi +fi + # FIXME: disable prebuilts until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 swift build $DISABLE_EXPERIMENTAL_PREBUILTS --disable-sandbox diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/ProtocolsTest.kt b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/ProtocolsTest.kt new file mode 100644 index 000000000..858882754 --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/ProtocolsTest.kt @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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.kotlin + +import com.example.kotlin.models.Profile +import com.example.kotlin.models.ProfileClient +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.swift.swiftkit.core.SwiftArena +import java.util.concurrent.CompletableFuture +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/// A Kotlin implementation of a Swift protocol. Requirements returning Swift types receive the +/// arena to allocate in; `async` requirements return a CompletableFuture. +class KotlinProfileClient( + private val fail: Boolean = false, + private val pending: CompletableFuture? = null, +) : ProfileClient { + override fun displayName(id: String): String = "user-$id" + + override fun profile(id: String, swiftArena: SwiftArena): Profile = Profile.init("profile-$id", swiftArena) + + override fun fetch(id: String, swiftArena: SwiftArena): CompletableFuture = + pending + ?: if (fail) { + CompletableFuture.failedFuture(IllegalStateException("offline")) + } else { + CompletableFuture.supplyAsync { Profile.init("fetched-$id", swiftArena) } + } +} + +class ProtocolsTest { + @Test + fun syncRequirement() { + assertEquals("user-1", ProfileService.init(KotlinProfileClient()).title("1")) + } + + @Test + fun requirementReturningAStruct() { + assertEquals("profile-1", ProfileService.init(KotlinProfileClient()).name("1")) + } + + @Test + fun asyncRequirement() = runBlocking { + assertEquals("fetched-1", ProfileService.init(KotlinProfileClient()).load("1").await()) + } + + @Test + fun asyncRequirementFailure() = runBlocking { + assertFailsWith { ProfileService.init(KotlinProfileClient(fail = true)).load("1").await() } + Unit + } + + @Test + fun cancellingTheSwiftTaskCancelsTheFuture() = runBlocking { + val pending = CompletableFuture() + val endedWithoutValue = ProfileService.init(KotlinProfileClient(pending = pending)).fetchThenCancel("1").await() + assertTrue(endedWithoutValue) + assertTrue(pending.isCancelled) + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/AsyncCallbackProtocol.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/AsyncCallbackProtocol.swift new file mode 100644 index 000000000..9e422c74e --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/AsyncCallbackProtocol.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + + +/// `async` requirements implemented in Java return a `CompletableFuture`, which Swift awaits. +public protocol AsyncCallbackProtocol { + func fetchValue(_ input: Int64) async -> Int64 + func fetchObject(_ input: Int64) async throws -> MySwiftClass +} + +public func callAsyncCallbacks(_ callbacks: some AsyncCallbackProtocol, input: Int64) async throws -> Int64 { + let value = await callbacks.fetchValue(input) + let object = try await callbacks.fetchObject(input) + return value + object.x +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncProtocolCallbacksTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncProtocolCallbacksTest.java new file mode 100644 index 000000000..ae8a252ba --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncProtocolCallbacksTest.java @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit.core.SwiftArena; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.*; + +public class AsyncProtocolCallbacksTest { + static class JavaAsyncCallbacks implements AsyncCallbackProtocol { + private final boolean fail; + + JavaAsyncCallbacks(boolean fail) { + this.fail = fail; + } + + @Override + public CompletableFuture fetchValue(long input) { + return CompletableFuture.supplyAsync(() -> input * 2); + } + + @Override + public CompletableFuture fetchObject(long input, SwiftArena swiftArena) { + if (fail) { + return CompletableFuture.failedFuture(new IllegalStateException("offline")); + } + return CompletableFuture.supplyAsync(() -> MySwiftClass.init(input, 0, swiftArena)); + } + } + + @Test + void asyncRequirementsAreAwaited() throws Exception { + // 5 * 2 from fetchValue, plus x = 5 from fetchObject + assertEquals(15, MySwiftLibrary.callAsyncCallbacks(new JavaAsyncCallbacks(false), 5).get()); + } + + @Test + void asyncRequirementFailurePropagates() { + var future = MySwiftLibrary.callAsyncCallbacks(new JavaAsyncCallbacks(true), 5); + assertThrows(ExecutionException.class, future::get); + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 236977e5e..40a114f6d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -34,7 +34,7 @@ extension JNISwift2JavaGenerator { } do { - let translator = JavaInterfaceProtocolWrapperGenerator() + let translator = JavaInterfaceProtocolWrapperGenerator(asyncFuncMode: config.effectiveAsyncFuncMode) wrappers[type] = try translator.generate(for: type) } catch { self.logger.warning("Failed to generate protocol wrapper for: '\(type.swiftNominal.qualifiedName)'; \(error)") @@ -104,6 +104,8 @@ extension JNISwift2JavaGenerator { } struct JavaInterfaceProtocolWrapperGenerator { + var asyncFuncMode: JExtractAsyncFuncMode = .default + func generate(for type: ExtractedNominalType) throws -> JavaInterfaceSwiftWrapper { if !type.initializers.isEmpty || type.methods.contains(where: \.isStatic) @@ -139,6 +141,30 @@ extension JNISwift2JavaGenerator { ) } + /// An `async` requirement implemented in Java returns a `CompletableFuture`, which the Swift + /// wrapper awaits; its value arrives as a plain Java object. Supported results: `Void`, types + /// translated directly (unboxed via `JavaBoxable`), and jextracted types. + private func validateAsyncRequirement(_ signature: SwiftFunctionSignature) throws { + guard asyncFuncMode == .completableFuture else { + // A `java.util.concurrent.Future` can't notify Swift when it completes. + throw JavaTranslationError.asyncRequirementNeedsCompletableFuture + } + let resultType = signature.result.type + if resultType.isVoid { + return + } + if case .nominal(let nominal) = resultType, let known = nominal.nominalTypeDecl.knownTypeKind { + if known.isDirectlyTranslatedToWrapJava && known != .uint16 { + return + } + throw JavaTranslationError.unsupportedSwiftType(resultType) + } + if case .nominal = resultType { + return + } + throw JavaTranslationError.unsupportedSwiftType(resultType) + } + /// Generates a synthetic closure function translation. /// This treats the closure as if it were a protocol with a single `apply` method, /// allowing it to use the same conversion infrastructure for optionals, arrays, etc. @@ -169,6 +195,10 @@ extension JNISwift2JavaGenerator { } private func translate(function: ExtractedFunc) throws -> JavaInterfaceSwiftWrapper.Function { + if function.functionSignature.isAsync { + try validateAsyncRequirement(function.functionSignature) + } + let parameters = try function.functionSignature.parameters.map { try self.translateParameter($0) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 7015e4a3e..3e598eda7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -933,7 +933,8 @@ extension JNISwift2JavaGenerator { } generics.append((name, extends)) } - .map { "\($0) extends \($1.compactMap(\.className).joined(separator: " & "))" } + // Fully qualified, so a protocol from another module (another Java package) resolves. + .map { "\($0) extends \($1.compactMap(\.fullyQualifiedClassName).joined(separator: " & "))" } .joined(separator: .comma) if !generics.isEmpty { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 5dbf8c8b4..65f0f0f58 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -29,6 +29,7 @@ extension JNISwift2JavaGenerator { knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, + javaCallbackModules: self.javaCallbackModules, errorTypes: self.swiftErrorTypes, javaIdentifiers: self.currentJavaIdentifiers ) @@ -115,6 +116,7 @@ extension JNISwift2JavaGenerator { knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, + javaCallbackModules: self.javaCallbackModules, errorTypes: self.swiftErrorTypes, javaIdentifiers: self.currentJavaIdentifiers, ) @@ -137,6 +139,7 @@ extension JNISwift2JavaGenerator { var knownTypes: SwiftKnownTypes let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger + var javaCallbackModules: Set = [] /// Module-qualified identities of extracted types that are surfaced to Java as `SwiftError` exceptions. let errorTypes: Set var javaIdentifiers: JavaIdentifierFactory @@ -210,6 +213,7 @@ extension JNISwift2JavaGenerator { knownTypes: self.knownTypes, protocolWrappers: self.protocolWrappers, logger: self.logger, + javaCallbackModules: self.javaCallbackModules, ) // Types with no parent will be outputted inside a "module" class. @@ -2271,6 +2275,9 @@ extension JNISwift2JavaGenerator { /// `inout` parameters have no Java equivalent yet. case inoutParametersNotSupported + /// Java can implement `async` protocol requirements only in `completableFuture` async mode. + case asyncRequirementNeedsCompletableFuture + // FIXME: Remove once we support protocol variables case protocolVariablesNotSupported diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index e40f409de..c3b4c580a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -26,6 +26,7 @@ extension JNISwift2JavaGenerator { var knownTypes: SwiftKnownTypes let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger + var javaCallbackModules: Set = [] /// Translates a Swift function into the native JNI method signature. func translate( @@ -505,6 +506,7 @@ extension JNISwift2JavaGenerator { // Swift wrappers for all the protocol types. let allowsJavaImplementations = protocolTypes.allSatisfy { protocolType in self.protocolWrappers.contains(where: { $0.value.protocolType == protocolType }) + || javaCallbackModules.contains(protocolType.nominalTypeDecl.moduleName) } return NativeParameter( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index bd53ed5f2..898428219 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -185,7 +185,8 @@ extension JNISwift2JavaGenerator { let inheritedWrappers = self.inheritedProtocols(of: translatedWrapper.importedType).compactMap { self.interfaceProtocolWrappers[$0] } let inheritedTypes = [translatedWrapper.swiftName] + inheritedWrappers.map(\.wrapperName) - printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(inheritedTypes.joined(separator: .comma))") { printer in + // Public, so modules that take this protocol as a parameter can wrap Java implementations of it. + printer.printBraceBlock("public protocol \(translatedWrapper.wrapperName): \(inheritedTypes.joined(separator: .comma))") { printer in printer.print( "var \(translatedWrapper.javaInterfaceVariableName): \(translatedWrapper.javaInterfaceName) { get }" ) @@ -193,7 +194,7 @@ extension JNISwift2JavaGenerator { printer.println() try printer.printBraceBlock("extension \(translatedWrapper.wrapperName)") { printer in for inherited in inheritedWrappers { - printer.printBraceBlock("var \(inherited.javaInterfaceVariableName): \(inherited.javaInterfaceName)") { printer in + printer.printBraceBlock("public var \(inherited.javaInterfaceVariableName): \(inherited.javaInterfaceName)") { printer in printer.print("\(translatedWrapper.javaInterfaceVariableName)") } printer.println() @@ -228,7 +229,12 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.protocolWasNotExtracted } - printer.printBraceBlock(function.swiftDecl.signatureString) { printer in + if function.originalFunctionSignature.isAsync { + printAsyncInterfaceWrapperFunctionImpl(&printer, function, inside: wrapper, translatedDecl: translatedDecl) + return + } + + printer.printBraceBlock(publicSignature(function.swiftDecl)) { printer in let resultType = function.originalFunctionSignature.result.type let returnStmt = !resultType.isVoid ? "return " : "" // If the protocol function is non-throwing, we have no option but to force try. @@ -244,6 +250,7 @@ extension JNISwift2JavaGenerator { let estimatedRefCount = paramCount * 2 + 4 printer.print("let environment$ = try! JavaVirtualMachine.shared().environment()") printer.printBraceBlock("\(returnStmt)\(withLocalFrameTryKeyword) environment$.withLocalFrame(capacity: \(estimatedRefCount))") { printer in + printCurrentThreadJavaInterface(&printer, wrapper) var upcallArguments = zip( function.originalFunctionSignature.parameters, function.parameterConversions, @@ -260,7 +267,7 @@ extension JNISwift2JavaGenerator { let tryClause = function.originalFunctionSignature.isThrowing ? "try " : "" let javaUpcall = - "\(tryClause)\(wrapper.javaInterfaceVariableName).\(function.swiftFunctionName)(\(upcallArguments.joined(separator: .comma)))" + "\(tryClause)javaInterface$.\(function.swiftFunctionName)(\(upcallArguments.joined(separator: .comma)))" let result = function.resultConversion.render(&printer, javaUpcall) printer.print("\(returnStmt)\(result)") @@ -268,6 +275,77 @@ extension JNISwift2JavaGenerator { } } + /// A requirement's signature, made `public` so the wrapper's default implementations are visible + /// to conformances in other modules. + private func publicSignature(_ decl: any DeclSyntaxProtocol) -> String { + let isPublic = + decl.asProtocol(WithModifiersSyntax.self)?.modifiers.contains { $0.name.tokenKind == .keyword(.public) } ?? false + return isPublic ? decl.signatureString : "public \(decl.signatureString)" + } + + /// The wrapped Java object, bound to this thread's `JNIEnv`. The stored interface keeps the + /// environment of the thread that created it, but Swift may call the requirement from any thread + /// (always, for `async` requirements run on the cooperative pool). + private func printCurrentThreadJavaInterface(_ printer: inout SwiftPrinter, _ wrapper: JavaInterfaceSwiftWrapper) { + printer.print( + "let javaInterface$ = \(wrapper.javaInterfaceName)(javaThis: \(wrapper.javaInterfaceVariableName).javaThis, environment: environment$)" + ) + } + + /// An `async` requirement implemented in Java: call the Java method, which returns a + /// `CompletableFuture`, then await it without blocking a thread and convert its value. + private func printAsyncInterfaceWrapperFunctionImpl( + _ printer: inout SwiftPrinter, + _ function: JavaInterfaceSwiftWrapper.Function, + inside wrapper: JavaInterfaceSwiftWrapper, + translatedDecl: TranslatedFunctionDecl + ) { + printer.printBraceBlock(publicSignature(function.swiftDecl)) { printer in + let signature = function.originalFunctionSignature + let resultType = signature.result.type + // A non-throwing requirement can't propagate a Java exception, so it traps instead. + let tryKeyword = signature.isThrowing ? "try" : "try!" + let paramCount = signature.parameters.count + let estimatedRefCount = paramCount * 2 + 4 + + printer.print("let environment$ = try! JavaVirtualMachine.shared().environment()") + printer.printBraceBlock("let future$ = \(tryKeyword) environment$.withLocalFrame(capacity: \(estimatedRefCount))") { printer in + printCurrentThreadJavaInterface(&printer, wrapper) + var upcallArguments = zip(signature.parameters, function.parameterConversions).map { param, conversion in + conversion.render(&printer, param.parameterName!) + } + if translatedDecl.translatedFunctionSignature.requiresSwiftArena { + // `JavaValue` is provided for optional Java objects, as wrap-java's parameters are. + upcallArguments.append("Optional(JavaSwiftArena.defaultAutoArena)") + } + // wrap-java doesn't bind methods returning a generic `CompletableFuture`, so call it dynamically. + let arguments = upcallArguments.isEmpty ? "" : "arguments: \(upcallArguments.joined(separator: .comma)), " + printer.print( + "return try javaInterface$.dynamicJavaMethodCall(methodName: \"\(function.swiftFunctionName)\", \(arguments)resultType: JavaCompletableFuture?.self)" + ) + } + printer.printBraceBlock("guard let future$ else") { printer in + printer.print("fatalError(\"Upcall to \(function.swiftFunctionName) unexpectedly returned a null future\")") + } + + if resultType.isVoid { + printer.print("_ = \(tryKeyword) await _awaitCompletableFuture(future$)") + return + } + printer.print("let result$ = \(tryKeyword) await _awaitCompletableFuture(future$)") + // The task may have resumed on another thread: use its environment from here on. + printer.print("let resumedEnvironment$ = try! JavaVirtualMachine.shared().environment()") + if resultType.isDirectlyTranslatedToWrapJava { + printer.print("return \(resultType).fromJavaObject(result$?.javaThis, in: resumedEnvironment$)") + } else { + printer.printBraceBlock("return try! resumedEnvironment$.withLocalFrame(capacity: 4)") { printer in + let result = function.resultConversion.render(&printer, "result$") + printer.print("return \(result)") + } + } + } + } + private func printerInterfaceWrapperVariable( _ printer: inout SwiftPrinter, _ variable: JavaInterfaceSwiftWrapper.Variable, @@ -544,13 +622,19 @@ extension JNISwift2JavaGenerator { // For each parameter that is a generic or a protocol, // we generate a Swift class that conforms to all of those. for (parameter, protocolTypes) in protocolParameters { - let protocolWrappers: [JavaInterfaceSwiftWrapper] = protocolTypes.compactMap { protocolType in - guard let importedType = self.asExtractedNominalTypeDecl(protocolType), - let wrapper = self.interfaceProtocolWrappers[importedType] - else { + // A protocol has a Swift wrapper if it was extracted here with one, or comes from a dependency + // extracted with Java callbacks. Either way the names follow from the protocol declaration. + let protocolWrappers: [SwiftNominalTypeDeclaration] = protocolTypes.compactMap { protocolType in + guard let nominal = protocolType.asNominalTypeDeclaration else { return nil } - return wrapper + if let importedType = self.asExtractedNominalTypeDecl(protocolType), self.interfaceProtocolWrappers[importedType] != nil { + return nominal + } + if nominal.moduleName != swiftModuleName, javaCallbackModules.contains(nominal.moduleName) { + return nominal + } + return nil } // Make sure we can generate wrappers for all the protocols @@ -570,9 +654,11 @@ extension JNISwift2JavaGenerator { parameterName: parameterName, parentName: decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedTypeName ?? SwiftQualifiedTypeName(swiftModuleName), ) - let implementingProtocols = protocolWrappers.map(\.wrapperName).joined(separator: .comma) + let implementingProtocols = protocolWrappers.map(\.javaInterfaceSwiftProtocolWrapperName).joined(separator: .comma) - printer.printBraceBlock("final class \(swiftClassName): \(implementingProtocols)") { printer in + // `@unchecked Sendable`: the protocols may require `Sendable`, and the wrapper only holds Java + // references, which every call re-binds to the calling thread. + printer.printBraceBlock("final class \(swiftClassName): \(implementingProtocols), @unchecked Sendable") { printer in let variables: [(String, String)] = protocolWrappers.map { wrapper in (wrapper.javaInterfaceVariableName, wrapper.javaInterfaceName) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 0e116d934..7d61a68a6 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -37,6 +37,9 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { let javaClassLookupTable: JavaClassLookupTable let moduleJavaPackages: ModuleJavaPackages + /// Dependency modules extracted with `enableJavaCallbacks`: their protocols have public Swift + /// wrappers, so Java implementations of them can be passed to this module too. + let javaCallbackModules: Set var javaPackagePath: String { javaPackage.replacingOccurrences(of: ".", with: "/") @@ -88,6 +91,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { javaOutputDirectory: String, javaClassLookupTable: JavaClassLookupTable, moduleJavaPackages: ModuleJavaPackages, + javaCallbackModules: Set = [], ) { self.config = config self.logger = Logger(label: "jni-generator", logLevel: translator.log.logLevel) @@ -98,6 +102,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { self.javaOutputDirectory = javaOutputDirectory self.javaClassLookupTable = javaClassLookupTable self.moduleJavaPackages = moduleJavaPackages + self.javaCallbackModules = javaCallbackModules self.lookupContext = translator.lookupContext // If we are forced to write empty files, construct the expected outputs. diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 1da75afe7..1bd29a7e9 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -139,7 +139,10 @@ public struct SwiftToJava { swiftOutputDirectory: outputSwiftDirectory, javaOutputDirectory: outputJavaDirectory, javaClassLookupTable: wrappedJavaClassesLookupTable, - moduleJavaPackages: moduleJavaPackages + moduleJavaPackages: moduleJavaPackages, + javaCallbackModules: Set( + dependencyConfigs.filter { $0.configuration.effectiveEnableJavaCallbacks }.compactMap(\.swiftModuleName) + ) ) try generator.generate() diff --git a/Sources/SwiftJava/Exceptions/Throwable+Error.swift b/Sources/SwiftJava/Exceptions/Throwable+Error.swift index b15972412..2efc263de 100644 --- a/Sources/SwiftJava/Exceptions/Throwable+Error.swift +++ b/Sources/SwiftJava/Exceptions/Throwable+Error.swift @@ -13,6 +13,12 @@ //===----------------------------------------------------------------------===// // Translate all Java Throwable instances in a Swift error. +// +// `Error` implies `Sendable`. The conformance is `@unchecked` here at the root so that subclasses, +// such as the classes wrap-java generates for Java exceptions in a Swift 6 language mode module, +// don't each fail the "non-final class cannot conform to 'Sendable'" check. +extension Throwable: @unchecked Sendable {} + extension Throwable: Error, CustomStringConvertible { public var description: String { toString() diff --git a/Sources/SwiftJavaRuntimeSupport/CompletableFutureAwait.swift b/Sources/SwiftJavaRuntimeSupport/CompletableFutureAwait.swift new file mode 100644 index 000000000..cc595d5cd --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/CompletableFutureAwait.swift @@ -0,0 +1,104 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 SwiftJava +import SwiftJavaJNICore + +@JavaClass("java.util.concurrent.CompletableFuture") +public class JavaCompletableFuture: JavaObject { + @JavaMethod + func cancel(_ mayInterruptIfRunning: Bool) -> Bool +} + +@JavaClass("org.swift.swiftkit.core.CompletableFutureContinuation") +class JavaCompletableFutureContinuation: JavaObject {} + +extension JavaClass { + @JavaStaticMethod + func resumeWhenComplete(_ future: JavaCompletableFuture?, _ continuation: Int64) +} + +/// The completion of a `CompletableFuture`, as global references so it can cross threads. +private struct CompletableFutureOutcome: @unchecked Sendable { + let value: jobject? + let error: jobject? +} + +private final class CompletableFutureContinuationBox: @unchecked Sendable { + let continuation: CheckedContinuation + + init(_ continuation: CheckedContinuation) { + self.continuation = continuation + } +} + +/// Awaits a Java `CompletableFuture` without blocking a thread: Java resumes this task when the +/// future completes. Cancelling the task cancels the future. Returns the future's value (nil for +/// `null`), or throws the exception it completed with. +/// +/// Used by generated code for `async` protocol requirements implemented in Java. +public func _awaitCompletableFuture(_ future: JavaCompletableFuture) async throws -> JavaObject? { + let environment = try JavaVirtualMachine.shared().environment() + // A global reference, usable from whichever thread completes, cancels or resumes. + nonisolated(unsafe) let futureRef = environment.interface.NewGlobalRef(environment, future.javaThis)! + defer { + let environment = try! JavaVirtualMachine.shared().environment() + environment.interface.DeleteGlobalRef(environment, futureRef) + } + + let outcome = await withTaskCancellationHandler { + await withCheckedContinuation { (continuation: CheckedContinuation) in + let box = Unmanaged.passRetained(CompletableFutureContinuationBox(continuation)).toOpaque() + let environment = try! JavaVirtualMachine.shared().environment() + let continuationClass = try! JavaClass(environment: environment) + continuationClass.resumeWhenComplete( + JavaCompletableFuture(javaThis: futureRef, environment: environment), + Int64(Int(bitPattern: box)) + ) + } + } onCancel: { + let environment = try! JavaVirtualMachine.shared().environment() + _ = JavaCompletableFuture(javaThis: futureRef, environment: environment).cancel(true) + } + + // Wrap the results with this thread's environment, then drop the cross-thread references. + let resumedEnvironment = try JavaVirtualMachine.shared().environment() + defer { + if let value = outcome.value { resumedEnvironment.interface.DeleteGlobalRef(resumedEnvironment, value) } + if let error = outcome.error { resumedEnvironment.interface.DeleteGlobalRef(resumedEnvironment, error) } + } + if let error = outcome.error { + throw Throwable(javaThis: error, environment: resumedEnvironment) + } + return outcome.value.map { JavaObject(javaThis: $0, environment: resumedEnvironment) } +} + +@_cdecl("Java_org_swift_swiftkit_core_CompletableFutureContinuation__00024resume") +public func Java_org_swift_swiftkit_core_CompletableFutureContinuation__00024resume( + environment: UnsafeMutablePointer!, + thisClass: jclass, + continuation: jlong, + value: jobject?, + error: jthrowable? +) { + let box = Unmanaged + .fromOpaque(UnsafeRawPointer(bitPattern: Int(continuation))!) + .takeRetainedValue() + box.continuation.resume( + returning: CompletableFutureOutcome( + value: value.flatMap { environment.interface.NewGlobalRef(environment, $0) }, + error: error.flatMap { environment.interface.NewGlobalRef(environment, $0) } + ) + ) +} diff --git a/Sources/SwiftJavaRuntimeSupport/JavaSwiftArena+Extensions.swift b/Sources/SwiftJavaRuntimeSupport/JavaSwiftArena+Extensions.swift index fe7d0dc41..e90821db6 100644 --- a/Sources/SwiftJavaRuntimeSupport/JavaSwiftArena+Extensions.swift +++ b/Sources/SwiftJavaRuntimeSupport/JavaSwiftArena+Extensions.swift @@ -15,8 +15,10 @@ import SwiftJava extension JavaSwiftArena { - /// A cache for the default auto arena found in SwiftKitCore - public static internal(set) var defaultAutoArena: JavaSwiftArena = { + /// A cache for the default auto arena found in SwiftKitCore. + /// + /// A constant (never reassigned), so generated code in Swift 6 language mode can read it. + public nonisolated(unsafe) static let defaultAutoArena: JavaSwiftArena = { let swiftMemoryClass = try! JavaClass() return swiftMemoryClass.defaultSwiftJavaAutoArena }() diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/CompletableFutureContinuation.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/CompletableFutureContinuation.java new file mode 100644 index 000000000..ac45ad104 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/CompletableFutureContinuation.java @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 org.swift.swiftkit.core; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * Resumes a suspended Swift task when a {@link CompletableFuture} completes. + *

+ * Used when Swift awaits a future returned by a Java implementation of an {@code async} + * Swift protocol requirement: no thread blocks while the future is pending. + */ +public final class CompletableFutureContinuation { + private CompletableFutureContinuation() {} + + /** + * Resumes the Swift continuation at {@code continuation} exactly once, when {@code future} + * completes normally, exceptionally, or is cancelled. + */ + public static void resumeWhenComplete(CompletableFuture future, long continuation) { + future.whenComplete((value, error) -> { + Throwable cause = error instanceof CompletionException && error.getCause() != null ? error.getCause() : error; + $resume(continuation, value, cause); + }); + } + + private static native void $resume(long continuation, Object value, Throwable error); +} diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index 4471b1776..e6dca8817 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -43,6 +43,8 @@ func assertOutput( /// dependency configs would carry. Forwarded to the generator so cross-module /// type references print with their fully-qualified Java name. moduleJavaPackages: [String: String] = [:], + /// Dependency modules extracted with `enableJavaCallbacks`. + javaCallbackModules: Set = [], expectedChunks: [String], notExpectedChunks: [String] = [], fileID: String = #fileID, @@ -92,7 +94,8 @@ func assertOutput( swiftOutputDirectory: "/fake", javaOutputDirectory: "/fake", javaClassLookupTable: javaClassLookupTable, - moduleJavaPackages: moduleJavaPackages + moduleJavaPackages: moduleJavaPackages, + javaCallbackModules: javaCallbackModules ) switch renderKind { diff --git a/Tests/JExtractSwiftTests/JNI/JNIJavaImplementedProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIJavaImplementedProtocolTests.swift new file mode 100644 index 000000000..41f0e464d --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIJavaImplementedProtocolTests.swift @@ -0,0 +1,178 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 SwiftJavaConfigurationShared +import Testing + +/// Swift protocols implemented in Java: `async` requirements, and protocols from other modules. +@Suite +struct JNIJavaImplementedProtocolTests { + var config: Configuration { + var config = Configuration() + config.enableJavaCallbacks = true + return config + } + + let asyncSource = """ + public struct Profile {} + public protocol ProfileClient { + func fetch(id: String) async throws -> Profile + func count() async -> Int64 + func refresh() async + } + """ + + @Test + func asyncRequirement_awaitsTheReturnedFuture() throws { + try assertOutput( + input: asyncSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public func fetch(id: String) async throws -> Profile { + let environment$ = try! JavaVirtualMachine.shared().environment() + let future$ = try environment$.withLocalFrame(capacity: 6) { + let javaInterface$ = JavaProfileClient(javaThis: _javaProfileClientInterface.javaThis, environment: environment$) + return try javaInterface$.dynamicJavaMethodCall(methodName: "fetch", arguments: id, Optional(JavaSwiftArena.defaultAutoArena), resultType: JavaCompletableFuture?.self) + } + guard let future$ else { + fatalError("Upcall to fetch unexpectedly returned a null future") + } + let result$ = try await _awaitCompletableFuture(future$) + let resumedEnvironment$ = try! JavaVirtualMachine.shared().environment() + """ + ] + ) + } + + @Test + func asyncRequirement_unboxesPrimitivesAndHandlesVoid() throws { + try assertOutput( + input: asyncSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + "return Int64.fromJavaObject(result$?.javaThis, in: resumedEnvironment$)", + "_ = try! await _awaitCompletableFuture(future$)", + ] + ) + } + + @Test + func asyncRequirement_needsCompletableFutureMode() throws { + var config = self.config + config.asyncFuncMode = .legacyFuture + try assertOutput( + input: asyncSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [], + notExpectedChunks: [ + "public protocol SwiftJavaProfileClientWrapper" + ] + ) + } + + @Test + func asyncRequirement_unsupportedResultSkipsTheWrapper() throws { + try assertOutput( + input: """ + public struct Profile {} + public protocol ProfileClient { + func fetchAll() async -> [Profile] + } + """, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [], + notExpectedChunks: [ + "public protocol SwiftJavaProfileClientWrapper" + ] + ) + } + + // MARK: Protocols from other modules + + let crossModuleSource = """ + import Core + public func use(client: any ProfileClient) {} + """ + let coreSource = """ + public protocol ProfileClient { + func name() -> String + } + """ + + @Test + func crossModuleProtocol_acceptsJavaImplementations() throws { + try assertOutput( + input: crossModuleSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + dependencySwiftSources: ["Core": coreSource], + moduleJavaPackages: ["Core": "com.example.core"], + javaCallbackModules: ["Core"], + expectedChunks: [ + "final class _SwiftModule_use_client_Wrapper: SwiftJavaProfileClientWrapper, @unchecked Sendable {", + "if environment.interface.IsInstanceOf(environment, client, _JNIMethodIDCache.JNISwiftInstance.class) != 0 {", + "clientswiftObject$ = _SwiftModule_use_client_Wrapper(_javaProfileClientInterface: JavaProfileClient(javaThis: client!, environment: environment))", + ] + ) + } + + @Test + func crossModuleProtocol_withoutCallbacksOnlyAcceptsSwiftInstances() throws { + try assertOutput( + input: crossModuleSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + dependencySwiftSources: ["Core": coreSource], + moduleJavaPackages: ["Core": "com.example.core"], + expectedChunks: [], + notExpectedChunks: [ + "_SwiftModule_use_client_Wrapper" + ] + ) + } + + @Test + func crossModuleProtocol_genericBoundIsQualified() throws { + try assertOutput( + input: crossModuleSource, + config: config, + .jni, + .java, + detectChunkByInitialLines: 1, + dependencySwiftSources: ["Core": coreSource], + moduleJavaPackages: ["Core": "com.example.core"], + expectedChunks: [ + "public static <_T0 extends com.example.core.ProfileClient> void use(_T0 client)" + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index eef6119d9..5f8ab0e40 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -191,7 +191,7 @@ struct JNIProtocolTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { + final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper, @unchecked Sendable { let _javaSomeProtocolInterface: JavaSomeProtocol init(_javaSomeProtocolInterface: JavaSomeProtocol) { self._javaSomeProtocolInterface = _javaSomeProtocolInterface @@ -267,7 +267,7 @@ struct JNIProtocolTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { + final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper, @unchecked Sendable { let _javaSomeProtocolInterface: JavaSomeProtocol init(_javaSomeProtocolInterface: JavaSomeProtocol) { self._javaSomeProtocolInterface = _javaSomeProtocolInterface @@ -323,7 +323,7 @@ struct JNIProtocolTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - final class _SwiftModule_takeComposite_x_Wrapper: SwiftJavaSomeProtocolWrapper, SwiftJavaBWrapper { + final class _SwiftModule_takeComposite_x_Wrapper: SwiftJavaSomeProtocolWrapper, SwiftJavaBWrapper, @unchecked Sendable { let _javaSomeProtocolInterface: JavaSomeProtocol let _javaBInterface: JavaB init(_javaSomeProtocolInterface: JavaSomeProtocol, _javaBInterface: JavaB) { @@ -376,7 +376,7 @@ struct JNIProtocolTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - protocol SwiftJavaSomeProtocolWrapper: SomeProtocol { + public protocol SwiftJavaSomeProtocolWrapper: SomeProtocol { var _javaSomeProtocolInterface: JavaSomeProtocol { get } } """, @@ -385,16 +385,18 @@ struct JNIProtocolTests { public func method() { let environment$ = try! JavaVirtualMachine.shared().environment() try! environment$.withLocalFrame(capacity: 4) { - _javaSomeProtocolInterface.method() + let javaInterface$ = JavaSomeProtocol(javaThis: _javaSomeProtocolInterface.javaThis, environment: environment$) + javaInterface$.method() } } public func withObject(c: SomeClass) -> SomeClass { let environment$ = try! JavaVirtualMachine.shared().environment() return try! environment$.withLocalFrame(capacity: 6) { + let javaInterface$ = JavaSomeProtocol(javaThis: _javaSomeProtocolInterface.javaThis, environment: environment$) let cClass = try! JavaClass(environment: JavaVirtualMachine.shared().environment()) let cPointer = UnsafeMutablePointer.allocate(capacity: 1) cPointer.initialize(to: c) - guard let unwrapped$ = _javaSomeProtocolInterface.withObject(cClass.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: cPointer))), JavaSwiftArena.defaultAutoArena) else { + guard let unwrapped$ = javaInterface$.withObject(cClass.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: cPointer))), JavaSwiftArena.defaultAutoArena) else { fatalError("Upcall to withObject unexpectedly returned nil") } let result$MemoryAddress$ = unwrapped$.as(JavaJNISwiftInstance.self)!.memoryAddress() @@ -405,7 +407,7 @@ struct JNIProtocolTests { } """, """ - protocol SwiftJavaBWrapper: B { + public protocol SwiftJavaBWrapper: B { var _javaBInterface: JavaB { get } } """, @@ -427,18 +429,18 @@ struct JNIProtocolTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - protocol SwiftJavaParentProtocolWrapper: ParentProtocol { + public protocol SwiftJavaParentProtocolWrapper: ParentProtocol { var _javaParentProtocolInterface: JavaParentProtocol { get } } """, """ - protocol SwiftJavaChildProtocolWrapper: ChildProtocol, SwiftJavaParentProtocolWrapper { + public protocol SwiftJavaChildProtocolWrapper: ChildProtocol, SwiftJavaParentProtocolWrapper { var _javaChildProtocolInterface: JavaChildProtocol { get } } """, """ extension SwiftJavaChildProtocolWrapper { - var _javaParentProtocolInterface: JavaParentProtocol { + public var _javaParentProtocolInterface: JavaParentProtocol { _javaChildProtocolInterface } public func childMethod() {