javm/lib.rs
1//! JAVM engine entrypoint: the javm-cap kernel personality on the nub
2//! substrate.
3//!
4//! [`Nub`] is THE handle callers (chain runtime, tests, benches, RPC,
5//! `jar-apply`) use to invoke JAVM. It wraps the generic
6//! [`nub::Nub`] substrate handle and layers the JAVM-typed surface on
7//! top: publish caller-built [`javm_cap::Cap`]s ([`Nub::put_cap`] /
8//! [`Nub::put_cap_with_hash`]) and invoke published `Cap::Instance`s
9//! by content hash ([`Nub::invoke_cached`]).
10//!
11//! Two backends, one surface:
12//!
13//! - [`Nub::local`] — the in-process PVM2 (RISC-V) interpreter
14//! ([`JavmLocal`], the JAVM [`nub::LocalKernel`]). Used for tests,
15//! deterministic replay, and any host that doesn't need real ring-0
16//! isolation.
17//! - [`Nub::hyperlight`] — the process-wide Hyperlight singleton
18//! running the `javm-guest-x86` guest blob (in-kernel JIT). This
19//! crate owns the blob (built by `build.rs`) and the singleton
20//! policy; the sandbox mechanics live in the generic substrate
21//! ([`nub::Nub::create_hyperlight`]).
22
23mod local;
24#[cfg(feature = "test-support")]
25mod test_support;
26
27use std::sync::Mutex;
28
29use anyhow::Result;
30
31pub use local::JavmLocal;
32#[cfg(feature = "heap-diag")]
33pub use nub::HeapStats;
34pub use nub::{
35 AbiCapHash, CapHash, InvocationResult, InvokeJob, InvokeJobId, InvokeRequest,
36 MAX_HYPERLIGHT_VCPUS, NubOptions, ObjHash, SCRATCHPAD_HEAD_LEN,
37};
38
39/// The JAVM kernel personality: javm-cap object semantics (rkyv-coded
40/// `Cap`s, SSZ content hashing) with [`JavmLocal`] as the in-process
41/// kernel.
42pub struct Javm;
43
44impl nub::Personality for Javm {
45 const NAME: &'static str = "javm";
46 type Local = local::JavmLocal;
47}
48
49/// Path to the cross-compiled Hyperlight guest blob. Set by
50/// `build.rs` via `nub_build::build`.
51const JAVM_GUEST_X86_BLOB_PATH: &str = env!("JAVM_GUEST_X86_BLOB");
52
53#[derive(Clone, Copy)]
54pub(crate) struct HyperlightBlob {
55 pub(crate) label: &'static str,
56 pub(crate) path: &'static str,
57}
58
59struct HyperlightSingleton {
60 blob: HyperlightBlob,
61 options: NubOptions,
62 nub: Nub,
63}
64
65static HYPERLIGHT_NUB: Mutex<Option<HyperlightSingleton>> = Mutex::new(None);
66
67/// Compatibility alias for tests/benches that name the returned
68/// Hyperlight singleton borrow. [`Nub`] is a cloneable handle;
69/// synchronization lives inside the handle.
70pub type HyperlightNubGuard = Nub;
71
72/// Uniform handle to the JAVM engine — a newtype over the generic
73/// [`nub::Nub`] substrate handle with the JAVM-typed publish surface.
74#[derive(Clone)]
75pub struct Nub {
76 inner: nub::Nub<Javm>,
77}
78
79impl Nub {
80 /// Construct a Nub backed by the in-process interpreter
81 /// ([`JavmLocal`]).
82 pub fn local() -> Self {
83 Self {
84 inner: nub::Nub::new_local(),
85 }
86 }
87
88 /// Borrow the process-wide Hyperlight-backed Nub loaded from the
89 /// `javm-guest-x86` production guest blob.
90 pub fn hyperlight() -> Result<HyperlightNubGuard> {
91 Self::hyperlight_with_options(NubOptions::default())
92 }
93
94 pub fn hyperlight_with_options(options: NubOptions) -> Result<HyperlightNubGuard> {
95 Self::hyperlight_with_blob(
96 HyperlightBlob {
97 label: "production",
98 path: JAVM_GUEST_X86_BLOB_PATH,
99 },
100 options,
101 )
102 }
103
104 pub(crate) fn hyperlight_with_blob(blob: HyperlightBlob, options: NubOptions) -> Result<Nub> {
105 let mut guard = HYPERLIGHT_NUB
106 .lock()
107 .map_err(|_| anyhow::anyhow!("Hyperlight Nub singleton mutex poisoned"))?;
108 match guard.as_ref() {
109 Some(existing) if existing.blob.path == blob.path && existing.options == options => {}
110 Some(existing) if existing.blob.path == blob.path => {
111 return Err(anyhow::anyhow!(
112 "Hyperlight Nub singleton already initialized with {} vCPU(s); \
113 cannot reconfigure it to {} vCPU(s)",
114 existing.options.vcpu_count,
115 options.vcpu_count,
116 ));
117 }
118 Some(existing) => {
119 return Err(anyhow::anyhow!(
120 "Hyperlight Nub singleton already initialized with {} guest ({:?}); \
121 cannot switch to {} guest ({:?})",
122 existing.blob.label,
123 existing.blob.path,
124 blob.label,
125 blob.path,
126 ));
127 }
128 None => {
129 let nub = Nub {
130 inner: nub::Nub::create_hyperlight(blob.path, options)?,
131 };
132 *guard = Some(HyperlightSingleton { blob, options, nub });
133 }
134 }
135 Ok(guard
136 .as_ref()
137 .expect("Hyperlight Nub singleton initialized")
138 .nub
139 .clone())
140 }
141
142 // --- Typed publish surface (caller-built `Cap`) ---
143
144 /// Put a caller-built [`javm_cap::Cap`] into the active cache.
145 /// Computes the cap's content hash and either clones the cap on
146 /// first put or bumps refcount on idempotent re-put. Returns the
147 /// cap's content hash.
148 pub fn put_cap(&self, cap: &javm_cap::Cap) -> Result<AbiCapHash> {
149 // Local: typed, encode-free.
150 if let Some(r) = self.inner.with_local(|l| l.put_cap(cap)) {
151 return r;
152 }
153 // Hyperlight: serialize once, ship opaque bytes. Encoding
154 // fails on unresolved `CapHashOrRef::Ref` handles.
155 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(cap)
156 .map_err(|e| anyhow::anyhow!("put_cap: rkyv encode (or Ref present): {e}"))?;
157 self.inner
158 .put_object(bytes.as_slice())
159 .map_err(|e| anyhow::anyhow!("put_cap: {e}"))
160 }
161
162 /// Pre-hashed variant. Caller computed `ssz::hash_tree_root(cap)`
163 /// at warmup and passes it explicitly; on the hot idempotent path
164 /// this lets both backends skip the SSZ merkleize entirely.
165 /// Debug-asserts the claimed hash matches the cap; release trusts
166 /// the caller.
167 ///
168 /// Hyperlight backend: short-circuits on a host-side set of blob
169 /// hashes this sandbox has already published — the typical bench /
170 /// replay workload re-publishes the same cap graph every iteration
171 /// and pays only one host-side `HashSet::contains`, with the rkyv
172 /// encode never running (see `nub::Nub::put_object_with_hash`).
173 pub fn put_cap_with_hash(&self, hash: AbiCapHash, cap: &javm_cap::Cap) -> Result<()> {
174 if let Some(r) = self.inner.with_local(|l| l.put_cap_with_hash(hash, cap)) {
175 return r;
176 }
177 // Lazy encode: never runs on the host-side published_blobs hit.
178 self.inner
179 .put_object_with_hash(hash, || {
180 rkyv::to_bytes::<rkyv::rancor::Error>(cap)
181 .map(|b| b.to_vec())
182 .map_err(|e| format!("rkyv encode (or Ref present): {e}"))
183 })
184 // Lead with the typed operation the caller invoked; the
185 // substrate layer's own "put_object_with_hash: ..." context
186 // stays underneath.
187 .map_err(|e| anyhow::anyhow!("put_cap_with_hash: {e}"))
188 }
189
190 // --- Invoke surface (forwards to the substrate handle) ---
191
192 /// Invoke a previously-published `Cap::Instance` by hash. V0 args
193 /// are 4 u64s laid into φ[7..=10] on top of the published
194 /// endpoint's `initial_regs` baseline.
195 pub fn invoke_cached(
196 &self,
197 root: AbiCapHash,
198 endpoint_idx: u8,
199 args: [u64; 4],
200 initial_gas: u64,
201 ) -> Result<InvocationResult> {
202 self.inner
203 .invoke_cached(root, endpoint_idx, args, initial_gas)
204 }
205
206 /// Submit an invocation and return an async job handle.
207 pub fn submit_invoke(&self, request: InvokeRequest) -> Result<InvokeJob> {
208 self.inner.submit_invoke(request)
209 }
210
211 /// Bench-only: clear the guest's JIT compile cache so the next
212 /// `invoke_cached` pays a full recompile. No-op on the Local
213 /// backend.
214 pub fn evict_jit_all(&self) -> Result<()> {
215 self.inner.evict_jit_all()
216 }
217
218 /// Current state root.
219 pub fn state_root(&self) -> CapHash {
220 self.inner.state_root()
221 }
222
223 /// Diagnostic: read the guest's talc allocation counters.
224 /// Hyperlight backend only. Requires the `heap-diag` feature.
225 #[cfg(feature = "heap-diag")]
226 pub fn heap_stats(&self) -> Result<HeapStats> {
227 self.inner.heap_stats()
228 }
229}