javy/
runtime.rs

1// use crate::quickjs::JSContextRef;
2use super::from_js_error;
3#[cfg(feature = "json")]
4use crate::apis::json;
5use crate::{
6    Config,
7    apis::{console, random, stream_io, text_encoding},
8    config::{JSIntrinsics, JavyIntrinsics},
9};
10
11use anyhow::{Result, bail};
12use rquickjs::{
13    Context, Module, Runtime as QRuntime, WriteOptions,
14    context::{Intrinsic, intrinsic},
15};
16use std::mem::ManuallyDrop;
17
18/// A JavaScript Runtime.
19///
20/// Javy's [`Runtime`] holds a [`rquickjs::Runtime`] and [`rquickjs::Context`],
21/// and provides accessors these two propoerties which enable working with
22/// [`rquickjs`] APIs.
23pub struct Runtime {
24    /// The QuickJS context.
25    // We use `ManuallyDrop` to avoid incurring in the cost of dropping the
26    // `rquickjs::Context` and its associated objects, which takes a substantial
27    // amount of time.
28    //
29    // This assumes that Javy is used for short-lived programs where the host
30    // will collect the instance's memory when execution ends, making these
31    // drops unnecessary.
32    //
33    // This might not be suitable for all use-cases, so we'll make this
34    // behaviour configurable.
35    context: ManuallyDrop<Context>,
36    /// The inner QuickJS runtime representation.
37    // Read above on the usage of `ManuallyDrop`.
38    inner: ManuallyDrop<QRuntime>,
39}
40
41impl Runtime {
42    /// Creates a new [Runtime].
43    pub fn new(config: Config) -> Result<Self> {
44        let rt = ManuallyDrop::new(QRuntime::new()?);
45
46        let context = Self::build_from_config(&rt, config)?;
47        Ok(Self { inner: rt, context })
48    }
49
50    fn build_from_config(rt: &QRuntime, cfg: Config) -> Result<ManuallyDrop<Context>> {
51        let cfg = cfg.validate()?;
52        let intrinsics = &cfg.intrinsics;
53        let javy_intrinsics = &cfg.javy_intrinsics;
54
55        rt.set_gc_threshold(cfg.gc_threshold);
56        rt.set_memory_limit(cfg.memory_limit);
57        rt.set_max_stack_size(cfg.max_stack_size);
58
59        let context = Context::base(rt)?;
60
61        // We use `Context::with` to ensure that there's a proper lock on the
62        // context, making it totally safe to add the intrinsics below.
63        context.with(|ctx| {
64            // We always set Random given that the principles around snapshotting and
65            // random are applicable when using Javy from the CLI (the usage of
66            // Wizer from the CLI is not optional).
67            // NB: Users of Javy as a crate are welcome to switch this config,
68            // however note that the usage of a custom `Random` implementation
69            // should not affect the output of `Math.random()`.
70            random::register(ctx.clone()).expect("registering `random` APIs to succeed");
71
72            if intrinsics.contains(JSIntrinsics::DATE) {
73                unsafe { intrinsic::Date::add_intrinsic(ctx.as_raw()) }
74            }
75
76            if intrinsics.contains(JSIntrinsics::EVAL) {
77                unsafe { intrinsic::Eval::add_intrinsic(ctx.as_raw()) }
78            }
79
80            if intrinsics.contains(JSIntrinsics::REGEXP_COMPILER) {
81                unsafe { intrinsic::RegExpCompiler::add_intrinsic(ctx.as_raw()) }
82            }
83
84            if intrinsics.contains(JSIntrinsics::REGEXP) {
85                unsafe { intrinsic::RegExp::add_intrinsic(ctx.as_raw()) }
86            }
87
88            if intrinsics.contains(JSIntrinsics::JSON) {
89                unsafe { intrinsic::Json::add_intrinsic(ctx.as_raw()) }
90            }
91
92            #[cfg(feature = "json")]
93            if cfg.simd_json_builtins {
94                json::register(ctx.clone()).expect("registering JSON builtins to succeed");
95            }
96
97            if intrinsics.contains(JSIntrinsics::PROXY) {
98                unsafe { intrinsic::Proxy::add_intrinsic(ctx.as_raw()) }
99            }
100
101            if intrinsics.contains(JSIntrinsics::MAP_SET) {
102                unsafe { intrinsic::MapSet::add_intrinsic(ctx.as_raw()) }
103            }
104
105            if intrinsics.contains(JSIntrinsics::TYPED_ARRAY) {
106                unsafe { intrinsic::TypedArrays::add_intrinsic(ctx.as_raw()) }
107            }
108
109            if intrinsics.contains(JSIntrinsics::PROMISE) {
110                unsafe { intrinsic::Promise::add_intrinsic(ctx.as_raw()) }
111            }
112
113            if intrinsics.contains(JSIntrinsics::BIG_INT) {
114                unsafe { intrinsic::BigInt::add_intrinsic(ctx.as_raw()) }
115            }
116
117            if intrinsics.contains(JSIntrinsics::TEXT_ENCODING) {
118                text_encoding::register(ctx.clone())
119                    .expect("registering TextEncoding APIs to succeed");
120            }
121
122            if intrinsics.contains(JSIntrinsics::WEAK_REF) {
123                unsafe { intrinsic::WeakRef::add_intrinsic(ctx.as_raw()) };
124            }
125
126            if intrinsics.contains(JSIntrinsics::PERFORMANCE) {
127                unsafe { intrinsic::Performance::add_intrinsic(ctx.as_raw()) };
128            }
129
130            console::register(ctx.clone(), cfg.log_stream, cfg.err_stream)
131                .expect("registering console to succeed");
132
133            if javy_intrinsics.contains(JavyIntrinsics::STREAM_IO) {
134                stream_io::register(ctx.clone())
135                    .expect("registering StreamIO functions to succeed");
136            }
137        });
138
139        Ok(ManuallyDrop::new(context))
140    }
141
142    /// A reference to the inner [Context].
143    pub fn context(&self) -> &Context {
144        &self.context
145    }
146
147    /// Resolves all the pending jobs in the queue.
148    pub fn resolve_pending_jobs(&self) -> Result<()> {
149        if self.inner.is_job_pending() {
150            loop {
151                let result = self.inner.execute_pending_job();
152                if let Ok(false) = result {
153                    break;
154                }
155
156                if let Err(e) = result {
157                    bail!("{e}")
158                }
159            }
160        }
161
162        Ok(())
163    }
164
165    /// Returns true if there are pending jobs in the queue.
166    pub fn has_pending_jobs(&self) -> bool {
167        self.inner.is_job_pending()
168    }
169
170    /// Compiles the given module to bytecode.
171    pub fn compile_to_bytecode(&self, name: &str, contents: &str) -> Result<Vec<u8>> {
172        self.context()
173            .with(|this| {
174                Module::declare(this.clone(), name, contents)?.write(WriteOptions::default())
175            })
176            .map_err(|e| self.context().with(|cx| from_js_error(cx.clone(), e)))
177    }
178}
179
180impl Default for Runtime {
181    /// Returns a [`Runtime`] with a default configuration.
182    ///
183    /// # Panics
184    /// This function panics if there is an error setting up the runtime.
185    fn default() -> Self {
186        Self::new(Config::default()).unwrap()
187    }
188}