Skip to content

JNI: AsyncStream results as SwiftAsyncStream - #14

Open
piercifani wants to merge 3 commits into
jni/09-object-identityfrom
jni/10-async-stream
Open

piercifani wants to merge 3 commits into
jni/09-object-identityfrom
jni/10-async-stream

Conversation

@piercifani

@piercifani piercifani commented Nov 3, 2026 •

Copy link
Copy Markdown

Stack 10/21 · base: jni/09-object-identity · previous: #13 · next: #15 (jni/11-async-throwing-stream)

Problem

jextract couldn't extract a function returning AsyncStream<T>: the type isn't in its model of the standard library, so the declaration was skipped (unknown(... AsyncStream<Item>)).

Changes

A Java type for Swift streams. SwiftKitCore gets SwiftAsyncStream<T>:

  • subscribe(Subscriber<T>) starts a Swift task that iterates the stream and calls onNext for each element and onComplete when the stream finishes. No Java thread is blocked.
  • The returned Subscription.cancel() cancels that task. Cancellation reaches the stream's onTermination, so an endless producer can stop.
  • map(...) returns a view with converted elements. An AsyncStream has a single consumer, so a stream (and its mapped views) can be subscribed to once; a second subscription throws IllegalStateException.
  • An internal Delivery owns the Swift task handle and releases it exactly once, whether the stream finishes or is cancelled first, including when it finishes before subscribe returns.

I chose this over java.util.concurrent.Flow.Publisher (Android API 30+) and over a blocking Iterator (a thread per stream).

Kotlin Flow: a new swiftkit-kotlin artifact. SwiftKitKotlin adds SwiftAsyncStream<T>.asFlow(), a callbackFlow whose cancellation cancels the Swift task; a slow collector makes the Swift task wait for buffer space. It's a separate artifact so SwiftKitCore stays a plain Java library without a kotlinx-coroutines dependency, as CompletableFuture results rely on kotlinx's await() without SwiftKitCore depending on it. It targets Java 11, like SwiftKitCore.

Runtime. SwiftJavaRuntimeSupport's _attachAsyncStream(_:to:environment:convert:) stores the stream and an element converter behind a handle on the Java object. Subscribing consumes the handle and starts the task, which converts and delivers each element with the current thread's JNIEnv.

Generator.

  • AsyncStream is added to the stdlib model (as Swift.AsyncStream, next to Set).
  • A function returning AsyncStream<T> is translated as returning T, then converted: Java passes a SwiftAsyncStream out-parameter (like future$ for async functions) and returns stream$.map(element$ -> <the conversion a T result would get>). The Swift thunk attaches the stream, with a converter that boxes primitives exactly as async results are boxed.
  • async functions returning a stream, and elements that need several Java values (tuples), are skipped with a reported reason.

Kotlin in this PR

StreamsTest.kt with Streams.swift:

  • asFlow() comes from SwiftKitKotlin (import org.swift.swiftkit.kotlin.asFlow); the sample depends on it instead of kotlinx-coroutines directly.
  • feed.prices(3).asFlow().map { it.id }.toList() collects a stream of structs.
  • feed.live().asFlow().take(2) cancels an endless Swift stream, whose onTermination then runs.

Why Skip needs it

Skip bridges AsyncStream to Kotlin Flow, which Compose collects with collectAsState. A production Skip Fuse app (16 Skip modules) returns 18 streams from Swift to Kotlin this way.

Testing

  • Kotlin sample: ./gradlew :SwiftKitKotlin:build :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes at this point in the stack (29 tests); swiftkit-kotlin classes are Java 11 bytecode.
  • New JNIAsyncStreamTests (4 tests): the Java method and native signature, the Swift attach with its element converter, jextracted elements mapped to their Java class, and unsupported streams skipped with a reason.
  • JNI sample: countdown (primitives) and shapes (jextracted Squares) collected in Java, plus the single-subscription rule.
  • Full swift test --disable-experimental-prebuilts --disable-sandbox and ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test pass.
  • swift format lint --strict on changed files; git diff --check.

Not in this PR

  • AsyncThrowingStream and other AsyncSequence types, and streams as parameters or properties.
  • The reverse direction: a Java implementation of a Swift protocol returning AsyncStream.
  • A stream that is returned but never subscribed to is not released.

🤖 Generated with Claude Code

piercifani and others added 3 commits November 3, 2026 17:35
Functions returning `AsyncStream<T>` were skipped: the type wasn't in the
modeled standard library.

- SwiftKitCore `SwiftAsyncStream<T>`: `subscribe` starts a Swift task that
  delivers each element and completion without blocking a Java thread; the
  returned `Subscription` cancels it. `map` gives a converted view. One
  subscriber, as for an AsyncStream; the task handle is released exactly once.
- SwiftJavaRuntimeSupport `_attachAsyncStream` stores the stream and an element
  converter behind a handle on the Java object.
- Generator: `AsyncStream` joins the stdlib model; a function returning
  `AsyncStream<T>` is translated as returning `T`, then Java passes a stream
  out-parameter and maps its elements with that conversion, and the Swift thunk
  attaches the stream. `async` functions returning streams and tuple elements
  are skipped with a reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new `swiftkit-kotlin` artifact with `SwiftAsyncStream<T>.asFlow()`, so
Kotlin callers collect Swift streams as a `Flow` without writing the
adapter themselves. It lives outside SwiftKitCore, which stays a plain
Java library with no kotlinx-coroutines dependency. Targets Java 11
like SwiftKitCore.

The Kotlin sample now uses it instead of its own copy.

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