Skip to main content

igraph/
error.rs

1//! Error handling: turning igraph error codes into Rust [`Result`]s.
2//!
3//! The C library reports failures by returning an [`igraph_error_t`] code and
4//! by invoking a (thread-local) *error handler*. The default handler aborts
5//! the whole process, which is hardly what a Rust program wants. The first time
6//! a thread calls into this crate, [`ensure_init`] installs a handler that
7//! records the error message (so that it can be reported in an [`Error`]) and
8//! then frees the current level of igraph's internal cleanup ("finally")
9//! stack, exactly like `igraph_error_handler_ignore` does; the failing
10//! function then returns its error code, which [`check`] converts into an
11//! [`Error`].
12//!
13//! Warnings emitted by igraph are collected in a thread-local buffer as well;
14//! retrieve them with [`take_warnings`].
15//!
16//! Rust callbacks invoked by igraph must never unwind into C: trampolines run
17//! them inside [`catch_panic`] (or [`catch_panic_or`]), which stores the
18//! panic and returns an error code to igraph; the wrapper's [`check`] then
19//! resumes the panic in Rust, after igraph has cleaned up.
20//!
21//! # Threads
22//!
23//! The crate requires igraph to be built in thread-safe mode (the
24//! `IGRAPH_THREAD_SAFE` macro of `igraph_threading.h` must be `1`, which is
25//! checked at compile time): every thread then has its own igraph state,
26//! namely error and warning handlers, "finally" stack (the list of
27//! temporary objects to free if the running function fails) and default
28//! random number generator. [`ensure_init`] sets them up the first time a
29//! thread calls into the crate, so igraph can be used from many threads at
30//! once. Nothing is shared between threads except what the user shares:
31//! a [`Graph`](crate::Graph) is [`Send`] but not [`Sync`], and seeding the
32//! random number generator (see [`rng`](crate::rng)) affects the calling
33//! thread only.
34//!
35//! # Callbacks that call igraph
36//!
37//! A Rust closure invoked by a running igraph function (a visitor, a clique
38//! or motif handler, ...) may itself call igraph functions, for instance to
39//! run a nested search. Two consequences of igraph's per-thread "finally"
40//! stack are handled here:
41//!
42//! - [`catch_panic`] and [`catch_panic_or`] run the closure in a fresh level
43//!   of the finally stack (`IGRAPH_FINALLY_ENTER` / `IGRAPH_FINALLY_EXIT`),
44//!   so an igraph call that fails inside the closure frees only its own
45//!   temporaries, never those of the outer function that is still running;
46//!   the closure receives an ordinary [`Err`] and may ignore it.
47//! - The finally stack holds at most 100 entries per thread, and igraph
48//!   aborts the process when it overflows. Each nested call adds its own
49//!   entries on top of the outer ones, so [`igraph_call!`](crate::igraph_call)
50//!   (and [`Graph::init_with`](crate::Graph::init_with)) refuse to start a C
51//!   call when the stack already holds more than
52//!   [`MAX_NESTED_FINALLY_ENTRIES`] entries, returning an
53//!   [`ErrorKind::Failure`] error ("igraph calls nested too deeply") instead;
54//!   in practice this allows a dozen or more levels of nested searches.
55//!
56//! | C (`igraph_error.h`) | Rust |
57//! |-------------------|------|
58//! | `igraph_error_t` return codes | [`Result`], [`Error`], [`ErrorKind`] ([`ErrorKind::from_raw`], [`ErrorKind::to_raw`]) |
59//! | `igraph_strerror` | [`strerror`], [`ErrorKind::description`], [`Display`](std::fmt::Display) |
60//! | `igraph_set_error_handler`, `igraph_set_warning_handler`, `igraph_setup` | [`ensure_init`] (automatic), [`is_initialized`] |
61//! | `IGRAPH_CHECK` | [`igraph_call!`](crate::igraph_call), [`check`] |
62//! | warnings (`IGRAPH_WARNING`) | [`take_warnings`] |
63//! | callbacks returning `IGRAPH_INTERRUPTED` | [`catch_panic`], [`catch_panic_or`], [`resume_panic`], [`has_pending_panic`] |
64//! | `IGRAPH_FINALLY_ENTER`, `IGRAPH_FINALLY_EXIT`, `IGRAPH_FINALLY_STACK_SIZE` | used by [`catch_panic`], [`catch_panic_or`] and [`igraph_call!`](crate::igraph_call); [`finally_stack_size`] |
65//!
66//! Not wrapped: the error and warning *reporting* functions (`igraph_error`,
67//! `igraph_errorf`, `igraph_errorvf`, `igraph_warning`, `igraph_warningf`,
68//! `igraph_fatal`, `igraph_fatalf`) are meant for C code that raises igraph
69//! errors, while Rust code returns an [`Error`] (see [`Error::new`]).
70//! `igraph_set_fatal_handler` is deliberately left alone: a fatal handler
71//! must not return, and a Rust function cannot unwind through the C frames
72//! above it, so igraph's default handler, which aborts the process, stays in
73//! place. Fatal errors signal broken internal invariants (e.g. a corrupt
74//! finally stack), not ordinary failures.
75//!
76//! See also [`misc::set_progress_handler`](crate::misc::set_progress_handler),
77//! [`misc::set_status_handler`](crate::misc::set_status_handler) and
78//! [`misc::set_interruption_handler`](crate::misc::set_interruption_handler)
79//! for igraph's other (progress, status and interruption) handlers.
80//!
81//! # Example
82//!
83//! ```
84//! use igraph::prelude::*;
85//!
86//! let mut g = Graph::new(3, false);
87//! // Vertex 7 does not exist: igraph reports an "invalid vertex id" error.
88//! let err = g.add_edge(0, 7).unwrap_err();
89//! assert_eq!(err.kind(), ErrorKind::InvalidVertexId);
90//! assert!(!err.message().is_empty());
91//! // The error names the C source location that raised it.
92//! assert!(err.file().ends_with(".c") && err.line() > 0);
93//! // Errors compose with `?` and `std::error::Error`.
94//! fn degree_of_7(g: &Graph) -> Result<i64> {
95//!     g.degree_of(7, NeighborMode::All, Loops::Twice)
96//! }
97//! let boxed: Box<dyn std::error::Error> = degree_of_7(&g).unwrap_err().into();
98//! assert!(boxed.to_string().starts_with(&ErrorKind::InvalidVertexId.description()));
99//! ```
100
101use crate::ffi::*;
102use std::{
103    any::Any,
104    cell::{Cell, RefCell},
105    ffi::{CStr, c_char, c_int},
106    fmt,
107};
108
109/// The kind of an igraph error, mirroring the C enumeration
110/// [`igraph_error_type_t`](https://igraph.org/c/html/latest/igraph-Error.html#igraph_error_type_t).
111///
112/// The error codes removed in igraph 1.0 (`IGRAPH_EINVEVECTOR`,
113/// `IGRAPH_NONSQUARE`, `IGRAPH_EDIVZERO`, `IGRAPH_EATTRIBUTES`,
114/// `IGRAPH_ELAPACK`, `IGRAPH_EDRL`, `IGRAPH_EGLP` and the `IGRAPH_GLP_*`
115/// codes, `IGRAPH_CPUTIME`) have no variant, nor do the ARPACK-specific codes
116/// that 1.0 moved to `igraph_arpack_error_t`; an unknown code maps to
117/// [`ErrorKind::Other`]. Code 8, `IGRAPH_NONSQUARE` before 1.0, is now
118/// `IGRAPH_EINVEID` ([`ErrorKind::InvalidEdgeId`]).
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120#[non_exhaustive]
121pub enum ErrorKind {
122    /// Generic failure (`IGRAPH_FAILURE`).
123    Failure,
124    /// Out of memory (`IGRAPH_ENOMEM`).
125    OutOfMemory,
126    /// Parse error while reading a file (`IGRAPH_PARSEERROR`).
127    Parse,
128    /// Invalid value, argument or combination of arguments (`IGRAPH_EINVAL`).
129    InvalidValue,
130    /// Object already exists (`IGRAPH_EXISTS`).
131    Exists,
132    /// Invalid vertex id (`IGRAPH_EINVVID`).
133    InvalidVertexId,
134    /// Invalid edge id (`IGRAPH_EINVEID`).
135    InvalidEdgeId,
136    /// Invalid neighbor mode (`IGRAPH_EINVMODE`).
137    InvalidMode,
138    /// File operation failed (`IGRAPH_EFILE`).
139    File,
140    /// Functionality not implemented (`IGRAPH_UNIMPLEMENTED`).
141    Unimplemented,
142    /// Computation interrupted, e.g. by a callback (`IGRAPH_INTERRUPTED`).
143    Interrupted,
144    /// Numeric procedure did not converge (`IGRAPH_DIVERGED`).
145    Diverged,
146    /// ARPACK error (`IGRAPH_EARPACK`).
147    Arpack,
148    /// Negative cycle detected in shortest path computation (`IGRAPH_ENEGCYCLE`).
149    NegativeCycle,
150    /// Internal igraph error, likely a bug (`IGRAPH_EINTERNAL`).
151    Internal,
152    /// Attribute combination error (`IGRAPH_EATTRCOMBINE`).
153    AttributeCombination,
154    /// Integer or double overflow (`IGRAPH_EOVERFLOW`).
155    Overflow,
156    /// Integer or double underflow (`IGRAPH_EUNDERFLOW`).
157    Underflow,
158    /// A random walk got stuck (`IGRAPH_ERWSTUCK`).
159    RandomWalkStuck,
160    /// Stop requested, e.g. by a callback (`IGRAPH_STOP`).
161    Stop,
162    /// Maximum vertex or edge count exceeded (`IGRAPH_ERANGE`).
163    Range,
164    /// The input problem has no solution (`IGRAPH_ENOSOL`).
165    NoSolution,
166    /// An error code not known to this version of the bindings.
167    Other(u32),
168    /// A Rust callback panicked while igraph was running; the panic is
169    /// resumed by the wrapper, so this kind is only observable in `catch_unwind`.
170    CallbackPanic,
171}
172
173impl ErrorKind {
174    /// Maps a raw igraph error code to an [`ErrorKind`].
175    pub fn from_raw(code: igraph_error_t) -> Self {
176        match code {
177            igraph_error_type_t_IGRAPH_FAILURE => Self::Failure,
178            igraph_error_type_t_IGRAPH_ENOMEM => Self::OutOfMemory,
179            igraph_error_type_t_IGRAPH_PARSEERROR => Self::Parse,
180            igraph_error_type_t_IGRAPH_EINVAL => Self::InvalidValue,
181            igraph_error_type_t_IGRAPH_EXISTS => Self::Exists,
182            igraph_error_type_t_IGRAPH_EINVVID => Self::InvalidVertexId,
183            igraph_error_type_t_IGRAPH_EINVEID => Self::InvalidEdgeId,
184            igraph_error_type_t_IGRAPH_EINVMODE => Self::InvalidMode,
185            igraph_error_type_t_IGRAPH_EFILE => Self::File,
186            igraph_error_type_t_IGRAPH_UNIMPLEMENTED => Self::Unimplemented,
187            igraph_error_type_t_IGRAPH_INTERRUPTED => Self::Interrupted,
188            igraph_error_type_t_IGRAPH_DIVERGED => Self::Diverged,
189            igraph_error_type_t_IGRAPH_EARPACK => Self::Arpack,
190            igraph_error_type_t_IGRAPH_ENEGCYCLE => Self::NegativeCycle,
191            igraph_error_type_t_IGRAPH_EINTERNAL => Self::Internal,
192            igraph_error_type_t_IGRAPH_EATTRCOMBINE => Self::AttributeCombination,
193            igraph_error_type_t_IGRAPH_EOVERFLOW => Self::Overflow,
194            igraph_error_type_t_IGRAPH_EUNDERFLOW => Self::Underflow,
195            igraph_error_type_t_IGRAPH_ERWSTUCK => Self::RandomWalkStuck,
196            igraph_error_type_t_IGRAPH_STOP => Self::Stop,
197            igraph_error_type_t_IGRAPH_ERANGE => Self::Range,
198            igraph_error_type_t_IGRAPH_ENOSOL => Self::NoSolution,
199            other => Self::Other(other),
200        }
201    }
202
203    /// The raw igraph error code corresponding to this kind (the inverse of
204    /// [`from_raw`](Self::from_raw)).
205    ///
206    /// [`ErrorKind::CallbackPanic`] maps to `IGRAPH_INTERRUPTED`, the code
207    /// returned to igraph when a Rust callback panics.
208    ///
209    /// ```
210    /// use igraph::prelude::*;
211    /// for kind in [ErrorKind::InvalidValue, ErrorKind::Parse, ErrorKind::Stop] {
212    ///     assert_eq!(ErrorKind::from_raw(kind.to_raw()), kind);
213    /// }
214    /// ```
215    pub fn to_raw(self) -> igraph_error_t {
216        match self {
217            Self::Failure => igraph_error_type_t_IGRAPH_FAILURE,
218            Self::OutOfMemory => igraph_error_type_t_IGRAPH_ENOMEM,
219            Self::Parse => igraph_error_type_t_IGRAPH_PARSEERROR,
220            Self::InvalidValue => igraph_error_type_t_IGRAPH_EINVAL,
221            Self::Exists => igraph_error_type_t_IGRAPH_EXISTS,
222            Self::InvalidVertexId => igraph_error_type_t_IGRAPH_EINVVID,
223            Self::InvalidEdgeId => igraph_error_type_t_IGRAPH_EINVEID,
224            Self::InvalidMode => igraph_error_type_t_IGRAPH_EINVMODE,
225            Self::File => igraph_error_type_t_IGRAPH_EFILE,
226            Self::Unimplemented => igraph_error_type_t_IGRAPH_UNIMPLEMENTED,
227            Self::Interrupted | Self::CallbackPanic => igraph_error_type_t_IGRAPH_INTERRUPTED,
228            Self::Diverged => igraph_error_type_t_IGRAPH_DIVERGED,
229            Self::Arpack => igraph_error_type_t_IGRAPH_EARPACK,
230            Self::NegativeCycle => igraph_error_type_t_IGRAPH_ENEGCYCLE,
231            Self::Internal => igraph_error_type_t_IGRAPH_EINTERNAL,
232            Self::AttributeCombination => igraph_error_type_t_IGRAPH_EATTRCOMBINE,
233            Self::Overflow => igraph_error_type_t_IGRAPH_EOVERFLOW,
234            Self::Underflow => igraph_error_type_t_IGRAPH_EUNDERFLOW,
235            Self::RandomWalkStuck => igraph_error_type_t_IGRAPH_ERWSTUCK,
236            Self::Stop => igraph_error_type_t_IGRAPH_STOP,
237            Self::Range => igraph_error_type_t_IGRAPH_ERANGE,
238            Self::NoSolution => igraph_error_type_t_IGRAPH_ENOSOL,
239            Self::Other(code) => code,
240        }
241    }
242
243    /// igraph's textual description of this kind of error
244    /// ([`igraph_strerror`](https://igraph.org/c/html/latest/igraph-Error.html#igraph_strerror)),
245    /// e.g. `"Invalid value"` for [`ErrorKind::InvalidValue`].
246    pub fn description(self) -> String {
247        strerror(self.to_raw())
248    }
249}
250
251impl fmt::Display for ErrorKind {
252    /// Writes igraph's description of the error kind, see [`ErrorKind::description`].
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        f.write_str(&self.description())
255    }
256}
257
258/// igraph's textual description of a raw error code
259/// ([`igraph_strerror`](https://igraph.org/c/html/latest/igraph-Error.html#igraph_strerror)).
260///
261/// ```
262/// use igraph::{error::strerror, ffi};
263/// assert_eq!(strerror(ffi::igraph_error_type_t_IGRAPH_SUCCESS), "No error");
264/// assert!(!strerror(ffi::igraph_error_type_t_IGRAPH_EINVVID).is_empty());
265/// ```
266pub fn strerror(code: igraph_error_t) -> String {
267    let ptr = unsafe { igraph_strerror(code) };
268    lossy(ptr)
269}
270
271/// An error reported by the igraph C library, or raised by a wrapper that
272/// validated its arguments on the Rust side.
273///
274/// It carries the [`ErrorKind`], the raw code, and the message, source file
275/// and line recorded by igraph's error handler (for errors raised in Rust,
276/// the file is empty and the line is `0`). [`Display`](fmt::Display) prints
277/// `<description>: <message> (<file>:<line>)`.
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct Error {
280    kind: ErrorKind,
281    code: igraph_error_t,
282    message: String,
283    file: String,
284    line: i32,
285}
286
287impl Error {
288    /// Builds an error from its parts; mainly useful to wrappers that validate
289    /// arguments on the Rust side before calling into igraph.
290    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
291        // Every kind maps to its own igraph code, so that `code()` and the
292        // `Display` description are consistent with `kind()`.
293        let code = kind.to_raw();
294        Self {
295            kind,
296            code,
297            message: message.into(),
298            file: String::new(),
299            line: 0,
300        }
301    }
302
303    /// Shorthand for an [`ErrorKind::InvalidValue`] error raised on the Rust side.
304    pub fn invalid(message: impl Into<String>) -> Self {
305        Self::new(ErrorKind::InvalidValue, message)
306    }
307
308    /// The kind of this error.
309    pub fn kind(&self) -> ErrorKind {
310        self.kind
311    }
312
313    /// The raw igraph error code.
314    pub fn code(&self) -> igraph_error_t {
315        self.code
316    }
317
318    /// The human readable message reported by igraph.
319    pub fn message(&self) -> &str {
320        &self.message
321    }
322
323    /// The C source file where the error was raised (empty if unknown).
324    pub fn file(&self) -> &str {
325        &self.file
326    }
327
328    /// The C source line where the error was raised (`0` if unknown).
329    pub fn line(&self) -> i32 {
330        self.line
331    }
332}
333
334impl fmt::Display for Error {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        write!(f, "{}", strerror(self.code))?;
337        if !self.message.is_empty() {
338            write!(f, ": {}", self.message)?;
339        }
340        if !self.file.is_empty() {
341            write!(f, " ({}:{})", self.file, self.line)?;
342        }
343        Ok(())
344    }
345}
346
347impl std::error::Error for Error {}
348
349/// Specialized [`Result`](std::result::Result) type used throughout the crate.
350pub type Result<T> = std::result::Result<T, Error>;
351
352struct Recorded {
353    message: String,
354    file: String,
355    line: i32,
356}
357
358thread_local! {
359    static INITIALIZED: Cell<bool> = const { Cell::new(false) };
360    static LAST_ERROR: RefCell<Option<Recorded>> = const { RefCell::new(None) };
361    static WARNINGS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
362    static PANIC: RefCell<Option<Box<dyn Any + Send + 'static>>> = const { RefCell::new(None) };
363}
364
365/// Maximum number of warnings kept in the thread-local buffer.
366const MAX_WARNINGS: usize = 1024;
367
368// The per-thread model described in the module docs needs a thread-safe
369// igraph build (thread-local handlers, finally stack and RNG pointer).
370const _: () = assert!(
371    IGRAPH_THREAD_SAFE == 1,
372    "igraph must be built in thread-safe mode (IGRAPH_THREAD_SAFE=1)"
373);
374
375/// Size of igraph's per-thread finally stack (fixed in `error.c`); igraph
376/// aborts the process when a function needs more entries.
377const FINALLY_STACK_CAPACITY: c_int = 100;
378
379/// The number of entries of igraph's (per-thread) finally stack above which
380/// [`igraph_call!`](crate::igraph_call) and
381/// [`Graph::init_with`](crate::Graph::init_with) refuse to start a C call,
382/// see the [module docs](self#callbacks-that-call-igraph).
383///
384/// The stack holds 100 entries; keeping half of them free leaves room for
385/// the temporaries of any single igraph function (and its callbacks).
386pub const MAX_NESTED_FINALLY_ENTRIES: usize = 50;
387
388/// The current number of entries of igraph's finally stack on the calling
389/// thread (`IGRAPH_FINALLY_STACK_SIZE`, undocumented in `igraph_error.h`).
390///
391/// It is zero outside of igraph calls; inside a callback it counts the
392/// temporaries of the igraph functions that are running.
393///
394/// ```
395/// assert_eq!(igraph::error::finally_stack_size(), 0);
396/// ```
397pub fn finally_stack_size() -> usize {
398    // SAFETY: reads a thread-local counter.
399    let size = unsafe { IGRAPH_FINALLY_STACK_SIZE() };
400    usize::try_from(size).unwrap_or(0)
401}
402
403/// Converts a Rust size (length, capacity, count) into an `igraph_int_t`.
404///
405/// Sizes above `igraph_int_t::MAX` would turn negative, which igraph either
406/// rejects with an assertion (aborting the process) or, for some containers
407/// such as bitsets, silently accepts and turns into out of bounds accesses.
408///
409/// # Panics
410/// Like [`Vec::with_capacity`], with "capacity overflow" for such sizes.
411#[track_caller]
412pub(crate) fn int_size(n: usize) -> igraph_int_t {
413    match igraph_int_t::try_from(n) {
414        Ok(n) => n,
415        Err(_) => panic!("capacity overflow: {n} does not fit in an igraph_int_t"),
416    }
417}
418
419fn lossy(ptr: *const c_char) -> String {
420    if ptr.is_null() {
421        String::new()
422    } else {
423        unsafe { CStr::from_ptr(ptr) }
424            .to_string_lossy()
425            .into_owned()
426    }
427}
428
429unsafe extern "C" fn record_error(
430    reason: *const c_char,
431    file: *const c_char,
432    line: c_int,
433    errno: igraph_error_t,
434) {
435    let recorded = Recorded {
436        message: lossy(reason),
437        file: lossy(file),
438        line,
439    };
440    // As an error propagates, igraph re-raises it with an empty message:
441    // keep the first, most informative, record (the root cause).
442    // `try_with`: this runs inside C code, where a panic would abort; during
443    // thread teardown the thread-local may be gone, then we record nothing.
444    let _ = LAST_ERROR.try_with(|e| {
445        let Ok(mut e) = e.try_borrow_mut() else {
446            return;
447        };
448        if e.as_ref()
449            .is_none_or(|old| old.message.is_empty() && !recorded.message.is_empty())
450        {
451            *e = Some(recorded);
452        }
453    });
454    // Frees the objects on igraph's "finally" stack, as the stock handler does.
455    unsafe { igraph_error_handler_ignore(reason, file, line, errno) };
456}
457
458unsafe extern "C" fn record_warning(reason: *const c_char, file: *const c_char, line: c_int) {
459    let warning = format!("{} ({}:{})", lossy(reason), lossy(file), line);
460    let _ = WARNINGS.try_with(|w| {
461        if let Ok(mut w) = w.try_borrow_mut()
462            && w.len() < MAX_WARNINGS
463        {
464            w.push(warning);
465        }
466    });
467}
468
469/// Initializes igraph for the calling thread, once.
470///
471/// It installs the error and warning handlers of this crate
472/// ([`igraph_set_error_handler`](https://igraph.org/c/html/latest/igraph-Error.html#igraph_set_error_handler),
473/// [`igraph_set_warning_handler`](https://igraph.org/c/html/latest/igraph-Error.html#igraph_set_warning_handler)), gives the
474/// thread its own randomly seeded default random number generator (in
475/// igraph 1.0.0 and 1.0.1 the default generator *pointer* is thread-local,
476/// but it initially points to a single global generator shared by all
477/// threads, which would make concurrent random functions race; see
478/// [`rng`](crate::rng)), and calls `igraph_setup()`.
479/// Every safe wrapper calls it (it's cheap after the first time), so users
480/// rarely need to call it explicitly. It is idempotent.
481pub fn ensure_init() {
482    // `try_with`: during thread teardown (e.g. when a value owning igraph
483    // memory is dropped by another thread-local destructor) the flag may be
484    // gone; the handlers were installed long before, so there is nothing to do.
485    let _ = INITIALIZED.try_with(|init| {
486        if !init.get() {
487            init.set(true);
488            unsafe {
489                igraph_set_error_handler(Some(record_error));
490                igraph_set_warning_handler(Some(record_warning));
491            }
492            // igraph's default RNG *pointer* is thread-local, but it points to
493            // one global generator shared by all threads: using it from two
494            // threads at once would be a data race. Give each thread its own.
495            crate::rng::install_thread_default_rng();
496            // Seeds the default generator only if not seeded yet: the
497            // per-thread one already is, so the shared global is not touched.
498            unsafe { igraph_setup() };
499        }
500    });
501}
502
503/// Whether [`ensure_init`] already ran on the calling thread.
504pub fn is_initialized() -> bool {
505    INITIALIZED.try_with(Cell::get).unwrap_or(false)
506}
507
508/// Forgets any error recorded by the handler and not yet consumed by
509/// [`check`]. Called by [`igraph_call!`] before every C call, so that a stale
510/// record left by an unchecked call can never be attached to a later error.
511#[doc(hidden)]
512pub fn reset_last_error() {
513    let _ = LAST_ERROR.try_with(|e| {
514        if let Ok(mut e) = e.try_borrow_mut() {
515            *e = None;
516        }
517    });
518}
519
520/// Converts a raw igraph return code into a [`Result`], attaching the message
521/// recorded by the error handler: `IGRAPH_SUCCESS` gives `Ok(())`, any
522/// other code an [`Error`] of the corresponding [`ErrorKind`].
523///
524/// If a Rust callback panicked during the call (see [`catch_panic`]), the
525/// panic is resumed here.
526pub fn check(code: igraph_error_t) -> Result<()> {
527    // Consume the record first: resuming a panic unwinds out of this
528    // function, and a stale record must not leak into the next error.
529    let recorded = LAST_ERROR
530        .try_with(|e| e.borrow_mut().take())
531        .ok()
532        .flatten();
533    resume_panic();
534    if code == igraph_error_type_t_IGRAPH_SUCCESS {
535        return Ok(());
536    }
537    let (message, file, line) = match recorded {
538        Some(Recorded {
539            message,
540            file,
541            line,
542        }) => (message, file, line),
543        None => (String::new(), String::new(), 0),
544    };
545    Err(Error {
546        kind: ErrorKind::from_raw(code),
547        code,
548        message,
549        file,
550        line,
551    })
552}
553
554/// Calls an igraph function returning an [`igraph_error_t`] and converts the
555/// outcome into a [`Result`]`<()>`, making sure the calling thread is
556/// initialized first (see [`ensure_init`]).
557///
558/// ```
559/// use igraph::{ffi, igraph_call};
560/// let mut v = std::mem::MaybeUninit::<ffi::igraph_vector_int_t>::uninit();
561/// igraph_call!(ffi::igraph_vector_int_init(v.as_mut_ptr(), 3)).unwrap();
562/// let v = unsafe { v.assume_init() }; // dropped (and destroyed) at scope exit
563/// assert_eq!(v.len(), 3);
564/// ```
565#[macro_export]
566macro_rules! igraph_call {
567    ($call:expr) => {{
568        match $crate::error::prepare_call() {
569            ::std::result::Result::Ok(()) => {
570                // Running the given raw FFI call inside `unsafe` is the
571                // documented purpose of this macro.
572                #[allow(unused_unsafe, clippy::macro_metavars_in_unsafe)]
573                let code = unsafe { $call };
574                $crate::error::check(code)
575            }
576            ::std::result::Result::Err(e) => ::std::result::Result::Err(e),
577        }
578    }};
579}
580
581/// Prepares the calling thread for a C call: [`ensure_init`], then
582/// [`reset_last_error`], then refuses (with [`ErrorKind::Failure`]) when the
583/// finally stack holds more than [`MAX_NESTED_FINALLY_ENTRIES`] entries, as
584/// the call could overflow it and make igraph abort the process. Called by
585/// [`igraph_call!`](crate::igraph_call) before every call.
586#[doc(hidden)]
587pub fn prepare_call() -> Result<()> {
588    ensure_init();
589    reset_last_error();
590    let size = finally_stack_size();
591    if size > MAX_NESTED_FINALLY_ENTRIES {
592        return Err(Error::new(
593            ErrorKind::Failure,
594            format!(
595                "igraph calls nested too deeply inside callbacks: igraph's finally stack \
596                 already holds {size} of its {FINALLY_STACK_CAPACITY} entries"
597            ),
598        ));
599    }
600    Ok(())
601}
602
603/// Drains and returns the warnings that igraph emitted on the calling thread
604/// (as `"<message> (<file>:<line>)"`), oldest first.
605///
606/// igraph reports non-fatal problems (e.g. a numerical method that did not
607/// fully converge) through its warning handler; this crate buffers them per
608/// thread (at most 1024, the later ones are dropped) instead of printing
609/// them to standard error like igraph's default handler does.
610pub fn take_warnings() -> Vec<String> {
611    WARNINGS.with(|w| std::mem::take(&mut *w.borrow_mut()))
612}
613
614/// Runs a Rust closure invoked *from C* (a callback), catching panics so that
615/// they never unwind across the FFI boundary.
616///
617/// If `f` panics, the payload is stored and `IGRAPH_INTERRUPTED` is returned
618/// to igraph, which stops the computation; the next [`check`] on the same
619/// thread then resumes the panic in Rust code. Use it in every
620/// `extern "C" fn` trampoline that calls user code:
621///
622/// ```ignore
623/// unsafe extern "C" fn trampoline(/* ... */, extra: *mut c_void) -> igraph_error_t {
624///     catch_panic(|| { /* call the user closure */ igraph_error_type_t_IGRAPH_SUCCESS })
625/// }
626/// ```
627///
628/// `f` runs in a new level of igraph's finally stack
629/// (`IGRAPH_FINALLY_ENTER` / `IGRAPH_FINALLY_EXIT`): when an igraph call
630/// made by `f` fails, the error handler frees the temporaries of that call
631/// only, not those of the igraph function that invoked the callback and is
632/// still running (see the [module docs](self#callbacks-that-call-igraph)).
633///
634/// ```
635/// use igraph::{error::{catch_panic, check}, ffi};
636/// // Outside of a callback the finally stack is empty, and stays so.
637/// let code = catch_panic(|| {
638///     // A failing igraph call inside the "callback" is an ordinary `Err`.
639///     assert!(igraph::prelude::Graph::new(2, false).add_edge(0, 5).is_err());
640///     ffi::igraph_error_type_t_IGRAPH_SUCCESS
641/// });
642/// assert!(check(code).is_ok());
643/// assert_eq!(igraph::error::finally_stack_size(), 0);
644/// ```
645pub fn catch_panic(f: impl FnOnce() -> igraph_error_t) -> igraph_error_t {
646    catch_panic_or(igraph_error_type_t_IGRAPH_INTERRUPTED, f)
647}
648
649/// Like [`catch_panic`], for callbacks whose C signature returns a value
650/// other than an error code (e.g. a boolean): `on_panic` is returned to C.
651///
652/// Like [`catch_panic`], it runs `f` in a new level of igraph's finally
653/// stack.
654pub fn catch_panic_or<T>(on_panic: T, f: impl FnOnce() -> T) -> T {
655    // SAFETY (for the finally-stack calls below): bookkeeping on igraph's
656    // thread-local finally stack. The entries below `entry_size` belong to
657    // the running igraph function(s) and are at the current level or lower,
658    // so a new level can be opened; the matching EXIT always runs, since
659    // `catch_unwind` returns on panic too.
660    let entry_size = unsafe { IGRAPH_FINALLY_STACK_SIZE() };
661    unsafe { IGRAPH_FINALLY_ENTER() };
662    let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
663    // Every igraph call made by `f` has returned by now, and each of them
664    // either popped its entries or (on failure) had the error handler free
665    // them. Entries left over would come from a C function that returned an
666    // error without reporting it: they point into stack frames that no
667    // longer exist, so drop them *without* running their destructors (a
668    // leak at worst) rather than leave them for a later error to "free".
669    let left_over = unsafe { IGRAPH_FINALLY_STACK_SIZE() } - entry_size;
670    if left_over > 0 {
671        unsafe { IGRAPH_FINALLY_CLEAN(left_over) };
672    }
673    unsafe { IGRAPH_FINALLY_EXIT() };
674    match outcome {
675        Ok(value) => value,
676        Err(payload) => {
677            store_panic(payload);
678            on_panic
679        }
680    }
681}
682
683/// Stores the payload of a caught panic; the *first* panic wins, later ones
684/// (e.g. from callbacks igraph keeps calling) are dropped.
685fn store_panic(payload: Box<dyn Any + Send + 'static>) {
686    let _ = PANIC.try_with(|p| {
687        if let Ok(mut p) = p.try_borrow_mut()
688            && p.is_none()
689        {
690            *p = Some(payload);
691        }
692    });
693}
694
695/// Resumes a panic caught by [`catch_panic`] on this thread, if any.
696pub fn resume_panic() {
697    if let Some(payload) = PANIC.try_with(|p| p.borrow_mut().take()).ok().flatten() {
698        std::panic::resume_unwind(payload);
699    }
700}
701
702/// Whether a panic caught by [`catch_panic`] is waiting to be resumed on the
703/// calling thread (by the next [`check`] or [`resume_panic`]).
704///
705/// Trampolines of callbacks that igraph keeps calling after an error can use
706/// it to skip the user code once it has panicked.
707pub fn has_pending_panic() -> bool {
708    PANIC.try_with(|p| p.borrow().is_some()).unwrap_or(false)
709}