Skip to main content

ri/java/
runtime.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of Ri.
4//! The Ri project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! you may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! # JNI Runtime Helpers
19//!
20//! A shared Tokio runtime for JNI entry points, plus panic-safe wrappers
21//! that convert Rust panics into Java exceptions instead of aborting the JVM.
22
23use std::future::Future;
24use std::panic::{catch_unwind, AssertUnwindSafe};
25use std::sync::OnceLock;
26use futures::executor::block_on;
27use jni::sys::jlong;
28use jni::JNIEnv;
29use tokio::runtime::Runtime;
30use crate::java::exception::throw_ri_error;
31
32static JNI_RUNTIME: OnceLock<Runtime> = OnceLock::new();
33
34/// Get (or lazily create) the process-wide JNI Tokio runtime.
35pub fn jni_runtime() -> &'static Runtime {
36    JNI_RUNTIME.get_or_init(|| {
37        tokio::runtime::Builder::new_multi_thread()
38            .worker_threads(2)
39            .enable_all()
40            .build()
41            .expect("Failed to build JNI Tokio runtime")
42    })
43}
44
45/// Convert a panic payload into a human-readable message.
46fn payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
47    if let Some(s) = payload.downcast_ref::<&str>() {
48        (*s).to_string()
49    } else if let Some(s) = payload.downcast_ref::<String>() {
50        s.clone()
51    } else {
52        "Ri JNI: unknown panic payload".to_string()
53    }
54}
55
56/// Block on a future from a JNI entry point, panic-safe.
57///
58/// - If a Tokio runtime is already active on this thread, `fut` is polled to
59///   completion inside that runtime (spawn + block_on the JoinHandle).
60/// - Otherwise the future runs on the process-wide JNI runtime.
61/// - Any panic inside `fut` is caught and re-thrown to Java as a
62///   `com.dunimd.ri.RiError` instead of aborting the JVM.
63///
64/// Returns `None` when a panic was caught (a Java exception is pending).
65pub fn block_on_jni<T: Send + 'static>(
66    env: &mut JNIEnv,
67    _context: &str,
68    fut: impl Future<Output = T> + Send + 'static,
69) -> Option<T> {
70    let result = catch_unwind(AssertUnwindSafe(|| {
71        match tokio::runtime::Handle::try_current() {
72            Ok(handle) => {
73                // Already inside a Tokio runtime thread: use the current one.
74                let join = handle.spawn(fut);
75                match block_on(join) {
76                    Ok(res) => res,
77                    Err(join_err) => {
78                        // The spawned task panicked; forward the panic.
79                        std::panic::resume_unwind(join_err.into_panic())
80                    }
81                }
82            }
83            Err(_) => jni_runtime().block_on(fut),
84        }
85    }));
86
87    match result {
88        Ok(value) => Some(value),
89        Err(payload) => {
90            throw_ri_error(env, &format!("Ri native panic: {}", payload_to_string(payload)));
91            None
92        }
93    }
94}
95
96/// Convert a Java TTL (jlong) into an optional TTL in seconds.
97/// Negative values mean "no TTL" per the Java binding contract.
98pub fn ttl_from_jlong(ttl_secs: jlong) -> Option<u64> {
99    if ttl_secs >= 0 {
100        Some(ttl_secs as u64)
101    } else {
102        None
103    }
104}
105
106/// Block on a future that borrows local (non-'static) data, panic-safe.
107///
108/// Unlike [`block_on_jni`], the future may borrow from the caller's stack;
109/// it runs on the shared JNI runtime via `Handle::block_on` semantics.
110/// Must NOT be called from within an existing Tokio runtime thread
111/// (that would deadlock); in that case it falls back to a fresh
112/// single-threaded runtime.
113pub fn block_on_local<T>(
114    env: &mut JNIEnv,
115    context: &str,
116    fut: impl Future<Output = T>,
117) -> Option<T> {
118    let result = catch_unwind(AssertUnwindSafe(|| {
119        if tokio::runtime::Handle::try_current().is_ok() {
120            // Calling Handle::block_on from inside a runtime would deadlock;
121            // run the future on a throwaway single-thread runtime instead.
122            tokio::runtime::Builder::new_current_thread()
123                .enable_all()
124                .build()
125                .expect("Failed to build fallback JNI runtime")
126                .block_on(fut)
127        } else {
128            jni_runtime().block_on(fut)
129        }
130    }));
131
132    match result {
133        Ok(value) => Some(value),
134        Err(payload) => {
135            throw_ri_error(env, &format!("Ri native panic ({context}): {}", payload_to_string(payload)));
136            None
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn jni_runtime_is_reusable() {
147        let rt = jni_runtime();
148        let rt2 = jni_runtime();
149        assert!(std::ptr::eq(rt, rt2));
150    }
151
152    #[test]
153    fn block_on_jni_runs_futures_off_thread() {
154        let rt = jni_runtime();
155        let val = rt.block_on(async { 41 + 1 });
156        assert_eq!(val, 42);
157    }
158
159    #[test]
160    fn ttl_from_jlong_maps_negative_to_none() {
161        assert_eq!(ttl_from_jlong(-1), None);
162        assert_eq!(ttl_from_jlong(0), Some(0));
163        assert_eq!(ttl_from_jlong(3600), Some(3600));
164    }
165}