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}