|
1 | | -use std::sync::LazyLock; |
| 1 | +use std::{ |
| 2 | + cell::Cell, |
| 3 | + ffi::c_void, |
| 4 | + panic::{AssertUnwindSafe, catch_unwind, resume_unwind}, |
| 5 | + ptr, |
| 6 | + sync::LazyLock, |
| 7 | +}; |
2 | 8 |
|
3 | 9 | use jni::EnvUnowned; |
4 | | -use jni::errors::ThrowRuntimeExAndDefault; |
| 10 | +use jni::errors::{ThrowRuntimeExAndDefault, jni_error_code_to_result}; |
5 | 11 | use jni::objects::{Global, JByteArray, JByteBuffer, JClass, JObject}; |
6 | 12 | use jni::sys::{jbyteArray, jint}; |
7 | 13 |
|
@@ -31,6 +37,75 @@ const MAX_RUNTIME_WORKERS: usize = 1024; |
31 | 37 |
|
32 | 38 | static RUNTIME_WORKER_THREADS: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new(); |
33 | 39 |
|
| 40 | +thread_local! { |
| 41 | + static ASYNC_DAEMON_ENV: Cell<*mut jni::sys::JNIEnv> = const { Cell::new(ptr::null_mut()) }; |
| 42 | +} |
| 43 | + |
| 44 | +fn attach_async_daemon_thread(jvm: &jni::JavaVM) -> jni::errors::Result<*mut jni::sys::JNIEnv> { |
| 45 | + let raw_vm = jvm.get_raw(); |
| 46 | + let mut env_ptr = ptr::null_mut::<c_void>(); |
| 47 | + let mut args = jni::sys::JavaVMAttachArgs { |
| 48 | + version: jni::JNIVersion::V1_4.into(), |
| 49 | + name: ptr::null_mut(), |
| 50 | + group: ptr::null_mut(), |
| 51 | + }; |
| 52 | + |
| 53 | + // SAFETY: `raw_vm` comes from `Env::get_java_vm()` and is therefore a valid |
| 54 | + // JavaVM pointer for this process. JNI 1.4 provides |
| 55 | + // `AttachCurrentThreadAsDaemon`; the returned `JNIEnv` is valid only on the |
| 56 | + // current OS thread and is cached in thread-local storage below. |
| 57 | + let res = unsafe { |
| 58 | + ((*(*raw_vm)).v1_4.AttachCurrentThreadAsDaemon)( |
| 59 | + raw_vm, |
| 60 | + &raw mut env_ptr, |
| 61 | + (&raw mut args).cast::<c_void>(), |
| 62 | + ) |
| 63 | + }; |
| 64 | + jni_error_code_to_result(res)?; |
| 65 | + if env_ptr.is_null() { |
| 66 | + return Err(jni::errors::Error::NullPtr("AttachCurrentThreadAsDaemon")); |
| 67 | + } |
| 68 | + |
| 69 | + Ok(env_ptr.cast()) |
| 70 | +} |
| 71 | + |
| 72 | +fn with_async_daemon_env<F, T, E>(jvm: &jni::JavaVM, callback: F) -> std::result::Result<T, E> |
| 73 | +where |
| 74 | + F: FnOnce(&mut jni::Env<'_>) -> std::result::Result<T, E>, |
| 75 | + E: From<jni::errors::Error>, |
| 76 | +{ |
| 77 | + ASYNC_DAEMON_ENV.with(|env_cell| { |
| 78 | + let mut env_ptr = env_cell.get(); |
| 79 | + if env_ptr.is_null() { |
| 80 | + env_ptr = attach_async_daemon_thread(jvm)?; |
| 81 | + env_cell.set(env_ptr); |
| 82 | + } |
| 83 | + |
| 84 | + // SAFETY: the pointer was produced for this exact Tokio worker thread |
| 85 | + // by `AttachCurrentThreadAsDaemon` and is never shared across threads |
| 86 | + // (TLS confines it). Tokio workers for the static runtime live until |
| 87 | + // process teardown, and daemon attachment means they do not keep the JVM |
| 88 | + // alive during shutdown. The per-call local frame prevents local-ref |
| 89 | + // accumulation on the permanently attached daemon thread. The explicit |
| 90 | + // post-call exception cleanup below replaces jni-rs scoped-detach |
| 91 | + // cleanup, which daemon attachments intentionally do not run. |
| 92 | + let mut guard = unsafe { jni::AttachGuard::from_unowned(env_ptr) }; |
| 93 | + let env = guard.borrow_env_mut(); |
| 94 | + let result = catch_unwind(AssertUnwindSafe(|| { |
| 95 | + env.with_local_frame(jni::DEFAULT_LOCAL_FRAME_CAPACITY, callback) |
| 96 | + })); |
| 97 | + |
| 98 | + if env.exception_check() { |
| 99 | + env.exception_clear(); |
| 100 | + } |
| 101 | + |
| 102 | + match result { |
| 103 | + Ok(callback_result) => callback_result, |
| 104 | + Err(payload) => resume_unwind(payload), |
| 105 | + } |
| 106 | + }) |
| 107 | +} |
| 108 | + |
34 | 109 | /// Worker thread count for the shared [`RUNTIME`], resolved once |
35 | 110 | /// (first hit wins, then fixed for the process lifetime): |
36 | 111 | /// |
@@ -391,10 +466,10 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchAsy |
391 | 466 | .await |
392 | 467 | .unwrap_or_else(|_| vespera_inprocess::error_wire(500, "panic in Rust engine")); |
393 | 468 |
|
394 | | - // Re-attach to JVM on this worker thread; subsequent |
395 | | - // dispatches on the same thread will hit the TLS fast |
396 | | - // path (cheap). |
397 | | - let _ = jvm.attach_current_thread(|env| -> jni::errors::Result<()> { |
| 469 | + // Complete on a cached daemon attachment for this Tokio |
| 470 | + // worker. This avoids attach/detach churn without making |
| 471 | + // runtime workers block JVM shutdown. |
| 472 | + let _ = with_async_daemon_env(&jvm, |env| -> jni::errors::Result<()> { |
398 | 473 | complete_future(env, &future_global, &response) |
399 | 474 | }); |
400 | 475 | }); |
|
0 commit comments