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
74/// Restores MapLibre Native's default async log severity mask.
75pub fn restore_default_async_log_severity_mask() -> Result<()> {
76    set_async_log_severity_mask(LogSeverityMask::DEFAULT)
77}
78
79unsafe extern "C" fn log_callback_trampoline(
80    user_data: *mut c_void,
81    severity: u32,
82    event: u32,
83    code: i64,
84    message: *const c_char,
85) -> u32 {
86    if user_data.is_null() {
87        return 0;
88    }
89
90    // SAFETY: set_log_callback installs Arc::as_ptr(&CallbackState) as
91    // user_data and retains the current Arc until native replacement or clear
92    // succeeds, so the pointer remains valid for native dispatch.
93    let state = unsafe { &*user_data.cast::<CallbackState>() };
94    invoke_callback(state, severity, event, code, message)
95}
96
97fn invoke_callback(
98    state: &CallbackState,
99    raw_severity: u32,
100    raw_event: u32,
101    code: i64,
102    message: *const c_char,
103) -> u32 {
104    // SAFETY: message is supplied by the C logging callback contract as a
105    // null-terminated string pointer. Invalid strings are treated as not
106    // consumed.
107    let Ok(record) = (unsafe {
108        maplibre_core::logging::copy_log_record(raw_severity, raw_event, code, message)
109    }) else {
110        return 0;
111    };
112
113    match panic::catch_unwind(AssertUnwindSafe(|| (state.callback)(record))) {
114        Ok(true) => 1,
115        Ok(false) | Err(_) => 0,
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::ffi::{CString, c_void};
122    use std::sync::atomic::{AtomicUsize, Ordering};
123    use std::sync::{Arc, Mutex, MutexGuard};
124
125    use super::*;
126    use crate::ErrorKind;
127    use maplibre_core::{LogEvent, LogSeverity};
128
129    static LOGGING_TEST_LOCK: Mutex<()> = Mutex::new(());
130
131    struct LoggingTestGuard {
132        _lock: MutexGuard<'static, ()>,
133    }
134
135    impl LoggingTestGuard {
136        fn new() -> Self {
137            let guard = Self {
138                _lock: LOGGING_TEST_LOCK
139                    .lock()
140                    .unwrap_or_else(|poisoned| poisoned.into_inner()),
141            };
142            clear_logging_after_test();
143            guard
144        }
145    }
146
147    impl Drop for LoggingTestGuard {
148        fn drop(&mut self) {
149            clear_logging_after_test();
150        }
151    }
152
153    fn clear_logging_after_test() {
154        let _ = clear_log_callback();
155        let _ = restore_default_async_log_severity_mask();
156    }
157
158    #[test]
159    fn log_callback_install_clear_and_trampoline_copy_record() {
160        let _guard = LoggingTestGuard::new();
161        let calls = Arc::new(AtomicUsize::new(0));
162        let test_calls = calls.clone();
163
164        set_log_callback(move |record| {
165            test_calls.fetch_add(1, Ordering::SeqCst);
166            if record.code == 42 {
167                assert_eq!(record.severity, LogSeverity::Warning);
168                assert_eq!(record.severity.raw_value(), sys::MLN_LOG_SEVERITY_WARNING);
169                assert_eq!(record.event, LogEvent::Render);
170                assert_eq!(record.event.raw_value(), sys::MLN_LOG_EVENT_RENDER);
171                assert_eq!(record.message, "hello");
172                return true;
173            }
174            false
175        })
176        .unwrap();
177
178        let baseline_calls = calls.load(Ordering::SeqCst);
179        let message = CString::new("hello").unwrap();
180        let current = {
181            let state = lock_log_callback_state();
182            state.as_ref().unwrap().clone()
183        };
184        let user_data = Arc::as_ptr(&current).cast_mut().cast::<c_void>();
185        assert_eq!(
186            unsafe {
187                log_callback_trampoline(
188                    user_data,
189                    sys::MLN_LOG_SEVERITY_WARNING,
190                    sys::MLN_LOG_EVENT_RENDER,
191                    42,
192                    message.as_ptr(),
193                )
194            },
195            1
196        );
197        assert_eq!(calls.load(Ordering::SeqCst), baseline_calls + 1);
198
199        clear_log_callback().unwrap();
200        assert!(lock_log_callback_state().is_none());
201        assert_eq!(
202            unsafe {
203                log_callback_trampoline(
204                    std::ptr::null_mut(),
205                    sys::MLN_LOG_SEVERITY_WARNING,
206                    sys::MLN_LOG_EVENT_RENDER,
207                    42,
208                    message.as_ptr(),
209                )
210            },
211            0
212        );
213        assert_eq!(calls.load(Ordering::SeqCst), baseline_calls + 1);
214    }
215
216    #[test]
217    fn invalid_utf8_log_messages_are_not_consumed() {
218        let _guard = LoggingTestGuard::new();
219        set_log_callback(|_| true).unwrap();
220        let invalid = b"\xff\0";
221        let current = {
222            let state = lock_log_callback_state();
223            state.as_ref().unwrap().clone()
224        };
225
226        assert_eq!(
227            invoke_callback(
228                &current,
229                sys::MLN_LOG_SEVERITY_ERROR,
230                sys::MLN_LOG_EVENT_GENERAL,
231                0,
232                invalid.as_ptr().cast(),
233            ),
234            0
235        );
236
237        clear_log_callback().unwrap();
238    }
239
240    #[test]
241    fn log_callback_panics_are_not_consumed() {
242        let _guard = LoggingTestGuard::new();
243        set_log_callback(|_| panic!("contained panic")).unwrap();
244
245        let message = CString::new("boom").unwrap();
246        let current = {
247            let state = lock_log_callback_state();
248            state.as_ref().unwrap().clone()
249        };
250
251        assert_eq!(
252            invoke_callback(
253                &current,
254                sys::MLN_LOG_SEVERITY_ERROR,
255                sys::MLN_LOG_EVENT_GENERAL,
256                0,
257                message.as_ptr(),
258            ),
259            0
260        );
261
262        clear_log_callback().unwrap();
263    }
264
265    #[test]
266    fn async_log_severity_mask_status_propagates_invalid_bits() {
267        let _guard = LoggingTestGuard::new();
268        let invalid_mask = LogSeverityMask::from_bits_retain(1 << 31);
269
270        let error = set_async_log_severity_mask(invalid_mask).unwrap_err();
271
272        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
273        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
274    }
275
276    #[test]
277    fn async_log_severity_mask_accepts_known_values() {
278        let _guard = LoggingTestGuard::new();
279
280        set_async_log_severity_mask(LogSeverityMask::INFO | LogSeverityMask::ERROR).unwrap();
281        restore_default_async_log_severity_mask().unwrap();
282    }
283}