Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ jobs:
distribution: 'temurin'
java-version: '25'

- name: Set up Kotlin
run: python tests/kotlin/install_kotlin.py --github-path

- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/update-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ jobs:
distribution: temurin
java-version: '25'

- name: Set up Kotlin
if: steps.latest.outputs.changed == 'true'
run: python tests/kotlin/install_kotlin.py --github-path

- name: Cache Cargo dependencies
if: steps.latest.outputs.changed == 'true'
uses: Swatinem/rust-cache@v2
Expand Down
5 changes: 4 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ The following example programs live in `tests/`, are compiled with the standard
|---|---|
| **[Lambda Callbacks](tests/integration/lambda_callbacks/Main.java)** | Passing native Java lambdas directly into Rust functions expecting `Fn` closures. |
| **[Trait Implementors](tests/integration/trait_implementors/Main.java)** | Implementing a Rust trait on a Java class and passing it to Rust dynamic dispatch (`&dyn Trait`). |
| **[Kotlin Async](tests/kotlin/async_interop/Main.kt)** | Awaiting Rust `async` functions from Kotlin `suspend` code. |

## Features & Standard Library Support

Expand All @@ -355,7 +356,7 @@ Beyond `core` and `alloc`, a large amount of the `std` is supported too, though
| Subsystem | Status | Details |
|---|---|---|
| **Threads & Sync** | **Supported** | Thread spawning, scoped threads, Mutex, RwLock, Condvar, TLS |
| **Async & Futures** | **Supported** | Async functions, blocks, closures and trait methods; boxed/recursive `dyn Future`; cancellation |
| **Async & Futures** | **Supported** | Async functions, blocks, closures and trait methods; boxed/recursive `dyn Future`; Kotlin `suspend` interop; cancellation |
| **Panic Unwinding** | **Supported** | Complete unwinding stack, `catch_unwind`, panic hooks, and abort-on-double-panic semantics |
| **Stdio & Env** | **Supported** | `println!`, `eprintln!`, `stdin`, `env::args`, `env::vars` |
| **Time & Random** | **Supported** | `SystemTime`, `Instant`, standard entropy seeds |
Expand Down Expand Up @@ -497,6 +498,8 @@ boolean equal = Root.eq(root, new Leaf.A(42));
- **Git**
- **cargo-jvm** - see install instructions in [its README](cargo-jvm/README.md)

The Kotlin compiler is only needed for `tests/kotlin`; CI installs the pinned version with `tests/kotlin/install_kotlin.py`.

## Usage

[`cargo-jvm`](cargo-jvm/README.md) is used to make building and running Rust projects on the JVM as seamless as possible. It wraps the standard Cargo workflow, forwarding all ordinary Cargo selection and feature arguments. For instructions on installing `cargo-jvm`, see [its README](cargo-jvm/README.md).
Expand Down
79 changes: 79 additions & 0 deletions Tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import json
import os
import re
import shutil
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

from test_harness import (
ROOT,
TEST_TARGET_DIR,
TestCase,
build_test,
cargo_jobs,
Expand Down Expand Up @@ -197,6 +200,82 @@ def run_java(
logs.append(f"|---- ❌ Invalid Java process test input: {error}")
return False

if test.kind == "kotlin":
kotlin_files = sorted(test.directory.glob("*.kt"))
kotlin_files.extend(sorted((ROOT / "runtime" / "kotlin").glob("*.kt")))
if not kotlin_files:
logs.append("|---- ❌ No .kt files found for Kotlin interop test")
return False

executable_name = "kotlinc.bat" if os.name == "nt" else "kotlinc"
configured = os.environ.get("KOTLINC")
kotlinc = (
Path(configured)
if configured
else Path(shutil.which("kotlinc") or "")
)
if not kotlinc.is_file():
local = (
ROOT
/ "target"
/ "tools"
/ "kotlin-2.4.10"
/ "kotlinc"
/ "bin"
/ executable_name
)
kotlinc = local if local.is_file() else Path()
if not kotlinc.is_file():
logs.append(
"|---- ❌ kotlinc is required for tests/kotlin; install Kotlin or set KOTLINC"
)
return False

profile = "release" if release else "debug"
kotlin_jar = TEST_TARGET_DIR / "kotlin" / profile / f"{test.name}.jar"
kotlin_jar.parent.mkdir(parents=True, exist_ok=True)
command = [
str(kotlinc),
"-jvm-target",
"1.8",
"-classpath",
str(jar),
"-include-runtime",
"-d",
str(kotlin_jar),
*(str(path) for path in kotlin_files),
]
if os.name == "nt" and kotlinc.suffix.lower() == ".bat":
command = [os.environ.get("COMSPEC", "cmd.exe"), "/d", "/c", *command]

logs.append("|--- 🟣 Compiling Kotlin interop test source...")
proc = run_command(command)
if proc.returncode != 0:
write_failure(test.directory / "kotlinc-fail.generated", proc)
logs.append(f"|---- ❌ kotlinc exited with code {proc.returncode}")
ci_diagnostic(logs, f"STDOUT:\n{proc.stdout}\nSTDERR:\n{proc.stderr}")
return False

logs.append("|--- 🤖 Running Kotlin suspend entry point...")
classpath = os.pathsep.join([str(kotlin_jar), str(jar)])
proc, diagnostics = run_java_command(
["java", "-cp", classpath, "MainKt", *java_arguments],
timeout=java_timeout,
input_text=stdin,
)
if diagnostics is not None:
output = (
f"Kotlin process exceeded the {java_timeout:g}s timeout.\n\n"
f"{diagnostics}\n\nSTDOUT:\n{proc.stdout}\n\nSTDERR:\n{proc.stderr}"
)
(test.directory / "java-timeout.generated").write_text(
output, encoding="utf-8"
)
logs.append(f"|---- ❌ Kotlin process timed out after {java_timeout:g}s")
ci_diagnostic(logs, output)
return False
return check_results(proc, test, release, logs)

if test.kind != "integration":
logs.append("|--- 🤖 Running with Java...")
proc, diagnostics = run_java_command(
Expand Down
110 changes: 110 additions & 0 deletions runtime/kotlin/RustFuture.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.rustlang.runtime

import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

private sealed class PollSignal

private class Ready(val value: Any?) : PollSignal()

private object Repoll : PollSignal()

/**
* Arbitrates wakes which race with a poll. A wake observed during the poll is
* deferred until its result is known, so a future which wakes and returns
* Ready is never polled again after completion.
*/
private class PollGate(
private val continuation: Continuation<PollSignal>,
) : Runnable {
private var polling = true
private var wakeRequested = false
private var resolved = false

override fun run() {
val resume = synchronized(this) {
when {
resolved -> false
polling -> {
wakeRequested = true
false
}
else -> {
resolved = true
true
}
}
}
if (resume) {
continuation.resume(Repoll)
}
}

fun finish(result: Any?) {
val signal = synchronized(this) {
polling = false
when {
resolved -> null
result !== KotlinFutureInterop.PENDING -> {
resolved = true
Ready(result)
}
wakeRequested -> {
resolved = true
Repoll
}
else -> null
}
}
if (signal != null) {
continuation.resume(signal)
}
}

fun fail(failure: Throwable) {
val resume = synchronized(this) {
polling = false
if (resolved) {
false
} else {
resolved = true
true
}
}
if (resume) {
continuation.resumeWithException(failure)
}
}
}

private suspend fun RustFuture.pollAfterWake(): PollSignal =
suspendCoroutine { continuation ->
val gate = PollGate(continuation)
try {
gate.finish(poll(gate))
} catch (failure: Throwable) {
gate.fail(failure)
}
}

/**
* Awaits a Rust `async` value. Every repoll is dispatched through the current
* Kotlin coroutine context; no global Java executor is imposed by the bridge.
*/
@Suppress("UNCHECKED_CAST")
suspend fun <T> RustFuture.await(): T {
try {
while (true) {
when (val signal = pollAfterWake()) {
Repoll -> continue
is Ready -> return (
if (signal.value === KotlinFutureInterop.UNIT) Unit else signal.value
) as T
}
}
} finally {
KotlinFutureInterop.dropRustFuture(this)
}
}
Loading
Loading