Skip to main content

maplibre_native_ffi/
events.rs

1use std::fmt;
2use std::marker::PhantomData;
3use std::str;
4
5use maplibre_native_ffi_core as maplibre_core;
6use maplibre_native_ffi_sys as sys;
7
8use crate::{Error, Result};
9pub use maplibre_core::events::{
10    CameraTransitionFinishedEvent, OfflineOperationCompletedEvent, OfflineRegionResponseErrorEvent,
11    OfflineRegionStatus, OfflineRegionStatusEvent, OfflineRegionTileCountLimitEvent,
12    RenderFrameEvent, RenderMapEvent, RenderingStats, RuntimeEventPayload, TileActionEvent, TileId,
13    UnknownRuntimeEventPayload,
14};
15pub(crate) use maplibre_core::{OfflineRegionDownloadState, RuntimeEventType};
16
17/// Identity for a map owned by a runtime. The value is the map's native handle,
18/// which names one map for the life of the process. It carries no ownership;
19/// map operations go through [`MapHandle`](crate::MapHandle).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct MapId(u64);
22
23impl MapId {
24    pub(crate) const fn new(value: u64) -> Self {
25        Self(value)
26    }
27
28    /// Returns the numeric map identity.
29    pub const fn get(self) -> u64 {
30        self.0
31    }
32}
33
34/// Source object that emitted a runtime event.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37pub enum RuntimeEventSource {
38    Runtime,
39    Map(MapId),
40    UnknownMap,
41    /// Source kind this version does not name, with the native source identity
42    /// the event carried.
43    Unknown {
44        source_type: u32,
45        source: u64,
46    },
47}
48
49impl RuntimeEventSource {
50    pub(crate) fn from_raw(source_type: u32, source: u64) -> Self {
51        match source_type {
52            sys::MLN_RUNTIME_EVENT_SOURCE_RUNTIME => Self::Runtime,
53            // The copied id grants nothing, so it is reported whether or not
54            // this runtime still holds a wrapper for that map.
55            sys::MLN_RUNTIME_EVENT_SOURCE_MAP if source != 0 => Self::Map(MapId::new(source)),
56            sys::MLN_RUNTIME_EVENT_SOURCE_MAP => Self::UnknownMap,
57            source_type => Self::Unknown {
58                source_type,
59                source,
60            },
61        }
62    }
63}
64
65/// Owned runtime event copied out of a drained batch.
66#[derive(Debug, Clone, PartialEq)]
67#[non_exhaustive]
68pub struct RuntimeEvent {
69    pub event_type: RuntimeEventType,
70    pub source: RuntimeEventSource,
71    /// Secondary event detail whose meaning `event_type` selects. Camera
72    /// change events decode as
73    /// `CameraChangeMode::from_raw(code as u32)`, offline operation-completion
74    /// events carry the operation's native status, and map loading-failure
75    /// events carry a load error ordinal whose text is in `message`.
76    pub code: i32,
77    pub message: Option<String>,
78    pub payload: RuntimeEventPayload,
79}
80
81/// Batch of runtime events borrowed from runtime-owned storage.
82///
83/// A batch borrows the [`RuntimeHandle`](crate::RuntimeHandle) it came from, so
84/// the next drain is a compile error while the batch lives, and an event read
85/// out of a batch borrows the batch. Take [`RuntimeEventRef::to_owned`] for a
86/// value that outlives either.
87pub struct RuntimeEventBatch<'a> {
88    raw: sys::mln_runtime_event_batch,
89    _storage: PhantomData<&'a [u8]>,
90}
91
92impl<'a> RuntimeEventBatch<'a> {
93    /// Reports one drained batch's runtime-owned storage.
94    ///
95    /// # Safety
96    ///
97    /// `raw` must be a batch that `mln_runtime_drain_events` filled, whose
98    /// event and message storage stays readable for `'a`.
99    pub(crate) unsafe fn new(raw: sys::mln_runtime_event_batch) -> Self {
100        Self {
101            raw,
102            _storage: PhantomData,
103        }
104    }
105
106    /// Returns how many events this batch reports.
107    pub fn len(&self) -> usize {
108        self.raw.event_count
109    }
110
111    /// Reports whether this batch has no events.
112    pub fn is_empty(&self) -> bool {
113        self.len() == 0
114    }
115
116    /// Returns how many events stayed queued after this batch. A nonzero count
117    /// means another drain reports more events.
118    pub fn remaining(&self) -> usize {
119        self.raw.remaining_count
120    }
121
122    /// Walks this batch's events in queue order.
123    ///
124    /// Each event borrows this batch, so safe code cannot read one after the
125    /// batch is gone:
126    ///
127    /// ```compile_fail,E0505
128    /// # use maplibre_native_ffi::{RuntimeHandle, RuntimeOptions};
129    /// let mut runtime = RuntimeHandle::with_options(&RuntimeOptions::default()).unwrap();
130    /// let batch = runtime.drain_events(0).unwrap();
131    /// let events = batch.iter().collect::<Vec<_>>();
132    /// drop(batch);
133    /// let _ = events.first().map(|event| event.message_bytes());
134    /// ```
135    pub fn iter(&self) -> impl Iterator<Item = RuntimeEventRef<'_>> {
136        (0..self.len()).map(move |index| {
137            // SAFETY: This batch's storage stays readable while it is borrowed,
138            // and index names one of its events.
139            RuntimeEventRef {
140                view: unsafe { maplibre_core::events::event_view(&self.raw, index) },
141            }
142        })
143    }
144}
145
146impl fmt::Debug for RuntimeEventBatch<'_> {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.debug_struct("RuntimeEventBatch")
149            .field("len", &self.len())
150            .field("remaining", &self.remaining())
151            .finish()
152    }
153}
154
155/// One event of a [`RuntimeEventBatch`], read from runtime-owned storage.
156#[derive(Clone, Copy)]
157pub struct RuntimeEventRef<'a> {
158    view: maplibre_core::NativeEventView<'a>,
159}
160
161impl<'a> RuntimeEventRef<'a> {
162    /// Returns this event's type.
163    pub fn event_type(&self) -> RuntimeEventType {
164        RuntimeEventType::from_raw(self.view.raw.type_)
165    }
166
167    /// Returns the object that emitted this event.
168    pub fn source(&self) -> RuntimeEventSource {
169        RuntimeEventSource::from_raw(self.view.raw.source_type, self.view.raw.source)
170    }
171
172    /// Returns the secondary detail whose meaning the event type selects. See
173    /// [`RuntimeEvent::code`].
174    pub fn code(&self) -> i32 {
175        self.view.raw.code
176    }
177
178    /// Decodes the payload union member that this event's payload type names.
179    /// A payload type this version does not define keeps its raw value and the
180    /// payload's copied byte window.
181    pub fn payload(&self) -> RuntimeEventPayload {
182        // SAFETY: The view came from a drained batch, so the payload union
183        // holds initialized bytes for the member payload_type names.
184        unsafe { maplibre_core::events::payload_from_view(&self.view) }
185    }
186
187    /// Returns this event's message as text, or `None` when it carries no
188    /// message. A message that is not UTF-8 fails on its own event.
189    pub fn message(&self) -> Result<Option<&'a str>> {
190        if self.view.message.is_empty() {
191            return Ok(None);
192        }
193        str::from_utf8(self.view.message)
194            .map(Some)
195            .map_err(|error| {
196                Error::invalid_argument(format!(
197                    "runtime event message was not valid UTF-8: {error}"
198                ))
199            })
200    }
201
202    /// Returns this event's message bytes, which are empty when it carries no
203    /// message.
204    pub fn message_bytes(&self) -> &'a [u8] {
205        self.view.message
206    }
207
208    /// Copies this event into a value that outlives the batch.
209    pub fn to_owned(&self) -> Result<RuntimeEvent> {
210        // SAFETY: The view came from a drained batch whose storage is readable
211        // for 'a, so every field copied here is live.
212        let copied = unsafe { maplibre_core::events::copied_event_from_view(&self.view) }?;
213        Ok(RuntimeEvent {
214            event_type: copied.event_type,
215            source: RuntimeEventSource::from_raw(
216                copied.source.source_type,
217                copied.source.source_id,
218            ),
219            code: copied.code,
220            message: copied.message,
221            payload: copied.payload,
222        })
223    }
224}
225
226impl fmt::Debug for RuntimeEventRef<'_> {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        f.debug_struct("RuntimeEventRef")
229            .field("event_type", &self.event_type())
230            .field("source", &self.source())
231            .field("code", &self.code())
232            .field("message_bytes", &self.message_bytes())
233            .finish()
234    }
235}
236
237/// Batch a test fills itself, so batch decoding is exercised without a live
238/// runtime.
239#[cfg(test)]
240pub(crate) struct SynthesizedBatch {
241    records: Vec<u8>,
242    messages: Vec<u8>,
243    stride: usize,
244    count: usize,
245}
246
247#[cfg(test)]
248impl SynthesizedBatch {
249    pub(crate) fn new() -> Self {
250        Self {
251            records: Vec::new(),
252            messages: Vec::new(),
253            stride: std::mem::size_of::<sys::mln_runtime_event>(),
254            count: 0,
255        }
256    }
257
258    /// Returns a zeroed event record, which is what the C API queues before it
259    /// fills the fields an event type uses.
260    pub(crate) fn zeroed_event(event_type: u32) -> sys::mln_runtime_event {
261        // SAFETY: Every member of an event record is plain data.
262        let mut event = unsafe { std::mem::zeroed::<sys::mln_runtime_event>() };
263        event.type_ = event_type;
264        event
265    }
266
267    pub(crate) fn push(&mut self, mut event: sys::mln_runtime_event, message: &[u8]) {
268        event.message_offset = u32::try_from(self.messages.len()).unwrap();
269        event.message_size = u32::try_from(message.len()).unwrap();
270        self.messages.extend_from_slice(message);
271        self.messages.push(0);
272
273        let record = self.records.len();
274        self.records.resize(record + self.stride, 0);
275        // SAFETY: event is a live local of exactly this many plain-data bytes.
276        let bytes = unsafe {
277            std::slice::from_raw_parts(
278                std::ptr::addr_of!(event).cast::<u8>(),
279                std::mem::size_of::<sys::mln_runtime_event>(),
280            )
281        };
282        self.records[record..record + bytes.len()].copy_from_slice(bytes);
283        self.count += 1;
284    }
285
286    pub(crate) fn raw(&self) -> sys::mln_runtime_event_batch {
287        sys::mln_runtime_event_batch {
288            size: std::mem::size_of::<sys::mln_runtime_event_batch>() as u32,
289            event_size: u32::try_from(self.stride).unwrap(),
290            events: self.records.as_ptr().cast(),
291            event_count: self.count,
292            messages: self.messages.as_ptr().cast(),
293            messages_size: self.messages.len(),
294            remaining_count: 0,
295        }
296    }
297
298    pub(crate) fn batch(&self) -> RuntimeEventBatch<'_> {
299        // SAFETY: This fixture's records and arena are laid out the way a drain
300        // fills them, and they outlive the borrow the batch takes.
301        unsafe { RuntimeEventBatch::new(self.raw()) }
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use crate::ResourceErrorReason;
308
309    use super::*;
310
311    #[test]
312    // Spec coverage: BND-086.
313    fn a_batch_applies_the_rust_source_policy_to_every_source_kind() {
314        let mut batch = SynthesizedBatch::new();
315        let mut runtime_event =
316            SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_OFFLINE_OPERATION_COMPLETED);
317        runtime_event.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_RUNTIME;
318        batch.push(runtime_event, b"");
319        let mut map_event = SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_STYLE_LOADED);
320        map_event.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_MAP;
321        map_event.source = 0x0200_0000_0000_002a;
322        batch.push(map_event, b"");
323        let mut unknown_map = SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_IDLE);
324        unknown_map.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_MAP;
325        batch.push(unknown_map, b"");
326        let mut unknown_source = SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_IDLE);
327        unknown_source.source_type = 999_003;
328        unknown_source.source = 0x0300_0000_0000_0063;
329        batch.push(unknown_source, b"");
330        let batch = batch.batch();
331
332        let sources = batch.iter().map(|event| event.source()).collect::<Vec<_>>();
333
334        assert_eq!(
335            sources,
336            vec![
337                RuntimeEventSource::Runtime,
338                RuntimeEventSource::Map(MapId::new(0x0200_0000_0000_002a)),
339                RuntimeEventSource::UnknownMap,
340                RuntimeEventSource::Unknown {
341                    source_type: 999_003,
342                    source: 0x0300_0000_0000_0063,
343                },
344            ]
345        );
346    }
347
348    #[test]
349    // Spec coverage: BND-085.
350    fn offline_events_decode_their_union_payloads_and_messages() {
351        let mut status = maplibre_core::events::empty_offline_region_status_native();
352        status.download_state = sys::MLN_OFFLINE_REGION_DOWNLOAD_ACTIVE;
353        status.completed_resource_count = 3;
354        status.complete = true;
355        let mut status_event =
356            SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_OFFLINE_REGION_STATUS_CHANGED);
357        status_event.payload_type = sys::MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_STATUS;
358        status_event.payload.offline_region_status = sys::mln_runtime_event_offline_region_status {
359            region_id: 7,
360            status,
361        };
362        let mut error_event =
363            SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_OFFLINE_REGION_RESPONSE_ERROR);
364        error_event.code = -1;
365        error_event.payload_type = sys::MLN_RUNTIME_EVENT_PAYLOAD_OFFLINE_REGION_RESPONSE_ERROR;
366        error_event.payload.offline_region_response_error =
367            sys::mln_runtime_event_offline_region_response_error {
368                region_id: 7,
369                reason: sys::MLN_RESOURCE_ERROR_REASON_OTHER,
370            };
371        let mut records = SynthesizedBatch::new();
372        records.push(status_event, b"");
373        records.push(error_event, b"offline failed");
374        let batch = records.batch();
375
376        let events = batch.iter().collect::<Vec<_>>();
377
378        assert_eq!(
379            events[0].event_type(),
380            RuntimeEventType::OfflineRegionStatusChanged
381        );
382        assert_eq!(events[0].source(), RuntimeEventSource::Runtime);
383        let RuntimeEventPayload::OfflineRegionStatus(status) = events[0].payload() else {
384            panic!("the first event should carry an offline region status payload");
385        };
386        assert_eq!(status.region_id, 7);
387        assert_eq!(
388            status.status.download_state,
389            OfflineRegionDownloadState::Active
390        );
391        assert_eq!(status.status.completed_resource_count, 3);
392        assert!(status.status.complete);
393
394        assert_eq!(events[1].message().unwrap(), Some("offline failed"));
395        assert_eq!(events[1].code(), -1);
396        let RuntimeEventPayload::OfflineRegionResponseError(error) = events[1].payload() else {
397            panic!("the second event should carry a response-error payload");
398        };
399        assert_eq!(error.region_id, 7);
400        assert_eq!(error.reason, ResourceErrorReason::Other);
401    }
402
403    #[test]
404    // Spec coverage: BND-092.
405    fn an_owned_event_copy_survives_the_storage_it_came_from() {
406        let mut records = SynthesizedBatch::new();
407        let mut event = SynthesizedBatch::zeroed_event(999_001);
408        event.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_MAP;
409        event.source = 42;
410        event.code = -7;
411        event.payload_type = 999_002;
412        event.payload.camera_transition_finished =
413            sys::mln_runtime_event_camera_transition_finished {
414                transition_id: 0x0102_0304_0506_0708,
415            };
416        records.push(event, b"future payload");
417        let batch = records.batch();
418        let borrowed = batch.iter().next().unwrap();
419
420        let owned = borrowed.to_owned().unwrap();
421        assert_eq!(borrowed.message_bytes(), b"future payload");
422        // The batch borrows these records, so releasing the storage the events
423        // were read from leaves only the copy.
424        drop(records);
425
426        assert_eq!(owned.event_type, RuntimeEventType::Unknown(999_001));
427        assert_eq!(owned.source, RuntimeEventSource::Map(MapId::new(42)));
428        assert_eq!(owned.code, -7);
429        assert_eq!(owned.message.as_deref(), Some("future payload"));
430        let RuntimeEventPayload::Unknown(payload) = &owned.payload else {
431            panic!("an undefined payload type should stay opaque");
432        };
433        assert_eq!(payload.raw_type, 999_002);
434        assert_eq!(
435            &payload.bytes[..8],
436            &0x0102_0304_0506_0708_u64.to_ne_bytes()
437        );
438    }
439
440    #[test]
441    fn a_message_that_is_not_utf8_fails_only_its_own_event() {
442        let mut records = SynthesizedBatch::new();
443        records.push(
444            SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_LOADING_FAILED),
445            &[0xff, 0xfe],
446        );
447        records.push(
448            SynthesizedBatch::zeroed_event(sys::MLN_RUNTIME_EVENT_MAP_STYLE_LOADED),
449            b"loaded",
450        );
451        let batch = records.batch();
452        let events = batch.iter().collect::<Vec<_>>();
453
454        let error = events[0].message().unwrap_err();
455        assert_eq!(error.kind(), crate::ErrorKind::InvalidArgument);
456        assert!(error.diagnostic().contains("not valid UTF-8"));
457        assert_eq!(events[0].message_bytes(), &[0xff, 0xfe]);
458        assert!(events[0].to_owned().is_err());
459        assert_eq!(events[1].message().unwrap(), Some("loaded"));
460        assert!(events[1].to_owned().is_ok());
461    }
462}