Skip to main content

rquickjs_core/runtime/
async.rs

1use alloc::{
2    ffi::CString,
3    sync::{Arc, Weak},
4    vec::Vec,
5};
6use core::{ptr::NonNull, result::Result as StdResult, task::Poll};
7#[cfg(feature = "std")]
8use std::println;
9
10#[cfg(feature = "parallel")]
11use std::sync::mpsc::{self, Sender};
12
13use async_lock::Mutex;
14
15use super::{
16    opaque::Opaque, raw::RawRuntime, schedular::SchedularPoll, spawner::DriveFuture,
17    InterruptHandler, MemoryUsage, PromiseHook, RejectionTracker,
18};
19use crate::allocator::Allocator;
20#[cfg(feature = "loader")]
21use crate::loader::{Loader, Resolver};
22#[cfg(feature = "parallel")]
23use crate::util::{AssertSendFuture, AssertSyncFuture};
24use crate::{
25    context::AsyncContext, qjs, result::AsyncJobException, util::ManualPoll, Ctx, Exception, Result,
26};
27
28#[derive(Debug)]
29pub(crate) struct InnerRuntime {
30    pub runtime: RawRuntime,
31}
32
33#[cfg(feature = "parallel")]
34unsafe impl Send for InnerRuntime {}
35
36/// A weak handle to the async runtime.
37///
38/// Holding onto this struct does not prevent the runtime from being dropped.
39#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
40#[derive(Clone)]
41pub struct AsyncWeakRuntime {
42    inner: Weak<Mutex<InnerRuntime>>,
43    #[cfg(feature = "parallel")]
44    pending_free: Sender<NonNull<qjs::JSContext>>,
45}
46
47impl AsyncWeakRuntime {
48    pub fn try_ref(&self) -> Option<AsyncRuntime> {
49        self.inner.upgrade().map(|inner| AsyncRuntime {
50            inner,
51            #[cfg(feature = "parallel")]
52            pending_free: self.pending_free.clone(),
53        })
54    }
55}
56
57/// Asynchronous QuickJS runtime, entry point of the library.
58#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
59#[derive(Clone)]
60pub struct AsyncRuntime {
61    // use Arc instead of Ref so we can use OwnedLock
62    pub(crate) inner: Arc<Mutex<InnerRuntime>>,
63    #[cfg(feature = "parallel")]
64    pub(crate) pending_free: Sender<NonNull<qjs::JSContext>>,
65}
66
67// Since all functions which use runtime are behind a mutex
68// sending the runtime to other threads should be fine.
69#[cfg(feature = "parallel")]
70unsafe impl Send for AsyncRuntime {}
71#[cfg(feature = "parallel")]
72unsafe impl Send for AsyncWeakRuntime {}
73
74// Since a global lock needs to be locked for safe use
75// using runtime in a sync way should be safe as
76// simultaneous accesses is synchronized behind a lock.
77#[cfg(feature = "parallel")]
78unsafe impl Sync for AsyncRuntime {}
79#[cfg(feature = "parallel")]
80unsafe impl Sync for AsyncWeakRuntime {}
81
82impl AsyncRuntime {
83    /// Create a new runtime.
84    ///
85    /// Will generally only fail if not enough memory was available.
86    ///
87    /// # Features
88    /// *If the `"rust-alloc"` feature is enabled the Rust's global allocator will be used in favor of libc's one.*
89    // Annoying false positive clippy lint
90    #[allow(clippy::arc_with_non_send_sync)]
91    pub fn new() -> Result<Self> {
92        let opaque = Opaque::with_spawner();
93
94        #[cfg(feature = "parallel")]
95        let (pending_free, pending_free_recv) = mpsc::channel();
96        let runtime = unsafe {
97            RawRuntime::new(
98                opaque,
99                #[cfg(feature = "parallel")]
100                pending_free_recv,
101            )
102        }?;
103
104        Ok(Self {
105            inner: Arc::new(Mutex::new(InnerRuntime { runtime })),
106            #[cfg(feature = "parallel")]
107            pending_free,
108        })
109    }
110
111    /// Create a new runtime using specified allocator
112    ///
113    /// Will generally only fail if not enough memory was available.
114    // Annoying false positive clippy lint
115    #[allow(clippy::arc_with_non_send_sync)]
116    pub fn new_with_alloc<A>(allocator: A) -> Result<Self>
117    where
118        A: Allocator + 'static,
119    {
120        let opaque = Opaque::with_spawner();
121
122        #[cfg(feature = "parallel")]
123        let (pending_free, pending_free_recv) = mpsc::channel();
124        let runtime = unsafe {
125            RawRuntime::new_with_allocator(
126                opaque,
127                allocator,
128                #[cfg(feature = "parallel")]
129                pending_free_recv,
130            )
131        }?;
132
133        Ok(Self {
134            inner: Arc::new(Mutex::new(InnerRuntime { runtime })),
135            #[cfg(feature = "parallel")]
136            pending_free,
137        })
138    }
139
140    /// Get weak ref to runtime
141    pub fn weak(&self) -> AsyncWeakRuntime {
142        AsyncWeakRuntime {
143            inner: Arc::downgrade(&self.inner),
144            #[cfg(feature = "parallel")]
145            pending_free: self.pending_free.clone(),
146        }
147    }
148
149    /// Set a closure which is called when a Promise is rejected.
150    #[inline]
151    pub async fn set_host_promise_rejection_tracker(&self, tracker: Option<RejectionTracker>) {
152        unsafe {
153            self.inner
154                .lock()
155                .await
156                .runtime
157                .set_host_promise_rejection_tracker(tracker);
158        }
159    }
160
161    /// Set a closure which is called when a promise is created, resolved, or chained.
162    #[inline]
163    pub async fn set_promise_hook(&self, tracker: Option<PromiseHook>) {
164        unsafe {
165            self.inner.lock().await.runtime.set_promise_hook(tracker);
166        }
167    }
168
169    /// Set a closure which is regularly called by the engine when it is executing code.
170    /// If the provided closure returns `true` the interpreter will raise and uncatchable
171    /// exception and return control flow to the caller.
172    #[inline]
173    pub async fn set_interrupt_handler(&self, handler: Option<InterruptHandler>) {
174        unsafe {
175            self.inner
176                .lock()
177                .await
178                .runtime
179                .set_interrupt_handler(handler);
180        }
181    }
182
183    /// Set the module loader
184    #[cfg(feature = "loader")]
185    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))]
186    pub async fn set_loader<R, L>(&self, resolver: R, loader: L)
187    where
188        R: Resolver + 'static,
189        L: Loader + 'static,
190    {
191        unsafe {
192            self.inner.lock().await.runtime.set_loader(resolver, loader);
193        }
194    }
195
196    /// Set the info of the runtime
197    pub async fn set_info<S: Into<Vec<u8>>>(&self, info: S) -> Result<()> {
198        let string = CString::new(info)?;
199        unsafe {
200            self.inner.lock().await.runtime.set_info(string);
201        }
202        Ok(())
203    }
204
205    /// Set a limit on the max amount of memory the runtime will use.
206    ///
207    /// Setting the limit to 0 is equivalent to unlimited memory.
208    ///
209    /// Note that is a Noop when a custom allocator is being used,
210    /// as is the case for the "rust-alloc" or "allocator" features.
211    pub async fn set_memory_limit(&self, limit: usize) {
212        unsafe {
213            self.inner.lock().await.runtime.set_memory_limit(limit);
214        }
215    }
216
217    /// Set a limit on the max size of stack the runtime will use.
218    ///
219    /// The default values is 256x1024 bytes.
220    pub async fn set_max_stack_size(&self, limit: usize) {
221        unsafe {
222            self.inner.lock().await.runtime.set_max_stack_size(limit);
223        }
224    }
225
226    /// Set a memory threshold for garbage collection.
227    pub async fn set_gc_threshold(&self, threshold: usize) {
228        unsafe {
229            self.inner.lock().await.runtime.set_gc_threshold(threshold);
230        }
231    }
232
233    /// Manually run the garbage collection.
234    ///
235    /// Most QuickJS values are reference counted and
236    /// will automatically free themselves when they have no more
237    /// references. The garbage collector is only for collecting
238    /// cyclic references.
239    pub async fn run_gc(&self) {
240        unsafe {
241            let mut lock = self.inner.lock().await;
242            lock.runtime.drain_pending_free();
243            lock.runtime.run_gc();
244        }
245    }
246
247    /// Get memory usage stats
248    pub async fn memory_usage(&self) -> MemoryUsage {
249        unsafe { self.inner.lock().await.runtime.memory_usage() }
250    }
251
252    /// Test for pending jobs
253    ///
254    /// Returns true when at least one job is pending.
255    #[inline]
256    pub async fn is_job_pending(&self) -> bool {
257        let lock = self.inner.lock().await;
258
259        lock.runtime.is_job_pending() || !lock.runtime.get_opaque().spawner_is_empty()
260    }
261
262    /// Execute first pending job
263    ///
264    /// Returns true when job was executed or false when queue is empty or error when exception thrown under execution.
265    #[inline]
266    pub async fn execute_pending_job(&self) -> StdResult<bool, AsyncJobException> {
267        let mut lock = self.inner.lock().await;
268        lock.runtime.update_stack_top();
269        lock.runtime.drain_pending_free();
270
271        let f = ManualPoll::new(|cx| {
272            let job_res = lock.runtime.execute_pending_job().map_err(|e| {
273                let ptr = NonNull::new(e)
274                    .expect("executing pending job returned a null context on error");
275                // JS_ExecutePendingJob returns a borrowed context pointer;
276                // dup it so AsyncContext can own a reference.
277                unsafe { qjs::JS_DupContext(ptr.as_ptr()) };
278                AsyncJobException(unsafe { AsyncContext::from_raw(ptr, self.clone()) })
279            })?;
280
281            if job_res {
282                return Poll::Ready(Ok(true));
283            }
284
285            match lock.runtime.get_opaque().poll(cx) {
286                SchedularPoll::ShouldYield => Poll::Pending,
287                SchedularPoll::Empty => Poll::Ready(Ok(false)),
288                SchedularPoll::Pending => Poll::Ready(Ok(false)),
289                SchedularPoll::PendingProgress => Poll::Ready(Ok(true)),
290            }
291        });
292
293        #[cfg(feature = "parallel")]
294        let f = unsafe { AssertSendFuture::assert(AssertSyncFuture::assert(f)) };
295
296        f.await
297    }
298
299    /// Run all futures and jobs in the runtime until all are finished.
300    #[inline]
301    pub async fn idle(&self) {
302        let mut lock = self.inner.lock().await;
303        lock.runtime.update_stack_top();
304        lock.runtime.drain_pending_free();
305
306        let f = ManualPoll::new(|cx| {
307            loop {
308                let pending = lock.runtime.execute_pending_job().map_err(|e| {
309                    let ptr = NonNull::new(e)
310                        .expect("executing pending job returned a null context on error");
311                    // JS_ExecutePendingJob returns a borrowed context pointer;
312                    // dup it so AsyncContext can own a reference.
313                    unsafe { qjs::JS_DupContext(ptr.as_ptr()) };
314                    AsyncJobException(unsafe { AsyncContext::from_raw(ptr, self.clone()) })
315                });
316                match pending {
317                    Err(e) => {
318                        // SAFETY: Runtime is already locked so creating a context is safe.
319                        let ctx = unsafe { Ctx::from_ptr(e.0 .0.ctx().as_ptr()) };
320                        let err = ctx.catch();
321                        if let Some(_x) = err.clone().into_object().and_then(Exception::from_object)
322                        {
323                            // TODO do something better with errors.
324                            #[cfg(feature = "std")]
325                            println!("error executing job: {}", _x);
326                        } else {
327                            #[cfg(feature = "std")]
328                            println!("error executing job: {:?}", err);
329                        }
330                    }
331                    Ok(true) => continue,
332                    Ok(false) => {}
333                }
334
335                match lock.runtime.get_opaque().poll(cx) {
336                    SchedularPoll::ShouldYield => return Poll::Pending,
337                    SchedularPoll::Empty => return Poll::Ready(()),
338                    SchedularPoll::Pending => return Poll::Pending,
339                    SchedularPoll::PendingProgress => {}
340                }
341            }
342        });
343
344        #[cfg(feature = "parallel")]
345        let f = unsafe { AssertSendFuture::assert(AssertSyncFuture::assert(f)) };
346
347        f.await
348    }
349
350    /// Returns a future that completes when the runtime is dropped.
351    /// If the future is polled it will drive futures spawned inside the runtime completing them
352    /// even if runtime is currently not in use.
353    pub fn drive(&self) -> DriveFuture {
354        DriveFuture::new(self.weak())
355    }
356}
357
358#[cfg(test)]
359macro_rules! async_test_case {
360    ($name:ident => ($rt:ident,$ctx:ident) { $($t:tt)* }) => {
361    #[test]
362    fn $name() {
363        #[cfg(feature = "parallel")]
364        let mut new_thread = tokio::runtime::Builder::new_multi_thread();
365
366        #[cfg(not(feature = "parallel"))]
367        let mut new_thread = tokio::runtime::Builder::new_current_thread();
368
369        let rt = new_thread
370            .enable_all()
371            .build()
372            .unwrap();
373
374        #[cfg(feature = "parallel")]
375        {
376            rt.block_on(async {
377                let $rt = crate::AsyncRuntime::new().unwrap();
378                let $ctx = crate::AsyncContext::full(&$rt).await.unwrap();
379
380                $($t)*
381
382            })
383        }
384        #[cfg(not(feature = "parallel"))]
385        {
386            let set = tokio::task::LocalSet::new();
387            set.block_on(&rt, async {
388                let $rt = crate::AsyncRuntime::new().unwrap();
389                let $ctx = crate::AsyncContext::full(&$rt).await.unwrap();
390
391                $($t)*
392            })
393        }
394    }
395    };
396}
397
398#[cfg(test)]
399mod test {
400    use std::time::Duration;
401
402    use crate::*;
403
404    use self::context::EvalOptions;
405
406    async_test_case!(basic => (_rt,ctx){
407        ctx.async_with(async |ctx|{
408            let res: i32 = ctx.eval("1 + 1").unwrap();
409            assert_eq!(res,2i32);
410        }).await;
411    });
412
413    async_test_case!(sleep_closure => (_rt,ctx){
414
415        let mut a = 1;
416        let a_ref = &mut a;
417
418
419        ctx.async_with(async |ctx|{
420            tokio::time::sleep(Duration::from_secs_f64(0.01)).await;
421            ctx.globals().set("foo","bar").unwrap();
422            *a_ref += 1;
423        }).await;
424        assert_eq!(a,2);
425    });
426
427    async_test_case!(drive => (rt,ctx){
428        use std::sync::{Arc, atomic::{Ordering,AtomicUsize}};
429
430        #[cfg(feature = "parallel")]
431        tokio::spawn(rt.drive());
432        #[cfg(not(feature = "parallel"))]
433        tokio::task::spawn_local(rt.drive());
434
435        // Give drive time to start.
436        tokio::time::sleep(Duration::from_secs_f64(0.01)).await;
437
438        let number = Arc::new(AtomicUsize::new(0));
439        let number_clone = number.clone();
440        let gate = Arc::new(tokio::sync::Notify::new());
441        let gate_clone = gate.clone();
442        let done = Arc::new(tokio::sync::Notify::new());
443        let done_clone = done.clone();
444
445        ctx.async_with(async |ctx|{
446            ctx.spawn(async move {
447                gate_clone.notified().await;
448                number_clone.store(1,Ordering::SeqCst);
449                done_clone.notify_one();
450            });
451        }).await;
452        // Task is blocked on gate, so value is definitely still 0.
453        assert_eq!(number.load(Ordering::SeqCst),0);
454        // Unblock the task and wait for it to complete.
455        gate.notify_one();
456        done.notified().await;
457        assert_eq!(number.load(Ordering::SeqCst),1);
458
459    });
460
461    async_test_case!(no_drive => (rt,ctx){
462        use std::sync::{Arc, atomic::{Ordering,AtomicUsize}};
463
464        let number = Arc::new(AtomicUsize::new(0));
465        let number_clone = number.clone();
466
467        ctx.async_with(async |ctx|{
468            ctx.spawn(async move {
469                tokio::task::yield_now().await;
470                number_clone.store(1,Ordering::SeqCst);
471            });
472        }).await;
473        assert_eq!(number.load(Ordering::SeqCst),0);
474        tokio::time::sleep(Duration::from_secs_f64(0.01)).await;
475        assert_eq!(number.load(Ordering::SeqCst),0);
476
477    });
478
479    async_test_case!(idle => (rt,ctx){
480        use std::sync::{Arc, atomic::{Ordering,AtomicUsize}};
481
482        let number = Arc::new(AtomicUsize::new(0));
483        let number_clone = number.clone();
484
485        ctx.async_with(async |ctx|{
486            ctx.spawn(async move {
487                tokio::task::yield_now().await;
488                number_clone.store(1,Ordering::SeqCst);
489            });
490        }).await;
491        assert_eq!(number.load(Ordering::SeqCst),0);
492        rt.idle().await;
493        assert_eq!(number.load(Ordering::SeqCst),1);
494
495    });
496
497    async_test_case!(recursive_spawn => (rt,ctx){
498        use tokio::sync::oneshot;
499
500        ctx.async_with(async |ctx|{
501            let ctx_clone = ctx.clone();
502            let (tx,rx) = oneshot::channel::<()>();
503            let (tx2,rx2) = oneshot::channel::<()>();
504            ctx.spawn(async move {
505                tokio::task::yield_now().await;
506
507                let ctx = ctx_clone.clone();
508
509                ctx_clone.spawn(async move {
510                    tokio::task::yield_now().await;
511                    ctx.spawn(async move {
512                        tokio::task::yield_now().await;
513                        tx2.send(()).unwrap();
514                        tokio::task::yield_now().await;
515                    });
516                    tokio::task::yield_now().await;
517                    tx.send(()).unwrap();
518                });
519
520                // Add a bunch of futures just to make sure possible segfaults are more likely to
521                // happen
522                for _ in 0..32{
523                    ctx_clone.spawn(async move {})
524                }
525
526            });
527            tokio::time::timeout(Duration::from_millis(500), rx).await.unwrap().unwrap();
528            tokio::time::timeout(Duration::from_millis(500), rx2).await.unwrap().unwrap();
529        }).await;
530
531    });
532
533    async_test_case!(recursive_spawn_from_script => (rt,ctx) {
534        use std::sync::atomic::{Ordering, AtomicUsize};
535        use crate::prelude::Func;
536
537        static COUNT: AtomicUsize = AtomicUsize::new(0);
538        static SCRIPT: &str = r#"
539
540        async function main() {
541
542          setTimeout(() => {
543            inc_count()
544            setTimeout(async () => {
545                inc_count()
546            }, 100);
547          }, 100);
548        }
549
550        main().catch(print);
551
552
553        "#;
554
555        fn inc_count(){
556            COUNT.fetch_add(1,Ordering::Relaxed);
557        }
558
559        fn set_timeout_spawn<'js>(ctx: Ctx<'js>, callback: Function<'js>, millis: usize) -> Result<()> {
560            ctx.spawn(async move {
561                tokio::time::sleep(Duration::from_millis(millis as u64)).await;
562                callback.call::<_, ()>(()).unwrap();
563            });
564
565            Ok(())
566        }
567
568
569        ctx.async_with(async |ctx|{
570
571            let res: Result<Promise> = (|| {
572                let globals = ctx.globals();
573
574                globals.set("inc_count", Func::from(inc_count))?;
575
576                globals.set("setTimeout", Func::from(set_timeout_spawn))?;
577                let options = EvalOptions{
578                    promise: true,
579                    strict: false,
580                    ..EvalOptions::default()
581                };
582
583                ctx.eval_with_options(SCRIPT, options)?
584            })();
585
586            match res.catch(&ctx){
587                Ok(promise) => {
588                    if let Err(err) = promise.into_future::<Value>().await.catch(&ctx){
589                        eprintln!("{}", err)
590                    }
591                },
592                Err(err) => {
593                    eprintln!("{}", err)
594                },
595            };
596
597        })
598        .await;
599
600        rt.idle().await;
601
602        assert_eq!(COUNT.load(Ordering::Relaxed),2);
603    });
604
605    async_test_case!(interrupt_handler_idle => (rt, ctx) {
606        use std::time::Instant;
607
608        let timeout = Duration::from_millis(100);
609        let start_time = Instant::now();
610
611        rt.set_interrupt_handler(Some(Box::new(move || start_time.elapsed() >= timeout)))
612            .await;
613
614        let _ = ctx.async_with(async |ctx| {
615            ctx.eval::<(), _>(r#"
616                async function example() {
617                    while (true) {
618                        await Promise.resolve();
619                    }
620                }
621                example();
622            "#)
623        }).await;
624
625        // This previously caused an assertion failure in gc_decref_child
626        // due to the interrupt handler corrupting reference counts during
627        // pending job execution.
628        rt.idle().await;
629    });
630
631    #[cfg(feature = "parallel")]
632    fn assert_is_send<T: Send>(t: T) -> T {
633        t
634    }
635
636    #[cfg(feature = "parallel")]
637    fn assert_is_sync<T: Send>(t: T) -> T {
638        t
639    }
640
641    #[cfg(feature = "parallel")]
642    #[tokio::test]
643    async fn ensure_types_are_send_sync() {
644        let rt = AsyncRuntime::new().unwrap();
645
646        std::mem::drop(assert_is_sync(rt.idle()));
647        std::mem::drop(assert_is_sync(rt.execute_pending_job()));
648        std::mem::drop(assert_is_sync(rt.drive()));
649
650        std::mem::drop(assert_is_send(rt.idle()));
651        std::mem::drop(assert_is_send(rt.execute_pending_job()));
652        std::mem::drop(assert_is_send(rt.drive()));
653    }
654}