rusteal-core 0.8.0

Safe Rust runtime for rusteal (object refs, lifecycle, dynamic calls)
Documentation
use std::cell::RefCell;

use rusteal_ffi::{FWeakObjectHandle, RustealErrorReport};

use crate::api::is_api_initialized;

struct PanicSite {
    file: String,
    line: u32,
    column: u32,
}

thread_local! {
    static PANIC_SITE: RefCell<Option<PanicSite>> = const { RefCell::new(None) };
}

pub fn install_panic_hook() {
    let previous = std::panic::take_hook();

    std::panic::set_hook(Box::new(move |info| {
        let site = info.location().map(|location| PanicSite {
            file: location.file().to_owned(),
            line: location.line(),
            column: location.column(),
        });

        PANIC_SITE.with(|slot| *slot.borrow_mut() = site);
        previous(info);
    }));
}

pub fn ffi_boundary<F, R>(default: R, f: F) -> R
where
    F: FnOnce() -> R + std::panic::UnwindSafe,
{
    match std::panic::catch_unwind(f) {
        Ok(value) => value,
        Err(payload) => {
            if is_api_initialized() {
                report_panic(&panic_message(&payload));
            }

            default
        }
    }
}

fn report_panic(message: &str) {
    let site = PANIC_SITE.with(|slot| slot.borrow_mut().take());
    let summary = format!("Rust panic: {message}");

    let (details, file, line, column) = match &site {
        Some(site) => {
            let file = crate::error::absolute_path(&site.file);

            (
                format!(
                    "{summary}\n    at {}:{}:{}",
                    site.file, site.line, site.column
                ),
                file,
                site.line,
                site.column,
            )
        }
        None => (summary.clone(), String::new(), 0, 0),
    };

    let report = RustealErrorReport {
        level: crate::logging::LOG_ERROR,
        script_error: true,
        summary: summary.as_ptr(),
        summary_len: summary.len() as u32,
        details: details.as_ptr(),
        details_len: details.len() as u32,
        object: FWeakObjectHandle {
            object_index: -1,
            object_serial_number: 0,
        },
        file: file.as_ptr(),
        file_len: file.len() as u32,
        line,
        column,
    };

    unsafe {
        crate::ffi_dispatch::logging_report(&report);
    }
}

fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
    if let Some(s) = payload.downcast_ref::<&str>() {
        (*s).to_owned()
    } else if let Some(s) = payload.downcast_ref::<String>() {
        s.clone()
    } else {
        "unknown payload".to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ffi_boundary_returns_value_on_success() {
        let result = ffi_boundary(0i32, || 42);
        assert_eq!(result, 42);
    }

    #[test]
    fn ffi_boundary_returns_default_on_panic() {
        let result = ffi_boundary(-1i32, || {
            panic!("test panic");
        });

        assert_eq!(result, -1);
    }

    #[test]
    fn ffi_boundary_returns_default_on_string_panic() {
        let result = ffi_boundary(false, || -> bool {
            panic!("{}", "formatted panic");
        });

        assert!(!result);
    }

    #[test]
    fn panic_messages_keep_their_text() {
        let payload: Box<dyn std::any::Any + Send> = Box::new("boom");
        assert_eq!(panic_message(&payload), "boom");
        let payload: Box<dyn std::any::Any + Send> = Box::new(String::from("bang"));
        assert_eq!(panic_message(&payload), "bang");
    }
}