Skip to content

Commit 274431b

Browse files
NathanFlurryclaude
andcommitted
feat: US-052 - Add ExternalReferences and snapshot creation/restore functions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0b8fed8 commit 274431b

3 files changed

Lines changed: 167 additions & 0 deletions

File tree

crates/v8-runtime/src/bridge.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,31 @@
33
use std::cell::RefCell;
44
use std::collections::HashMap;
55
use std::ffi::c_void;
6+
use std::sync::OnceLock;
67

8+
use v8::MapFnTo;
79
use v8::ValueDeserializerHelper;
810
use v8::ValueSerializerHelper;
911

1012
use crate::host_call::BridgeCallContext;
1113

14+
/// External references for V8 snapshot serialization.
15+
/// Maps function pointer indices in the snapshot to current addresses.
16+
/// Must be identical at snapshot creation and restore time.
17+
pub fn external_refs() -> &'static v8::ExternalReferences {
18+
static REFS: OnceLock<v8::ExternalReferences> = OnceLock::new();
19+
REFS.get_or_init(|| {
20+
v8::ExternalReferences::new(&[
21+
v8::ExternalReference {
22+
function: sync_bridge_callback.map_fn_to(),
23+
},
24+
v8::ExternalReference {
25+
function: async_bridge_callback.map_fn_to(),
26+
},
27+
])
28+
})
29+
}
30+
1231
// Minimal delegate for V8 ValueSerializer — throws DataCloneError as a V8 exception
1332
struct DefaultSerializerDelegate;
1433

crates/v8-runtime/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod host_call;
99
mod timeout;
1010
mod stream;
1111
mod session;
12+
mod snapshot;
1213

1314
use std::collections::HashMap;
1415
use std::fs;

crates/v8-runtime/src/snapshot.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// V8 startup snapshots: fast isolate creation from pre-compiled bridge code
2+
3+
use crate::bridge::external_refs;
4+
use crate::isolate::init_v8_platform;
5+
6+
/// Maximum allowed snapshot blob size (50MB).
7+
/// Prevents resource exhaustion from degenerate bridge code.
8+
const MAX_SNAPSHOT_BLOB_BYTES: usize = 50 * 1024 * 1024;
9+
10+
/// Create a V8 startup snapshot with the given bridge code pre-compiled.
11+
///
12+
/// Consumes a temporary isolate. The returned StartupData contains the
13+
/// serialized V8 heap with compiled bytecode.
14+
///
15+
/// Returns an error if the bridge code fails to compile or the resulting
16+
/// snapshot exceeds MAX_SNAPSHOT_BLOB_BYTES.
17+
pub fn create_snapshot(bridge_code: &str) -> Result<v8::StartupData, String> {
18+
init_v8_platform();
19+
20+
let mut isolate = v8::Isolate::snapshot_creator(Some(external_refs()), None);
21+
{
22+
let scope = &mut v8::HandleScope::new(&mut isolate);
23+
let context = v8::Context::new(scope, Default::default());
24+
let scope = &mut v8::ContextScope::new(scope, context);
25+
26+
// Compile and run bridge code — bytecode is captured in snapshot
27+
let source = v8::String::new(scope, bridge_code)
28+
.ok_or_else(|| "failed to create V8 string for bridge code".to_string())?;
29+
let script = v8::Script::compile(scope, source, None)
30+
.ok_or_else(|| "bridge code compilation failed during snapshot creation".to_string())?;
31+
script.run(scope);
32+
33+
scope.set_default_context(context);
34+
}
35+
let blob = isolate
36+
.create_blob(v8::FunctionCodeHandling::Keep)
37+
.ok_or_else(|| "V8 snapshot creation failed".to_string())?;
38+
39+
// Reject oversized snapshots
40+
if blob.len() > MAX_SNAPSHOT_BLOB_BYTES {
41+
return Err(format!(
42+
"snapshot blob too large: {} bytes (max {})",
43+
blob.len(),
44+
MAX_SNAPSHOT_BLOB_BYTES
45+
));
46+
}
47+
48+
Ok(blob)
49+
}
50+
51+
/// Create a V8 isolate restored from a snapshot blob.
52+
///
53+
/// The external references must match those used during snapshot creation
54+
/// (provided by bridge::external_refs()).
55+
///
56+
/// `blob` must be owned or 'static data — `Vec<u8>`, `Box<[u8]>`, or
57+
/// `v8::StartupData` all work. The data is copied into the isolate during
58+
/// creation; V8 does not retain a reference after `Isolate::new()` returns.
59+
pub fn create_isolate_from_snapshot<B>(
60+
blob: B,
61+
heap_limit_mb: Option<u32>,
62+
) -> v8::OwnedIsolate
63+
where
64+
B: std::ops::Deref<Target = [u8]> + std::borrow::Borrow<[u8]> + 'static,
65+
{
66+
init_v8_platform();
67+
68+
let mut params = v8::CreateParams::default()
69+
.snapshot_blob(blob)
70+
.external_references(&**external_refs());
71+
if let Some(limit) = heap_limit_mb {
72+
let limit_bytes = (limit as usize) * 1024 * 1024;
73+
params = params.heap_limits(0, limit_bytes);
74+
}
75+
v8::Isolate::new(params)
76+
}
77+
78+
#[cfg(test)]
79+
mod tests {
80+
use super::*;
81+
82+
fn eval(isolate: &mut v8::OwnedIsolate, code: &str) -> String {
83+
let scope = &mut v8::HandleScope::new(isolate);
84+
let context = v8::Context::new(scope, Default::default());
85+
let scope = &mut v8::ContextScope::new(scope, context);
86+
let source = v8::String::new(scope, code).unwrap();
87+
let script = v8::Script::compile(scope, source, None).unwrap();
88+
let result = script.run(scope).unwrap();
89+
result.to_rust_string_lossy(scope)
90+
}
91+
92+
/// All snapshot tests consolidated into one #[test] to avoid inter-test
93+
/// SIGSEGV from V8 global state issues (same pattern as execution::tests).
94+
#[test]
95+
fn snapshot_consolidated_tests() {
96+
init_v8_platform();
97+
let _ = external_refs();
98+
99+
// --- Part 1: Snapshot creation returns non-empty blob ---
100+
{
101+
let bridge_code = "(function() { globalThis.__bridge_init = true; })();";
102+
let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
103+
assert!(blob.len() > 0, "snapshot blob should be non-empty");
104+
}
105+
106+
// --- Part 2: Restored isolate executes JS correctly ---
107+
{
108+
let bridge_code = "(function() { globalThis.__testValue = 42; })();";
109+
let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
110+
let mut isolate = create_isolate_from_snapshot(blob, None);
111+
// Fresh context on restored isolate — bridge globals are in snapshot's
112+
// default context, not in a new context. Verify isolate is functional.
113+
assert_eq!(eval(&mut isolate, "1 + 1"), "2");
114+
}
115+
116+
// --- Part 3: Restored isolate respects heap_limit_mb ---
117+
{
118+
let bridge_code = "/* empty bridge */";
119+
let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
120+
let mut isolate = create_isolate_from_snapshot(blob, Some(8));
121+
assert_eq!(eval(&mut isolate, "'heap ok'"), "heap ok");
122+
}
123+
124+
// --- Part 4: Normal blob is under 50MB limit ---
125+
{
126+
let bridge_code = "(function() { globalThis.x = 1; })();";
127+
let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
128+
assert!(
129+
blob.len() < MAX_SNAPSHOT_BLOB_BYTES,
130+
"normal bridge code should produce blob under 50MB limit"
131+
);
132+
}
133+
134+
// --- Part 5: Three sequential restores from same snapshot data ---
135+
{
136+
let bridge_code = "(function() { globalThis.__counter = 0; })();";
137+
let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
138+
let blob_bytes: Vec<u8> = blob.to_vec();
139+
140+
for i in 0..3 {
141+
let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
142+
let result = eval(&mut isolate, &format!("{} + 1", i));
143+
assert_eq!(result, format!("{}", i + 1));
144+
}
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)