From 1c52f7ff6ce98154280b0cddebbd1bd01304e862 Mon Sep 17 00:00:00 2001 From: Mat Sadler Date: Thu, 31 Aug 2023 22:03:11 -0700 Subject: [PATCH] add Fiber (no docs) --- CHANGELOG.md | 1 + src/fiber.rs | 444 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 25 +-- src/method.rs | 8 +- 4 files changed, 463 insertions(+), 15 deletions(-) create mode 100644 src/fiber.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ee13e9e8..cc808012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Added - `Thread`, `Ruby::thread_create`/`thread_create_from_fn` and other thread APIs. +- `Fiber`, `Ruby::fiber_new`/`fiber_new_from_fn` and other fiber APIs. - `Ruby::ary_try_from_iter` is an efficient way to create a Ruby array from a fallible Rust iterator. - `Ruby::hash_from_iter` and `Ruby::hash_try_from_iter`. diff --git a/src/fiber.rs b/src/fiber.rs new file mode 100644 index 00000000..cd4faef2 --- /dev/null +++ b/src/fiber.rs @@ -0,0 +1,444 @@ +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(ruby_lt_3_1)] +use crate::class::RClass; +#[cfg(ruby_gte_3_1)] +use crate::exception::Exception; +#[cfg(any(ruby_gte_3_2, docsrs))] +use crate::r_hash::RHash; +use crate::{ + api::Ruby, + block::Proc, + data_type_builder, + error::{protect, Error}, + 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, + }, +}; + +impl Ruby { + /// # 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(()) + /// })?; + /// + /// # #[cfg(ruby_gte_3_1)] + /// # { + /// 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) + }) + } + } + + /// # 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(()) + /// })?; + /// + /// # #[cfg(ruby_gte_3_1)] + /// # { + /// 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 + }) + } + + pub fn fiber_current(&self) -> Fiber { + unsafe { Fiber::from_rb_value_unchecked(rb_fiber_current()) } + } + + 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) + } + } +} + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct Fiber(RTypedData); + +impl Fiber { + #[inline] + pub fn from_value(val: Value) -> Option { + #[cfg(ruby_lt_3_1)] + let fiber = { + let fiber_class: RClass = Ruby::get_with(val) + .class_object() + .funcall("const_get", ("Fiber",)) + .ok()?; + RTypedData::from_value(val) + .filter(|_| val.is_kind_of(fiber_class)) + .map(Self) + }; + #[cfg(ruby_gte_3_1)] + let fiber = unsafe { + Value::new(rb_obj_is_fiber(val.as_rb_value())) + .to_bool() + .then(|| Self::from_rb_value_unchecked(val.as_rb_value())) + }; + fiber + } + + #[inline] + pub(crate) unsafe fn from_rb_value_unchecked(val: VALUE) -> Self { + Self(RTypedData::from_rb_value_unchecked(val)) + } + + pub fn is_alive(self) -> bool { + unsafe { Value::new(rb_fiber_alive_p(self.as_rb_value())).to_bool() } + } + + /// # 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(()) + /// })?; + /// + /// # #[cfg(ruby_gte_3_1)] + /// # { + /// 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) + } + } + + #[cfg(any(ruby_gte_3_1, docsrs))] + #[cfg_attr(docsrs, doc(cfg(ruby_gte_3_1)))] + 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) + } + } + + #[cfg(any(ruby_gte_3_1, docsrs))] + #[cfg_attr(docsrs, doc(cfg(ruby_gte_3_1)))] + 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() + },), + ) + }) + } +} + +pub enum Storage { + Inherit, + #[cfg(any(ruby_gte_3_2, docsrs))] + #[cfg_attr(docsrs, doc(cfg(ruby_gte_3_2)))] + Lazy, + #[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) } +} diff --git a/src/lib.rs b/src/lib.rs index 74fd98d5..fdc9e80b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -693,12 +693,13 @@ // * `rb_fd_term`: // * `rb_fd_zero`: // * `rb_feature_provided`: -// * `rb_fiber_alive_p`: -// * `rb_fiber_current`: -// * `rb_fiber_new`: -// * `rb_fiber_raise`: -// * `rb_fiber_resume`: -// * `rb_fiber_resume_kw`: +//! * `rb_fiber_alive_p`: [`Fiber::is_alive`]. +//! * `rb_fiber_current`: [`Ruby::fiber_current`] +//! * `rb_fiber_new`: See [`Ruby::fiber_new`] & [`Ruby::fiber_new_from_fn`]. +//! * `rb_fiber_new_storage`: [`Ruby::fiber_new`] & [`Ruby::fiber_new_from_fn`]. +//! * `rb_fiber_raise`: [`Fiber::raise`]. +//! * `rb_fiber_resume`: See [`Fiber::resume`]. +//! * `rb_fiber_resume_kw`: [`Fiber::resume`]. // * `rb_fiber_scheduler_address_resolve`: // * `rb_fiber_scheduler_block`: // * `rb_fiber_scheduler_close`: @@ -723,10 +724,10 @@ // * `rb_fiber_scheduler_process_wait`: // * `rb_fiber_scheduler_set`: // * `rb_fiber_scheduler_unblock`: -// * `rb_fiber_transfer`: -// * `rb_fiber_transfer_kw`: -// * `rb_fiber_yield`: -// * `rb_fiber_yield_kw`: +//! * `rb_fiber_transfer`: See [`Fiber::transfer`]. +//! * `rb_fiber_transfer_kw`: [`Fiber::transfer`]. +//! * `rb_fiber_yield`: See [`Ruby::fiber_yield`]. +//! * `rb_fiber_yield_kw`: [`Ruby::fiber_yield`]. //! * `rb_filesystem_encindex`: [`encoding::Index::filesystem`]. //! * `rb_filesystem_encoding`: //! [`RbEncoding::filesystem`](encoding::RbEncoding::filesystem). @@ -1177,7 +1178,7 @@ // * `rb_obj_instance_eval`: // * `rb_obj_instance_exec`: // * `rb_obj_instance_variables`: -// * `rb_obj_is_fiber`: +//! * `rb_obj_is_fiber`: [`Fiber::from_value`]. // * `rb_obj_is_instance_of`: //! * `rb_obj_is_kind_of`: [`Value::is_kind_of`]. // * `rb_obj_is_method`: @@ -1799,6 +1800,7 @@ pub mod encoding; mod enumerator; pub mod error; pub mod exception; +pub mod fiber; mod float; pub mod gc; mod integer; @@ -1856,6 +1858,7 @@ pub use crate::{ enumerator::Enumerator, error::Error, exception::{Exception, ExceptionClass}, + fiber::Fiber, float::Float, integer::Integer, into_value::{ArgList, IntoValue, IntoValueFromNative, KwArgs, RArrayArgList}, diff --git a/src/method.rs b/src/method.rs index c483b19d..55e6fb76 100644 --- a/src/method.rs +++ b/src/method.rs @@ -367,17 +367,17 @@ where #[doc(hidden)] pub trait Block where - Self: Sized + FnMut(&Ruby, &[Value], Option) -> Res, + Self: Sized + FnOnce(&Ruby, &[Value], Option) -> Res, Res: BlockReturn, { #[inline] unsafe fn call_convert_value( - mut self, + self, argc: c_int, argv: *const Value, blockarg: Value, ) -> Result { - let ruby = unsafe { Ruby::get_unchecked() }; + let ruby = Ruby::get_unchecked(); let args = slice::from_raw_parts(argv, argc as usize); (self)(&ruby, args, Proc::from_value(blockarg)).into_block_return() } @@ -399,7 +399,7 @@ where impl Block for Func where - Func: FnMut(&Ruby, &[Value], Option) -> Res, + Func: FnOnce(&Ruby, &[Value], Option) -> Res, Res: BlockReturn, { }