prebindgen_jni_runtime/iface_method.rs
1//! Process-wide cached interface-method handles for generated upcalls.
2//!
3//! Return-site callbacks (output-expansion `build` lambdas, `fold` lambdas,
4//! `onError` handlers) arrive as fresh objects on every extern call — there
5//! is no long-lived creation site to hoist a method lookup to. But each of
6//! them implements a *generated* `fun interface` whose FQN and method
7//! descriptor are fixed at codegen time, and JNI permits resolving a method
8//! ID on a supertype (the interface class) and invoking it virtually on any
9//! implementing instance. So generated code declares one
10//! `static CACHED: CachedIfaceMethod` per call site; the first call resolves
11//! and pins the interface class, every later call is a single
12//! `CallObjectMethodA` — no descriptor parsing, no symbol-table lookup
13//! (jni-rs's safe `call_method` re-does both on every call).
14
15use std::sync::OnceLock;
16
17use jni::{
18 objects::{GlobalRef, JMethodID, JObject},
19 signature::ReturnType,
20 sys::jvalue,
21 JNIEnv,
22};
23
24/// A `(pinned interface class, method ID)` pair resolved once per process.
25/// Declare as `static`; the embedded [`OnceLock`] handles the one-time
26/// resolution race (both winners produce equivalent values).
27pub struct CachedIfaceMethod {
28 cell: OnceLock<Resolved>,
29}
30
31struct Resolved {
32 /// Pins the interface class so the method ID below stays valid.
33 _class: GlobalRef,
34 method: JMethodID,
35}
36
37impl CachedIfaceMethod {
38 pub const fn new() -> Self {
39 Self {
40 cell: OnceLock::new(),
41 }
42 }
43
44 fn resolve(
45 &self,
46 env: &mut JNIEnv,
47 class_fqn: &str,
48 method: &str,
49 descr: &str,
50 ) -> Result<&Resolved, String> {
51 if let Some(r) = self.cell.get() {
52 return Ok(r);
53 }
54 let class = env
55 .find_class(class_fqn)
56 .map_err(|e| format!("find callback interface {class_fqn}: {e}"))?;
57 let id = env
58 .get_method_id(&class, method, descr)
59 .map_err(|e| format!("resolve {class_fqn}.{method}{descr}: {e}"))?;
60 let class = env
61 .new_global_ref(&class)
62 .map_err(|e| format!("global-ref callback interface {class_fqn}: {e}"))?;
63 let _ = self.cell.set(Resolved {
64 _class: class,
65 method: id,
66 });
67 Ok(self.cell.get().expect("cell was just set"))
68 }
69
70 /// Invoke the interface method on `obj`, returning its `Object` result.
71 /// Resolves (and pins) the interface class on first use.
72 ///
73 /// SAFETY contract carried for the caller: `obj` must implement the
74 /// interface named by `class_fqn`, and `descr` must be the method's
75 /// exact JVM descriptor — both are generated from the same plan, so
76 /// they agree by construction.
77 pub fn call_object<'local>(
78 &self,
79 env: &mut JNIEnv<'local>,
80 class_fqn: &str,
81 method: &str,
82 descr: &str,
83 obj: &JObject,
84 args: &[jvalue],
85 ) -> Result<JObject<'local>, String> {
86 let r = self.resolve(env, class_fqn, method, descr)?;
87 // SAFETY: see the doc contract above; the GlobalRef pins the class.
88 unsafe { env.call_method_unchecked(obj, r.method, ReturnType::Object, args) }
89 .and_then(|v| v.l())
90 .map_err(|e| format!("invoke {class_fqn}.{method}: {e}"))
91 }
92}
93
94impl Default for CachedIfaceMethod {
95 fn default() -> Self {
96 Self::new()
97 }
98}