Skip to main content

quickjs_oxide/engine/api/core/
runtime.rs

1//! The API's runtime: one `Rc<RefCell<…>>` at the
2//! boundary around `oxide_core::Runtime`. Every method borrows the cell for
3//! one engine entry; a host function or callback that calls back in through
4//! a `Context` or `Runtime` instead of its scope gets
5//! [`RuntimeError::Reentered`].
6
7use std::cell::{Cell, RefCell, RefMut};
8use std::collections::HashMap;
9use std::rc::Rc;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12
13use oxide_core::Handle;
14use oxide_core::host::Host;
15use oxide_core::vm::module::ModuleTables;
16use oxide_core::{MemoryReport, NativeCx, RuntimeOptions};
17
18use super::RuntimeError;
19use super::context::Context;
20use super::module::ModuleLoader;
21use super::ops::View;
22use super::property::{
23    CompleteOrdinaryPropertyDescriptor, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol,
24};
25use super::value::{CallableRef, ObjectRef, Slot, SymbolRef, Value};
26use crate::engine::code::debug::DebugInfoMode;
27use crate::engine::value::JsString;
28
29/// Interrupts a runtime's running script from any thread: the script stops
30/// at its next poll (about every thousand loop back-edges and calls)
31/// with [`RuntimeError::Interrupted`], uncatchable by the script. The flag
32/// stays set until [`InterruptHandle::clear`], so every script after it is
33/// interrupted too.
34#[derive(Clone, Debug)]
35pub struct InterruptHandle {
36    flag: Arc<AtomicBool>,
37}
38
39impl InterruptHandle {
40    pub fn interrupt(&self) {
41        self.flag.store(true, Ordering::Relaxed);
42    }
43
44    pub fn clear(&self) {
45        self.flag.store(false, Ordering::Relaxed);
46    }
47
48    pub fn is_set(&self) -> bool {
49        self.flag.load(Ordering::Relaxed)
50    }
51}
52
53/// A runtime: one heap, one realm, one job queue. Cloning shares it.
54#[derive(Clone)]
55pub struct Runtime {
56    pub(crate) shared: Rc<Shared>,
57}
58
59/// A module compiled by `Context::compile_module*` or defined by
60/// `Context::define_module`, waiting for the core to load it by name.
61pub(crate) enum Preloaded {
62    Compiled {
63        function: ObjectRef,
64        tables: ModuleTables,
65        import_meta: Vec<(String, Value)>,
66    },
67    Synthetic(Vec<(String, Value)>),
68}
69
70pub(crate) struct Shared {
71    pub(crate) inner: RefCell<Inner>,
72    /// Handles whose last reference dropped while the runtime was borrowed;
73    /// released by the next entry or when the host function running ends.
74    released: RefCell<Vec<Handle>>,
75    /// A host function panicked: every later entry fails.
76    pub(crate) poisoned: Cell<bool>,
77    /// The core's interrupt flag, which `InterruptHandle`s share.
78    interrupt: Arc<AtomicBool>,
79    pub(crate) loader: RefCell<Option<Box<dyn ModuleLoader>>>,
80    pub(crate) modules: RefCell<HashMap<String, Preloaded>>,
81    pub(crate) debug_info: Cell<DebugInfoMode>,
82    /// Execution turns in progress (`with_execution_turn`).
83    turns: Cell<u32>,
84}
85
86impl Shared {
87    /// Releases `handle` now, or at the next entry when the runtime is
88    /// borrowed (a value dropped inside a host callback). Never fails.
89    pub(crate) fn release(&self, handle: Handle) {
90        match self.inner.try_borrow_mut() {
91            Ok(mut inner) => inner.core.release(handle),
92            Err(_) => self.released.borrow_mut().push(handle),
93        }
94    }
95
96    /// Releases the handles dropped while the runtime was borrowed.
97    pub(crate) fn drain_released(&self, core: &mut NativeCx<'_>) {
98        let released = std::mem::take(&mut *self.released.borrow_mut());
99        for handle in released {
100            core.release(handle);
101        }
102    }
103}
104
105pub(crate) struct Inner {
106    pub(crate) core: oxide_core::Runtime,
107}
108
109/// Host stack an outermost entry keeps below the loop's re-entry budget for
110/// the frames that build and unwind a stack overflow error.
111const NATIVE_RESERVE: usize = if cfg!(debug_assertions) {
112    256 * 1024
113} else {
114    64 * 1024
115};
116
117thread_local! {
118    /// The runtimes with an entry in progress on this thread, innermost
119    /// last: how the core's module and compile callbacks find the API.
120    static CURRENT: RefCell<Vec<Rc<Shared>>> = const { RefCell::new(Vec::new()) };
121}
122
123/// The runtime of the innermost entry on this thread.
124pub(crate) fn current() -> Option<Rc<Shared>> {
125    CURRENT.with(|current| current.borrow().last().cloned())
126}
127
128pub(crate) fn current_debug_info() -> Option<DebugInfoMode> {
129    current().map(|shared| shared.debug_info.get())
130}
131
132/// One engine entry: the borrowed runtime.
133pub(crate) struct Entry<'a> {
134    pub(crate) inner: RefMut<'a, Inner>,
135    pub(crate) shared: &'a Rc<Shared>,
136}
137
138impl Drop for Entry<'_> {
139    fn drop(&mut self) {
140        CURRENT.with(|current| current.borrow_mut().pop());
141    }
142}
143
144impl Entry<'_> {
145    /// The operations' view of the entered runtime.
146    pub(crate) fn view(&mut self) -> View<'_> {
147        View::new(self.inner.core.native_cx(), self.shared)
148    }
149}
150
151impl std::ops::Deref for Entry<'_> {
152    type Target = Inner;
153
154    fn deref(&self) -> &Inner {
155        &self.inner
156    }
157}
158
159impl std::ops::DerefMut for Entry<'_> {
160    fn deref_mut(&mut self) -> &mut Inner {
161        &mut self.inner
162    }
163}
164
165/// Makes a handle-owning reference from a held handle.
166pub(crate) fn slot(shared: &Rc<Shared>, handle: Handle, value: oxide_core::Value) -> Rc<Slot> {
167    Rc::new(Slot {
168        shared: Rc::downgrade(shared),
169        handle,
170        value,
171    })
172}
173
174/// What `run_pending_job` did.
175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
176pub enum PendingJobOutcome {
177    /// The queue was empty.
178    NoJob,
179    /// One job ran.
180    Executed,
181}
182
183impl PendingJobOutcome {
184    pub const fn executed(self) -> bool {
185        matches!(self, Self::Executed)
186    }
187}
188
189/// A promise's state.
190#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
191pub enum PromiseState {
192    Pending,
193    Fulfilled,
194    Rejected,
195}
196
197/// A promise's state and result, read without running JavaScript.
198#[derive(Clone, Debug, PartialEq)]
199pub struct PromiseSnapshot {
200    state: PromiseState,
201    result: Value,
202}
203
204impl PromiseSnapshot {
205    pub(crate) fn new(state: PromiseState, result: Value) -> Self {
206        PromiseSnapshot { state, result }
207    }
208
209    pub fn try_clone(&self) -> Result<Self, RuntimeError> {
210        Ok(self.clone())
211    }
212
213    pub const fn state(&self) -> PromiseState {
214        self.state
215    }
216
217    /// The fulfillment value or rejection reason; `undefined` while pending.
218    pub const fn result(&self) -> &Value {
219        &self.result
220    }
221}
222
223/// When the collector runs.
224#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
225pub enum GcPolicy {
226    /// When the heap has grown by the growth factor since the last
227    /// collection (and at the cap).
228    #[default]
229    Automatic,
230    /// Only at the cap and on `Runtime::run_gc`.
231    Manual,
232}
233
234/// The collector's statistics.
235pub use oxide_core::heap::gc::GcStats;
236
237/// A default host over the process: its streams, the system clock, UTC.
238pub use oxide_core::host::SystemHost;
239
240impl Default for Runtime {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246impl Runtime {
247    /// A runtime with the default host and options.
248    pub fn new() -> Self {
249        Self::with_options(RuntimeOptions::default())
250    }
251
252    pub fn with_options(options: RuntimeOptions) -> Self {
253        Self::with_host(SystemHost, options)
254    }
255
256    /// A runtime over `host` (the clock, the time zone, output, files).
257    pub fn with_host(host: impl Host + 'static, options: RuntimeOptions) -> Self {
258        let mut core = oxide_core::Runtime::new(options)
259            .expect("the initial realm fits any heap cap the options allow");
260        core.set_host(Box::new(host));
261        core.set_compiler(super::compile::compile_for_core);
262        core.set_module_host(oxide_core::vm::module::ModuleHost {
263            normalize: super::module::normalize_shim,
264            load: super::module::load_shim,
265        });
266        let interrupt = core.native_cx().interrupt_flag();
267        Runtime {
268            shared: Rc::new(Shared {
269                interrupt,
270                inner: RefCell::new(Inner { core }),
271                released: RefCell::new(Vec::new()),
272                poisoned: Cell::new(false),
273                loader: RefCell::new(None),
274                modules: RefCell::new(HashMap::new()),
275                debug_info: Cell::new(DebugInfoMode::Full),
276                turns: Cell::new(0),
277            }),
278        }
279    }
280
281    /// [`Runtime::with_host`] with the default options (the current API's
282    /// name).
283    pub fn new_with_host_services(host: impl Host + 'static) -> Self {
284        Self::with_host(host, RuntimeOptions::default())
285    }
286
287    /// Starts one engine entry.
288    pub(crate) fn enter(&self) -> Result<Entry<'_>, RuntimeError> {
289        if self.shared.poisoned.get() {
290            return Err(RuntimeError::Poisoned);
291        }
292        let mut inner = self
293            .shared
294            .inner
295            .try_borrow_mut()
296            .map_err(|_| RuntimeError::Reentered)?;
297        self.shared.drain_released(&mut inner.core.native_cx());
298        if CURRENT.with(|current| current.borrow().is_empty()) {
299            inner.core.native_cx().clear_interrupted();
300            // The loop's re-entries may use the thread's stack past what this
301            // entry needs to unwind an error: QuickJS's one-MiB budget, which
302            // its frames reach at the depths its nesting tests pin, takes
303            // larger debug frames here.
304            // An undeclared thread keeps the default as a ceiling.
305            let (available, declared) = crate::engine::compiler::stack_guard::available_stack();
306            let budget = available
307                .saturating_sub(available / 8)
308                .saturating_sub(NATIVE_RESERVE)
309                .max(available / 2);
310            let budget = if declared {
311                budget
312            } else {
313                budget.min(oxide_core::vm::NATIVE_STACK_BUDGET)
314            };
315            inner.core.set_native_stack_budget(budget);
316        }
317        CURRENT.with(|current| current.borrow_mut().push(Rc::clone(&self.shared)));
318        Ok(Entry {
319            inner,
320            shared: &self.shared,
321        })
322    }
323
324    /// The runtime's one realm.
325    pub fn context(&self) -> Context {
326        Context {
327            runtime: self.clone(),
328        }
329    }
330
331    /// The runtime's one realm (the current API's name: a second call
332    /// returns the same realm, never a new one).
333    pub fn new_context(&self) -> Result<Context, RuntimeError> {
334        Ok(self.context())
335    }
336
337    /// Whether `self` and `other` are the same runtime.
338    pub fn is_same_runtime(&self, other: &Self) -> bool {
339        Rc::ptr_eq(&self.shared, &other.shared)
340    }
341
342    /// The handle another thread interrupts this runtime's scripts with.
343    pub fn interrupt_handle(&self) -> InterruptHandle {
344        InterruptHandle {
345            flag: Arc::clone(&self.shared.interrupt),
346        }
347    }
348
349    /// Whether `Atomics.wait` may block.
350    pub fn set_can_block(&self, can_block: bool) -> Result<(), RuntimeError> {
351        self.enter()?.core.set_can_block(can_block);
352        Ok(())
353    }
354
355    /// The debug information functions compiled from now on keep.
356    pub fn set_debug_info_mode(&self, mode: DebugInfoMode) -> Result<(), RuntimeError> {
357        self.shared.debug_info.set(mode);
358        Ok(())
359    }
360
361    pub fn debug_info_mode(&self) -> Result<DebugInfoMode, RuntimeError> {
362        Ok(self.shared.debug_info.get())
363    }
364
365    /// Installs the module loader; `import` and `import()` go through it.
366    pub fn set_module_loader(
367        &self,
368        loader: impl ModuleLoader + 'static,
369    ) -> Result<(), RuntimeError> {
370        let mut slot = self
371            .shared
372            .loader
373            .try_borrow_mut()
374            .map_err(|_| RuntimeError::Reentered)?;
375        *slot = Some(Box::new(loader));
376        Ok(())
377    }
378
379    // ---- jobs ----
380
381    /// Whether a job is pending.
382    pub fn is_job_pending(&self) -> Result<bool, RuntimeError> {
383        Ok(self.enter()?.core.has_pending_jobs())
384    }
385
386    /// Runs the oldest pending job. A job that throws returns
387    /// `RuntimeError::Exception` with the thrown value; the queue keeps the
388    /// jobs after it. Rejected inside an execution turn,
389    /// leaving the job queued.
390    pub fn run_pending_job(&self) -> Result<PendingJobOutcome, RuntimeError> {
391        if self.shared.turns.get() > 0 {
392            return Err(RuntimeError::Invariant(
393                "a job was dispatched inside an execution turn",
394            ));
395        }
396        let mut entry = self.enter()?;
397        let result = entry.inner.core.run_pending_job();
398        match entry.view().complete_with(result)? {
399            true => Ok(PendingJobOutcome::Executed),
400            false => Ok(PendingJobOutcome::NoJob),
401        }
402    }
403
404    /// [`Runtime::run_pending_job`] (the current API's name).
405    pub fn execute_pending_job(&self) -> Result<PendingJobOutcome, RuntimeError> {
406        self.run_pending_job()
407    }
408
409    /// Runs jobs until the queue is empty or one throws.
410    pub fn run_jobs(&self) -> Result<(), RuntimeError> {
411        while self.run_pending_job()?.executed() {}
412        Ok(())
413    }
414
415    /// Runs `body` as one synchronous execution sequence: objects a
416    /// `WeakRef` keeps alive stay alive until the turn ends
417    /// (ECMASCRIPT-WEAKREF-LIFETIME-001).
418    pub fn with_execution_turn<T>(
419        &self,
420        body: impl FnOnce() -> Result<T, RuntimeError>,
421    ) -> Result<T, RuntimeError> {
422        self.shared.turns.set(self.shared.turns.get() + 1);
423        let result = body();
424        let turns = self.shared.turns.get() - 1;
425        self.shared.turns.set(turns);
426        if turns == 0 {
427            self.enter()?.core.clear_kept_objects();
428        }
429        result
430    }
431
432    /// A promise's state and result; `None` when `promise` is not one.
433    pub fn promise_snapshot(
434        &self,
435        promise: &ObjectRef,
436    ) -> Result<Option<PromiseSnapshot>, RuntimeError> {
437        self.enter()?.view().promise_snapshot(promise)
438    }
439
440    /// Keeps every rejected promise without a handler until one is attached
441    /// (QuickJS's `js_std_promise_rejection_tracker`).
442    pub fn set_track_rejections(&self, track: bool) -> Result<(), RuntimeError> {
443        self.enter()?.core.set_track_rejections(track);
444        Ok(())
445    }
446
447    /// The tracked unhandled rejections, promise and reason, in rejection
448    /// order; the list is emptied.
449    pub fn take_unhandled_rejections(&self) -> Result<Vec<(ObjectRef, Value)>, RuntimeError> {
450        let mut entry = self.enter()?;
451        let promises = entry.inner.core.unhandled_rejections().to_vec();
452        let mut rejections = Vec::with_capacity(promises.len());
453        let mut view = entry.view();
454        for promise in promises {
455            let reason = view
456                .core
457                .promise_state(promise)
458                .map_or(oxide_core::Value::UNDEFINED, |(_, reason)| reason);
459            let promise = view.lift(promise)?;
460            let reason = view.lift(reason)?;
461            if let Value::Object(promise) = promise {
462                rejections.push((promise, reason));
463            }
464        }
465        drop(view);
466        entry.inner.core.clear_unhandled_rejections();
467        Ok(rejections)
468    }
469
470    // ---- memory ----
471
472    /// Runs a full collection; the collector's statistics after it.
473    pub fn run_gc(&self) -> Result<GcStats, RuntimeError> {
474        let mut entry = self.enter()?;
475        entry.core.run_gc();
476        Ok(entry.core.heap().gc().stats)
477    }
478
479    pub fn gc_stats(&self) -> Result<GcStats, RuntimeError> {
480        Ok(self.enter()?.core.heap().gc().stats)
481    }
482
483    /// What every part of the runtime holds, without a collection.
484    pub fn memory_report(&self) -> Result<MemoryReport, RuntimeError> {
485        Ok(self.enter()?.core.memory_report())
486    }
487
488    /// When the collector runs: on heap growth (the default) or only at the
489    /// cap and on [`Runtime::run_gc`].
490    pub fn set_gc_policy(&self, policy: GcPolicy) -> Result<(), RuntimeError> {
491        self.enter()?
492            .core
493            .heap_mut()
494            .set_gc_manual(policy == GcPolicy::Manual);
495        Ok(())
496    }
497
498    pub fn gc_policy(&self) -> Result<GcPolicy, RuntimeError> {
499        Ok(if self.enter()?.core.heap().gc().is_manual() {
500            GcPolicy::Manual
501        } else {
502            GcPolicy::Automatic
503        })
504    }
505
506    /// Collects before every `interval`-th allocating operation (`qjs
507    /// --gc-stress=N`), or stops (`None`); only in a build with the stress
508    /// collector ([`Runtime::GC_STRESS_AVAILABLE`]).
509    pub fn set_gc_stress(&self, interval: Option<u32>) -> Result<(), RuntimeError> {
510        let mut entry = self.enter()?;
511        #[cfg(feature = "gc-stress")]
512        {
513            let gc = entry.core.heap_mut().gc_mut();
514            gc.stress = interval.is_some_and(|interval| interval > 0);
515            gc.stress_interval = interval.unwrap_or(1).max(1);
516            Ok(())
517        }
518        #[cfg(not(feature = "gc-stress"))]
519        {
520            let _ = &mut entry;
521            match interval {
522                Some(_) => Err(RuntimeError::Unsupported(
523                    "stress collections in this build",
524                )),
525                None => Ok(()),
526            }
527        }
528    }
529
530    /// Whether this build has the stress collector.
531    pub const GC_STRESS_AVAILABLE: bool = cfg!(feature = "gc-stress");
532
533    /// Whether this build counts inline-cache hits (`ic_report` returns
534    /// `Some`).
535    pub const IC_STATS_AVAILABLE: bool = cfg!(feature = "ic-stats");
536
537    /// Records every collection's pause (for [`Runtime::gc_report`]).
538    pub fn set_record_gc_pauses(&self, record: bool) -> Result<(), RuntimeError> {
539        self.enter()?.core.heap_mut().gc_mut().record_pauses = record;
540        Ok(())
541    }
542
543    /// `--gc-stats`'s line: collections, pauses and their percentiles, live
544    /// and reserved bytes.
545    pub fn gc_report(&self) -> Result<String, RuntimeError> {
546        let entry = self.enter()?;
547        let gc = entry.core.heap().gc();
548        let stats = gc.stats;
549        let mut pauses = gc.pauses.clone();
550        pauses.sort();
551        let percentile = |p: usize| {
552            pauses
553                .get((pauses.len() * p / 100).min(pauses.len().saturating_sub(1)))
554                .map_or(0.0, |pause| pause.as_secs_f64() * 1e3)
555        };
556        Ok(format!(
557            "gc: {} collections, total pause {:.1} ms, p50 {:.3} ms, p99 {:.3} ms, max {:.3} ms, \
558             live {} KiB, reserved {} KiB\n",
559            stats.collections,
560            stats.total_pause.as_secs_f64() * 1e3,
561            percentile(50),
562            percentile(99),
563            stats.max_pause.as_secs_f64() * 1e3,
564            stats.live_bytes / 1024,
565            entry.core.heap().bytes() / 1024,
566        ))
567    }
568
569    /// The inline-cache report (`--ic-stats`), when the build counts.
570    pub fn ic_report(&self, worst: usize) -> Result<Option<String>, RuntimeError> {
571        Ok(self.enter()?.core.ic_report(worst))
572    }
573
574    /// The disassembly of every unit `source` compiles to, root first.
575    pub fn disassemble(&self, source: &[u8], filename: &str) -> Result<String, RuntimeError> {
576        let mut entry = self.enter()?;
577        let debug_info = self.shared.debug_info.get();
578        let lowered = {
579            let mut compiler = entry.inner.core.compiler();
580            super::compile::compile_script(&mut compiler, source, filename, debug_info)
581        };
582        let lowered = match lowered {
583            Ok(lowered) => lowered,
584            Err(super::compile::ScriptCompileError::Front(error)) => {
585                return Err(RuntimeError::Compile(error));
586            }
587            Err(super::compile::ScriptCompileError::Lower(error)) => {
588                return Err(RuntimeError::Internal(error.to_string()));
589            }
590        };
591        let mut text = String::new();
592        for unit in entry.core.units().tree(lowered.root) {
593            text.push_str(&entry.core.disassemble(unit));
594            text.push('\n');
595        }
596        Ok(text)
597    }
598
599    // ---- keys and symbols ----
600
601    pub fn intern_property_key(&self, text: &str) -> Result<PropertyKey, RuntimeError> {
602        Ok(PropertyKey::from_js_string(JsString::try_from_utf8(text)?))
603    }
604
605    pub fn intern_property_key_js_string(
606        &self,
607        text: &JsString,
608    ) -> Result<PropertyKey, RuntimeError> {
609        Ok(PropertyKey::from_js_string(text.clone()))
610    }
611
612    /// The key as a string; a symbol key's description (empty when it has
613    /// none).
614    pub fn property_key_to_js_string(&self, key: &PropertyKey) -> Result<JsString, RuntimeError> {
615        match key.as_symbol() {
616            Some(symbol) => Ok(self
617                .symbol_description(symbol)?
618                .unwrap_or_else(|| JsString::from_static(""))),
619            None => key
620                .to_js_string()
621                .ok_or(RuntimeError::Invariant("a string key")),
622        }
623    }
624
625    pub fn new_symbol(&self, description: Option<JsString>) -> Result<SymbolRef, RuntimeError> {
626        self.enter()?.view().new_symbol(description)
627    }
628
629    pub fn symbol_description(&self, symbol: &SymbolRef) -> Result<Option<JsString>, RuntimeError> {
630        self.enter()?.view().symbol_description(symbol)
631    }
632
633    pub fn well_known_symbol(&self, symbol: WellKnownSymbol) -> Result<SymbolRef, RuntimeError> {
634        self.enter()?.view().well_known_symbol(symbol)
635    }
636
637    // ---- objects ----
638
639    pub fn get_prototype_of(&self, object: &ObjectRef) -> Result<Option<ObjectRef>, RuntimeError> {
640        self.enter()?.view().get_prototype_of(object)
641    }
642
643    pub fn set_prototype_of(
644        &self,
645        object: &ObjectRef,
646        prototype: Option<&ObjectRef>,
647    ) -> Result<bool, RuntimeError> {
648        self.enter()?.view().set_prototype_of(object, prototype)
649    }
650
651    /// The object as a callable reference; `None` when it is not callable.
652    pub fn as_callable(&self, object: &ObjectRef) -> Result<Option<CallableRef>, RuntimeError> {
653        self.enter()?.view().as_callable(object)
654    }
655
656    pub fn is_constructor(&self, object: &ObjectRef) -> Result<bool, RuntimeError> {
657        self.enter()?.view().is_constructor(object)
658    }
659
660    /// `IsArray`.
661    pub fn is_array_object(&self, object: &ObjectRef) -> Result<bool, RuntimeError> {
662        self.enter()?.view().is_array_object(object)
663    }
664
665    pub fn is_error_object(&self, object: &ObjectRef) -> Result<bool, RuntimeError> {
666        self.enter()?.view().is_error_object(object)
667    }
668
669    pub fn define_own_property(
670        &self,
671        object: &ObjectRef,
672        key: &PropertyKey,
673        descriptor: &OrdinaryPropertyDescriptor,
674    ) -> Result<bool, RuntimeError> {
675        self.context().define_own_property(object, key, descriptor)
676    }
677
678    pub fn get_own_property(
679        &self,
680        object: &ObjectRef,
681        key: &PropertyKey,
682    ) -> Result<Option<CompleteOrdinaryPropertyDescriptor>, RuntimeError> {
683        self.context().get_own_property(object, key)
684    }
685
686    pub fn delete_property(
687        &self,
688        object: &ObjectRef,
689        key: &PropertyKey,
690    ) -> Result<bool, RuntimeError> {
691        self.enter()?.view().delete_property(object, key)
692    }
693
694    pub fn has_property(
695        &self,
696        object: &ObjectRef,
697        key: &PropertyKey,
698    ) -> Result<bool, RuntimeError> {
699        self.enter()?.view().has_property(object, key)
700    }
701
702    pub fn has_own_property(
703        &self,
704        object: &ObjectRef,
705        key: &PropertyKey,
706    ) -> Result<bool, RuntimeError> {
707        self.enter()?.view().has_own_property(object, key)
708    }
709
710    pub fn own_property_keys(&self, object: &ObjectRef) -> Result<Vec<PropertyKey>, RuntimeError> {
711        self.enter()?.view().own_property_keys(object)
712    }
713
714    /// `typeof value`.
715    pub fn type_of(&self, value: &Value) -> Result<String, RuntimeError> {
716        self.enter()?.view().type_of(value)
717    }
718
719    /// QuickJS's `JS_PrintValue`: what `print` and an uncaught-exception dump
720    /// show. Runs no JavaScript (no accessor, trap or coercion).
721    pub fn print_value(&self, value: &Value) -> Result<String, RuntimeError> {
722        self.enter()?.view().print_value(value)
723    }
724
725    /// [`Runtime::print_value`] as the bytes QuickJS writes: a lone
726    /// surrogate of a raw name, message or stack as its own three bytes
727    /// (WTF-8) instead of U+FFFD.
728    pub fn qjs_print_value_bytes(&self, value: &Value) -> Result<Vec<u8>, RuntimeError> {
729        self.enter()?.view().qjs_print_value_bytes(value)
730    }
731}