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}