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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

public enum UploadError: Error {
case rejected
}

/// Swift errors as values: Kotlin `Throwable`s.
public enum Uploads {
/// A completion handler taking an optional error.
public static func upload(_ id: String, onDone: @escaping (Error?) -> Void) {
onDone(id.isEmpty ? UploadError.rejected : nil)
}

public static func failure(for id: String) -> Error? {
id.isEmpty ? UploadError.rejected : nil
}

public static func isRejected(_ error: Error) -> Bool {
(error as? UploadError) == .rejected
}

public static func describe(_ error: Error) -> String {
"\(error)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//===----------------------------------------------------------------------===//
//
// 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 kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertNull
import kotlin.test.assertTrue

// Swift `Error` values are Kotlin `Throwable`s, in both directions.
class ErrorsTest {
@Test
fun completionHandler() {
val seen = mutableListOf<Throwable?>()
Uploads.upload("") { error -> seen += error.orElse(null) }
Uploads.upload("a") { error -> seen += error.orElse(null) }
assertIs<UploadError>(seen[0])
assertNull(seen[1])
}

@Test
fun resultsAndParameters() {
val error: Throwable? = Uploads.failure("")
assertFailsWith<UploadError> { throw error!! }
assertTrue(Uploads.isRejected(error!!)) // back in Swift, the same case
assertNull(Uploads.failure("a"))

val kotlinError = IllegalStateException("disk full")
assertFalse(Uploads.isRejected(kotlinError))
assertTrue(Uploads.describe(kotlinError).contains("disk full"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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
//
//===----------------------------------------------------------------------===//

public enum SampleError: Error {
case offline
case timeout
}

/// Not jextracted: crosses as a `SwiftErrorException` holding the Swift value.
struct CodedError: Error {
let code: Int64
}

/// Swift errors as values: Java `Throwable`s.
public enum ErrorValues {
public static func make(offline: Bool) -> Error? {
offline ? SampleError.offline : nil
}

public static func isOffline(_ error: Error) -> Bool {
(error as? SampleError) == .offline
}

public static func coded(_ code: Int64) -> any Error {
CodedError(code: code)
}

public static func code(_ error: any Error) -> Int64 {
(error as? CodedError)?.code ?? -1
}

public static func describe(_ error: Error) -> String {
"\(error)"
}

public static func roundTrip(_ error: Error?) -> Error? {
error
}

public static func report(_ callback: @escaping (Error?) -> Void) {
callback(SampleError.timeout)
callback(nil)
}

public static func makeLogger() -> (Error) -> String {
{ "logged: \($0)" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//===----------------------------------------------------------------------===//
//
// 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.SwiftErrorException;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

public class ErrorValuesTest {
@Test
void jextractedErrorRoundTrip() {
Throwable error = ErrorValues.make(true).orElseThrow();
assertInstanceOf(SampleError.class, error);
// Back in Swift, it's the same case.
assertTrue(ErrorValues.isOffline(error));
assertTrue(ErrorValues.make(false).isEmpty());
}

@Test
void otherSwiftErrorRoundTrip() {
Throwable error = ErrorValues.coded(7);
assertInstanceOf(SwiftErrorException.class, error);
assertEquals(7, ErrorValues.code(error));
}

@Test
void javaExceptionRoundTrip() {
IllegalStateException exception = new IllegalStateException("offline");
assertTrue(ErrorValues.describe(exception).contains("offline"));
assertFalse(ErrorValues.isOffline(exception));
// A Java exception comes back as the same object.
assertSame(exception, ErrorValues.roundTrip(Optional.of(exception)).orElseThrow());
assertTrue(ErrorValues.roundTrip(Optional.empty()).isEmpty());
}

@Test
void closureArguments() {
List<Optional<Throwable>> seen = new ArrayList<>();
ErrorValues.report(seen::add);
assertEquals(2, seen.size());
assertInstanceOf(SampleError.class, seen.get(0).orElseThrow());
assertTrue(seen.get(1).isEmpty());
}

@Test
void returnedClosure() {
ErrorValues.makeLogger.result log = ErrorValues.makeLogger();
assertTrue(log.apply(new RuntimeException("disk full")).contains("disk full"));
}
}
6 changes: 3 additions & 3 deletions Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ enum JNIJavaTypeTranslator {
.array,
.dictionary,
.set,
.asyncStream, .asyncThrowingStream,
.asyncStream, .asyncThrowingStream, .error,
.foundationDate, .essentialsDate,
.foundationUUID, .essentialsUUID,
.foundationURL, .essentialsURL:
Expand Down Expand Up @@ -82,7 +82,7 @@ enum JNIJavaTypeTranslator {
.array,
.dictionary,
.set,
.asyncStream, .asyncThrowingStream,
.asyncStream, .asyncThrowingStream, .error,
.foundationDate, .essentialsDate,
.foundationUUID, .essentialsUUID,
.foundationURL, .essentialsURL:
Expand Down Expand Up @@ -111,7 +111,7 @@ enum JNIJavaTypeTranslator {
.array,
.dictionary,
.set,
.asyncStream, .asyncThrowingStream,
.asyncStream, .asyncThrowingStream, .error,
.foundationDate, .essentialsDate,
.foundationUUID, .essentialsUUID,
.foundationURL, .essentialsURL:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ extension JNISwift2JavaGenerator {
/// The Swift spelling of a closure parameter or result in the `@JavaInterface` wrapper.
/// Jextracted types are passed as their Java class, through its JavaKit wrapper.
private func wrapJavaTypeName(_ type: SwiftType, isResult: Bool) -> String {
if type.isSwiftError {
return isResult ? "Throwable!" : "Throwable?"
}
if !isResult, optionalWrapped(type)?.isSwiftError == true {
return "JavaOptional<Throwable>?"
}
if let nominal = jextractedNominal(type) {
return nominal.generatedJavaClassMacroName + (isResult ? "!" : "?")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ extension JNISwift2JavaGenerator {
if type.isDirectlyTranslatedToWrapJava {
return .placeholder
}
if type.isSwiftError {
return .toJavaThrowable(.placeholder)
}

switch type {
case .nominal(let nominalType):
Expand Down Expand Up @@ -425,6 +428,9 @@ enum UpcallConversionStep {

indirect case toJavaOptional(UpcallConversionStep)

/// A Swift error as SwiftJava's `Throwable`.
indirect case toJavaThrowable(UpcallConversionStep)

indirect case fromJavaOptional(UpcallConversionStep)

indirect case map(UpcallConversionStep, body: UpcallConversionStep)
Expand Down Expand Up @@ -480,6 +486,10 @@ enum UpcallConversionStep {
let inner = inner.render(&printer, placeholder)
return "\(inner).toJavaOptional()"

case .toJavaThrowable(let inner):
let inner = inner.render(&printer, placeholder)
return "_makeThrowable(for: \(inner))"

case .fromJavaOptional(let inner):
let inner = inner.render(&printer, placeholder)
return "Optional(javaOptional: \(inner))"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,13 +589,31 @@ extension JNISwift2JavaGenerator {
// If the result type should cause any annotations on the method, include them here.
let parameterAnnotations: [JavaAnnotation] = getJavaTypeAnnotations(swiftType: swiftType, config: config)

// A Swift error is a Java `Throwable`.
if swiftType.isSwiftError {
return TranslatedParameter(
parameter: JavaParameter(name: parameterName, type: .javaLangThrowable, annotations: parameterAnnotations),
conversion: .placeholder
)
}

switch swiftType {
case .nominal(let nominalType):
let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName

if let knownType = nominalType.asKnownType {
switch knownType {
case .optional(let wrapped):
if wrapped.isSwiftError {
return TranslatedParameter(
parameter: JavaParameter(
name: parameterName,
type: .optional(.javaLangThrowable),
annotations: parameterAnnotations
),
conversion: .method(.placeholder, function: "orElse", arguments: [.constant("null")])
)
}
if let (functionType, _) = Self.closureType(swiftType) {
let closure = translateFunctionParameter(
swiftType: .function(functionType),
Expand Down Expand Up @@ -1235,11 +1253,31 @@ extension JNISwift2JavaGenerator {
// If the result type should cause any annotations on the method, include them here.
let resultAnnotations: [JavaAnnotation] = getJavaTypeAnnotations(swiftType: swiftType, config: config)

// A Swift error is a Java `Throwable`, `null` for `nil`.
if swiftType.isSwiftError {
return TranslatedResult(
javaType: .javaLangThrowable,
nativeJavaType: .javaLangThrowable,
annotations: resultAnnotations,
outParameters: [],
conversion: .placeholder
)
}

switch swiftType {
case .nominal(let nominalType):
let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName
if let knownType = nominalType.asKnownType {
switch knownType {
case .optional(let wrapped) where wrapped.isSwiftError:
return TranslatedResult(
javaType: .optional(.javaLangThrowable),
nativeJavaType: .javaLangThrowable,
annotations: resultAnnotations,
outParameters: [],
conversion: .method(.constant("java.util.Optional"), function: "ofNullable", arguments: [.placeholder])
)

case .optional(let wrapped):
return try translateOptionalResult(
wrappedType: wrapped,
Expand Down
Loading
Loading