From e54406d33c1b7120f3351fd601810005694c3ca7 Mon Sep 17 00:00:00 2001 From: Mees Delzenne Date: Sat, 20 Apr 2024 17:14:52 +0200 Subject: [PATCH] Overhaul promises --- core/src/allocator/rust.rs | 2 +- core/src/context/ctx.rs | 74 ++++-- core/src/lib.rs | 10 +- core/src/promise.rs | 258 ------------------- core/src/result.rs | 5 +- core/src/runtime/base.rs | 4 +- core/src/runtime/raw.rs | 1 - core/src/runtime/schedular/atomic_waker.rs | 4 - core/src/value.rs | 15 +- core/src/value/exception.rs | 2 +- core/src/value/function/types.rs | 1 + core/src/value/module.rs | 17 +- core/src/value/promise.rs | 220 ++++++++++++++++ sys/build.rs | 2 +- sys/src/bindings/x86_64-unknown-linux-gnu.rs | 9 - sys/src/inlines/common.rs | 2 +- 16 files changed, 319 insertions(+), 307 deletions(-) delete mode 100644 core/src/promise.rs create mode 100644 core/src/value/promise.rs diff --git a/core/src/allocator/rust.rs b/core/src/allocator/rust.rs index 4ea28cc59..cd536eddf 100644 --- a/core/src/allocator/rust.rs +++ b/core/src/allocator/rust.rs @@ -57,7 +57,7 @@ unsafe impl Allocator for RustAllocator { header.size = size; } - unsafe { ptr.add(HEADER_SIZE) } + unsafe { dbg!(ptr.add(HEADER_SIZE)) } } #[allow(clippy::not_unsafe_ptr_arg_deref)] diff --git a/core/src/context/ctx.rs b/core/src/context/ctx.rs index 1cb3f921a..4c1575228 100644 --- a/core/src/context/ctx.rs +++ b/core/src/context/ctx.rs @@ -1,6 +1,7 @@ use std::{ ffi::{CStr, CString}, - fs, mem, + fs, + mem::{self, MaybeUninit}, path::Path, ptr::NonNull, }; @@ -12,7 +13,7 @@ use std::future::Future; use crate::AsyncContext; use crate::{ markers::Invariant, qjs, runtime::raw::Opaque, Context, Error, FromJs, Function, IntoJs, - Module, Object, Result, String, Value, + Module, Object, Promise, Result, String, Value, }; /// Eval options. @@ -127,6 +128,7 @@ impl<'js> Ctx<'js> { file_name.as_ptr(), flag, ); + dbg!(qjs::JS_VALUE_GET_PTR(val)); self.handle_exception(val) } @@ -344,8 +346,8 @@ impl<'js> Ctx<'js> { } } - // Creates promise and resolving functions. - pub fn promise(&self) -> Result<(Object<'js>, Function<'js>, Function<'js>)> { + /// Creates javascipt promise along with its reject and resolve functions. + pub fn promise(&self) -> Result<(Promise<'js>, Function<'js>, Function<'js>)> { let mut funcs = mem::MaybeUninit::<(qjs::JSValue, qjs::JSValue)>::uninit(); Ok(unsafe { @@ -353,15 +355,26 @@ impl<'js> Ctx<'js> { self.ctx.as_ptr(), funcs.as_mut_ptr() as _, ))?; - let (then, catch) = funcs.assume_init(); + let (resolve, reject) = funcs.assume_init(); ( - Object::from_js_value(self.clone(), promise), - Function::from_js_value(self.clone(), then), - Function::from_js_value(self.clone(), catch), + Promise::from_js_value(self.clone(), promise), + Function::from_js_value(self.clone(), resolve), + Function::from_js_value(self.clone(), reject), ) }) } + /// Executes a quickjs job. + /// + /// Returns wether a job was actually executed. + /// If this function returned false, no job was pending. + pub fn execute_pending_job(&self) -> bool { + let mut ptr = MaybeUninit::<*mut qjs::JSContext>::uninit(); + let rt = unsafe { qjs::JS_GetRuntime(self.ctx.as_ptr()) }; + let res = unsafe { qjs::JS_ExecutePendingJob(rt, ptr.as_mut_ptr()) }; + res != 0 + } + pub(crate) unsafe fn get_opaque(&self) -> *mut Opaque<'js> { let rt = qjs::JS_GetRuntime(self.ctx.as_ptr()); qjs::JS_GetRuntimeOpaque(rt).cast::() @@ -407,18 +420,20 @@ impl<'js> Ctx<'js> { self.ctx } - /// Frees modules which aren't evaluated. - /// - /// When a module is compiled and the compilation results in an error the module can already - /// have resolved several modules. Originally QuickJS freed all these module when compiling - /// (but not when a it was dynamically imported), this library patched that behavior out - /// because it proved to be hard to make safe. This function will free those modules. - /// - /// # Safety - /// Caller must ensure that this method is not called from a module being evaluated. + /* + // Frees modules which aren't evaluated. + // + // When a module is compiled and the compilation results in an error the module can already + // have resolved several modules. Originally QuickJS freed all these module when compiling + // (but not when a it was dynamically imported), this library patched that behavior out + // because it proved to be hard to make safe. This function will free those modules. + // + // # Safety + // Caller must ensure that this method is not called from a module being evaluated. pub unsafe fn free_unevaluated_modules(&self) { qjs::JS_FreeUnevaluatedModules(self.ctx.as_ptr()) } + */ } #[cfg(test)] @@ -440,6 +455,19 @@ mod test { }); } + #[test] + fn compile_minimal_test() { + use crate::{Context, Runtime}; + + let runtime = Runtime::new().unwrap(); + let ctx = Context::full(&runtime).unwrap(); + ctx.with(|ctx| { + let module = ctx.compile("test", "export let foo = 1 + 1;").unwrap(); + let v: i32 = module.get("foo").unwrap(); + assert_eq!(v, 2) + }) + } + #[test] fn eval() { use crate::{Context, Runtime}; @@ -464,6 +492,18 @@ mod test { }) } + #[test] + fn eval_minimal_test() { + use crate::{Context, Runtime}; + + let runtime = Runtime::new().unwrap(); + let ctx = Context::full(&runtime).unwrap(); + ctx.with(|ctx| { + let res: i32 = ctx.eval(" 1 + 1 ").unwrap(); + assert_eq!(2, res); + }) + } + #[test] #[should_panic(expected = "'foo' is not defined")] fn eval_with_sloppy_code() { diff --git a/core/src/lib.rs b/core/src/lib.rs index 73651b974..aa24d282b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -59,9 +59,9 @@ mod persistent; mod value; pub use persistent::{Outlive, Persistent}; pub use value::{ - array, atom, convert, function, module, object, Array, Atom, BigInt, Coerced, Exception, - Filter, FromAtom, FromIteratorJs, FromJs, Function, IntoAtom, IntoJs, IteratorJs, Module, Null, - Object, String, Symbol, Type, Undefined, Value, + array, atom, convert, function, module, object, promise, Array, Atom, BigInt, Coerced, + Exception, Filter, FromAtom, FromIteratorJs, FromJs, Function, IntoAtom, IntoJs, IteratorJs, + Module, Null, Object, Promise, String, Symbol, Type, Undefined, Value, }; pub mod class; @@ -73,10 +73,6 @@ pub use value::{ArrayBuffer, TypedArray}; pub(crate) use std::{result::Result as StdResult, string::String as StdString}; -#[cfg(feature = "futures")] -#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))] -pub mod promise; - #[cfg(feature = "allocator")] #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "allocator")))] pub mod allocator; diff --git a/core/src/promise.rs b/core/src/promise.rs deleted file mode 100644 index 3e2c2735a..000000000 --- a/core/src/promise.rs +++ /dev/null @@ -1,258 +0,0 @@ -//! Utilities for converting promises to futures and vice versa. - -use std::{ - cell::Cell, - future::Future, - pin::Pin, - task::{Context as TaskContext, Poll, Waker}, -}; - -use crate::{ - atom::PredefinedAtom, function::This, qjs, safe_ref::Ref, CatchResultExt, CaughtError, - CaughtResult, Ctx, Exception, FromJs, Function, IntoJs, Object, Result, ThrowResultExt, Value, -}; - -/// Future-aware promise -#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))] -pub struct Promise<'js, T> { - state: Ref>, - promise: Object<'js>, -} - -struct State<'js, T> { - waker: Cell>, - result: Cell>>, -} - -impl<'js, T: 'js> State<'js, T> { - fn poll(&self, waker: Waker) -> Option { - self.waker.replace(Some(waker)) - } - - fn take_result(&self) -> Option> { - self.result.take() - } - - fn resolve(&self, result: CaughtResult<'js, T>) { - self.result.set(Some(result)); - self.waker - .take() - .expect("promise resolved before being polled") - .wake(); - } -} - -unsafe impl<'js, T> Send for State<'js, T> {} -unsafe impl<'js, T> Sync for State<'js, T> {} - -impl<'js, T> FromJs<'js> for Promise<'js, T> -where - T: FromJs<'js> + 'js, -{ - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let promise = Object::from_js(ctx, value)?; - let state = Ref::new(State { - waker: Cell::new(None), - result: Cell::new(None), - }); - - Ok(Promise { state, promise }) - } -} - -impl<'js, T> Future for Promise<'js, T> -where - T: FromJs<'js> + 'js, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut TaskContext) -> Poll { - let ctx = self.promise.ctx(); - if let Some(x) = self.state.take_result() { - return Poll::Ready(x.throw(ctx)); - } - - if self.state.poll(cx.waker().clone()).is_none() { - let then: Function = self.promise.get(PredefinedAtom::Then)?; - let state = self.state.clone(); - let resolve = Function::new(ctx.clone(), move |ctx: Ctx<'js>, value: Value<'js>| { - let t = T::from_js(&ctx, value).catch(&ctx); - state.resolve(t); - }); - let state = self.state.clone(); - let reject = Function::new(ctx.clone(), move |value: Value<'js>| { - let e = - if let Some(e) = value.clone().into_object().and_then(Exception::from_object) { - CaughtError::Exception(e) - } else { - CaughtError::Value(value) - }; - state.resolve(Err(e)) - }); - then.call((This(self.promise.clone()), resolve, reject))?; - }; - Poll::Pending - } -} - -/// Wrapper for futures to convert to JS promises -#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))] -#[repr(transparent)] -pub struct Promised(pub T); - -impl From for Promised { - fn from(future: T) -> Self { - Self(future) - } -} - -impl<'js, T, R> IntoJs<'js> for Promised -where - T: Future + 'js, - R: IntoJs<'js> + 'js, -{ - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - let (promise, resolve, reject) = ctx.promise()?; - let ctx_clone = ctx.clone(); - - let future = async move { - let err = match self.0.await.into_js(&ctx_clone).catch(&ctx_clone) { - Ok(x) => resolve.call::<_, ()>((x,)), - Err(e) => match e { - CaughtError::Exception(e) => reject.call::<_, ()>((e,)), - CaughtError::Value(e) => reject.call::<_, ()>((e,)), - CaughtError::Error(e) => { - let is_exception = unsafe { qjs::JS_IsException(e.throw(&ctx_clone)) }; - debug_assert!(is_exception); - let e = ctx_clone.catch(); - reject.call::<_, ()>((e,)) - } - }, - }; - // TODO figure out something better to do here. - if let Err(e) = err { - println!("promise handle function returned error:{}", e); - } - }; - ctx.spawn(future); - Ok(promise.into_value()) - } -} - -#[cfg(test)] -mod test { - use std::time::Duration; - - use super::*; - use crate::{ - async_with, - function::{Async, Func}, - AsyncContext, AsyncRuntime, - }; - - async fn set_timeout<'js>(cb: Function<'js>, number: f64) -> Result<()> { - tokio::time::sleep(Duration::from_secs_f64(number / 1000.0)).await; - cb.call::<_, ()>(()) - } - - #[tokio::test] - async fn promise() { - let rt = AsyncRuntime::new().unwrap(); - let ctx = AsyncContext::full(&rt).await.unwrap(); - - async_with!(ctx => |ctx| { - ctx.globals().set("setTimeout",Func::from(Async(set_timeout))).unwrap(); - - let func = ctx - .eval::( - r" - (function(){ - return new Promise((resolve) => { - setTimeout(x => { - resolve(42) - },100) - }) - }) - ", - ) - .catch(&ctx) - .unwrap(); - let promise: Promise = func.call(()).unwrap(); - assert_eq!(promise.await.catch(&ctx).unwrap(), 42); - - let func = ctx - .eval::( - r" - (function(){ - return new Promise((_,reject) => { - setTimeout(x => { - reject(42) - },100) - }) - }) - ", - ) - .catch(&ctx) - .unwrap(); - let promise: Promise<()> = func.call(()).unwrap(); - let err = promise.await.catch(&ctx); - match err { - Err(CaughtError::Value(v)) => { - assert_eq!(v.as_int().unwrap(), 42) - } - _ => panic!(), - } - }) - .await - } - - #[tokio::test] - async fn promised() { - let rt = AsyncRuntime::new().unwrap(); - let ctx = AsyncContext::full(&rt).await.unwrap(); - - async_with!(ctx => |ctx| { - let promised = Promised::from(async { - tokio::time::sleep(Duration::from_millis(100)).await; - 42 - }); - - let function = ctx.eval::(r" - (async function(v){ - let val = await v; - if(val !== 42){ - throw new Error('not correct value') - } - }) - ").catch(&ctx).unwrap(); - - function.call::<_,Promise<()>>((promised,)).unwrap().await.unwrap(); - - let ctx_clone = ctx.clone(); - let promised = Promised::from(async move { - tokio::time::sleep(Duration::from_millis(100)).await; - Result::<()>::Err(Exception::throw_message(&ctx_clone, "some_message")) - }); - - let function = ctx.eval::(r" - (async function(v){ - try{ - await v; - }catch(e) { - if (e.message !== 'some_message'){ - throw new Error('wrong error') - } - return - } - throw new Error('no error thrown') - }) - ") - .catch(&ctx) - .unwrap(); - - - function.call::<_,Promise<()>>((promised,)).unwrap().await.unwrap() - }) - .await - } -} diff --git a/core/src/result.rs b/core/src/result.rs index d29f076e7..d0b84f908 100644 --- a/core/src/result.rs +++ b/core/src/result.rs @@ -117,11 +117,13 @@ pub enum Error { name: StdString, message: Option, }, - #[cfg(feature = "array-buffer")] AsSlice(AsSliceError), /// Error when restoring a Persistent in a runtime other than the original runtime. UnrelatedRuntime, + /// An error returned by a blocked on promise if block on the promise would result in a dead + /// lock. + WouldBlock, /// An error from QuickJS from which the specifics are unknown. /// Should eventually be removed as development progresses. Unknown, @@ -453,6 +455,7 @@ impl Display for Error { "Error borrowing function: ".fmt(f)?; x.fmt(f)?; } + WouldBlock => "Error blocking on a promise resulted in a dead lock".fmt(f)?, #[cfg(feature = "array-buffer")] AsSlice(x) => { "Could not convert array buffer to slice: ".fmt(f)?; diff --git a/core/src/runtime/base.rs b/core/src/runtime/base.rs index 1395614fc..0aea2a2ae 100644 --- a/core/src/runtime/base.rs +++ b/core/src/runtime/base.rs @@ -160,7 +160,9 @@ impl Runtime { /// Returns true when job was executed or false when queue is empty or error when exception thrown under execution. #[inline] pub fn execute_pending_job(&self) -> StdResult { - self.inner.lock().execute_pending_job().map_err(|e| { + let mut lock = self.inner.lock(); + lock.update_stack_top(); + lock.execute_pending_job().map_err(|e| { JobException(unsafe { Context::from_raw( NonNull::new(e).expect("QuickJS returned null ptr for job error"), diff --git a/core/src/runtime/raw.rs b/core/src/runtime/raw.rs index ebbc5ac46..8c381778c 100644 --- a/core/src/runtime/raw.rs +++ b/core/src/runtime/raw.rs @@ -155,7 +155,6 @@ impl RawRuntime { pub fn execute_pending_job(&mut self) -> StdResult { let mut ctx_ptr = mem::MaybeUninit::<*mut qjs::JSContext>::uninit(); - self.update_stack_top(); let result = unsafe { qjs::JS_ExecutePendingJob(self.rt.as_ptr(), ctx_ptr.as_mut_ptr()) }; if result == 0 { // no jobs executed diff --git a/core/src/runtime/schedular/atomic_waker.rs b/core/src/runtime/schedular/atomic_waker.rs index cc593c618..6b2efffff 100644 --- a/core/src/runtime/schedular/atomic_waker.rs +++ b/core/src/runtime/schedular/atomic_waker.rs @@ -10,10 +10,6 @@ use core::task::Waker; use atomic::AtomicUsize; use atomic::Ordering::{AcqRel, Acquire, Release}; -#[cfg(feature = "portable-atomic")] -use portable_atomic as atomic; - -#[cfg(not(feature = "portable-atomic"))] use core::sync::atomic; /// A synchronization primitive for task wakeup. diff --git a/core/src/value.rs b/core/src/value.rs index ced353858..675b592d7 100644 --- a/core/src/value.rs +++ b/core/src/value.rs @@ -9,6 +9,7 @@ pub(crate) mod exception; pub mod function; pub mod module; pub mod object; +pub mod promise; mod string; mod symbol; @@ -20,6 +21,7 @@ pub use exception::Exception; pub use function::{Constructor, Function}; pub use module::Module; pub use object::{Filter, Object}; +pub use promise::Promise; pub use string::String; pub use symbol::Symbol; @@ -99,7 +101,7 @@ impl<'js> fmt::Debug for Value<'js> { unsafe { self.ref_string() }.to_string().fmt(f)?; write!(f, ")")?; } - Symbol | Object | Array | Function | Constructor => { + Symbol | Object | Array | Function | Constructor | Promise => { write!(f, "(")?; unsafe { self.get_ptr() }.fmt(f)?; write!(f, ")")?; @@ -124,11 +126,13 @@ impl<'js> Value<'js> { // unsafe because the value must belong the context and the lifetime must be constrained by its lifetime #[inline] pub(crate) unsafe fn from_js_value(ctx: Ctx<'js>, value: qjs::JSValue) -> Self { + dbg!(qjs::JS_VALUE_GET_PTR(value)); Self { ctx, value } } #[inline] pub(crate) unsafe fn from_js_value_const(ctx: Ctx<'js>, value: qjs::JSValueConst) -> Self { + dbg!(qjs::JS_VALUE_GET_PTR(value)); let value = qjs::JS_DupValue(value); Self { ctx, value } } @@ -359,6 +363,12 @@ impl<'js> Value<'js> { 0 != unsafe { qjs::JS_IsConstructor(self.ctx.as_ptr(), self.value) } } + /// Check if the value is a promise. + #[inline] + pub fn is_promise(&self) -> bool { + (unsafe { qjs::JS_PromiseState(self.ctx.as_ptr(), self.value) } > 0) + } + /// Check if the value is an exception #[inline] pub fn is_exception(&self) -> bool { @@ -495,6 +505,7 @@ macro_rules! type_impls { (@cond Array $self:expr) => { $self.is_array() }; (@cond Constructor $self:expr) => { $self.is_constructor() }; (@cond Function $self:expr) => { $self.is_function() }; + (@cond Promise $self:expr) => { $self.is_promise() }; (@cond Exception $self:expr) => { $self.is_error() }; (@cond $type:ident $self:expr) => { true }; } @@ -511,6 +522,7 @@ type_impls! { Array: array => JS_TAG_OBJECT, Constructor: constructor => JS_TAG_OBJECT, Function: function => JS_TAG_OBJECT, + Promise: promise => JS_TAG_OBJECT, Exception: exception => JS_TAG_OBJECT, Object: object => JS_TAG_OBJECT, Module: module => JS_TAG_MODULE, @@ -700,6 +712,7 @@ sub_types! { Object->Value as_object ref_object into_object try_into_object from_object, Function->Object->Value as_function ref_function into_function try_into_function from_function, Constructor->Function->Object->Value as_constructor ref_constructor into_constructor try_into_constructor from_constructor, + Promise->Object->Value as_promise ref_promise into_promise try_into_promise from_promise, Array->Object->Value as_array ref_array into_array try_into_array from_array, Exception->Object->Value as_exception ref_exception into_exception try_into_exception from_exception, BigInt->Value as_big_int ref_big_int into_big_int try_into_big_int from_big_int, diff --git a/core/src/value/exception.rs b/core/src/value/exception.rs index df122bc4f..a1babefc2 100644 --- a/core/src/value/exception.rs +++ b/core/src/value/exception.rs @@ -1,4 +1,4 @@ -use std::{error::Error as ErrorTrait, ffi::CStr, fmt, usize}; +use std::{error::Error as ErrorTrait, ffi::CStr, fmt}; use crate::{atom::PredefinedAtom, convert::Coerced, qjs, Ctx, Error, Object, Result, Value}; diff --git a/core/src/value/function/types.rs b/core/src/value/function/types.rs index 8a5c5f4a6..038cac507 100644 --- a/core/src/value/function/types.rs +++ b/core/src/value/function/types.rs @@ -64,6 +64,7 @@ pub struct Flat(pub T); /// Helper type for making an parameter set exhaustive. pub struct Exhaustive; +#[cfg(feature = "futures")] /// Helper type for creating a function from a closure which returns a future. pub struct Async(pub T); diff --git a/core/src/value/module.rs b/core/src/value/module.rs index 1555f0368..c2527647b 100644 --- a/core/src/value/module.rs +++ b/core/src/value/module.rs @@ -668,10 +668,13 @@ impl<'js> Module<'js> { let flag = qjs::JS_EVAL_TYPE_MODULE | qjs::JS_EVAL_FLAG_STRICT | qjs::JS_EVAL_FLAG_COMPILE_ONLY; - let module = unsafe { ctx.eval_raw(source, name.as_c_str(), flag as i32)? }; - let module = ctx.handle_exception(module)?; - debug_assert_eq!(qjs::JS_TAG_MODULE, unsafe { qjs::JS_VALUE_GET_TAG(module) }); - let module = qjs::JS_VALUE_GET_PTR(module).cast::(); + let module_val = unsafe { ctx.eval_raw(source, name.as_c_str(), flag as i32)? }; + let module_val = ctx.handle_exception(module_val)?; + debug_assert_eq!(qjs::JS_TAG_MODULE, unsafe { + qjs::JS_VALUE_GET_TAG(module_val) + }); + let module = qjs::JS_VALUE_GET_PTR(module_val).cast::(); + qjs::JS_FreeValue(ctx.as_ptr(), module_val); // QuickJS should throw an exception on allocation errors // So this should always be non-null. let module = NonNull::new(module).unwrap(); @@ -756,6 +759,7 @@ impl<'js> Module<'js> { Ok(()) } + /* /// Import and evaluate a module /// /// This will work similar to an `await import(specifier)` statement in JavaScript but will return the import and not a promise @@ -768,6 +772,7 @@ impl<'js> Module<'js> { specifier.len() as qjs::size_t, ); let js_string = qjs::JS_ToCString(ctx.as_ptr(), js_string_val); + let va = qjs::JS_LoadModule(ctx, basename, filename) let val = qjs::JS_DynamicImportSync(ctx.as_ptr(), js_string); qjs::JS_FreeValue(ctx.as_ptr(), js_string_val); let val = ctx.handle_exception(val)?; @@ -776,6 +781,7 @@ impl<'js> Module<'js> { V::from_js(ctx, val) } + */ } #[cfg(feature = "exports")] @@ -986,6 +992,7 @@ mod test { }) } + /* #[test] fn import() { test_with(|ctx| { @@ -997,6 +1004,7 @@ mod test { }) } + #[test] #[should_panic(expected = "kaboom")] fn import_crashing() { @@ -1009,6 +1017,7 @@ mod test { let _: Value = Module::import(&ctx, "bad_rust_mod").catch(&ctx).unwrap(); }); } + */ #[test] fn holding_onto_unevaluated() { diff --git a/core/src/value/promise.rs b/core/src/value/promise.rs new file mode 100644 index 000000000..7b15880bc --- /dev/null +++ b/core/src/value/promise.rs @@ -0,0 +1,220 @@ +//! Javascript promises and future integration. +#[cfg(feature = "futures")] +use std::{ + cell::RefCell, + future::Future, + marker::PhantomData, + pin::Pin, + rc::Rc, + task::{Context as TaskContext, Poll, Waker}, +}; + +use crate::{atom::PredefinedAtom, qjs, Ctx, Error, FromJs, Function, Result, Value}; +#[cfg(feature = "futures")] +use crate::{function::This, CatchResultExt, CaughtError, IntoJs}; + +use super::Object; + +/// The execution state of a promise. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum PromiseState { + /// The promise has not yet completed. + Pending, + /// The promise completed succefully. + Resolved, + /// The promise completed with an error. + Rejected, +} + +/// A JavaScript promise. +#[derive(Debug, PartialEq, Clone, Hash, Eq)] +#[repr(transparent)] +pub struct Promise<'js>(pub(crate) Object<'js>); + +impl<'js> Promise<'js> { + #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))] + #[cfg(feature = "futures")] + pub fn wrap_future(ctx: &Ctx<'js>, future: F) -> Result + where + F: Future + 'js, + R: IntoJs<'js>, + { + let (promise, resolve, reject) = ctx.promise()?; + let ctx_clone = ctx.clone(); + let future = async move { + let err = match future.await.into_js(&ctx_clone).catch(&ctx_clone) { + Ok(x) => resolve.call::<_, ()>((x,)), + Err(e) => match e { + CaughtError::Exception(e) => reject.call::<_, ()>((e,)), + CaughtError::Value(e) => reject.call::<_, ()>((e,)), + CaughtError::Error(e) => { + let is_exception = unsafe { qjs::JS_IsException(e.throw(&ctx_clone)) }; + debug_assert!(is_exception); + let e = ctx_clone.catch(); + reject.call::<_, ()>((e,)) + } + }, + }; + // TODO figure out something better to do here. + if let Err(e) = err { + println!("promise handle function returned error:{}", e); + } + }; + ctx.spawn(future); + Ok(promise) + } + + /// Create a new JavaScript promise along with its resolve and reject functions. + pub fn new(ctx: &Ctx<'js>) -> Result<(Self, Function<'js>, Function<'js>)> { + ctx.promise() + } + + /// Returns the state of the promise, either pending,resolved or rejected. + pub fn state(&self) -> PromiseState { + let v = unsafe { qjs::JS_PromiseState(self.ctx().as_ptr(), self.as_js_value()) }; + match v { + qjs::JSPromiseStateEnum_JS_PROMISE_PENDING => PromiseState::Pending, + qjs::JSPromiseStateEnum_JS_PROMISE_FULFILLED => PromiseState::Resolved, + qjs::JSPromiseStateEnum_JS_PROMISE_REJECTED => PromiseState::Rejected, + _ => unreachable!(), + } + } + + /// Returns the `then` function, used for chaining promises. + pub fn then(&self) -> Result> { + self.get(PredefinedAtom::Then) + } + + /// Returns the `catch` function, used for retrieving the result of a rejected promise. + pub fn catch(&self) -> Result> { + self.get(PredefinedAtom::Catch) + } + + /// Returns the result of the future if there is one. + /// + /// Returns None if the promise has not yet been completed, Ok if the promise was resolved, and + /// [`Error::Exception`] if the promise rejected with the rejected value as the thrown + /// value retrievable via [`Ctx::catch`]. + pub fn result>(&self) -> Option> { + match self.state() { + PromiseState::Pending => None, + PromiseState::Resolved => { + let v = unsafe { qjs::JS_PromiseResult(self.ctx().as_ptr(), self.as_js_value()) }; + let v = unsafe { Value::from_js_value(self.ctx().clone(), v) }; + Some(FromJs::from_js(self.ctx(), v)) + } + PromiseState::Rejected => { + unsafe { + let v = qjs::JS_PromiseResult(self.ctx().as_ptr(), self.as_js_value()); + qjs::JS_Throw(self.ctx().as_ptr(), v); + }; + Some(Err(Error::Exception)) + } + } + } + + /// Runs the quickjs job queue until the promise is either rejected or resolved. + /// + /// If blocking on the promise would result in blocking, i.e. when the job queue runs out of + /// jobs before the promise can be resolved, this function returns [`Error::WouldBlock`] + /// indicating that no more work can be done at the momement. + /// + /// This function only drives the quickjs job queue, futures are not polled. + pub fn finish>(&self) -> Result { + loop { + if let Some(x) = self.result() { + return x; + } + + if !self.ctx.execute_pending_job() { + return Err(Error::WouldBlock); + } + } + } + + /// Wrap the promise into a struct which can be polled as a rust future. + #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))] + #[cfg(feature = "futures")] + pub fn into_future(self) -> PromiseFuture<'js, T> + where + T: FromJs<'js>, + { + PromiseFuture { + state: None, + promise: self, + _marker: PhantomData, + } + } +} + +/// Future-aware promise +#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))] +#[cfg(feature = "futures")] +pub struct PromiseFuture<'js, T> { + state: Option>>, + promise: Promise<'js>, + _marker: PhantomData, +} + +#[cfg(feature = "futures")] +impl<'js, T> Future for PromiseFuture<'js, T> +where + T: FromJs<'js>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll { + // Nothing is actually pinned so this is safe. + let this = unsafe { self.get_unchecked_mut() }; + + if let Some(x) = this.promise.result() { + return Poll::Ready(x); + } + + if this.state.is_none() { + let inner = Rc::new(RefCell::new(cx.waker().clone())); + this.state = Some(inner.clone()); + + let resolve = Function::new(this.promise.ctx.clone(), move || { + inner.borrow().wake_by_ref(); + })?; + + this.promise + .then()? + .call((This(this.promise.clone()), resolve.clone(), resolve))?; + return Poll::Pending; + } + + this.state + .as_ref() + .unwrap() + .borrow_mut() + .clone_from(cx.waker()); + + Poll::Pending + } +} + +/// Wrapper for futures to convert to JS promises +#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))] +#[repr(transparent)] +#[cfg(feature = "futures")] +pub struct Promised(pub T); + +#[cfg(feature = "futures")] +impl From for Promised { + fn from(future: T) -> Self { + Self(future) + } +} + +#[cfg(feature = "futures")] +impl<'js, T, R> IntoJs<'js> for Promised +where + T: Future + 'js, + R: IntoJs<'js> + 'js, +{ + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + Promise::wrap_future(ctx, self.0).map(|x| x.into_value()) + } +} diff --git a/sys/build.rs b/sys/build.rs index 7b38c1926..314f90c69 100644 --- a/sys/build.rs +++ b/sys/build.rs @@ -148,7 +148,7 @@ fn main() { "get_function_proto.patch", "check_stack_overflow.patch", "infinity_handling.patch", - "dynamic_import_sync.patch", + //"dynamic_import_sync.patch", ]; let mut defines = vec![ diff --git a/sys/src/bindings/x86_64-unknown-linux-gnu.rs b/sys/src/bindings/x86_64-unknown-linux-gnu.rs index 412fe6964..e05151390 100644 --- a/sys/src/bindings/x86_64-unknown-linux-gnu.rs +++ b/sys/src/bindings/x86_64-unknown-linux-gnu.rs @@ -1925,9 +1925,6 @@ extern "C" { extern "C" { pub fn JS_GetModuleNamespace(ctx: *mut JSContext, m: *mut JSModuleDef) -> JSValue; } -extern "C" { - pub fn JS_FreeUnevaluatedModules(ctx: *mut JSContext); -} pub type JSJobFunc = ::std::option::Option< unsafe extern "C" fn( ctx: *mut JSContext, @@ -2664,12 +2661,6 @@ extern "C" { len: ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } -extern "C" { - pub fn JS_DynamicImportSync( - ctx: *mut JSContext, - specifier: *const ::std::os::raw::c_char, - ) -> JSValue; -} extern "C" { pub fn JS_GetModuleExport( ctx: *mut JSContext, diff --git a/sys/src/inlines/common.rs b/sys/src/inlines/common.rs index b9416ee43..4be3dbbf7 100644 --- a/sys/src/inlines/common.rs +++ b/sys/src/inlines/common.rs @@ -171,7 +171,7 @@ pub unsafe fn JS_SetProperty( prop: JSAtom, val: JSValue, ) -> i32 { - JS_SetPropertyInternal(ctx, this_obj, prop, val, JS_PROP_THROW as i32) + JS_SetPropertyInternal(ctx, this_obj, prop, val, this_obj, JS_PROP_THROW as i32) } #[inline]