Skip to main content

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}