Skip to main content

maplibre_native_ffi/
error.rs

1use std::fmt;
2use std::mem::MaybeUninit;
3
4use maplibre_native_ffi_sys as sys;
5
6pub type Result<T> = std::result::Result<T, Error>;
7
8use crate::Status;
9
10/// A failure that native or this binding reports.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Error {
13    /// A native status and the diagnostic that native copied for it.
14    Status {
15        status: Status,
16        diagnostic: String,
17        /// The map snapshot generation that a failed or cancelled map command
18        /// published, and zero for any other failure.
19        generation: u64,
20    },
21    AbiVersionMismatch {
22        expected: u32,
23        actual: u32,
24    },
25}
26
27impl Error {
28    pub fn new(status: Status, diagnostic: impl Into<String>) -> Self {
29        Self::Status {
30            status,
31            diagnostic: diagnostic.into(),
32            generation: 0,
33        }
34    }
35
36    pub fn from_status_and_diagnostic(status: i32, diagnostic: impl Into<String>) -> Self {
37        Self::new(Status::from_raw(status), diagnostic)
38    }
39
40    pub fn invalid_argument(diagnostic: impl Into<String>) -> Self {
41        Self::new(Status::InvalidArgument, diagnostic)
42    }
43
44    pub fn abi_version_mismatch(expected: u32, actual: u32) -> Self {
45        Self::AbiVersionMismatch { expected, actual }
46    }
47
48    pub fn status(&self) -> Status {
49        match self {
50            Self::Status { status, .. } => *status,
51            Self::AbiVersionMismatch { .. } => Status::Unsupported,
52        }
53    }
54
55    pub fn diagnostic(&self) -> &str {
56        match self {
57            Self::Status { diagnostic, .. } => diagnostic,
58            Self::AbiVersionMismatch { .. } => "C ABI version mismatch",
59        }
60    }
61
62    /// The map snapshot generation that a failed or cancelled map command
63    /// published, and zero for any other failure.
64    pub fn generation(&self) -> u64 {
65        match self {
66            Self::Status { generation, .. } => *generation,
67            Self::AbiVersionMismatch { .. } => 0,
68        }
69    }
70}
71
72impl fmt::Display for Error {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            Self::Status {
76                status, diagnostic, ..
77            } => write!(f, "{status}: {diagnostic}"),
78            Self::AbiVersionMismatch { expected, actual } => write!(
79                f,
80                "unsupported MapLibre Native C ABI version {actual}; expected {expected}"
81            ),
82        }
83    }
84}
85
86impl std::error::Error for Error {}
87
88/// A consuming call that native refused, such as a release while a child
89/// handle is still live. It hands the handle back, so the caller can retry or
90/// drop it.
91///
92/// It converts into its [`Error`], so `?` works where the handle isn't needed:
93///
94/// ```no_run
95/// # async fn run(runtime: maplibre_native_ffi::RuntimeHandle) -> maplibre_native_ffi::Result<()> {
96/// runtime.release()?.await?;
97/// # Ok(())
98/// # }
99/// ```
100pub struct ReleaseError<H> {
101    /// The handle, still live and owned by the caller.
102    pub handle: H,
103    /// Why native refused.
104    pub error: Error,
105}
106
107impl<H: fmt::Debug> fmt::Debug for ReleaseError<H> {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.debug_struct("ReleaseError")
110            .field("handle", &self.handle)
111            .field("error", &self.error)
112            .finish()
113    }
114}
115
116impl<H: fmt::Debug> fmt::Display for ReleaseError<H> {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "native refused to end {:?}", self.handle)
119    }
120}
121
122impl<H: fmt::Debug> std::error::Error for ReleaseError<H> {
123    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
124        Some(&self.error)
125    }
126}
127
128impl<H> From<ReleaseError<H>> for Error {
129    fn from(error: ReleaseError<H>) -> Self {
130        error.error
131    }
132}
133
134/// Calls a status-returning C function with a diagnostic for its final
135/// `out_diagnostic` parameter, and converts a failed status and that diagnostic
136/// into an error.
137pub fn check(call: impl FnOnce(*mut sys::mln_diagnostic) -> sys::mln_status) -> Result<()> {
138    let mut diagnostic = MaybeUninit::<sys::mln_diagnostic>::uninit();
139    let raw = diagnostic.as_mut_ptr();
140    // SAFETY: raw points to storage for one mln_diagnostic. Native writes the
141    // message, so only the size and an empty message are initialized here;
142    // the empty message covers a call that fails without reaching native.
143    unsafe {
144        (&raw mut (*raw).size).write(std::mem::size_of::<sys::mln_diagnostic>() as u32);
145        (&raw mut (*raw).message).cast::<u8>().write(0);
146    }
147    let status = call(raw);
148    if status == sys::MLN_STATUS_OK {
149        return Ok(());
150    }
151    // SAFETY: the message is null-terminated, either by native or above.
152    Err(Error::from_status_and_diagnostic(status, unsafe {
153        diagnostic_message(raw)
154    }))
155}
156
157/// Copies a diagnostic's message, which is initialized through its null byte.
158unsafe fn diagnostic_message(diagnostic: *const sys::mln_diagnostic) -> String {
159    // SAFETY: the caller promises the diagnostic outlives this call.
160    let message = unsafe { &raw const (*diagnostic).message }.cast::<u8>();
161    let capacity = crate::DIAGNOSTIC_MESSAGE_CAPACITY as usize;
162    let mut length = 0;
163    // SAFETY: the caller promises a null byte within the message capacity, and
164    // every byte before it is initialized.
165    while length < capacity && unsafe { message.add(length).read() } != 0 {
166        length += 1;
167    }
168    // SAFETY: the first length bytes are initialized, as read above.
169    String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(message, length) }).into_owned()
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    /// Runs a call that fails with `status` after writing `message` into its
177    /// diagnostic, the way a native call reports a failure.
178    fn failing_call(status: sys::mln_status, message: &[u8]) -> Error {
179        check(|diagnostic| {
180            // SAFETY: check hands the call one diagnostic, and every message
181            // written here fits its capacity.
182            unsafe {
183                let out = (&raw mut (*diagnostic).message).cast::<u8>();
184                std::ptr::copy_nonoverlapping(message.as_ptr(), out, message.len());
185            }
186            status
187        })
188        .unwrap_err()
189    }
190
191    #[test]
192    fn a_failed_call_carries_its_status_and_keeps_an_unknown_code() {
193        let error = failing_call(sys::MLN_STATUS_INVALID_STATE, b"failed\0");
194        assert_eq!(error.status(), Status::InvalidState);
195        assert_eq!(error.diagnostic(), "failed");
196        assert_eq!(error.to_string(), "INVALID_STATE: failed");
197
198        let error = failing_call(-123_456, b"future status\0");
199        assert_eq!(error.status(), Status::Unknown(-123_456));
200        assert_eq!(error.status().as_raw(), -123_456);
201        assert_eq!(error.diagnostic(), "future status");
202        assert_eq!(error.to_string(), "UNKNOWN(-123456): future status");
203    }
204
205    #[test]
206    fn a_diagnostic_is_bounded_by_its_capacity_and_empty_when_none_was_written() {
207        let capacity = crate::DIAGNOSTIC_MESSAGE_CAPACITY as usize;
208        let error = failing_call(sys::MLN_STATUS_INVALID_STATE, &vec![b'x'; capacity]);
209        assert_eq!(error.diagnostic(), "x".repeat(capacity));
210
211        let error = failing_call(sys::MLN_STATUS_INVALID_STATE, b"bad \xff byte\0");
212        assert_eq!(error.diagnostic(), "bad \u{fffd} byte");
213
214        // Each call starts from an empty message, so a call that fails without
215        // writing one carries none, not the previous call's.
216        let error = check(|_| sys::MLN_STATUS_INVALID_ARGUMENT).unwrap_err();
217        assert_eq!(error.status(), Status::InvalidArgument);
218        assert_eq!(error.diagnostic(), "");
219    }
220}