Skip to main content

maplibre_native/
logging.rs

1use std::ffi::{c_char, c_void};
2use std::panic::{self, AssertUnwindSafe};
3use std::sync::{Arc, Mutex, MutexGuard};
4
5use crate::{Result, sys};
6use maplibre_core::LogSeverityMask;
7use maplibre_native_core as maplibre_core;
8
9pub use maplibre_core::LogRecord;
10
11type LogCallback = dyn Fn(LogRecord) -> bool + Send + Sync + 'static;
12
13struct CallbackState {
14    callback: Box<LogCallback>,
15}
16
17static LOG_CALLBACK_STATE: Mutex<Option<Arc<CallbackState>>> = Mutex::new(None);
18
19fn lock_log_callback_state() -> MutexGuard<'static, Option<Arc<CallbackState>>> {
20    LOG_CALLBACK_STATE
21        .lock()
22        .unwrap_or_else(|poisoned| poisoned.into_inner())
23}
24
25/// Installs or replaces the process-global MapLibre Native log callback.
26///
27/// MapLibre Native may invoke the callback from logging or worker threads. The
28/// callback state must therefore be `Send + Sync + 'static`. The callback
29/// should return quickly and avoid calling MapLibre Native APIs. Panics are
30/// caught and reported to native logging as "not consumed".
31pub fn set_log_callback<F>(callback: F) -> Result<()>
32where
33    F: Fn(LogRecord) -> bool + Send + Sync + 'static,
34{
35    let replacement = Arc::new(CallbackState {
36        callback: Box::new(callback),
37    });
38    let user_data = Arc::as_ptr(&replacement).cast_mut().cast::<c_void>();
39    let previous = {
40        let mut current = lock_log_callback_state();
41        // SAFETY: log_callback_trampoline has the C callback ABI. user_data
42        // points at replacement, which is retained in current until the native
43        // callback is replaced or cleared.
44        maplibre_core::check(unsafe {
45            sys::mln_log_set_callback(Some(log_callback_trampoline), user_data)
46        })?;
47
48        current.replace(replacement)
49    };
50    drop(previous);
51    Ok(())
52}
53
54/// Clears the process-global MapLibre Native log callback.
55pub fn clear_log_callback() -> Result<()> {
56    // SAFETY: mln_log_clear_callback takes no arguments and clears native's
57    // process-global callback slot.
58    let previous = {
59        let mut current = lock_log_callback_state();
60        maplibre_core::check(unsafe { sys::mln_log_clear_callback() })?;
61        current.take()
62    };
63    drop(previous);
64    Ok(())
65}
66
67/// Configures severities that MapLibre Native may dispatch asynchronously.
68pub fn set_async_log_severity_mask(mask: LogSeverityMask) -> Result<()> {
69    // SAFETY: mask is passed by value. The C API validates unknown bits and
70    // reports them as MLN_STATUS_INVALID_ARGUMENT.
71    maplibre_core::check(unsafe { sys::mln_log_set_async_severity_mask(mask.bits()) })
72}
73
74unsafe extern "C" fn log_callback_trampoline(
75    user_data: *mut c_void,
76    severity: u32,
77    event: u32,
78    code: i64,
79    message: *const c_char,
80) -> u32 {
81    if user_data.is_null() {
82        return 0;
83    }
84
85    // SAFETY: set_log_callback installs Arc::as_ptr(&CallbackState) as
86    // user_data and retains the current Arc until native replacement or clear
87    // succeeds, so the pointer remains valid for native dispatch.
88    let state = unsafe { &*user_data.cast::<CallbackState>() };
89    invoke_callback(state, severity, event, code, message)
90}
91
92fn invoke_callback(
93    state: &CallbackState,
94    raw_severity: u32,
95    raw_event: u32,
96    code: i64,
97    message: *const c_char,
98) -> u32 {
99    // SAFETY: message is supplied by the C logging callback contract as a
100    // null-terminated string pointer. Invalid strings are treated as not
101    // consumed.
102    let Ok(record) = (unsafe {
103        maplibre_core::logging::copy_log_record(raw_severity, raw_event, code, message)
104    }) else {
105        return 0;
106    };
107
108    match panic::catch_unwind(AssertUnwindSafe(|| (state.callback)(record))) {
109        Ok(true) => 1,
110        Ok(false) | Err(_) => 0,
111    }
112}
113
114#[cfg(test)]
115pub(crate) mod test_support {
116    use std::sync::{Mutex, MutexGuard};
117
118    use super::{LogSeverityMask, clear_log_callback, set_async_log_severity_mask};
119
120    static LOGGING_TEST_LOCK: Mutex<()> = Mutex::new(());
121
122    /// Serializes tests that install the process-global log callback and
123    /// restores default logging when each one finishes.
124    pub(crate) struct LoggingTestGuard {
125        _lock: MutexGuard<'static, ()>,
126    }
127
128    impl LoggingTestGuard {
129        pub(crate) fn new() -> Self {
130            let guard = Self {
131                _lock: LOGGING_TEST_LOCK
132                    .lock()
133                    .unwrap_or_else(|poisoned| poisoned.into_inner()),
134            };
135            clear_logging_after_test();
136            guard
137        }
138    }
139
140    impl Drop for LoggingTestGuard {
141        fn drop(&mut self) {
142            clear_logging_after_test();
143        }
144    }
145
146    fn clear_logging_after_test() {
147        let _ = clear_log_callback();
148        let _ = set_async_log_severity_mask(LogSeverityMask::DEFAULT);
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use std::ffi::{CString, c_void};
155    use std::sync::Arc;
156    use std::sync::atomic::{AtomicUsize, Ordering};
157
158    use super::test_support::LoggingTestGuard;
159    use super::*;
160    use crate::ErrorKind;
161    use maplibre_core::{LogEvent, LogSeverity};
162
163    #[test]
164    // Spec coverage: BND-120.
165    fn log_callback_install_clear_and_trampoline_copy_record() {
166        let _guard = LoggingTestGuard::new();
167        let calls = Arc::new(AtomicUsize::new(0));
168        let test_calls = calls.clone();
169
170        set_log_callback(move |record| {
171            test_calls.fetch_add(1, Ordering::SeqCst);
172            if record.code == 42 {
173                assert_eq!(record.severity, LogSeverity::Warning);
174                assert_eq!(record.severity.raw_value(), sys::MLN_LOG_SEVERITY_WARNING);
175                assert_eq!(record.event, LogEvent::Render);
176                assert_eq!(record.event.raw_value(), sys::MLN_LOG_EVENT_RENDER);
177                assert_eq!(record.message, "hello");
178                return true;
179            }
180            false
181        })
182        .unwrap();
183
184        let baseline_calls = calls.load(Ordering::SeqCst);
185        let message = CString::new("hello").unwrap();
186        let current = {
187            let state = lock_log_callback_state();
188            state.as_ref().unwrap().clone()
189        };
190        let user_data = Arc::as_ptr(&current).cast_mut().cast::<c_void>();
191        assert_eq!(
192            unsafe {
193                log_callback_trampoline(
194                    user_data,
195                    sys::MLN_LOG_SEVERITY_WARNING,
196                    sys::MLN_LOG_EVENT_RENDER,
197                    42,
198                    message.as_ptr(),
199                )
200            },
201            1
202        );
203        assert_eq!(calls.load(Ordering::SeqCst), baseline_calls + 1);
204
205        clear_log_callback().unwrap();
206        assert!(lock_log_callback_state().is_none());
207        assert_eq!(
208            unsafe {
209                log_callback_trampoline(
210                    std::ptr::null_mut(),
211                    sys::MLN_LOG_SEVERITY_WARNING,
212                    sys::MLN_LOG_EVENT_RENDER,
213                    42,
214                    message.as_ptr(),
215                )
216            },
217            0
218        );
219        assert_eq!(calls.load(Ordering::SeqCst), baseline_calls + 1);
220    }
221
222    #[test]
223    // Spec coverage: BND-120.
224    fn log_callback_replacement_invokes_only_replacement() {
225        let _guard = LoggingTestGuard::new();
226        let first_calls = Arc::new(AtomicUsize::new(0));
227        let second_calls = Arc::new(AtomicUsize::new(0));
228        let first_callback_calls = Arc::clone(&first_calls);
229        set_log_callback(move |_| {
230            first_callback_calls.fetch_add(1, Ordering::SeqCst);
231            true
232        })
233        .unwrap();
234
235        let second_callback_calls = Arc::clone(&second_calls);
236        set_log_callback(move |_| {
237            second_callback_calls.fetch_add(1, Ordering::SeqCst);
238            true
239        })
240        .unwrap();
241
242        let current = {
243            let state = lock_log_callback_state();
244            state.as_ref().unwrap().clone()
245        };
246        let message = CString::new("replacement").unwrap();
247        assert_eq!(
248            invoke_callback(
249                &current,
250                sys::MLN_LOG_SEVERITY_INFO,
251                sys::MLN_LOG_EVENT_GENERAL,
252                7,
253                message.as_ptr(),
254            ),
255            1
256        );
257
258        assert_eq!(first_calls.load(Ordering::SeqCst), 0);
259        assert_eq!(second_calls.load(Ordering::SeqCst), 1);
260
261        clear_log_callback().unwrap();
262        assert!(lock_log_callback_state().is_none());
263    }
264
265    #[test]
266    // Spec coverage: BND-121.
267    fn invalid_utf8_log_messages_are_not_consumed() {
268        let _guard = LoggingTestGuard::new();
269        set_log_callback(|_| true).unwrap();
270        let invalid = b"\xff\0";
271        let current = {
272            let state = lock_log_callback_state();
273            state.as_ref().unwrap().clone()
274        };
275
276        assert_eq!(
277            invoke_callback(
278                &current,
279                sys::MLN_LOG_SEVERITY_ERROR,
280                sys::MLN_LOG_EVENT_GENERAL,
281                0,
282                invalid.as_ptr().cast(),
283            ),
284            0
285        );
286
287        clear_log_callback().unwrap();
288    }
289
290    #[test]
291    // Spec coverage: BND-121.
292    fn log_callback_panics_are_not_consumed() {
293        let _guard = LoggingTestGuard::new();
294        set_log_callback(|_| panic!("contained panic")).unwrap();
295
296        let message = CString::new("boom").unwrap();
297        let current = {
298            let state = lock_log_callback_state();
299            state.as_ref().unwrap().clone()
300        };
301
302        assert_eq!(
303            invoke_callback(
304                &current,
305                sys::MLN_LOG_SEVERITY_ERROR,
306                sys::MLN_LOG_EVENT_GENERAL,
307                0,
308                message.as_ptr(),
309            ),
310            0
311        );
312
313        clear_log_callback().unwrap();
314    }
315
316    #[test]
317    // Spec coverage: BND-020.
318    fn async_log_severity_mask_status_propagates_invalid_bits() {
319        let _guard = LoggingTestGuard::new();
320        let invalid_mask = LogSeverityMask::from_bits_retain(1 << 31);
321
322        let error = set_async_log_severity_mask(invalid_mask).unwrap_err();
323
324        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
325        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
326    }
327
328    #[test]
329    // Spec coverage: BND-120.
330    fn async_log_severity_mask_accepts_known_values() {
331        let _guard = LoggingTestGuard::new();
332
333        set_async_log_severity_mask(LogSeverityMask::INFO | LogSeverityMask::ERROR).unwrap();
334        set_async_log_severity_mask(LogSeverityMask::DEFAULT).unwrap();
335    }
336}