diff --git a/Sources/SkipSyntax/Kotlin/KotlinBridgeToSwiftVisitor.swift b/Sources/SkipSyntax/Kotlin/KotlinBridgeToSwiftVisitor.swift index e697434a..0f8e930b 100644 --- a/Sources/SkipSyntax/Kotlin/KotlinBridgeToSwiftVisitor.swift +++ b/Sources/SkipSyntax/Kotlin/KotlinBridgeToSwiftVisitor.swift @@ -476,17 +476,14 @@ final class KotlinBridgeToSwiftVisitor { callbackFunction.isGenerated = true var taskSourceCode: [String] = [] - taskSourceCode.append("Task {") if variableDeclaration.apiFlags.throwsType == .none { + taskSourceCode.append("Task {") taskSourceCode.append(1, "f_return_callback(\(variableDeclaration.propertyName)())") + taskSourceCode.append("}") } else { - taskSourceCode.append(1, "try {") - taskSourceCode.append(2, "f_return_callback(\(variableDeclaration.propertyName)(), null)") - taskSourceCode.append(1, "} catch(t: Throwable) {") - taskSourceCode.append(2, "f_return_callback(null, t)") - taskSourceCode.append(1, "}") + callbackFunction.returnType = .kotlinJob + taskSourceCode = cancellableTaskSourceCode(invocation: "\(variableDeclaration.propertyName)()", isVoid: false) } - taskSourceCode.append("}") callbackFunction.body = KotlinCodeBlock(statements: taskSourceCode.map { KotlinRawStatement(sourceCode: $0) }) (variableDeclaration.parent as? KotlinStatement)?.insert(statements: [callbackFunction], after: variableDeclaration) } @@ -631,9 +628,16 @@ final class KotlinBridgeToSwiftVisitor { if isDeclaredByVariable { indentation = indentation.inc() } + var cancellationHandlerLevel: Int? = nil if isAsync { if isThrows { - swift.append(indentation, returnCallString + "await withCheckedThrowingContinuation { f_continuation in") + // The Kotlin callback function returns the Job running the call; cancelling the awaiting + // Swift task cancels it through the handler + swift.append(indentation, "let f_job = BridgedJob()") + swift.append(indentation, returnCallString + "await withTaskCancellationHandler {") + cancellationHandlerLevel = indentation.level + indentation = indentation.inc() + swift.append(indentation, "try await withCheckedThrowingContinuation { f_continuation in") } else { swift.append(indentation, returnCallString + "await withCheckedContinuation { f_continuation in") } @@ -658,7 +662,7 @@ final class KotlinBridgeToSwiftVisitor { } indentation = indentation.inc() swift.append(indentation, "if let f_error {") - swift.append(indentation.inc(), "f_continuation.resume(throwing: JThrowable.toError(f_error, options: \(optionsString))!)") + swift.append(indentation.inc(), "f_continuation.resume(throwing: f_job.error(f_error, options: \(optionsString)))") swift.append(indentation, "} else {") if callbackType.parameters.count == 1 { swift.append(indentation.inc(), "f_continuation.resume()") @@ -735,7 +739,12 @@ final class KotlinBridgeToSwiftVisitor { } argumentsString += "f_return_callback_java" let call = "\(tryType) \(targetIdentifier).\(callType)(method: \(callMethod), options: \(optionsString), args: [\(argumentsString)])" - swift.append(indentation, call) + if isThrows { + swift.append(indentation, "let f_job_java: JavaObjectPointer = " + call) + swift.append(indentation, "f_job.attach(f_job_java)") + } else { + swift.append(indentation, call) + } } else { let callType = inType == nil ? "callStatic" : "call" let callMethod = inType == nil || modifiers.isStatic ? methodIdentifier : "Self." + methodIdentifier @@ -764,6 +773,10 @@ final class KotlinBridgeToSwiftVisitor { swift.append(1, "super.init(Java_peer: Java_peer)") } indentation = indentation.dec() + if indentation.level == cancellationHandlerLevel { + swift.append(indentation, "} onCancel: {") + swift.append(indentation.inc(), "f_job.cancel()") + } swift.append(indentation, "}") } return swift @@ -795,7 +808,7 @@ final class KotlinBridgeToSwiftVisitor { functionName = "callback_" + preEscapedName let callbackType = bridgable.return.isGenericEntry ? TypeSignature.any.asOptional(bridgable.return.kotlinType.isOptional) : bridgable.return.kotlinType kotlinParameters.append(TypeSignature.Parameter(type: callbackType.callbackClosureType(apiFlags: apiFlags, kotlin: true))) - kotlinReturnType = .void + kotlinReturnType = apiFlags.throwsType == .none ? .void : .kotlinJob } else { functionName = name kotlinReturnType = bridgable.return.isGenericEntry ? TypeSignature.any.asOptional(bridgable.return.kotlinType.isOptional) : bridgable.return.kotlinType @@ -822,35 +835,49 @@ final class KotlinBridgeToSwiftVisitor { let invocationSourceCode = invocationSourceCode(for: functionDeclaration) var taskSourceCode: [String] = [] - taskSourceCode.append("Task {") if functionDeclaration.apiFlags.throwsType == .none { + taskSourceCode.append("Task {") if callbackType.parameters.isEmpty { taskSourceCode.append(1, invocationSourceCode) taskSourceCode.append(1, "f_return_callback()") } else { taskSourceCode.append(1, "f_return_callback(\(invocationSourceCode))") } + taskSourceCode.append("}") } else { - taskSourceCode.append(1, "try {") - if callbackType.parameters.count == 1 { - taskSourceCode.append(2, invocationSourceCode) - taskSourceCode.append(2, "f_return_callback(null)") - } else { - taskSourceCode.append(2, "f_return_callback(\(invocationSourceCode), null)") - } - taskSourceCode.append(1, "} catch(t: Throwable) {") - if callbackType.parameters.count == 1 { - taskSourceCode.append(2, "f_return_callback(t)") - } else { - taskSourceCode.append(2, "f_return_callback(null, t)") - } - taskSourceCode.append(1, "}") + callbackFunction.returnType = .kotlinJob + taskSourceCode = cancellableTaskSourceCode(invocation: invocationSourceCode, isVoid: callbackType.parameters.count == 1) } - taskSourceCode.append("}") callbackFunction.body = KotlinCodeBlock(statements: taskSourceCode.map { KotlinRawStatement(sourceCode: $0) }) (functionDeclaration.parent as? KotlinStatement)?.insert(statements: [callbackFunction], after: functionDeclaration) } + /// The body of the `callback_` function for a throwing async API. + /// + /// The call runs under its own `Job`, which is returned to Swift: cancelling the awaiting Swift + /// task cancels the job, so a Kotlin implementation suspended in e.g. `suspendCancellableCoroutine` + /// observes the cancellation instead of running to completion. Cancelling the job before the + /// call starts makes `withContext` throw, so the callback always fires exactly once. + private static func cancellableTaskSourceCode(invocation: String, isVoid: Bool) -> [String] { + var source: [String] = [] + source.append("val f_job = kotlinx.coroutines.Job()") + source.append("Task {") + source.append(1, "try {") + if isVoid { + source.append(2, "kotlinx.coroutines.withContext(f_job) { \(invocation) }") + source.append(2, "f_return_callback(null)") + } else { + source.append(2, "val f_return = kotlinx.coroutines.withContext(f_job) { \(invocation) }") + source.append(2, "f_return_callback(f_return, null)") + } + source.append(1, "} catch(t: Throwable) {") + source.append(2, isVoid ? "f_return_callback(t)" : "f_return_callback(null, t)") + source.append(1, "}") + source.append("}") + source.append("return f_job") + return source + } + private static func invocationSourceCode(for functionDeclaration: KotlinFunctionDeclaration) -> String { let argumentsString = functionDeclaration.parameters.map { let label = $0.externalLabel ?? $0.internalLabel diff --git a/Sources/SkipSyntax/Kotlin/KotlinBridgeTransformer.swift b/Sources/SkipSyntax/Kotlin/KotlinBridgeTransformer.swift index fc409466..4802c322 100644 --- a/Sources/SkipSyntax/Kotlin/KotlinBridgeTransformer.swift +++ b/Sources/SkipSyntax/Kotlin/KotlinBridgeTransformer.swift @@ -380,6 +380,8 @@ extension TypeSignature { return kotlin ? .named("skip.bridge.SwiftObjectPointer", []) : .named("SwiftObjectPointer", []) } static let nothing: TypeSignature = .named("Nothing", []) + /// The `kotlinx.coroutines.Job` a throwing async `callback_` function returns. + static let kotlinJob: TypeSignature = .module("kotlinx.coroutines", .named("Job", [])) static func javaVoid(kotlin: Bool) -> TypeSignature { return kotlin ? .module("java.lang", .named("Void", [])) : .void } diff --git a/Tests/SkipSyntaxTests/BridgeSendabilityTests.swift b/Tests/SkipSyntaxTests/BridgeSendabilityTests.swift index 361229fc..ad82f121 100644 --- a/Tests/SkipSyntaxTests/BridgeSendabilityTests.swift +++ b/Tests/SkipSyntaxTests/BridgeSendabilityTests.swift @@ -121,6 +121,24 @@ final class BridgeSendabilityTests: XCTestCase { assertReturnIsolation(bridge, unsafe: true) } + /// The index just past the `}` closing the first `{` at or after `index`. + private static func closingBraceIndex(after index: String.Index, in text: String) -> String.Index { + var depth = 0 + var i = index + while i < text.endIndex { + if text[i] == "{" { + depth += 1 + } else if text[i] == "}" { + depth -= 1 + if depth == 0 { + return text.index(after: i) + } + } + i = text.index(after: i) + } + return text.endIndex + } + func testGeneratedContinuationCompilesWithoutWarnings() async throws { #if compiler(<6.0) throw XCTSkip("This regression check requires Swift 6 language mode.") @@ -140,8 +158,10 @@ final class BridgeSendabilityTests: XCTestCase { """ let bridge = try await generateBridge(swift: declarations, native: true) let start = try XCTUnwrap(bridge.range(of: "public func fetch()")) - let end = try XCTUnwrap(bridge.range(of: "jniContext {", range: start.lowerBound.. any Error { BridgeTestError.failure } } + func jniContext(_ block: () throws -> T) rethrows -> T { try block() } + \(continuation) """ let file = try tmpFile(named: "Continuation.swift", contents: source) let process = Process() diff --git a/Tests/SkipSyntaxTests/BridgeToKotlinTests.swift b/Tests/SkipSyntaxTests/BridgeToKotlinTests.swift index 2434e231..15a525df 100644 --- a/Tests/SkipSyntaxTests/BridgeToKotlinTests.swift +++ b/Tests/SkipSyntaxTests/BridgeToKotlinTests.swift @@ -4708,14 +4708,17 @@ final class BridgeToKotlinTests: XCTestCase { """, kotlins: [""" interface P { suspend fun f(url: java.net.URI): Int - fun callback_f(url: java.net.URI, f_return_callback: (Int?, Throwable?) -> Unit) { + fun callback_f(url: java.net.URI, f_return_callback: (Int?, Throwable?) -> Unit): kotlinx.coroutines.Job { + val f_job = kotlinx.coroutines.Job() Task { try { - f_return_callback(f(url = url), null) + val f_return = kotlinx.coroutines.withContext(f_job) { f(url = url) } + f_return_callback(f_return, null) } catch(t: Throwable) { f_return_callback(null, t) } } + return f_job } } class C: P, skip.bridge.SwiftPeerBridged, skip.lib.SwiftProjecting { @@ -4787,23 +4790,29 @@ final class BridgeToKotlinTests: XCTestCase { Java_peer = JObject(Java_ptr) } public func f(url p_0: URL) async throws -> Int { - return try await withCheckedThrowingContinuation { f_continuation in - let f_return_callback: @Sendable (Int?, JavaObjectPointer?) -> Void = { f_return, f_error in - if let f_error { - f_continuation.resume(throwing: JThrowable.toError(f_error, options: [.kotlincompat])!) - } else { - let f_return_value = f_return! - f_continuation.resume(returning: f_return_value) + let f_job = BridgedJob() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { f_continuation in + let f_return_callback: @Sendable (Int?, JavaObjectPointer?) -> Void = { f_return, f_error in + if let f_error { + f_continuation.resume(throwing: f_job.error(f_error, options: [.kotlincompat])) + } else { + let f_return_value = f_return! + f_continuation.resume(returning: f_return_value) + } + } + jniContext { + let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: [.kotlincompat]).toJavaParameter(options: [.kotlincompat]) + let p_0_java = p_0.toJavaObject(options: [.kotlincompat])!.toJavaParameter(options: [.kotlincompat]) + let f_job_java: JavaObjectPointer = try! Java_peer.call(method: Self.Java_f_0_methodID, options: [.kotlincompat], args: [p_0_java, f_return_callback_java]) + f_job.attach(f_job_java) } } - jniContext { - let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: [.kotlincompat]).toJavaParameter(options: [.kotlincompat]) - let p_0_java = p_0.toJavaObject(options: [.kotlincompat])!.toJavaParameter(options: [.kotlincompat]) - try! Java_peer.call(method: Self.Java_f_0_methodID, options: [.kotlincompat], args: [p_0_java, f_return_callback_java]) - } + } onCancel: { + f_job.cancel() } } - nonisolated private static let Java_f_0_methodID = Java_class.getMethodID(name: "callback_f", sig: "(Ljava/net/URI;Lkotlin/jvm/functions/Function2;)V")! + nonisolated private static let Java_f_0_methodID = Java_class.getMethodID(name: "callback_f", sig: "(Ljava/net/URI;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job;")! nonisolated public static func fromJavaObject(_ obj: JavaObjectPointer?, options: JConvertibleOptions) -> Self { return .init(Java_ptr: obj!) } @@ -5931,15 +5940,18 @@ final class BridgeToKotlinTests: XCTestCase { """, kotlins: [""" interface FooAPIClientNetworkFetcher { suspend fun fetchData(with: java.net.URI, method: String, httpHeaders: kotlin.collections.Map, body: String?): String - fun callback_fetchData(with: java.net.URI, method: String, httpHeaders: kotlin.collections.Map, body: String?, f_return_callback: (String?, Throwable?) -> Unit) { + fun callback_fetchData(with: java.net.URI, method: String, httpHeaders: kotlin.collections.Map, body: String?, f_return_callback: (String?, Throwable?) -> Unit): kotlinx.coroutines.Job { val url = with + val f_job = kotlinx.coroutines.Job() Task { try { - f_return_callback(fetchData(with = with, method = method, httpHeaders = httpHeaders, body = body), null) + val f_return = kotlinx.coroutines.withContext(f_job) { fetchData(with = with, method = method, httpHeaders = httpHeaders, body = body) } + f_return_callback(f_return, null) } catch(t: Throwable) { f_return_callback(null, t) } } + return f_job } } """, """ @@ -5953,26 +5965,32 @@ final class BridgeToKotlinTests: XCTestCase { Java_peer = JObject(Java_ptr) } public func fetchData(with p_0: URL, method p_1: String, httpHeaders p_2: [String: String], body p_3: String?) async throws -> String { - return try await withCheckedThrowingContinuation { f_continuation in - let f_return_callback: @Sendable (String?, JavaObjectPointer?) -> Void = { f_return, f_error in - if let f_error { - f_continuation.resume(throwing: JThrowable.toError(f_error, options: [.kotlincompat])!) - } else { - let f_return_value = f_return! - f_continuation.resume(returning: f_return_value) + let f_job = BridgedJob() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { f_continuation in + let f_return_callback: @Sendable (String?, JavaObjectPointer?) -> Void = { f_return, f_error in + if let f_error { + f_continuation.resume(throwing: f_job.error(f_error, options: [.kotlincompat])) + } else { + let f_return_value = f_return! + f_continuation.resume(returning: f_return_value) + } + } + jniContext { + let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: [.kotlincompat]).toJavaParameter(options: [.kotlincompat]) + let p_0_java = p_0.toJavaObject(options: [.kotlincompat])!.toJavaParameter(options: [.kotlincompat]) + let p_1_java = p_1.toJavaParameter(options: [.kotlincompat]) + let p_2_java = p_2.toJavaObject(options: [.kotlincompat])!.toJavaParameter(options: [.kotlincompat]) + let p_3_java = p_3.toJavaParameter(options: [.kotlincompat]) + let f_job_java: JavaObjectPointer = try! Java_peer.call(method: Self.Java_fetchData_0_methodID, options: [.kotlincompat], args: [p_0_java, p_1_java, p_2_java, p_3_java, f_return_callback_java]) + f_job.attach(f_job_java) } } - jniContext { - let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: [.kotlincompat]).toJavaParameter(options: [.kotlincompat]) - let p_0_java = p_0.toJavaObject(options: [.kotlincompat])!.toJavaParameter(options: [.kotlincompat]) - let p_1_java = p_1.toJavaParameter(options: [.kotlincompat]) - let p_2_java = p_2.toJavaObject(options: [.kotlincompat])!.toJavaParameter(options: [.kotlincompat]) - let p_3_java = p_3.toJavaParameter(options: [.kotlincompat]) - try! Java_peer.call(method: Self.Java_fetchData_0_methodID, options: [.kotlincompat], args: [p_0_java, p_1_java, p_2_java, p_3_java, f_return_callback_java]) - } + } onCancel: { + f_job.cancel() } } - nonisolated private static let Java_fetchData_0_methodID = Java_class.getMethodID(name: "callback_fetchData", sig: "(Ljava/net/URI;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V")! + nonisolated private static let Java_fetchData_0_methodID = Java_class.getMethodID(name: "callback_fetchData", sig: "(Ljava/net/URI;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job;")! nonisolated public static func fromJavaObject(_ obj: JavaObjectPointer?, options: JConvertibleOptions) -> Self { return .init(Java_ptr: obj!) } diff --git a/Tests/SkipSyntaxTests/BridgeToSwiftTests.swift b/Tests/SkipSyntaxTests/BridgeToSwiftTests.swift index 31c29861..83aaf279 100644 --- a/Tests/SkipSyntaxTests/BridgeToSwiftTests.swift +++ b/Tests/SkipSyntaxTests/BridgeToSwiftTests.swift @@ -592,36 +592,45 @@ final class BridgeToSwiftTests: XCTestCase { suspend fun i(): Int = Async.run l@{ return@l 0 } - fun callback_i(f_return_callback: (Int?, Throwable?) -> Unit) { + fun callback_i(f_return_callback: (Int?, Throwable?) -> Unit): kotlinx.coroutines.Job { + val f_job = kotlinx.coroutines.Job() Task { try { - f_return_callback(i(), null) + val f_return = kotlinx.coroutines.withContext(f_job) { i() } + f_return_callback(f_return, null) } catch(t: Throwable) { f_return_callback(null, t) } } + return f_job } """, swiftBridgeSupport: """ private let Java_SourceKt = try! JClass(name: "SourceKt") public var i: Int { get async throws { - return try await withCheckedThrowingContinuation { f_continuation in - let f_return_callback: @Sendable (Int?, JavaObjectPointer?) -> Void = { f_return, f_error in - if let f_error { - f_continuation.resume(throwing: JThrowable.toError(f_error, options: [])!) - } else { - let f_return_value = f_return! - f_continuation.resume(returning: f_return_value) + let f_job = BridgedJob() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { f_continuation in + let f_return_callback: @Sendable (Int?, JavaObjectPointer?) -> Void = { f_return, f_error in + if let f_error { + f_continuation.resume(throwing: f_job.error(f_error, options: [])) + } else { + let f_return_value = f_return! + f_continuation.resume(returning: f_return_value) + } + } + jniContext { + let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: []).toJavaParameter(options: []) + let f_job_java: JavaObjectPointer = try! Java_SourceKt.callStatic(method: Java_i_methodID, options: [], args: [f_return_callback_java]) + f_job.attach(f_job_java) } } - jniContext { - let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: []).toJavaParameter(options: []) - try! Java_SourceKt.callStatic(method: Java_i_methodID, options: [], args: [f_return_callback_java]) - } + } onCancel: { + f_job.cancel() } } } - private let Java_i_methodID = Java_SourceKt.getStaticMethodID(name: "callback_i", sig: "(Lkotlin/jvm/functions/Function2;)V")! + private let Java_i_methodID = Java_SourceKt.getStaticMethodID(name: "callback_i", sig: "(Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job;")! """, transformers: transformers) } @@ -1501,34 +1510,43 @@ final class BridgeToSwiftTests: XCTestCase { suspend fun f(): Int = Async.run l@{ return@l 1 } - fun callback_f(f_return_callback: (Int?, Throwable?) -> Unit) { + fun callback_f(f_return_callback: (Int?, Throwable?) -> Unit): kotlinx.coroutines.Job { + val f_job = kotlinx.coroutines.Job() Task { try { - f_return_callback(f(), null) + val f_return = kotlinx.coroutines.withContext(f_job) { f() } + f_return_callback(f_return, null) } catch(t: Throwable) { f_return_callback(null, t) } } + return f_job } """, swiftBridgeSupport: """ private let Java_SourceKt = try! JClass(name: "SourceKt") public func f() async throws -> Int { - return try await withCheckedThrowingContinuation { f_continuation in - let f_return_callback: @Sendable (Int?, JavaObjectPointer?) -> Void = { f_return, f_error in - if let f_error { - f_continuation.resume(throwing: JThrowable.toError(f_error, options: [])!) - } else { - let f_return_value = f_return! - f_continuation.resume(returning: f_return_value) + let f_job = BridgedJob() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { f_continuation in + let f_return_callback: @Sendable (Int?, JavaObjectPointer?) -> Void = { f_return, f_error in + if let f_error { + f_continuation.resume(throwing: f_job.error(f_error, options: [])) + } else { + let f_return_value = f_return! + f_continuation.resume(returning: f_return_value) + } + } + jniContext { + let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: []).toJavaParameter(options: []) + let f_job_java: JavaObjectPointer = try! Java_SourceKt.callStatic(method: Java_f_0_methodID, options: [], args: [f_return_callback_java]) + f_job.attach(f_job_java) } } - jniContext { - let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: []).toJavaParameter(options: []) - try! Java_SourceKt.callStatic(method: Java_f_0_methodID, options: [], args: [f_return_callback_java]) - } + } onCancel: { + f_job.cancel() } } - private let Java_f_0_methodID = Java_SourceKt.getStaticMethodID(name: "callback_f", sig: "(Lkotlin/jvm/functions/Function2;)V")! + private let Java_f_0_methodID = Java_SourceKt.getStaticMethodID(name: "callback_f", sig: "(Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job;")! """, transformers: transformers) } @@ -1540,35 +1558,43 @@ final class BridgeToSwiftTests: XCTestCase { #endif """, kotlin: """ suspend fun f(i: Int): Unit = Unit - fun callback_f(i: Int, f_return_callback: (Throwable?) -> Unit) { + fun callback_f(i: Int, f_return_callback: (Throwable?) -> Unit): kotlinx.coroutines.Job { + val f_job = kotlinx.coroutines.Job() Task { try { - f(i = i) + kotlinx.coroutines.withContext(f_job) { f(i = i) } f_return_callback(null) } catch(t: Throwable) { f_return_callback(t) } } + return f_job } """, swiftBridgeSupport: """ private let Java_SourceKt = try! JClass(name: "SourceKt") public func f(i p_0: Int) async throws { - return try await withCheckedThrowingContinuation { f_continuation in - let f_return_callback: @Sendable (JavaObjectPointer?) -> Void = { f_error in - if let f_error { - f_continuation.resume(throwing: JThrowable.toError(f_error, options: [])!) - } else { - f_continuation.resume() + let f_job = BridgedJob() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { f_continuation in + let f_return_callback: @Sendable (JavaObjectPointer?) -> Void = { f_error in + if let f_error { + f_continuation.resume(throwing: f_job.error(f_error, options: [])) + } else { + f_continuation.resume() + } + } + jniContext { + let f_return_callback_java = SwiftClosure1.javaObject(for: f_return_callback, options: []).toJavaParameter(options: []) + let p_0_java = Int32(p_0).toJavaParameter(options: []) + let f_job_java: JavaObjectPointer = try! Java_SourceKt.callStatic(method: Java_f_0_methodID, options: [], args: [p_0_java, f_return_callback_java]) + f_job.attach(f_job_java) } } - jniContext { - let f_return_callback_java = SwiftClosure1.javaObject(for: f_return_callback, options: []).toJavaParameter(options: []) - let p_0_java = Int32(p_0).toJavaParameter(options: []) - try! Java_SourceKt.callStatic(method: Java_f_0_methodID, options: [], args: [p_0_java, f_return_callback_java]) - } + } onCancel: { + f_job.cancel() } } - private let Java_f_0_methodID = Java_SourceKt.getStaticMethodID(name: "callback_f", sig: "(ILkotlin/jvm/functions/Function1;)V")! + private let Java_f_0_methodID = Java_SourceKt.getStaticMethodID(name: "callback_f", sig: "(ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/Job;")! """, transformers: transformers) } @@ -5388,35 +5414,44 @@ final class BridgeToSwiftTests: XCTestCase { suspend fun f(p: T): T = Async.run l@{ return@l p.sref() } - fun callback_f(p: T, f_return_callback: (T?, Throwable?) -> Unit) { + fun callback_f(p: T, f_return_callback: (T?, Throwable?) -> Unit): kotlinx.coroutines.Job { + val f_job = kotlinx.coroutines.Job() Task { try { - f_return_callback(f(p = p), null) + val f_return = kotlinx.coroutines.withContext(f_job) { f(p = p) } + f_return_callback(f_return, null) } catch(t: Throwable) { f_return_callback(null, t) } } + return f_job } """, swiftBridgeSupport: """ private let Java_SourceKt = try! JClass(name: "SourceKt") public func f(p p_0: T) async throws -> T { - return try await withCheckedThrowingContinuation { f_continuation in - let f_return_callback: @Sendable (T?, JavaObjectPointer?) -> Void = { f_return, f_error in - if let f_error { - f_continuation.resume(throwing: JThrowable.toError(f_error, options: [])!) - } else { - nonisolated(unsafe) let f_return_value = f_return! - f_continuation.resume(returning: f_return_value) + let f_job = BridgedJob() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { f_continuation in + let f_return_callback: @Sendable (T?, JavaObjectPointer?) -> Void = { f_return, f_error in + if let f_error { + f_continuation.resume(throwing: f_job.error(f_error, options: [])) + } else { + nonisolated(unsafe) let f_return_value = f_return! + f_continuation.resume(returning: f_return_value) + } + } + jniContext { + let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: []).toJavaParameter(options: []) + let p_0_java = AnyBridging.toJavaObject(p_0, options: [])!.toJavaParameter(options: []) + let f_job_java: JavaObjectPointer = try! Java_SourceKt.callStatic(method: Java_f_0_methodID, options: [], args: [p_0_java, f_return_callback_java]) + f_job.attach(f_job_java) } } - jniContext { - let f_return_callback_java = SwiftClosure2.javaObject(for: f_return_callback, options: []).toJavaParameter(options: []) - let p_0_java = AnyBridging.toJavaObject(p_0, options: [])!.toJavaParameter(options: []) - try! Java_SourceKt.callStatic(method: Java_f_0_methodID, options: [], args: [p_0_java, f_return_callback_java]) - } + } onCancel: { + f_job.cancel() } } - private let Java_f_0_methodID = Java_SourceKt.getStaticMethodID(name: "callback_f", sig: "(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V")! + private let Java_f_0_methodID = Java_SourceKt.getStaticMethodID(name: "callback_f", sig: "(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job;")! """, transformers: transformers) }