Skip to main content

maplibre_native_ffi/
custom_mvt_vector.rs

1use std::fmt;
2use std::os::raw::c_void;
3use std::panic::{AssertUnwindSafe, catch_unwind};
4use std::ptr;
5use std::sync::{Condvar, Mutex};
6
7use maplibre_native_ffi_core as maplibre_core;
8use maplibre_native_ffi_sys as sys;
9
10use crate::custom_geometry::CanonicalTileId;
11
12type TileCallback = dyn Fn(CanonicalTileId) + Send + Sync + 'static;
13
14/// Options used when adding a custom MVT vector source.
15///
16/// Custom MVT vector callbacks may run on native worker threads. Keep callbacks
17/// quick, and hand work back to the map owner thread before calling map APIs
18/// such as `set_custom_mvt_vector_source_tile_data` or
19/// `set_custom_mvt_vector_source_tile_error`.
20#[non_exhaustive]
21pub struct CustomMvtVectorSourceOptions {
22    fetch_tile: Box<TileCallback>,
23    cancel_tile: Option<Box<TileCallback>>,
24    /// Minimum zoom level at which the source produces tiles.
25    pub min_zoom: Option<f64>,
26    /// Maximum zoom level at which the source produces tiles.
27    pub max_zoom: Option<f64>,
28}
29
30impl fmt::Debug for CustomMvtVectorSourceOptions {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        f.debug_struct("CustomMvtVectorSourceOptions")
33            .field("has_cancel_tile", &self.cancel_tile.is_some())
34            .field("min_zoom", &self.min_zoom)
35            .field("max_zoom", &self.max_zoom)
36            .finish_non_exhaustive()
37    }
38}
39
40impl CustomMvtVectorSourceOptions {
41    pub fn new<F>(fetch_tile: F) -> Self
42    where
43        F: Fn(CanonicalTileId) + Send + Sync + 'static,
44    {
45        Self {
46            fetch_tile: Box::new(fetch_tile),
47            cancel_tile: None,
48            min_zoom: None,
49            max_zoom: None,
50        }
51    }
52
53    pub fn with_cancel_tile<F>(mut self, cancel_tile: F) -> Self
54    where
55        F: Fn(CanonicalTileId) + Send + Sync + 'static,
56    {
57        self.cancel_tile = Some(Box::new(cancel_tile));
58        self
59    }
60}
61
62#[derive(Debug, Default)]
63struct CallbackLifecycle {
64    active: usize,
65    closing: bool,
66    closed: bool,
67}
68
69/// Callback state for one custom MVT vector source, owned by the C API from a
70/// successful add until it invokes the release callback that frees this box.
71pub(crate) struct CustomMvtVectorSourceState {
72    fetch_tile: Box<TileCallback>,
73    cancel_tile: Option<Box<TileCallback>>,
74    min_zoom: Option<f64>,
75    max_zoom: Option<f64>,
76    lifecycle: Mutex<CallbackLifecycle>,
77    idle: Condvar,
78}
79
80impl fmt::Debug for CustomMvtVectorSourceState {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        f.debug_struct("CustomMvtVectorSourceState")
83            .finish_non_exhaustive()
84    }
85}
86
87impl CustomMvtVectorSourceState {
88    pub(crate) fn new(options: CustomMvtVectorSourceOptions) -> Box<Self> {
89        Box::new(Self {
90            fetch_tile: options.fetch_tile,
91            cancel_tile: options.cancel_tile,
92            min_zoom: options.min_zoom,
93            max_zoom: options.max_zoom,
94            lifecycle: Mutex::new(CallbackLifecycle::default()),
95            idle: Condvar::new(),
96        })
97    }
98
99    pub(crate) fn descriptor(&self) -> sys::mln_custom_mvt_vector_source_options {
100        maplibre_core::style::custom_mvt_vector_source_options_to_native(
101            maplibre_core::style::CustomMvtVectorSourceDescriptorFields {
102                fetch_tile: Some(fetch_tile_trampoline),
103                cancel_tile: self
104                    .cancel_tile
105                    .as_ref()
106                    .map(|_| cancel_tile_trampoline as _),
107                release_user_data: Some(release_trampoline),
108                user_data: ptr::from_ref(self).cast_mut().cast::<c_void>(),
109                min_zoom: self.min_zoom,
110                max_zoom: self.max_zoom,
111            },
112        )
113    }
114
115    pub(crate) fn close(&self) {
116        let mut lifecycle = self
117            .lifecycle
118            .lock()
119            .unwrap_or_else(|poisoned| poisoned.into_inner());
120        if lifecycle.closed {
121            return;
122        }
123        lifecycle.closing = true;
124        while lifecycle.active != 0 {
125            lifecycle = self
126                .idle
127                .wait(lifecycle)
128                .unwrap_or_else(|poisoned| poisoned.into_inner());
129        }
130        lifecycle.closed = true;
131    }
132
133    fn invoke_fetch(&self, tile_id: CanonicalTileId) {
134        let Some(_guard) = self.enter_callback() else {
135            return;
136        };
137        let _ = catch_unwind(AssertUnwindSafe(|| (self.fetch_tile)(tile_id)));
138    }
139
140    fn invoke_cancel(&self, tile_id: CanonicalTileId) {
141        let Some(_guard) = self.enter_callback() else {
142            return;
143        };
144        if let Some(cancel_tile) = &self.cancel_tile {
145            let _ = catch_unwind(AssertUnwindSafe(|| cancel_tile(tile_id)));
146        }
147    }
148
149    fn enter_callback(&self) -> Option<CallbackGuard<'_>> {
150        let mut lifecycle = self
151            .lifecycle
152            .lock()
153            .unwrap_or_else(|poisoned| poisoned.into_inner());
154        if lifecycle.closing || lifecycle.closed {
155            return None;
156        }
157        lifecycle.active += 1;
158        Some(CallbackGuard { state: self })
159    }
160
161    fn exit_callback(&self) {
162        let mut lifecycle = self
163            .lifecycle
164            .lock()
165            .unwrap_or_else(|poisoned| poisoned.into_inner());
166        lifecycle.active -= 1;
167        if lifecycle.active == 0 {
168            self.idle.notify_all();
169        }
170    }
171}
172
173impl Drop for CustomMvtVectorSourceState {
174    fn drop(&mut self) {
175        self.close();
176    }
177}
178
179struct CallbackGuard<'a> {
180    state: &'a CustomMvtVectorSourceState,
181}
182
183impl Drop for CallbackGuard<'_> {
184    fn drop(&mut self) {
185        self.state.exit_callback();
186    }
187}
188
189unsafe extern "C" fn fetch_tile_trampoline(
190    user_data: *mut c_void,
191    tile_id: sys::mln_canonical_tile_id,
192) {
193    let Some(state) = ptr::NonNull::new(user_data.cast::<CustomMvtVectorSourceState>()) else {
194        return;
195    };
196    // SAFETY: user_data is installed from CustomMvtVectorSourceState::descriptor
197    // and remains valid until source/style/map teardown waits for in-flight callbacks.
198    unsafe { state.as_ref() }.invoke_fetch(CanonicalTileId::from_native(tile_id));
199}
200
201unsafe extern "C" fn release_trampoline(user_data: *mut c_void) {
202    let Some(state) = ptr::NonNull::new(user_data.cast::<CustomMvtVectorSourceState>()) else {
203        return;
204    };
205    // SAFETY: The C API invokes this once, with the pointer
206    // add_custom_mvt_vector_source handed it, after it stops referencing the
207    // state, so this call owns the box. Dropping it waits for in-flight tile
208    // callbacks before it frees the host's own callbacks.
209    let state = unsafe { Box::from_raw(state.as_ptr()) };
210    let _ = catch_unwind(AssertUnwindSafe(move || drop(state)));
211}
212
213unsafe extern "C" fn cancel_tile_trampoline(
214    user_data: *mut c_void,
215    tile_id: sys::mln_canonical_tile_id,
216) {
217    let Some(state) = ptr::NonNull::new(user_data.cast::<CustomMvtVectorSourceState>()) else {
218        return;
219    };
220    // SAFETY: user_data is installed from CustomMvtVectorSourceState::descriptor
221    // and remains valid until source/style/map teardown waits for in-flight callbacks.
222    unsafe { state.as_ref() }.invoke_cancel(CanonicalTileId::from_native(tile_id));
223}
224
225#[cfg(test)]
226mod tests {
227    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
228    use std::sync::{Arc, Condvar, Mutex};
229    use std::time::Duration;
230
231    use super::*;
232
233    fn tile(z: u32, x: u32, y: u32) -> sys::mln_canonical_tile_id {
234        CanonicalTileId::new(z, x, y).to_native()
235    }
236
237    #[test]
238    // Spec coverage: BND-124.
239    fn custom_mvt_vector_callbacks_invoke_fetch_and_cancel_with_copied_tile_id() {
240        let fetched = Arc::new(Mutex::new(Vec::new()));
241        let cancelled = Arc::new(Mutex::new(Vec::new()));
242        let fetched_callback = Arc::clone(&fetched);
243        let cancelled_callback = Arc::clone(&cancelled);
244        let state = CustomMvtVectorSourceState::new(
245            CustomMvtVectorSourceOptions::new(move |tile_id| {
246                fetched_callback.lock().unwrap().push(tile_id);
247            })
248            .with_cancel_tile(move |tile_id| {
249                cancelled_callback.lock().unwrap().push(tile_id);
250            }),
251        );
252        let descriptor = state.descriptor();
253
254        unsafe {
255            descriptor.fetch_tile.unwrap()(descriptor.user_data, tile(1, 2, 3));
256            descriptor.cancel_tile.unwrap()(descriptor.user_data, tile(4, 5, 6));
257        }
258
259        assert_eq!(
260            fetched.lock().unwrap().as_slice(),
261            &[CanonicalTileId::new(1, 2, 3)]
262        );
263        assert_eq!(
264            cancelled.lock().unwrap().as_slice(),
265            &[CanonicalTileId::new(4, 5, 6)]
266        );
267    }
268
269    #[test]
270    // Spec coverage: BND-121.
271    fn custom_mvt_vector_callbacks_contain_panics() {
272        let cancel_called = Arc::new(AtomicBool::new(false));
273        let cancel_called_callback = Arc::clone(&cancel_called);
274        let state = CustomMvtVectorSourceState::new(
275            CustomMvtVectorSourceOptions::new(|_| panic!("fetch panic")).with_cancel_tile(
276                move |_| {
277                    cancel_called_callback.store(true, Ordering::SeqCst);
278                    panic!("cancel panic");
279                },
280            ),
281        );
282        let descriptor = state.descriptor();
283
284        unsafe {
285            descriptor.fetch_tile.unwrap()(descriptor.user_data, tile(0, 0, 0));
286            descriptor.cancel_tile.unwrap()(descriptor.user_data, tile(0, 0, 0));
287        }
288
289        assert!(cancel_called.load(Ordering::SeqCst));
290    }
291
292    #[test]
293    // Spec coverage: BND-124.
294    fn custom_mvt_vector_state_release_waits_for_active_upcalls() {
295        let entered = Arc::new((Mutex::new(false), Condvar::new()));
296        let release = Arc::new((Mutex::new(false), Condvar::new()));
297        let closed = Arc::new(AtomicBool::new(false));
298        let close_attempts = Arc::new(AtomicUsize::new(0));
299        let entered_callback = Arc::clone(&entered);
300        let release_callback = Arc::clone(&release);
301        let state = CustomMvtVectorSourceState::new(CustomMvtVectorSourceOptions::new(move |_| {
302            let (entered_lock, entered_cvar) = &*entered_callback;
303            *entered_lock.lock().unwrap() = true;
304            entered_cvar.notify_all();
305
306            let (release_lock, release_cvar) = &*release_callback;
307            let released = release_lock.lock().unwrap();
308            let (_released, timeout) = release_cvar
309                .wait_timeout_while(released, Duration::from_secs(5), |released| !*released)
310                .unwrap();
311            assert!(!timeout.timed_out());
312        }));
313        let descriptor = state.descriptor();
314        let callback = descriptor.fetch_tile.unwrap();
315        let user_data = descriptor.user_data as usize;
316
317        std::thread::scope(|scope| {
318            scope.spawn(move || unsafe {
319                callback(user_data as *mut c_void, tile(1, 1, 1));
320            });
321            let (entered_lock, entered_cvar) = &*entered;
322            let entered_guard = entered_lock.lock().unwrap();
323            let (_entered_guard, timeout) = entered_cvar
324                .wait_timeout_while(entered_guard, Duration::from_secs(5), |entered| !*entered)
325                .unwrap();
326            assert!(!timeout.timed_out());
327
328            let closed_for_thread = Arc::clone(&closed);
329            let close_attempts_for_thread = Arc::clone(&close_attempts);
330            let state_ref = &*state;
331            scope.spawn(move || {
332                close_attempts_for_thread.fetch_add(1, Ordering::SeqCst);
333                state_ref.close();
334                closed_for_thread.store(true, Ordering::SeqCst);
335            });
336
337            std::thread::sleep(Duration::from_millis(50));
338            assert_eq!(close_attempts.load(Ordering::SeqCst), 1);
339            assert!(!closed.load(Ordering::SeqCst));
340            let (release_lock, release_cvar) = &*release;
341            *release_lock.lock().unwrap() = true;
342            release_cvar.notify_all();
343        });
344
345        assert!(closed.load(Ordering::SeqCst));
346        unsafe {
347            descriptor.fetch_tile.unwrap()(descriptor.user_data, tile(9, 9, 9));
348        }
349    }
350}