Skip to content

Samples: Swift 6 JNI sample used from Kotlin, with main-actor view models (on swiftlang#938) - #11

Open
piercifani wants to merge 2 commits into
upstream/pr-938from
jni/01-main-actor-thunks
Open

piercifani wants to merge 2 commits into
upstream/pr-938from
jni/01-main-actor-thunks

Conversation

@piercifani

@piercifani piercifani commented Nov 3, 2026 •

Copy link
Copy Markdown

Stack 1/21 · base: upstream/pr-938 · next: #2 (jni/02-closures-jextracted-types)

This PR's base, upstream/pr-938, is the exact head commit (f6c879d5) of upstream swiftlang/swift-java#938 by Himeshi (teamhimeh on GitHub), unchanged and Apache-2.0. The rest of this stack builds on it. Like any upstream code it stays out of this fork's PR diffs, so this PR reviews only the Kotlin sample commits. When swiftlang#938 merges upstream, main gets synced, this PR is retargeted to main, and upstream/pr-938 is deleted.

This replaces #1, which carried the swiftlang#938 commit inside the PR. GitHub doesn't allow changing the base of the first PR in a stack, so it was reopened here.

What swiftlang#938 does (in the base branch)

JNI extraction kept synchronous Java signatures for @MainActor APIs but emitted nonisolated Swift thunks, so the generated bindings didn't compile when they touched isolated initializers, methods, properties or subscripts.

  • Synchronous JNI entry points are annotated @MainActor when the declaration, its enclosing type or its extension is explicitly main-actor isolated.
  • The thunk calls MainActor.preconditionIsolated() before converting arguments. Java callers must already be on the main actor's executor; there's no hop.
  • Explicitly nonisolated members and async downcalls are unchanged.

Why Skip needs it

In Skip Fuse apps every view model is a @MainActor class read from Compose on the UI thread. In a production Skip Fuse app (16 Skip modules), 4,180 bridged entrypoints are main-actor isolated, and all 190 view models write @MainActor explicitly, so swiftlang#938's explicit-attribute detection covers them.

Kotlin in this PR

This PR's first commit adds Samples/SwiftJavaExtractJNIKotlinSampleApp. Its Swift modules build in Swift 6 language mode, and its bindings are used from Kotlin: one test file per feature, kotlin.test plus kotlinx-coroutines for await() on the generated futures. It's in the CI sample matrices like the other samples. Each later PR in the stack adds its own Swift fixture and Kotlin test, so every PR shows the Kotlin it enables.

  • BaselineTest.kt: what works on main today. A struct, a throwing function, an async function awaited as a coroutine, an enum with a payload, and a type from the other module.
  • MainActorTest.kt with MainActorViewModel.swift: a @MainActor view model called synchronously from Kotlin. It runs on macOS only, where the JVM test thread can be the process's main thread (-XstartOnFirstThread), as Android's UI thread is; on Linux the JVM never runs tests on the process's main thread.

Extra validation done here

  • Kotlin sample: ./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes (the baseline tests also pass on main without this PR).
  • JNI: preserve MainActor isolation in synchronous thunks swiftlang/swift-java#938's 10 generator tests pass.
  • A standalone Swift 6 package with a @MainActor view model, called from Kotlin on the JVM, passes when the test JVM runs on the process's first thread (-XstartOnFirstThread on macOS, which matches Android's UI thread being the process main thread). Off the main thread the JVM stops with SIGTRAP from the precondition, as intended.

Note for Skip: today Skip's bridge uses assumeMainActorUnchecked, which lets off-main calls through silently. With this change they trap, which will surface latent threading bugs in apps.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 3, 2026 16:53
A jextract (JNI mode) sample whose Swift modules build in Swift 6 language
mode and whose bindings are exercised from Kotlin tests: one test file per
feature under src/test/kotlin. KotlinSampleModels is a separate module, so
types (and later protocols) cross module boundaries.

The baseline covers what works today: a struct, a throwing function, an async
function awaited as a coroutine, an enum with a payload, and a type from
another module. Added to the CI sample matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kotlin test for synchronous @mainactor bindings. It runs on macOS only, where the
JVM test thread can be the process's main thread (-XstartOnFirstThread), as
Android's UI thread is.

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