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)]
115mod tests {
116    use std::ffi::{CString, c_void};
117    use std::sync::atomic::{AtomicUsize, Ordering};
118    use std::sync::{Arc, Mutex, MutexGuard};
119
120    use super::*;
121    use crate::ErrorKind;
122    use maplibre_core::{LogEvent, LogSeverity};
123
124    static LOGGING_TEST_LOCK: Mutex<()> = Mutex::new(());
125
126    struct LoggingTestGuard {
127        _lock: MutexGuard<'static, ()>,
128    }
129
130    impl LoggingTestGuard {
131        fn new() -> Self {
132            let guard = Self {
133                _lock: LOGGING_TEST_LOCK
134                    .lock()
135                    .unwrap_or_else(|poisoned| poisoned.into_inner()),
136            };
137            clear_logging_after_test();
138            guard
139        }
140    }
141
142    impl Drop for LoggingTestGuard {
143        fn drop(&mut self) {
144            clear_logging_after_test();
145        }
146    }
147
148    fn clear_logging_after_test() {
149        let _ = clear_log_callback();
150        let _ = set_async_log_severity_mask(LogSeverityMask::DEFAULT);
151    }
152
153    #[test]
154    // Spec coverage: BND-120.
155    fn log_callback_install_clear_and_trampoline_copy_record() {
156        let _guard = LoggingTestGuard::new();
157        let calls = Arc::new(AtomicUsize::new(0));
158        let test_calls = calls.clone();
159
160        set_log_callback(move |record| {
161            test_calls.fetch_add(1, Ordering::SeqCst);
162            if record.code == 42 {
163                assert_eq!(record.severity, LogSeverity::Warning);
164                assert_eq!(record.severity.raw_value(), sys::MLN_LOG_SEVERITY_WARNING);
165                assert_eq!(record.event, LogEvent::Render);
166                assert_eq!(record.event.raw_value(), sys::MLN_LOG_EVENT_RENDER);
167                assert_eq!(record.message, "hello");
168                return true;
169            }
170            false
171        })
172        .unwrap();
173
174        let baseline_calls = calls.load(Ordering::SeqCst);
175        let message = CString::new("hello").unwrap();
176        let current = {
177            let state = lock_log_callback_state();
178            state.as_ref().unwrap().clone()
179        };
180        let user_data = Arc::as_ptr(&current).cast_mut().cast::<c_void>();
181        assert_eq!(
182            unsafe {
183                log_callback_trampoline(
184                    user_data,
185                    sys::MLN_LOG_SEVERITY_WARNING,
186                    sys::MLN_LOG_EVENT_RENDER,
187                    42,
188                    message.as_ptr(),
189                )
190            },
191            1
192        );
193        assert_eq!(calls.load(Ordering::SeqCst), baseline_calls + 1);
194
195        clear_log_callback().unwrap();
196        assert!(lock_log_callback_state().is_none());
197        assert_eq!(
198            unsafe {
199                log_callback_trampoline(
200                    std::ptr::null_mut(),
201                    sys::MLN_LOG_SEVERITY_WARNING,
202                    sys::MLN_LOG_EVENT_RENDER,
203                    42,
204                    message.as_ptr(),
205                )
206            },
207            0
208        );
209        assert_eq!(calls.load(Ordering::SeqCst), baseline_calls + 1);
210    }
211
212    #[test]
213    // Spec coverage: BND-120.
214    fn log_callback_replacement_invokes_only_replacement() {
215        let _guard = LoggingTestGuard::new();
216        let first_calls = Arc::new(AtomicUsize::new(0));
217        let second_calls = Arc::new(AtomicUsize::new(0));
218        let first_callback_calls = Arc::clone(&first_calls);
219        set_log_callback(move |_| {
220            first_callback_calls.fetch_add(1, Ordering::SeqCst);
221            true
222        })
223        .unwrap();
224
225        let second_callback_calls = Arc::clone(&second_calls);
226        set_log_callback(move |_| {
227            second_callback_calls.fetch_add(1, Ordering::SeqCst);
228            true
229        })
230        .unwrap();
231
232        let current = {
233            let state = lock_log_callback_state();
234            state.as_ref().unwrap().clone()
235        };
236        let message = CString::new("replacement").unwrap();
237        assert_eq!(
238            invoke_callback(
239                &current,
240                sys::MLN_LOG_SEVERITY_INFO,
241                sys::MLN_LOG_EVENT_GENERAL,
242                7,
243                message.as_ptr(),
244            ),
245            1
246        );
247
248        assert_eq!(first_calls.load(Ordering::SeqCst), 0);
249        assert_eq!(second_calls.load(Ordering::SeqCst), 1);
250
251        clear_log_callback().unwrap();
252        assert!(lock_log_callback_state().is_none());
253    }
254
255    #[test]
256    // Spec coverage: BND-121.
257    fn invalid_utf8_log_messages_are_not_consumed() {
258        let _guard = LoggingTestGuard::new();
259        set_log_callback(|_| true).unwrap();
260        let invalid = b"\xff\0";
261        let current = {
262            let state = lock_log_callback_state();
263            state.as_ref().unwrap().clone()
264        };
265
266        assert_eq!(
267            invoke_callback(
268                &current,
269                sys::MLN_LOG_SEVERITY_ERROR,
270                sys::MLN_LOG_EVENT_GENERAL,
271                0,
272                invalid.as_ptr().cast(),
273            ),
274            0
275        );
276
277        clear_log_callback().unwrap();
278    }
279
280    #[test]
281    // Spec coverage: BND-121.
282    fn log_callback_panics_are_not_consumed() {
283        let _guard = LoggingTestGuard::new();
284        set_log_callback(|_| panic!("contained panic")).unwrap();
285
286        let message = CString::new("boom").unwrap();
287        let current = {
288            let state = lock_log_callback_state();
289            state.as_ref().unwrap().clone()
290        };
291
292        assert_eq!(
293            invoke_callback(
294                &current,
295                sys::MLN_LOG_SEVERITY_ERROR,
296                sys::MLN_LOG_EVENT_GENERAL,
297                0,
298                message.as_ptr(),
299            ),
300            0
301        );
302
303        clear_log_callback().unwrap();
304    }
305
306    #[test]
307    // Spec coverage: BND-020.
308    fn async_log_severity_mask_status_propagates_invalid_bits() {
309        let _guard = LoggingTestGuard::new();
310        let invalid_mask = LogSeverityMask::from_bits_retain(1 << 31);
311
312        let error = set_async_log_severity_mask(invalid_mask).unwrap_err();
313
314        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
315        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
316    }
317
318    #[test]
319    // Spec coverage: BND-120.
320    fn async_log_severity_mask_accepts_known_values() {
321        let _guard = LoggingTestGuard::new();
322
323        set_async_log_severity_mask(LogSeverityMask::INFO | LogSeverityMask::ERROR).unwrap();
324        set_async_log_severity_mask(LogSeverityMask::DEFAULT).unwrap();
325    }
326}