Skip to main content

nodejs/stdlib/
v8.rs

1//! Node `v8` module — a compatibility shim, NOT real V8 introspection.
2//!
3//! node-js has no V8: JS is lowered to `fusevm` bytecode and runs on a Rust heap
4//! (see `host.rs`). There is therefore no V8 heap to measure and no V8 binary
5//! serialization format to emit. This module exists so code that *calls* the `v8`
6//! surface (metrics collectors, `v8.serialize`-based caches) keeps working, with
7//! every deviation documented rather than faked:
8//!
9//!   - `getHeapStatistics` / `getHeapSpaceStatistics` / `getHeapCodeStatistics`
10//!     return the correct *shape* (all the keys Node produces) with **zeroed**
11//!     values. They are not read from any live allocator — reporting a fabricated
12//!     non-zero heap size would be a lie, so the honest answer is 0.
13//!   - `serialize` / `deserialize` round-trip through **JSON**, not V8's
14//!     structured-clone binary format. The returned Buffer is UTF-8 JSON bytes and
15//!     is byte-incompatible with Node's `v8.serialize`; it cannot carry cyclic
16//!     graphs, `Map`/`Set`, typed arrays, `BigInt`, or `undefined` the way the
17//!     real format does. Use it only for plain JSON-representable values.
18//!   - `setFlagsFromString` is a no-op (there are no V8 flags to set).
19//!   - `getHeapSnapshot` throws: node-js cannot produce a V8 heap snapshot.
20
21use crate::host::{with_host, JsObj};
22use fusevm::Value;
23use indexmap::IndexMap;
24
25pub const METHODS: &[&str] = &[
26    "getHeapStatistics",
27    "getHeapSpaceStatistics",
28    "getHeapCodeStatistics",
29    "serialize",
30    "deserialize",
31    "setFlagsFromString",
32    "getHeapSnapshot",
33    "cachedDataVersionTag",
34];
35
36/// A FIXED compatibility tag returned by `v8.cachedDataVersionTag()`. Node derives
37/// this from the V8 version + build flags; node-js has no V8, so a single stable
38/// constant is returned (callers use it only to invalidate a code cache when the
39/// runtime changes — a constant is honest for a runtime that never emits V8 code
40/// cache data in the first place).
41const CACHED_DATA_VERSION_TAG: f64 = 3_527_742_766.0;
42
43/// Methods dispatched on an `@@native = "Serializer"` object (JSON-backed shim;
44/// reported to the parent for `instance_has_method` / `instance_call` wiring).
45pub const SERIALIZER_METHODS: &[&str] = &["writeHeader", "writeValue", "releaseBuffer"];
46
47/// Methods dispatched on an `@@native = "Deserializer"` object.
48pub const DESERIALIZER_METHODS: &[&str] = &["readHeader", "readValue"];
49
50pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
51    Some(match method {
52        "getHeapStatistics" => Ok(heap_statistics()),
53        // No V8 heap spaces to enumerate.
54        "getHeapSpaceStatistics" => Ok(with_host(|h| h.new_array(Vec::new()))),
55        "getHeapCodeStatistics" => Ok(heap_code_statistics()),
56        "serialize" => serialize(args),
57        "deserialize" => deserialize(args),
58        // Nothing to configure; accepted silently for compatibility.
59        "setFlagsFromString" => Ok(Value::Undef),
60        "getHeapSnapshot" => Err(crate::host::type_error(
61            "v8.getHeapSnapshot is not supported: node-js does not run on V8",
62        )),
63        "cachedDataVersionTag" => Ok(Value::Float(CACHED_DATA_VERSION_TAG)),
64        _ => return None,
65    })
66}
67
68/// Non-function members of the `v8` namespace, exposed as constructor values so
69/// `require('v8').Serializer` (etc.) resolve and `new` reaches `construct`.
70/// Requires the parent to route `"v8"` into `stdlib::constant`.
71pub fn constant(name: &str) -> Option<Value> {
72    match name {
73        "Serializer" | "Deserializer" | "DefaultSerializer" | "DefaultDeserializer" => {
74            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
75        }
76        _ => None,
77    }
78}
79
80/// `new v8.Serializer()` / `new v8.Deserializer(buffer)` (and the `Default*`
81/// aliases). node-js has no V8 structured-clone binary format, so these are
82/// JSON-backed shims: a `Serializer` accumulates ONE `writeValue`d value as JSON
83/// and hands it back from `releaseBuffer` as a UTF-8 Buffer; a `Deserializer`
84/// parses that JSON back with `readValue`. The granular byte writers/readers
85/// (`writeUint32`/`writeDouble`/`writeRawBytes`/…) are NOT modeled — they only
86/// make sense against the real binary layout (see the report's deferred list).
87pub fn construct(name: &str, args: &[Value]) -> Result<Value, String> {
88    match name {
89        "Serializer" | "DefaultSerializer" => Ok(with_host(|h| {
90            let mut m = IndexMap::new();
91            m.insert("@@native".into(), h.new_str("Serializer"));
92            m.insert("@@json".into(), Value::Undef);
93            h.new_object(m)
94        })),
95        "Deserializer" | "DefaultDeserializer" => {
96            // Decode the incoming Buffer's bytes as UTF-8 JSON text.
97            let json = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
98            Ok(with_host(|h| {
99                let jv = h.new_str(json);
100                let mut m = IndexMap::new();
101                m.insert("@@native".into(), h.new_str("Deserializer"));
102                m.insert("@@json".into(), jv);
103                h.new_object(m)
104            }))
105        }
106        _ => Err(crate::host::type_error(&format!(
107            "v8.{name} is not a constructor"
108        ))),
109    }
110}
111
112/// Dispatch a method on a `Serializer`/`Deserializer` instance.
113pub fn instance_call(
114    tag: &str,
115    recv: &Value,
116    method: &str,
117    args: Vec<Value>,
118) -> Result<Value, String> {
119    match (tag, method) {
120        // Serializer: `writeHeader` is a no-op (no binary header to emit).
121        ("Serializer", "writeHeader") => Ok(Value::Undef),
122        ("Serializer", "writeValue") => {
123            let json = crate::builtins::call_builtin_function(
124                "JSON.stringify",
125                vec![args.first().cloned().unwrap_or(Value::Undef)],
126            )?;
127            let s = with_host(|h| h.str_of(&json));
128            with_host(|h| {
129                let sv = h.new_str(s);
130                if let Some(JsObj::Object(p)) = h.get_mut(recv) {
131                    p.insert("@@json".into(), sv);
132                }
133            });
134            Ok(Value::Bool(true))
135        }
136        ("Serializer", "releaseBuffer") => {
137            let s = with_host(|h| match h.get(recv) {
138                Some(JsObj::Object(p)) => match p.get("@@json") {
139                    Some(Value::Undef) | None => String::new(),
140                    Some(v) => h.str_of(v),
141                },
142                _ => String::new(),
143            });
144            Ok(super::buffer::from_bytes(s.as_bytes()))
145        }
146        // Deserializer: `readHeader` is a no-op; `readValue` parses the JSON back.
147        ("Deserializer", "readHeader") => Ok(Value::Undef),
148        ("Deserializer", "readValue") => {
149            let sv = with_host(|h| match h.get(recv) {
150                Some(JsObj::Object(p)) => p.get("@@json").cloned().unwrap_or(Value::Undef),
151                _ => Value::Undef,
152            });
153            crate::builtins::call_builtin_function("JSON.parse", vec![sv])
154        }
155        _ => Err(crate::host::type_error(&format!(
156            "{method} is not a function"
157        ))),
158    }
159}
160
161/// `v8.getHeapStatistics()` — the full key set Node returns, all zeroed. These are
162/// NOT measured from a live V8 heap (node-js has none); see the module docs.
163fn heap_statistics() -> Value {
164    zeros_object(&[
165        "total_heap_size",
166        "total_heap_size_executable",
167        "total_physical_size",
168        "total_available_size",
169        "used_heap_size",
170        "heap_size_limit",
171        "malloced_memory",
172        "peak_malloced_memory",
173        "does_zap_garbage",
174        "number_of_native_contexts",
175        "number_of_detached_contexts",
176        "total_global_handles_size",
177        "used_global_handles_size",
178        "external_memory",
179    ])
180}
181
182/// `v8.getHeapCodeStatistics()` — shape-correct, zeroed (no V8 code space here).
183fn heap_code_statistics() -> Value {
184    zeros_object(&[
185        "code_and_metadata_size",
186        "bytecode_and_metadata_size",
187        "external_script_source_size",
188        "cpu_profiler_metadata_size",
189    ])
190}
191
192/// Build an object mapping each key to `0`.
193fn zeros_object(keys: &[&str]) -> Value {
194    with_host(|h| {
195        let mut m = IndexMap::new();
196        for k in keys {
197            m.insert((*k).to_string(), Value::Float(0.0));
198        }
199        h.new_object(m)
200    })
201}
202
203/// `v8.serialize(value)` — JSON round-trip into a Buffer (NOT the V8 binary
204/// structured-clone format; see module docs).
205fn serialize(args: &[Value]) -> Result<Value, String> {
206    let v = args.first().cloned().unwrap_or(Value::Undef);
207    let json = crate::builtins::call_builtin_function("JSON.stringify", vec![v])?;
208    let s = with_host(|h| h.str_of(&json));
209    let sval = with_host(|h| h.new_str(s));
210    // Encode the JSON text as a UTF-8 Buffer.
211    super::buffer::static_call("from", std::slice::from_ref(&sval)).unwrap_or(Ok(Value::Undef))
212}
213
214/// `v8.deserialize(buffer)` — parse the Buffer's UTF-8 JSON back into a value
215/// (the inverse of this module's `serialize`, not Node's).
216fn deserialize(args: &[Value]) -> Result<Value, String> {
217    // `str_of` on a native Buffer decodes its bytes as UTF-8 (see host.rs).
218    let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
219    let sval = with_host(|h| h.new_str(s));
220    crate::builtins::call_builtin_function("JSON.parse", vec![sval])
221}