Skip to content

JNI: Swift Error values cross as Java Throwables - #25

Open
piercifani wants to merge 2 commits into
jni/20-nullability-annotationsfrom
jni/21-error-values
Open

piercifani wants to merge 2 commits into
jni/20-nullability-annotationsfrom
jni/21-error-values

Conversation

@piercifani

Copy link
Copy Markdown

Stack 21/21 · base: jni/20-nullability-annotations · previous: #24

Problem

Error as a value, such as a completion handler's (Error?) -> Void, wasn't supported, and in the most common spelling the output didn't compile.

  • Error: jextract mistook it for one of the module's own types. The generated Java called Error.wrapMemoryAddressUnsafe(…) and error.$memoryAddress(). In Java, Error means java.lang.Error, which has neither, and the generated Swift failed the same way. Neither side compiled.
  • any Error: results and closure arguments were skipped. A parameter became <T extends Error>, again java.lang.Error.

Thrown errors already worked: a Swift error enum becomes its own exception class, and any other error becomes SwiftErrorException.

Changes

Swift.Error is now a known type, and Error or any Error is a java.lang.Throwable in these positions:

The conversions mirror thrown errors, so a round trip keeps the error:

  • Swift to Java: makeJavaThrowable(for:), which thrown errors already use.
    • A jextracted error type becomes its exception class.
    • Any other Swift error becomes a SwiftErrorException that holds the Swift value.
    • A Java exception that went through Swift comes back as the same object.
    • Results are returned as a new local reference, since the reference held by SwiftJava's Throwable wrapper belongs to that wrapper.
  • Java to Swift: the new makeSwiftError(for:).
    • A SwiftError, the base of SwiftErrorException and of jextracted error types, gives back a copy of the Swift error it holds, so error as? UploadError works after a round trip.
    • Any other exception is SwiftJava's Throwable, which is already a Swift Error.

A positive side effect: as a known type, Error in a position this PR doesn't cover is skipped, instead of producing code that doesn't compile.

Kotlin in this PR

ErrorsTest.kt with Uploads.swift:

// public static func upload(_ id: String, onDone: @escaping (Error?) -> Void)
Uploads.upload("") { error -> seen += error.orElse(null) }   // an UploadError

// public static func failure(for id: String) -> Error?
val error: Throwable? = Uploads.failure("")
assertFailsWith<UploadError> { throw error!! }
assertTrue(Uploads.isRejected(error))                        // back in Swift: the same case
assertFalse(Uploads.isRejected(IllegalStateException("disk full")))

Why Skip needs it

Skip bridges Swift Error to Kotlin Throwable in every position. Completion handlers taking Error? are common in Swift code: there are about 250 (Error?) -> closure types in the app's packages.

One difference from Skip: Skip turns a Swift error that has no Kotlin counterpart into an exception carrying only its description. Here it keeps the Swift value.

Testing

  • Kotlin sample: ./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes (49 tests).
  • New JNIErrorValuesTests (2 tests): the Java signatures for every position, and the Swift conversions, the closure wrappers and ErrorBridge.
  • JNI sample: ErrorValuesTest (5 tests):
    • a jextracted error and a boxed internal Swift error both survive a round trip;
    • a Java exception comes back as the same object (assertSame);
    • nil handling;
    • a closure taking Error?;
    • a returned closure taking Error.
  • Full swift test --disable-experimental-prebuilts --disable-sandbox (692 tests), ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test (307 tests) and ./gradlew :SwiftKitCore:test pass.
  • swift format format leaves changed files unchanged; git diff --check.

Not in this PR

  • Error as a result of a closure implemented in Java, or as an array element.
  • Typed Failure beyond any Error.
  • FFM mode.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 3, 2026 22:03
`Error` as a value, such as a completion handler's `Error?`, wasn't
supported. Written `Error`, it was mistaken for a type of the module:
the generated Java called `Error.wrapMemoryAddressUnsafe`, which
`java.lang.Error` doesn't have, and neither side compiled. Written
`any Error`, it was skipped, or a parameter became `<T extends Error>`.

`Swift.Error` is now a known type, and `Error` or `any Error` is a
`java.lang.Throwable` in parameters, results and optionals of them, in
closures implemented in Java, and in closures returned to Java.

- Swift to Java uses `makeJavaThrowable(for:)`, as thrown errors do: a
  jextracted error type becomes its exception class, another Swift
  error a `SwiftErrorException` holding it, and a Java exception that
  went through Swift is the same object again. A result is returned as
  a new local reference.
- Java to Swift uses the new `makeSwiftError(for:)`: a `SwiftError`
  gives back a copy of the Swift error it holds, so `error as?
  LoadError` works after a round trip; any other exception is
  SwiftJava's `Throwable`.
- `ErrorBridge` does both for closures returned to Java.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant