Skip to main content

rquickjs_core/runtime/
base.rs

1//! QuickJS runtime related types.
2
3use super::{
4    opaque::Opaque, raw::RawRuntime, InterruptHandler, MemoryUsage, PromiseHook, RejectionTracker,
5};
6use crate::allocator::Allocator;
7#[cfg(feature = "loader")]
8use crate::loader::{Loader, Resolver};
9use crate::{qjs, result::JobException, Context, Mut, Ref, Result, Weak};
10use alloc::{ffi::CString, vec::Vec};
11use core::{ptr::NonNull, result::Result as StdResult};
12#[cfg(feature = "parallel")]
13use std::sync::mpsc::{self, Sender};
14
15/// A weak handle to the runtime.
16///
17/// Holding onto this struct does not prevent the runtime from being dropped.
18#[derive(Clone)]
19pub struct WeakRuntime {
20    inner: Weak<Mut<RawRuntime>>,
21    #[cfg(feature = "parallel")]
22    pending_free: Sender<NonNull<qjs::JSContext>>,
23}
24
25impl WeakRuntime {
26    pub fn try_ref(&self) -> Option<Runtime> {
27        self.inner.upgrade().map(|inner| Runtime {
28            inner,
29            #[cfg(feature = "parallel")]
30            pending_free: self.pending_free.clone(),
31        })
32    }
33}
34
35/// QuickJS runtime, entry point of the library.
36#[derive(Clone)]
37pub struct Runtime {
38    pub(crate) inner: Ref<Mut<RawRuntime>>,
39    #[cfg(feature = "parallel")]
40    pub(crate) pending_free: Sender<NonNull<qjs::JSContext>>,
41}
42
43impl Runtime {
44    /// Create a new runtime.
45    ///
46    /// Will generally only fail if not enough memory was available.
47    ///
48    /// # Features
49    /// *If the `"rust-alloc"` feature is enabled the Rust's global allocator will be used in favor of libc's one.*
50    pub fn new() -> Result<Self> {
51        let opaque = Opaque::new();
52        #[cfg(feature = "parallel")]
53        let (pending_free, pending_free_recv) = mpsc::channel();
54        let rt = unsafe {
55            RawRuntime::new(
56                opaque,
57                #[cfg(feature = "parallel")]
58                pending_free_recv,
59            )?
60        };
61        Ok(Self {
62            inner: Ref::new(Mut::new(rt)),
63            #[cfg(feature = "parallel")]
64            pending_free,
65        })
66    }
67
68    /// Create a new runtime using specified allocator
69    ///
70    /// Will generally only fail if not enough memory was available.
71    pub fn new_with_alloc<A>(allocator: A) -> Result<Self>
72    where
73        A: Allocator + 'static,
74    {
75        let opaque = Opaque::new();
76        #[cfg(feature = "parallel")]
77        let (pending_free, pending_free_recv) = mpsc::channel();
78        let rt = unsafe {
79            RawRuntime::new_with_allocator(
80                opaque,
81                allocator,
82                #[cfg(feature = "parallel")]
83                pending_free_recv,
84            )?
85        };
86        Ok(Self {
87            inner: Ref::new(Mut::new(rt)),
88            #[cfg(feature = "parallel")]
89            pending_free,
90        })
91    }
92
93    /// Get weak ref to runtime
94    pub fn weak(&self) -> WeakRuntime {
95        WeakRuntime {
96            inner: Ref::downgrade(&self.inner),
97            #[cfg(feature = "parallel")]
98            pending_free: self.pending_free.clone(),
99        }
100    }
101
102    /// Set a closure which is called when a promise is created, resolved, or chained.
103    #[inline]
104    pub fn set_promise_hook(&self, tracker: Option<PromiseHook>) {
105        unsafe {
106            self.inner.lock().set_promise_hook(tracker);
107        }
108    }
109
110    /// Set a closure which is called when a Promise is rejected.
111    #[inline]
112    pub fn set_host_promise_rejection_tracker(&self, tracker: Option<RejectionTracker>) {
113        unsafe {
114            self.inner
115                .lock()
116                .set_host_promise_rejection_tracker(tracker);
117        }
118    }
119
120    /// Set a closure which is regularly called by the engine when it is executing code.
121    /// If the provided closure returns `true` the interpreter will raise and uncatchable
122    /// exception and return control flow to the caller.
123    #[inline]
124    pub fn set_interrupt_handler(&self, handler: Option<InterruptHandler>) {
125        unsafe {
126            self.inner.lock().set_interrupt_handler(handler);
127        }
128    }
129
130    /// Set the module loader
131    #[cfg(feature = "loader")]
132    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))]
133    pub fn set_loader<R, L>(&self, resolver: R, loader: L)
134    where
135        R: Resolver + 'static,
136        L: Loader + 'static,
137    {
138        unsafe {
139            self.inner.lock().set_loader(resolver, loader);
140        }
141    }
142
143    /// Set the info of the runtime
144    pub fn set_info<S: Into<Vec<u8>>>(&self, info: S) -> Result<()> {
145        let string = CString::new(info)?;
146        unsafe {
147            self.inner.lock().set_info(string);
148        }
149        Ok(())
150    }
151
152    /// Set a limit on the max amount of memory the runtime will use.
153    ///
154    /// Setting the limit to 0 is equivalent to unlimited memory.
155    ///
156    /// Note that is a Noop when a custom allocator is being used,
157    /// as is the case for the "rust-alloc" or "allocator" features.
158    pub fn set_memory_limit(&self, limit: usize) {
159        unsafe {
160            self.inner.lock().set_memory_limit(limit);
161        }
162    }
163
164    /// Set a limit on the max size of stack the runtime will use.
165    ///
166    /// The default values is 256x1024 bytes.
167    pub fn set_max_stack_size(&self, limit: usize) {
168        unsafe {
169            self.inner.lock().set_max_stack_size(limit);
170        }
171    }
172
173    /// Set a memory threshold for garbage collection.
174    pub fn set_gc_threshold(&self, threshold: usize) {
175        unsafe {
176            self.inner.lock().set_gc_threshold(threshold);
177        }
178    }
179
180    /// Set debug flags for dumping memory
181    pub fn set_dump_flags(&self, flags: u64) {
182        unsafe {
183            self.inner.lock().set_dump_flags(flags);
184        }
185    }
186
187    /// Manually run the garbage collection.
188    ///
189    /// Most of QuickJS values are reference counted and
190    /// will automatically free themselves when they have no more
191    /// references. The garbage collector is only for collecting
192    /// cyclic references.
193    pub fn run_gc(&self) {
194        unsafe {
195            self.inner.lock().run_gc();
196        }
197    }
198
199    /// Get memory usage stats
200    pub fn memory_usage(&self) -> MemoryUsage {
201        unsafe { self.inner.lock().memory_usage() }
202    }
203
204    /// Test for pending jobs
205    ///
206    /// Returns true when at least one job is pending.
207    #[inline]
208    pub fn is_job_pending(&self) -> bool {
209        self.inner.lock().is_job_pending()
210    }
211
212    /// Execute first pending job
213    ///
214    /// Returns true when job was executed or false when queue is empty or error when exception thrown under execution.
215    #[inline]
216    pub fn execute_pending_job(&self) -> StdResult<bool, JobException> {
217        let mut lock = self.inner.lock();
218        lock.update_stack_top();
219        lock.execute_pending_job().map_err(|e| {
220            let ptr = NonNull::new(e).expect("QuickJS returned null ptr for job error");
221            // JS_ExecutePendingJob returns a borrowed context pointer;
222            // dup it so Context can own a reference.
223            unsafe { qjs::JS_DupContext(ptr.as_ptr()) };
224            JobException(unsafe { Context::from_raw(ptr, self.clone()) })
225        })
226    }
227}
228
229// Since all functions which use runtime are behind a mutex
230// sending the runtime to other threads should be fine.
231#[cfg(feature = "parallel")]
232unsafe impl Send for Runtime {}
233#[cfg(feature = "parallel")]
234unsafe impl Send for WeakRuntime {}
235
236// Since a global lock needs to be locked for safe use
237// using runtime in a sync way should be safe as
238// simultaneous accesses is synchronized behind a lock.
239#[cfg(feature = "parallel")]
240unsafe impl Sync for Runtime {}
241#[cfg(feature = "parallel")]
242unsafe impl Sync for WeakRuntime {}
243
244#[cfg(test)]
245mod test {
246    use super::*;
247    #[test]
248    fn base_runtime() {
249        let rt = Runtime::new().unwrap();
250        rt.set_info("test runtime").unwrap();
251        rt.set_memory_limit(0xFFFF);
252        rt.set_gc_threshold(0xFF);
253        rt.run_gc();
254    }
255
256    #[test]
257    fn set_max_stack_size_large_values() {
258        let rt = Runtime::new().unwrap();
259        rt.set_max_stack_size(usize::MAX);
260        let ctx = crate::Context::full(&rt).unwrap();
261        ctx.with(|ctx| {
262            ctx.eval::<i32, _>("1 + 1").unwrap();
263        });
264        rt.set_max_stack_size(isize::MAX as usize);
265        ctx.with(|ctx| {
266            ctx.eval::<i32, _>("1 + 1").unwrap();
267        });
268        rt.set_max_stack_size(0);
269        ctx.with(|ctx| {
270            ctx.eval::<i32, _>("1 + 1").unwrap();
271        });
272        rt.set_max_stack_size(256 * 1024);
273        ctx.with(|ctx| {
274            ctx.eval::<i32, _>("1 + 1").unwrap();
275        });
276    }
277
278    #[test]
279    fn context_dropped_while_lock_held() {
280        let rt = Runtime::new().unwrap();
281        let ctx1 = crate::Context::full(&rt).unwrap();
282        let ctx2 = crate::Context::full(&rt).unwrap();
283
284        ctx1.with(|_| {
285            drop(ctx2);
286        });
287    }
288
289    #[test]
290    #[cfg(feature = "parallel")]
291    fn context_parked_by_other_thread_is_still_freed() {
292        use std::sync::{Arc, Barrier};
293        use std::{thread, time::Duration};
294
295        let rt = Runtime::new().unwrap();
296        let ctx1 = crate::Context::full(&rt).unwrap();
297        let ctx2 = crate::Context::full(&rt).unwrap();
298
299        let barrier = Arc::new(Barrier::new(2));
300        let barrier_holder = barrier.clone();
301        let holder = thread::spawn(move || {
302            ctx1.with(|_| {
303                barrier_holder.wait();
304                thread::sleep(Duration::from_millis(100));
305            });
306        });
307
308        barrier.wait();
309        // The lock is held by the other thread, so this parks rather than
310        // freeing. Tearing the runtime down afterwards has to release it;
311        // `JS_FreeRuntime` aborts if any context is still alive.
312        drop(ctx2);
313
314        holder.join().unwrap();
315        drop(rt);
316    }
317}