Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 54 additions & 27 deletions Sources/SkipSyntax/Kotlin/KotlinBridgeToSwiftVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
}
Expand All @@ -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()")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Sources/SkipSyntax/Kotlin/KotlinBridgeTransformer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
33 changes: 28 additions & 5 deletions Tests/SkipSyntaxTests/BridgeSendabilityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand All @@ -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..<bridge.endIndex))
let continuation = String(bridge[start.lowerBound..<end.lowerBound])
let function = String(bridge[start.lowerBound..<Self.closingBraceIndex(after: start.lowerBound, in: bridge)])
let dispatch = try XCTUnwrap(function.range(of: "jniContext {"))
let dispatchEnd = Self.closingBraceIndex(after: dispatch.lowerBound, in: function)
let continuation = function.replacingCharacters(in: dispatch.upperBound..<function.index(before: dispatchEnd), with: " _ = f_return_callback ")
let source = """
\(component)
typealias JavaObjectPointer = OpaquePointer
Expand All @@ -151,10 +171,13 @@ final class BridgeSendabilityTests: XCTestCase {
BridgeTestError.failure
}
}
\(continuation)
_ = f_return_callback
}
final class BridgedJob: Sendable {
func attach(_ job: JavaObjectPointer) {}
func cancel() {}
func error(_ throwable: JavaObjectPointer, options: [Int]) -> any Error { BridgeTestError.failure }
}
func jniContext<T>(_ block: () throws -> T) rethrows -> T { try block() }
\(continuation)
"""
let file = try tmpFile(named: "Continuation.swift", contents: source)
let process = Process()
Expand Down
Loading
Loading