//! Types and functions for working with Ruby's Fiber class. use std::{fmt, mem::size_of, os::raw::c_int, slice}; #[cfg(ruby_lt_3_2)] use rb_sys::rb_fiber_new; #[cfg(ruby_gte_3_2)] use rb_sys::rb_fiber_new_storage; use rb_sys::{ rb_data_typed_object_wrap, rb_fiber_alive_p, rb_fiber_current, rb_fiber_resume_kw, rb_fiber_yield_kw, VALUE, }; #[cfg(ruby_gte_3_1)] use rb_sys::{rb_fiber_raise, rb_fiber_transfer_kw, rb_obj_is_fiber}; #[cfg(any(ruby_gte_3_2, docsrs))] use crate::r_hash::RHash; use crate::{ api::Ruby, block::Proc, data_type_builder, error::{protect, Error}, exception::Exception, gc, into_value::{kw_splat, ArgList, IntoValue}, method::{Block, BlockReturn}, object::Object, r_typed_data::RTypedData, try_convert::TryConvert, typed_data::{DataType, DataTypeFunctions}, value::{ private::{self, ReprValue as _}, ReprValue, Value, QUNDEF, }, }; /// # `Fiber` /// /// Functions to create and work with Ruby `Fiber`s. /// /// See also the [`Fiber`] type. impl Ruby { /// Create a Ruby Fiber. /// /// As `func` is a function pointer, only functions and closures that do /// not capture any variables are permitted. For more flexibility (at the /// cost of allocating) see [`fiber_new_from_fn`](Ruby::fiber_new_from_fn). /// /// # Examples /// /// ``` /// use magnus::{prelude::*, rb_assert, Error, Ruby, Value}; /// /// fn example(ruby: &Ruby) -> Result<(), Error> { /// let fib = ruby.fiber_new(Default::default(), |ruby, args, _block| { /// let mut a = u64::try_convert(*args.get(0).unwrap())?; /// let mut b = u64::try_convert(*args.get(1).unwrap())?; /// while let Some(c) = a.checked_add(b) { /// let _: Value = ruby.fiber_yield((c,))?; /// a = b; /// b = c; /// } /// Ok(()) /// })?; /// /// rb_assert!(ruby, "fib.resume(0, 1) == 1", fib); /// rb_assert!(ruby, "fib.resume == 2", fib); /// rb_assert!(ruby, "fib.resume == 3", fib); /// rb_assert!(ruby, "fib.resume == 5", fib); /// rb_assert!(ruby, "fib.resume == 8", fib); /// /// Ok(()) /// } /// # Ruby::init(example).unwrap() /// ``` pub fn fiber_new( &self, storage: Storage, func: fn(&Ruby, &[Value], Option) -> R, ) -> Result where R: BlockReturn, { unsafe extern "C" fn call( _yielded_arg: VALUE, callback_arg: VALUE, argc: c_int, argv: *const VALUE, blockarg: VALUE, ) -> VALUE where R: BlockReturn, { let func = std::mem::transmute::) -> R>(callback_arg); func.call_handle_error(argc, argv as *const Value, Value::new(blockarg)) .as_rb_value() } let call_func = call:: as unsafe extern "C" fn(VALUE, VALUE, c_int, *const VALUE, VALUE) -> VALUE; unsafe { protect(|| { #[cfg(ruby_gte_3_2)] let value = rb_fiber_new_storage(Some(call_func), func as VALUE, storage.as_rb_value()); #[cfg(ruby_lt_3_2)] let value = rb_fiber_new(Some(call_func), func as VALUE); Fiber::from_rb_value_unchecked(value) }) } } /// Create a Ruby Fiber. /// /// See also [`fiber_new`](Ruby::fiber_new), which is more efficient when /// `func` is a function or closure that does not capture any variables. /// /// # Examples /// /// ``` /// use magnus::{rb_assert, Error, Ruby, Value}; /// /// fn example(ruby: &Ruby) -> Result<(), Error> { /// let mut a = 0_u64; /// let mut b = 1_u64; /// /// let fib = ruby.fiber_new_from_fn(Default::default(), move |ruby, _args, _block| { /// while let Some(c) = a.checked_add(b) { /// let _: Value = ruby.fiber_yield((c,))?; /// a = b; /// b = c; /// } /// Ok(()) /// })?; /// /// rb_assert!(ruby, "fib.resume == 1", fib); /// rb_assert!(ruby, "fib.resume == 2", fib); /// rb_assert!(ruby, "fib.resume == 3", fib); /// rb_assert!(ruby, "fib.resume == 5", fib); /// rb_assert!(ruby, "fib.resume == 8", fib); /// /// Ok(()) /// } /// # Ruby::init(example).unwrap() /// ``` pub fn fiber_new_from_fn(&self, storage: Storage, func: F) -> Result where F: 'static + Send + FnOnce(&Ruby, &[Value], Option) -> R, R: BlockReturn, { unsafe extern "C" fn call( _yielded_arg: VALUE, callback_arg: VALUE, argc: c_int, argv: *const VALUE, blockarg: VALUE, ) -> VALUE where F: FnOnce(&Ruby, &[Value], Option) -> R, R: BlockReturn, { let closure = (*(callback_arg as *mut Option)).take().unwrap(); closure .call_handle_error(argc, argv as *const Value, Value::new(blockarg)) .as_rb_value() } let (closure, keepalive) = wrap_closure(func); let call_func = call:: as unsafe extern "C" fn(VALUE, VALUE, c_int, *const VALUE, VALUE) -> VALUE; protect(|| { #[cfg(ruby_gte_3_2)] let fiber = unsafe { Fiber::from_rb_value_unchecked(rb_fiber_new_storage( Some(call_func), closure as VALUE, storage.as_rb_value(), )) }; #[cfg(ruby_lt_3_2)] let fiber = unsafe { Fiber::from_rb_value_unchecked(rb_fiber_new(Some(call_func), closure as VALUE)) }; // ivar without @ prefix is invisible from Ruby fiber.ivar_set("__rust_closure", keepalive).unwrap(); fiber }) } /// Return the currently executing Fiber. /// /// # Examples /// /// ``` /// use magnus::{rb_assert, Error, Ruby}; /// /// fn example(ruby: &Ruby) -> Result<(), Error> { /// let fiber = ruby.fiber_current(); /// /// rb_assert!(ruby, "fiber.is_a?(Fiber)", fiber); /// /// Ok(()) /// } /// # Ruby::init(example).unwrap() /// ``` pub fn fiber_current(&self) -> Fiber { unsafe { Fiber::from_rb_value_unchecked(rb_fiber_current()) } } /// Transfer execution back to where the current Fiber was resumed. /// /// # Examples /// /// ``` /// use magnus::{rb_assert, value::Opaque, Error, Ruby, Value}; /// /// fn example(ruby: &Ruby) -> Result<(), Error> { /// let ary = ruby.ary_new(); /// let send_array = Opaque::from(ary); /// /// let fiber = ruby.fiber_new_from_fn(Default::default(), move |ruby, _args, _block| { /// let ary = ruby.get_inner(send_array); /// ary.push(1)?; /// let _: Value = ruby.fiber_yield(())?; /// ary.push(2)?; /// let _: Value = ruby.fiber_yield(())?; /// ary.push(3)?; /// let _: Value = ruby.fiber_yield(())?; /// Ok(()) /// })?; /// /// ary.push("a")?; /// let _: Value = fiber.resume(())?; /// ary.push("b")?; /// let _: Value = fiber.resume(())?; /// ary.push("c")?; /// let _: Value = fiber.resume(())?; /// /// rb_assert!(ruby, r#"ary == ["a", 1, "b", 2, "c", 3]"#, ary); /// /// Ok(()) /// } /// # Ruby::init(example).unwrap() /// ``` pub fn fiber_yield(&self, args: A) -> Result where A: ArgList, T: TryConvert, { let kw_splat = kw_splat(&args); let args = args.into_arg_list_with(self); let slice = args.as_ref(); unsafe { protect(|| { Value::new(rb_fiber_yield_kw( slice.len() as c_int, slice.as_ptr() as *const VALUE, kw_splat as c_int, )) }) .and_then(TryConvert::try_convert) } } } /// Wrapper type for a Value known to be an instance of Ruby's Fiber class. /// /// See the [`ReprValue`] and [`Object`] traits for additional methods /// available on this type. See [`Ruby`](Ruby#fiber) for methods to create a /// `Fiber` and [`Ruby::fiber_yield`]. #[derive(Clone, Copy)] #[repr(transparent)] pub struct Fiber(RTypedData); impl Fiber { /// Return `Some(Fiber)` if `val` is a `Fiber`, `None` otherwise. /// /// # Examples /// /// ``` /// use magnus::eval; /// # let _cleanup = unsafe { magnus::embed::init() }; /// /// assert!(magnus::Fiber::from_value(eval("Fiber.new {1 + 2}").unwrap()).is_some()); /// assert!(magnus::Fiber::from_value(eval("Thread.new {1 + 2}").unwrap()).is_none()); /// assert!(magnus::Fiber::from_value(eval("Proc.new {1 + 2}").unwrap()).is_none()); /// ``` #[inline] pub fn from_value(val: Value) -> Option { unsafe { Value::new(rb_obj_is_fiber(val.as_rb_value())) .to_bool() .then(|| Self::from_rb_value_unchecked(val.as_rb_value())) } } #[inline] pub(crate) unsafe fn from_rb_value_unchecked(val: VALUE) -> Self { Self(RTypedData::from_rb_value_unchecked(val)) } /// Return `true` if `self` can be resumed, `false` otherwise. /// /// # Examples /// /// ``` /// use magnus::{Error, Ruby, Value}; /// /// fn example(ruby: &Ruby) -> Result<(), Error> { /// let fiber = ruby.fiber_new(Default::default(), move |ruby, _args, _block| { /// let _: Value = ruby.fiber_yield((1,))?; /// let _: Value = ruby.fiber_yield((3,))?; /// Ok(5) /// })?; /// /// assert!(fiber.is_alive()); /// assert_eq!(fiber.resume::<_, u64>(())?, 1); /// assert!(fiber.is_alive()); /// assert_eq!(fiber.resume::<_, u64>(())?, 3); /// assert!(fiber.is_alive()); /// assert_eq!(fiber.resume::<_, u64>(())?, 5); /// assert!(!fiber.is_alive()); /// /// Ok(()) /// } /// # Ruby::init(example).unwrap() /// ``` pub fn is_alive(self) -> bool { unsafe { Value::new(rb_fiber_alive_p(self.as_rb_value())).to_bool() } } /// Resume the execution of `self`. /// /// # Examples /// /// ``` /// use magnus::{prelude::*, Error, Ruby, Value}; /// /// fn example(ruby: &Ruby) -> Result<(), Error> { /// let fib = ruby.fiber_new(Default::default(), |ruby, args, _block| { /// let mut a = u64::try_convert(*args.get(0).unwrap())?; /// let mut b = u64::try_convert(*args.get(1).unwrap())?; /// while let Some(c) = a.checked_add(b) { /// let _: Value = ruby.fiber_yield((c,))?; /// a = b; /// b = c; /// } /// Ok(()) /// })?; /// /// assert_eq!(fib.resume::<_, u64>((0, 1))?, 1); /// assert_eq!(fib.resume::<_, u64>(())?, 2); /// assert_eq!(fib.resume::<_, u64>(())?, 3); /// assert_eq!(fib.resume::<_, u64>(())?, 5); /// assert_eq!(fib.resume::<_, u64>(())?, 8); /// /// Ok(()) /// } /// # Ruby::init(example).unwrap() /// ``` pub fn resume(self, args: A) -> Result where A: ArgList, T: TryConvert, { let kw_splat = kw_splat(&args); let args = args.into_arg_list_with(&Ruby::get_with(self)); let slice = args.as_ref(); unsafe { protect(|| { Value::new(rb_fiber_resume_kw( self.as_rb_value(), slice.len() as c_int, slice.as_ptr() as *const VALUE, kw_splat as c_int, )) }) .and_then(TryConvert::try_convert) } } /// Transfer control to another Fiber. /// /// `transfer` is an alternate API to /// [`resume`](Fiber::resume)/[`yield`](Ruby::fiber_yield). The two APIs /// can not be mixed, a Fiber muse use *either* `transfer` or /// `resume`/`yield` to pass control. /// /// # Examples /// /// ``` /// use magnus::{Error, Fiber, Ruby, TryConvert, Value}; /// /// fn example(ruby: &Ruby) -> Result<(), Error> { /// let fib = ruby.fiber_new(Default::default(), |_ruby, args, _block| { /// let root = Fiber::try_convert(*args.get(0).unwrap())?; /// let mut a = u64::try_convert(*args.get(1).unwrap())?; /// let mut b = u64::try_convert(*args.get(2).unwrap())?; /// while let Some(c) = a.checked_add(b) { /// let _: Value = root.transfer((c,))?; /// a = b; /// b = c; /// } /// Ok(()) /// })?; /// /// assert_eq!(fib.transfer::<_, u64>((ruby.fiber_current(), 0, 1))?, 1); /// assert_eq!(fib.transfer::<_, u64>(())?, 2); /// assert_eq!(fib.transfer::<_, u64>(())?, 3); /// assert_eq!(fib.transfer::<_, u64>(())?, 5); /// assert_eq!(fib.transfer::<_, u64>(())?, 8); /// /// Ok(()) /// } /// # Ruby::init(example).unwrap() /// ``` pub fn transfer(self, args: A) -> Result where A: ArgList, T: TryConvert, { let kw_splat = kw_splat(&args); let args = args.into_arg_list_with(&Ruby::get_with(self)); let slice = args.as_ref(); unsafe { protect(|| { Value::new(rb_fiber_transfer_kw( self.as_rb_value(), slice.len() as c_int, slice.as_ptr() as *const VALUE, kw_splat as c_int, )) }) .and_then(TryConvert::try_convert) } } /// Resume the execution of `self`, raising the exception `e`. /// /// # Examples /// /// ``` /// use magnus::{prelude::*, Error, Ruby, Value}; /// /// fn example(ruby: &Ruby) -> Result<(), Error> { /// let fiber = ruby.fiber_new(Default::default(), move |ruby, _args, _block| { /// assert!(ruby.fiber_yield::<_, Value>(()).is_err()); /// })?; /// /// let _: Value = fiber.resume(())?; /// let _: Value = fiber.raise(ruby.exception_runtime_error().new_instance(("oh no!",))?)?; /// /// Ok(()) /// } /// # Ruby::init(example).unwrap() /// ``` pub fn raise(self, e: Exception) -> Result where T: TryConvert, { unsafe { protect(|| { Value::new(rb_fiber_raise( self.as_rb_value(), 1, &e.as_rb_value() as *const VALUE, )) }) .and_then(TryConvert::try_convert) } } } impl fmt::Display for Fiber { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", unsafe { self.to_s_infallible() }) } } impl fmt::Debug for Fiber { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.inspect()) } } impl IntoValue for Fiber { #[inline] fn into_value_with(self, _: &Ruby) -> Value { self.0.as_value() } } impl Object for Fiber {} unsafe impl private::ReprValue for Fiber {} impl ReprValue for Fiber {} impl TryConvert for Fiber { fn try_convert(val: Value) -> Result { Self::from_value(val).ok_or_else(|| { Error::new( Ruby::get_with(val).exception_type_error(), format!("no implicit conversion of {} into Fiber", unsafe { val.classname() },), ) }) } } /// Options for initialising Fiber-local storage. pub enum Storage { /// Inherit the storage from the current Fiber. Inherit, /// Initialise a new empty storage when needed. #[cfg(any(ruby_gte_3_2, docsrs))] #[cfg_attr(docsrs, doc(cfg(ruby_gte_3_2)))] Lazy, /// Use the given Hash as the Fiber's storage. #[cfg(any(ruby_gte_3_2, docsrs))] #[cfg_attr(docsrs, doc(cfg(ruby_gte_3_2)))] Use(RHash), } #[cfg(ruby_gte_3_2)] impl Storage { unsafe fn as_rb_value(&self) -> VALUE { #[cfg(ruby_gte_3_2)] let ruby = Ruby::get_unchecked(); match self { Self::Inherit => QUNDEF.as_value().as_rb_value(), #[cfg(ruby_gte_3_2)] Self::Lazy => ruby.qnil().as_rb_value(), #[cfg(ruby_gte_3_2)] Self::Use(hash) => hash.as_rb_value(), } } } impl Default for Storage { fn default() -> Self { Self::Inherit } } fn wrap_closure(func: F) -> (*mut Option, Value) where F: FnOnce(&Ruby, &[Value], Option) -> R, R: BlockReturn, { struct Closure(Option, DataType); unsafe impl Send for Closure {} impl DataTypeFunctions for Closure { fn mark(&self, marker: &gc::Marker) { // Attempt to mark any Ruby values captured in a closure. // Rust's closures are structs that contain all the values they // have captured. This reads that struct as a slice of VALUEs and // calls rb_gc_mark_locations which calls gc_mark_maybe which // marks VALUEs and ignores non-VALUEs marker.mark_slice(unsafe { slice::from_raw_parts( &self.0 as *const _ as *const Value, size_of::() / size_of::(), ) }); } } let data_type = data_type_builder!(Closure, "rust closure") .free_immediately() .mark() .build(); let boxed = Box::new(Closure(Some(func), data_type)); let ptr = Box::into_raw(boxed); let value = unsafe { Value::new(rb_data_typed_object_wrap( 0, // using 0 for the class will hide the object from ObjectSpace ptr as *mut _, (*ptr).1.as_rb_data_type() as *const _, )) }; unsafe { (&mut (*ptr).0 as *mut Option, value) } }