magnus/error.rs
1//! Rust types for working with Ruby Exceptions and other interrupts.
2//!
3//! See also [`Ruby`](Ruby#errors) for more error related methods.
4
5use std::{any::Any, borrow::Cow, ffi::CString, fmt, mem::transmute, os::raw::c_int};
6
7use rb_sys::{
8 rb_bug, rb_ensure, rb_errinfo, rb_exc_fatal, rb_exc_raise, rb_iter_break_value, rb_jump_tag,
9 rb_protect, rb_set_errinfo, rb_warning, ruby_special_consts, VALUE,
10};
11
12use crate::{
13 class::Class,
14 exception::Exception,
15 into_value::IntoValue,
16 module::Module,
17 value::{private::ReprValue as _, ReprValue, Value},
18 ExceptionClass, Ruby,
19};
20
21/// An error returned to indicate an attempt to interact with the Ruby API from
22/// a non-Ruby thread or without acquiring the GVL.
23#[derive(Debug)]
24pub enum RubyUnavailableError {
25 /// GVL is not locked.
26 GvlUnlocked,
27 /// Current thread is not a Ruby thread.
28 NonRubyThread,
29}
30
31impl fmt::Display for RubyUnavailableError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 Self::NonRubyThread => write!(f, "Current thread is not a Ruby thread."),
35 Self::GvlUnlocked => write!(f, "GVL is not locked."),
36 }
37 }
38}
39
40impl std::error::Error for RubyUnavailableError {}
41
42/// # Errors
43///
44/// Functions for working with errors and flow control encoded as an [`Error`].
45///
46/// See also [`Error`] and the [`error`](self) module.
47impl Ruby {
48 /// Create a new error that will break from a loop when returned to Ruby.
49 ///
50 /// # Examples
51 ///
52 /// ```
53 /// use magnus::{prelude::*, Error, Ruby};
54 ///
55 /// fn example(ruby: &Ruby) -> Result<(), Error> {
56 /// let i: i64 =
57 /// ruby.range_new(1, 100, false)?
58 /// .block_call("each", (), |ruby, args, _block| {
59 /// let i = i64::try_convert(*args.get(0).unwrap())?;
60 /// if i % 3 == 0 && i % 5 == 0 {
61 /// Err(ruby.iter_break_value(i))
62 /// } else {
63 /// Ok(())
64 /// }
65 /// })?;
66 ///
67 /// assert_eq!(i, 15);
68 /// Ok(())
69 /// }
70 /// # Ruby::init(example).unwrap()
71 /// ```
72 pub fn iter_break_value<T>(&self, val: T) -> Error
73 where
74 T: IntoValue,
75 {
76 let val = self.into_value(val);
77 protect(|| {
78 unsafe { rb_iter_break_value(val.as_rb_value()) };
79 // we never get here, but this is needed to satisfy the type system
80 #[allow(unreachable_code)]
81 self.qnil()
82 })
83 .unwrap_err()
84 }
85
86 /// Outputs `s` to Ruby's stderr if Ruby is configured to output warnings.
87 pub fn warning(&self, s: &str) {
88 let s = CString::new(s).unwrap();
89 unsafe { rb_warning(s.as_ptr()) };
90 }
91}
92
93/// Shorthand for `std::result::Result<T, magnus::Error>`.
94pub type Result<T> = std::result::Result<T, Error>;
95
96/// The possible types of [`Error`].
97#[derive(Debug, Clone)]
98pub enum ErrorType {
99 /// An interrupt, such as `break` or `throw`.
100 Jump(Tag),
101 /// An error generated in Rust code that will raise an exception when
102 /// returned to Ruby.
103 Error(ExceptionClass, Cow<'static, str>),
104 /// A Ruby `Exception` captured from Ruby as an Error.
105 Exception(Exception),
106}
107
108/// Wrapper type for Ruby `Exception`s or other interrupts.
109#[derive(Debug, Clone)]
110pub struct Error(ErrorType);
111
112impl Error {
113 /// Create a new `Error` that can be raised as a Ruby `Exception` with
114 /// `msg`.
115 ///
116 /// # Examples
117 ///
118 /// ```
119 /// use magnus::{function, prelude::*, Error, Exception, Ruby};
120 ///
121 /// fn bang(ruby: &Ruby) -> Result<(), Error> {
122 /// Err(Error::new(ruby.exception_runtime_error(), "BANG"))
123 /// }
124 ///
125 /// fn example(ruby: &Ruby) -> Result<(), Error> {
126 /// ruby.define_global_function("bang", function!(bang, 0));
127 ///
128 /// let error: Exception = ruby.eval(
129 /// "
130 /// begin
131 /// bang
132 /// rescue => e
133 /// e
134 /// end
135 /// ",
136 /// )?;
137 ///
138 /// assert!(error.is_kind_of(ruby.exception_runtime_error()));
139 /// let msg: String = error.funcall("message", ())?;
140 /// assert_eq!(msg, "BANG");
141 ///
142 /// Ok(())
143 /// }
144 /// # Ruby::init(example).unwrap()
145 /// ```
146 pub fn new<T>(class: ExceptionClass, msg: T) -> Self
147 where
148 T: Into<Cow<'static, str>>,
149 {
150 Self(ErrorType::Error(class, msg.into()))
151 }
152
153 pub(crate) fn from_tag(tag: Tag) -> Self {
154 Self(ErrorType::Jump(tag))
155 }
156
157 /// Create a new error that will break from a loop when returned to Ruby.
158 ///
159 /// # Panics
160 ///
161 /// Panics if called from a non-Ruby thread. See [`Ruby::iter_break_value`]
162 /// for the non-panicking version.
163 ///
164 /// # Examples
165 ///
166 /// ```
167 /// # #![allow(deprecated)]
168 /// use magnus::{prelude::*, Error};
169 /// # let _cleanup = unsafe { magnus::embed::init() };
170 ///
171 /// let i: i64 = magnus::Range::new(1, 100, false)
172 /// .unwrap()
173 /// .block_call("each", (), |_ruby, args, _block| {
174 /// let i = i64::try_convert(*args.get(0).unwrap())?;
175 /// if i % 3 == 0 && i % 5 == 0 {
176 /// Err(Error::iter_break(i))
177 /// } else {
178 /// Ok(())
179 /// }
180 /// })
181 /// .unwrap();
182 ///
183 /// assert_eq!(i, 15);
184 /// ```
185 #[cfg_attr(
186 not(feature = "old-api"),
187 deprecated(note = "please use `Ruby::iter_break_value` instead")
188 )]
189 #[cfg_attr(docsrs, doc(cfg(feature = "old-api")))]
190 #[inline]
191 pub fn iter_break<T>(val: T) -> Self
192 where
193 T: IntoValue,
194 {
195 get_ruby!().iter_break_value(val)
196 }
197
198 /// Matches the internal `Exception` against `class` with same semantics as
199 /// Ruby's `rescue`.
200 ///
201 /// # Examples
202 ///
203 /// ```
204 /// use magnus::{exception::ExceptionClass, prelude::*, Error, RModule, Ruby, TryConvert, Value};
205 ///
206 /// fn example(ruby: &Ruby) -> Result<(), Error> {
207 /// let err: Error = ruby
208 /// .eval::<Value>(
209 /// "
210 /// class ExampleError < StandardError
211 /// end
212 /// module Tag
213 /// end
214 /// class SpecificError < ExampleError
215 /// include Tag
216 /// end
217 /// raise SpecificError
218 /// ",
219 /// )
220 /// .unwrap_err();
221 ///
222 /// fn get<T: TryConvert>(ruby: &Ruby, name: &str) -> Result<T, Error> {
223 /// ruby.class_object().const_get::<_, T>(name)
224 /// }
225 /// assert!(err.is_kind_of(get::<ExceptionClass>(ruby, "SpecificError")?));
226 /// assert!(err.is_kind_of(get::<ExceptionClass>(ruby, "ExampleError")?));
227 /// assert!(err.is_kind_of(get::<ExceptionClass>(ruby, "StandardError")?));
228 /// assert!(err.is_kind_of(get::<ExceptionClass>(ruby, "Exception")?));
229 /// assert!(err.is_kind_of(get::<RModule>(ruby, "Tag")?));
230 ///
231 /// assert!(!err.is_kind_of(get::<ExceptionClass>(ruby, "NoMethodError")?));
232 /// assert!(!err.is_kind_of(get::<RModule>(ruby, "Math")?));
233 ///
234 /// Ok(())
235 /// }
236 /// # Ruby::init(example).unwrap()
237 /// ```
238 pub fn is_kind_of<T>(&self, class: T) -> bool
239 where
240 T: ReprValue + Module,
241 {
242 match self.0 {
243 ErrorType::Jump(_) => false,
244 ErrorType::Error(c, _) => c.is_inherited(class),
245 ErrorType::Exception(e) => e.is_kind_of(class),
246 }
247 }
248
249 /// Consumes `self`, returning an `Exception`.
250 ///
251 /// # Panics
252 ///
253 /// Panics if called on an `Error::Jump`.
254 fn exception(self) -> Exception {
255 let handle = unsafe { Ruby::get_unchecked() };
256 match self.0 {
257 ErrorType::Jump(_) => panic!("Error::exception() called on {}", self),
258 ErrorType::Error(class, msg) => {
259 match class.new_instance((handle.str_new(msg.as_ref()),)) {
260 Ok(e) | Err(Error(ErrorType::Exception(e))) => e,
261 Err(err) => unreachable!("*very* unexpected error: {}", err),
262 }
263 }
264 ErrorType::Exception(e) => e,
265 }
266 }
267
268 /// Returns the [`ErrorType`] for self.
269 pub fn error_type(&self) -> &ErrorType {
270 &self.0
271 }
272
273 /// Returns the inner [`Value`] of `self`, if there is one.
274 ///
275 /// The returned `Value` may be a subclass or an instance of `Exception`.
276 ///
277 /// This function is provided for rare cases where the `Error` needs to be
278 /// stored on the heap and the inner value needs to be
279 /// [marked](`crate::gc::Marker::mark`) to avoid being garbage collected.
280 pub fn value(&self) -> Option<Value> {
281 match self.0 {
282 ErrorType::Jump(_) => None,
283 ErrorType::Error(c, _) => Some(c.as_value()),
284 ErrorType::Exception(e) => Some(e.as_value()),
285 }
286 }
287
288 /// Create an `Error` from the error value of [`std::panic::catch_unwind`].
289 ///
290 /// The Ruby Exception will be `fatal`, terminating the Ruby process, but
291 /// allowing cleanup code to run.
292 pub(crate) fn from_panic(e: Box<dyn Any + Send + 'static>) -> Self {
293 let msg = if let Some(&m) = e.downcast_ref::<&'static str>() {
294 m.into()
295 } else if let Some(m) = e.downcast_ref::<String>() {
296 m.clone().into()
297 } else {
298 "panic".into()
299 };
300 Self(ErrorType::Error(
301 unsafe { Ruby::get_unchecked().exception_fatal() },
302 msg,
303 ))
304 }
305}
306
307impl fmt::Display for Error {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 match &self.0 {
310 ErrorType::Jump(s) => s.fmt(f),
311 ErrorType::Error(e, m) => write!(f, "{}: {}", e, m),
312 ErrorType::Exception(e) => e.fmt(f),
313 }
314 }
315}
316
317impl From<Exception> for Error {
318 fn from(val: Exception) -> Self {
319 Self(ErrorType::Exception(val))
320 }
321}
322
323/// Conversions into [`Error`].
324pub trait IntoError {
325 /// Convert `self` into [`Error`].
326 fn into_error(self, ruby: &Ruby) -> Error;
327}
328
329impl IntoError for Error {
330 #[inline]
331 fn into_error(self, _: &Ruby) -> Error {
332 self
333 }
334}
335
336/// A wrapper to make a [`Error`] [`Send`] + [`Sync`].
337///
338/// [`Error`] is not [`Send`] or [`Sync`] as it provides a way to call some of
339/// Ruby's APIs, which are not safe to call from a non-Ruby thread.
340///
341/// [`Error`] is safe to send between Ruby threads, but Rust's trait system
342/// currently can not model this detail.
343///
344/// To resolve this, the `OpaqueError` type makes an [`Error`] [`Send`] +
345/// [`Sync`] by removing the ability use it with any Ruby APIs.
346///
347/// [`OpaqueError::into_error_with`] provides a way to safely get an [`Error`]
348/// from a `OpaqueError`].
349///
350/// Note that `OpaqueError` contains a Ruby value, so must be kept on the stack
351/// of a Ruby thread to prevent it from being Garbage Collected (or otherwise
352/// protected from premature GC).
353#[derive(Clone)]
354pub struct OpaqueError(ErrorType);
355
356unsafe impl Send for OpaqueError {}
357unsafe impl Sync for OpaqueError {}
358
359impl OpaqueError {
360 /// Convert an `OpaqueError` into an [`Error`].
361 ///
362 /// # Examples
363 ///
364 /// ```
365 /// use magnus::{error::OpaqueError, Error, Ruby};
366 /// # let _cleanup = unsafe { magnus::embed::init() };
367 ///
368 /// let ruby = Ruby::get().unwrap(); // errors on non-Ruby thread
369 /// let opaque_err = OpaqueError::from(Error::new(ruby.exception_runtime_error(), "test"));
370 ///
371 /// // send to another Ruby thread
372 ///
373 /// let ruby = Ruby::get().unwrap(); // errors on non-Ruby thread
374 /// let err = OpaqueError::into_error_with(opaque_err, &ruby);
375 /// assert!(err.is_kind_of(ruby.exception_runtime_error()));
376 /// ```
377 #[allow(unused_variables)]
378 pub fn into_error_with(this: Self, handle: &Ruby) -> Error {
379 Error(this.0)
380 }
381}
382
383impl From<Error> for OpaqueError {
384 fn from(err: Error) -> Self {
385 Self(err.0)
386 }
387}
388
389impl IntoError for OpaqueError {
390 #[inline]
391 fn into_error(self, _: &Ruby) -> Error {
392 Error(self.0)
393 }
394}
395
396/// The state of a call to Ruby exiting early, interrupting the normal flow
397/// of code.
398#[derive(Debug, Clone, Copy)]
399#[repr(i32)]
400pub enum Tag {
401 // None = 0,
402 /// Early return from a block.
403 Return = 1,
404 /// Break from a block.
405 Break = 2,
406 /// Early return from a block, continuing to next block call.
407 Next = 3,
408 /// Break from a block after an error, block will be subsequently re-run.
409 Retry = 4,
410 /// Break from a block that will be subsequently re-run.
411 Redo = 5,
412 /// Ruby stack unwound with an error.
413 Raise = 6,
414 /// Ruby stack unwound as flow control.
415 Throw = 7,
416 /// Block or method exiting early due to unrecoverable error.
417 Fatal = 8,
418}
419
420impl Tag {
421 fn resume(self) -> ! {
422 unsafe { rb_jump_tag(self as c_int) };
423 }
424}
425
426impl fmt::Display for Tag {
427 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428 match self {
429 // Self::None => write!(f, "None"),
430 Self::Return => write!(f, "Return"),
431 Self::Break => write!(f, "Break"),
432 Self::Next => write!(f, "Next"),
433 Self::Retry => write!(f, "Retry"),
434 Self::Redo => write!(f, "Redo"),
435 Self::Raise => write!(f, "Raise"),
436 Self::Throw => write!(f, "Throw"),
437 Self::Fatal => write!(f, "Fatal"),
438 }
439 }
440}
441
442/// Calls the given closure, rescuing Ruby Exceptions and returning them as
443/// an [`Error`].
444///
445/// All functions exposed by magnus that call Ruby in a way that may raise
446/// already use this internally, but this is provided for anyone calling
447/// the Ruby C API directly.
448pub(crate) fn protect<F, T>(func: F) -> Result<T>
449where
450 F: FnOnce() -> T,
451 T: ReprValue,
452{
453 // nested function as this is totally unsafe to call out of this context
454 // arg should not be a VALUE, but a mutable pointer to F, cast to VALUE
455 unsafe extern "C" fn call<F, T>(arg: VALUE) -> VALUE
456 where
457 F: FnOnce() -> T,
458 T: ReprValue,
459 {
460 let closure = (*(arg as *mut Option<F>)).take().unwrap();
461 (closure)().as_rb_value()
462 }
463
464 // Tag::None
465 let mut state = 0;
466 // rb_protect takes:
467 // arg1: function pointer that returns a VALUE
468 // arg2: a VALUE
469 // arg3: a pointer to an int.
470 // rb_protect then calls arg1 with arg2 and returns the VALUE that arg1
471 // returns. If a Ruby exception is raised (or other interrupt) the VALUE
472 // returned is instead Qnil, and arg3 is set to non-zero.
473 // As arg2 is only ever passed to arg1 and otherwise not touched we can
474 // pack in whatever data we want that will fit into a VALUE. This is part
475 // of the api and safe to do.
476 // In this case we use arg2 to pass a pointer the Rust closure we actually
477 // want to call, and arg1 is just a simple adapter to call arg2.
478 let result = unsafe {
479 let mut some_func = Some(func);
480 let closure = &mut some_func as *mut Option<F> as VALUE;
481 rb_protect(Some(call::<F, T>), closure, &mut state as *mut c_int)
482 };
483
484 match state {
485 // Tag::None
486 0 => unsafe { Ok(T::from_value_unchecked(Value::new(result))) },
487 // Tag::Raise
488 6 => unsafe {
489 let ex = Exception::from_rb_value_unchecked(rb_errinfo());
490 rb_set_errinfo(Ruby::get_unchecked().qnil().as_rb_value());
491 Err(ex.into())
492 },
493 other => Err(Error::from_tag(unsafe { transmute(other) })),
494 }
495}
496
497pub(crate) fn ensure<F1, F2, T>(func: F1, ensure: F2) -> T
498where
499 F1: FnOnce() -> T,
500 F2: FnOnce(),
501 T: ReprValue,
502{
503 unsafe extern "C" fn call_func<F1, T>(arg: VALUE) -> VALUE
504 where
505 F1: FnOnce() -> T,
506 T: ReprValue,
507 {
508 let closure = (*(arg as *mut Option<F1>)).take().unwrap();
509 (closure)().as_rb_value()
510 }
511
512 unsafe extern "C" fn call_ensure<F2>(arg: VALUE) -> VALUE
513 where
514 F2: FnOnce(),
515 {
516 let closure = (*(arg as *mut Option<F2>)).take().unwrap();
517 (closure)();
518 ruby_special_consts::RUBY_Qnil as VALUE
519 }
520
521 let result = unsafe {
522 let call_func_ptr = call_func::<F1, T> as unsafe extern "C" fn(VALUE) -> VALUE;
523 let mut some_func = Some(func);
524 let func_closure = &mut some_func as *mut Option<F1> as VALUE;
525 let call_ensure_ptr = call_ensure::<F2> as unsafe extern "C" fn(VALUE) -> VALUE;
526 let mut some_ensure = Some(ensure);
527 let ensure_closure = &mut some_ensure as *mut Option<F2> as VALUE;
528 rb_ensure(
529 Some(call_func_ptr),
530 func_closure,
531 Some(call_ensure_ptr),
532 ensure_closure,
533 )
534 };
535
536 unsafe { T::from_value_unchecked(Value::new(result)) }
537}
538
539pub(crate) fn raise(e: Error) -> ! {
540 match e.0 {
541 ErrorType::Jump(tag) => tag.resume(),
542 ErrorType::Error(class, _)
543 if class.as_rb_value()
544 == unsafe { Ruby::get_unchecked().exception_fatal().as_rb_value() } =>
545 {
546 unsafe { rb_exc_fatal(e.exception().as_rb_value()) }
547 // friendly reminder: we really never get here, and as such won't
548 // drop any values still in scope, make sure everything has been
549 // consumed/dropped
550 }
551 _ => {
552 unsafe { rb_exc_raise(e.exception().as_rb_value()) }
553 // friendly reminder: we really never get here, and as such won't
554 // drop any values still in scope, make sure everything has been
555 // consumed/dropped
556 }
557 };
558}
559
560pub(crate) fn bug_from_panic(e: Box<dyn Any + Send + 'static>, or: &str) -> ! {
561 let msg: Cow<'_, str> = if let Some(&m) = e.downcast_ref::<&'static str>() {
562 m.into()
563 } else if let Some(m) = e.downcast_ref::<String>() {
564 m.clone().into()
565 } else {
566 or.into()
567 };
568 bug(&msg)
569}
570
571/// Immediately terminate the process, printing Ruby internal state for
572/// debugging.
573pub fn bug(s: &str) -> ! {
574 let s = CString::new(s).unwrap_or_else(|_| CString::new("panic").unwrap());
575 unsafe { rb_bug(s.as_ptr()) };
576 // as we never get here `s` isn't dropped, technically this is a memory
577 // leak, in practice we don't care because we just hard crashed
578}
579
580/// Outputs `s` to Ruby's stderr if Ruby is configured to output warnings.
581///
582/// Otherwise does nothing.
583///
584/// # Panics
585///
586/// Panics if called from a non-Ruby thread. See [`Ruby::warning`] for the
587/// non-panicking version.
588#[cfg_attr(
589 not(feature = "old-api"),
590 deprecated(note = "please use `Ruby::warning` instead")
591)]
592#[cfg_attr(docsrs, doc(cfg(feature = "old-api")))]
593#[inline]
594pub fn warning(s: &str) {
595 get_ruby!().warning(s)
596}