Skip to main content

agent_sim/sim/
project.rs

1use crate::load::ResolvedFlashRegion;
2use crate::sim::error::{ProjectError, SimError};
3use crate::sim::project_loader::{decode_owned_cstr, next_capacity, validate_written};
4use crate::sim::types::{
5    SignalMeta, SignalType, SignalValue, SimCanBusDesc, SimCanBusDescRaw, SimCanFrame,
6    SimCanFrameRaw, SimSharedDesc, SimSharedDescRaw, SimSharedSlot, SimSharedSlotRaw,
7    SimSignalDescRaw, SimValueRaw,
8};
9use crate::sim::validation::{
10    validate_can_metadata, validate_shared_metadata, validate_signal_metadata,
11};
12use libloading::Library;
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16type SimInitFn = unsafe extern "C" fn() -> u32;
17type SimResetFn = unsafe extern "C" fn() -> u32;
18type SimTickFn = unsafe extern "C" fn() -> u32;
19type SimReadValFn = unsafe extern "C" fn(u32, *mut SimValueRaw) -> u32;
20type SimWriteValFn = unsafe extern "C" fn(u32, *const SimValueRaw) -> u32;
21type SimGetSignalCountFn = unsafe extern "C" fn(*mut u32) -> u32;
22type SimGetSignalsFn = unsafe extern "C" fn(*mut SimSignalDescRaw, u32, *mut u32) -> u32;
23type SimGetApiVersionFn = unsafe extern "C" fn(*mut u32, *mut u32) -> u32;
24type SimGetTickDurationUsFn = unsafe extern "C" fn(*mut u32) -> u32;
25type SimFlashWriteFn = unsafe extern "C" fn(u32, *const u8, u32) -> u32;
26type SimCanGetBusesFn = unsafe extern "C" fn(*mut SimCanBusDescRaw, u32, *mut u32) -> u32;
27type SimCanRxFn = unsafe extern "C" fn(u32, *const SimCanFrameRaw, u32) -> u32;
28type SimCanTxFn = unsafe extern "C" fn(u32, *mut SimCanFrameRaw, u32, *mut u32) -> u32;
29type SimSharedGetChannelsFn = unsafe extern "C" fn(*mut SimSharedDescRaw, u32, *mut u32) -> u32;
30type SimSharedReadFn =
31    unsafe extern "C" fn(u32, *const crate::sim::types::SimSharedSlotRaw, u32) -> u32;
32type SimSharedWriteFn =
33    unsafe extern "C" fn(u32, *mut crate::sim::types::SimSharedSlotRaw, u32, *mut u32) -> u32;
34
35const STATUS_OK: u32 = 0;
36const STATUS_NOT_INITIALIZED: u32 = 1;
37const STATUS_INVALID_SIGNAL: u32 = 3;
38const STATUS_TYPE_MISMATCH: u32 = 4;
39const STATUS_BUFFER_TOO_SMALL: u32 = 5;
40const SUPPORTED_API_VERSION_MAJOR: u32 = 2;
41const SUPPORTED_API_VERSION_MINOR: u32 = 0;
42
43struct ProjectCanApi {
44    sim_can_rx: SimCanRxFn,
45    sim_can_tx: SimCanTxFn,
46}
47
48struct ProjectSharedApi {
49    sim_shared_read: SimSharedReadFn,
50    sim_shared_write: SimSharedWriteFn,
51}
52
53pub struct Project {
54    pub libpath: PathBuf,
55    tick_duration_us: u32,
56    signals: Vec<SignalMeta>,
57    can_buses: Vec<SimCanBusDesc>,
58    shared_channels: Vec<SimSharedDesc>,
59    signal_name_to_id: HashMap<String, u32>,
60    signal_id_to_index: HashMap<u32, usize>,
61    sim_reset: SimResetFn,
62    sim_tick: SimTickFn,
63    sim_read_val: SimReadValFn,
64    sim_write_val: SimWriteValFn,
65    _sim_get_signal_count: SimGetSignalCountFn,
66    _sim_get_signals: SimGetSignalsFn,
67    _sim_get_tick_duration_us: SimGetTickDurationUsFn,
68    can_api: Option<ProjectCanApi>,
69    shared_api: Option<ProjectSharedApi>,
70    _library: Library,
71}
72
73impl Project {
74    pub fn load(
75        libpath: impl AsRef<Path>,
76        flash_regions: &[ResolvedFlashRegion],
77    ) -> Result<Self, ProjectError> {
78        let path = libpath.as_ref().to_path_buf();
79        let library =
80            unsafe { Library::new(&path) }.map_err(|e| ProjectError::LibraryLoad(e.to_string()))?;
81
82        let sim_init: SimInitFn = *unsafe { library.get::<SimInitFn>(b"sim_init\0") }
83            .map_err(|_| ProjectError::MissingSymbol("sim_init"))?;
84        let sim_reset: SimResetFn = *unsafe { library.get::<SimResetFn>(b"sim_reset\0") }
85            .map_err(|_| ProjectError::MissingSymbol("sim_reset"))?;
86        let sim_tick: SimTickFn = *unsafe { library.get::<SimTickFn>(b"sim_tick\0") }
87            .map_err(|_| ProjectError::MissingSymbol("sim_tick"))?;
88        let sim_read_val: SimReadValFn = *unsafe { library.get::<SimReadValFn>(b"sim_read_val\0") }
89            .map_err(|_| ProjectError::MissingSymbol("sim_read_val"))?;
90        let sim_write_val: SimWriteValFn =
91            *unsafe { library.get::<SimWriteValFn>(b"sim_write_val\0") }
92                .map_err(|_| ProjectError::MissingSymbol("sim_write_val"))?;
93        let sim_get_signal_count: SimGetSignalCountFn =
94            *unsafe { library.get::<SimGetSignalCountFn>(b"sim_get_signal_count\0") }
95                .map_err(|_| ProjectError::MissingSymbol("sim_get_signal_count"))?;
96        let sim_get_signals: SimGetSignalsFn =
97            *unsafe { library.get::<SimGetSignalsFn>(b"sim_get_signals\0") }
98                .map_err(|_| ProjectError::MissingSymbol("sim_get_signals"))?;
99        let sim_get_api_version: SimGetApiVersionFn =
100            *unsafe { library.get::<SimGetApiVersionFn>(b"sim_get_api_version\0") }
101                .map_err(|_| ProjectError::MissingSymbol("sim_get_api_version"))?;
102        let sim_get_tick_duration_us: SimGetTickDurationUsFn =
103            *unsafe { library.get::<SimGetTickDurationUsFn>(b"sim_get_tick_duration_us\0") }
104                .map_err(|_| ProjectError::MissingSymbol("sim_get_tick_duration_us"))?;
105        let sim_flash_write = unsafe { library.get::<SimFlashWriteFn>(b"sim_flash_write\0") }
106            .ok()
107            .map(|symbol| *symbol);
108        let sim_can_get_buses = unsafe { library.get::<SimCanGetBusesFn>(b"sim_can_get_buses\0") }
109            .ok()
110            .map(|symbol| *symbol);
111        let sim_can_rx = unsafe { library.get::<SimCanRxFn>(b"sim_can_rx\0") }
112            .ok()
113            .map(|symbol| *symbol);
114        let sim_can_tx = unsafe { library.get::<SimCanTxFn>(b"sim_can_tx\0") }
115            .ok()
116            .map(|symbol| *symbol);
117        let sim_shared_get_channels =
118            unsafe { library.get::<SimSharedGetChannelsFn>(b"sim_shared_get_channels\0") }
119                .ok()
120                .map(|symbol| *symbol);
121        let sim_shared_read = unsafe { library.get::<SimSharedReadFn>(b"sim_shared_read\0") }
122            .ok()
123            .map(|symbol| *symbol);
124        let sim_shared_write = unsafe { library.get::<SimSharedWriteFn>(b"sim_shared_write\0") }
125            .ok()
126            .map(|symbol| *symbol);
127
128        {
129            let mut major = 0_u32;
130            let mut minor = 0_u32;
131            let status =
132                unsafe { sim_get_api_version(&mut major as *mut u32, &mut minor as *mut u32) };
133            if status != STATUS_OK {
134                return Err(ProjectError::LibraryLoad(format!(
135                    "sim_get_api_version failed with status {status}"
136                )));
137            }
138            if major != SUPPORTED_API_VERSION_MAJOR || minor != SUPPORTED_API_VERSION_MINOR {
139                return Err(ProjectError::LibraryLoad(format!(
140                    "ABI version mismatch: project reports {major}.{minor}, runtime requires {}.{}",
141                    SUPPORTED_API_VERSION_MAJOR, SUPPORTED_API_VERSION_MINOR
142                )));
143            }
144        }
145
146        let tick_duration_us = {
147            let mut value = 0_u32;
148            let status = unsafe { sim_get_tick_duration_us(&mut value as *mut u32) };
149            if status != STATUS_OK {
150                return Err(ProjectError::LibraryLoad(format!(
151                    "sim_get_tick_duration_us failed with status {status}"
152                )));
153            }
154            value
155        };
156
157        let signals = {
158            let mut count = 0_u32;
159            let status = unsafe { sim_get_signal_count(&mut count as *mut u32) };
160            if status != STATUS_OK {
161                return Err(ProjectError::LibraryLoad(format!(
162                    "sim_get_signal_count failed with status {status}"
163                )));
164            }
165
166            let mut capacity = count.max(1);
167            loop {
168                let mut raw = vec![
169                    SimSignalDescRaw {
170                        id: 0,
171                        name: std::ptr::null(),
172                        signal_type: 0,
173                        units: std::ptr::null(),
174                    };
175                    capacity as usize
176                ];
177                let mut written = 0_u32;
178                let status = unsafe {
179                    sim_get_signals(raw.as_mut_ptr(), capacity, &mut written as *mut u32)
180                };
181                if status == STATUS_BUFFER_TOO_SMALL {
182                    capacity = next_capacity(capacity, "sim_get_signals")
183                        .map_err(ProjectError::FfiContract)?;
184                    continue;
185                }
186                if status != STATUS_OK {
187                    return Err(ProjectError::LibraryLoad(format!(
188                        "sim_get_signals failed with status {status}"
189                    )));
190                }
191                raw.truncate(
192                    validate_written(written, capacity, "sim_get_signals")
193                        .map_err(ProjectError::FfiContract)?,
194                );
195                break raw
196                    .into_iter()
197                    .map(|entry| {
198                        let name = decode_owned_cstr(entry.name, "signal name")
199                            .map_err(ProjectError::InvalidSignalMetadata)?;
200                        let units = if entry.units.is_null() {
201                            None
202                        } else {
203                            Some(
204                                decode_owned_cstr(entry.units, "signal units")
205                                    .map_err(ProjectError::InvalidSignalMetadata)?,
206                            )
207                        };
208                        let signal_type =
209                            SignalType::try_from(entry.signal_type).map_err(|_| {
210                                ProjectError::InvalidSignalMetadata(format!(
211                                    "signal '{}' uses invalid type tag {}",
212                                    name, entry.signal_type
213                                ))
214                            })?;
215                        Ok(SignalMeta {
216                            id: entry.id,
217                            name,
218                            signal_type,
219                            units,
220                        })
221                    })
222                    .collect::<Result<Vec<_>, _>>()?;
223            }
224        };
225
226        if !flash_regions.is_empty() {
227            let flash_write = sim_flash_write.ok_or_else(|| {
228                ProjectError::Flash(
229                    "flash regions were configured, but the project does not export sim_flash_write"
230                        .to_string(),
231                )
232            })?;
233            for region in flash_regions {
234                let len = u32::try_from(region.data.len()).map_err(|_| {
235                    ProjectError::Flash(format!(
236                        "flash region at 0x{:08X} is too large ({} bytes)",
237                        region.base_addr,
238                        region.data.len()
239                    ))
240                })?;
241                let status = unsafe { flash_write(region.base_addr, region.data.as_ptr(), len) };
242                if status != STATUS_OK {
243                    return Err(ProjectError::Flash(format!(
244                        "sim_flash_write failed for region 0x{:08X} ({} bytes) with status {}",
245                        region.base_addr,
246                        region.data.len(),
247                        status
248                    )));
249                }
250            }
251        }
252
253        let init_status = unsafe { sim_init() };
254        if init_status != STATUS_OK {
255            return Err(ProjectError::LibraryLoad(format!(
256                "sim_init failed with status {init_status}"
257            )));
258        }
259
260        let (can_api, can_buses) = match (sim_can_get_buses, sim_can_rx, sim_can_tx) {
261            (None, None, None) => (None, Vec::new()),
262            (Some(get_buses), Some(can_rx), Some(can_tx)) => (
263                Some(ProjectCanApi {
264                    sim_can_rx: can_rx,
265                    sim_can_tx: can_tx,
266                }),
267                Self::load_can_buses(get_buses)?,
268            ),
269            _ => {
270                return Err(ProjectError::InvalidCanExports(
271                    "if any CAN symbol is exported, sim_can_get_buses/sim_can_rx/sim_can_tx must all be exported"
272                        .to_string(),
273                ));
274            }
275        };
276
277        let (shared_api, shared_channels) = match (
278            sim_shared_get_channels,
279            sim_shared_read,
280            sim_shared_write,
281        ) {
282            (None, None, None) => (None, Vec::new()),
283            (Some(get_channels), Some(shared_read), Some(shared_write)) => (
284                Some(ProjectSharedApi {
285                    sim_shared_read: shared_read,
286                    sim_shared_write: shared_write,
287                }),
288                Self::load_shared_channels(get_channels)?,
289            ),
290            _ => {
291                return Err(ProjectError::InvalidSharedExports(
292                            "if any shared-state symbol is exported, sim_shared_get_channels/sim_shared_read/sim_shared_write must all be exported"
293                                .to_string(),
294                        ));
295            }
296        };
297        validate_signal_metadata(&signals)?;
298        validate_can_metadata(&can_buses)?;
299        validate_shared_metadata(&shared_channels)?;
300
301        let signal_name_to_id = signals
302            .iter()
303            .map(|s| (s.name.clone(), s.id))
304            .collect::<HashMap<_, _>>();
305        let signal_id_to_index = signals
306            .iter()
307            .enumerate()
308            .map(|(idx, s)| (s.id, idx))
309            .collect::<HashMap<_, _>>();
310
311        Ok(Self {
312            libpath: path,
313            tick_duration_us,
314            signals,
315            can_buses,
316            shared_channels,
317            signal_name_to_id,
318            signal_id_to_index,
319            sim_reset,
320            sim_tick,
321            sim_read_val,
322            sim_write_val,
323            _sim_get_signal_count: sim_get_signal_count,
324            _sim_get_signals: sim_get_signals,
325            _sim_get_tick_duration_us: sim_get_tick_duration_us,
326            can_api,
327            shared_api,
328            _library: library,
329        })
330    }
331
332    pub fn tick_duration_us(&self) -> u32 {
333        self.tick_duration_us
334    }
335
336    pub fn signals(&self) -> &[SignalMeta] {
337        &self.signals
338    }
339
340    pub fn can_buses(&self) -> &[SimCanBusDesc] {
341        &self.can_buses
342    }
343
344    pub fn shared_channels(&self) -> &[SimSharedDesc] {
345        &self.shared_channels
346    }
347
348    pub fn signal_by_id(&self, id: u32) -> Option<&SignalMeta> {
349        self.signal_id_to_index
350            .get(&id)
351            .and_then(|idx| self.signals.get(*idx))
352    }
353
354    pub fn signal_id_by_name(&self, name: &str) -> Option<u32> {
355        self.signal_name_to_id.get(name).copied()
356    }
357
358    fn shared_channel_by_id(&self, channel_id: u32) -> Option<&SimSharedDesc> {
359        self.shared_channels
360            .iter()
361            .find(|channel| channel.id == channel_id)
362    }
363
364    pub(crate) fn reset(&self) -> Result<(), SimError> {
365        self.map_status(unsafe { (self.sim_reset)() }, None, None)
366    }
367
368    pub(crate) fn tick(&self) -> Result<(), SimError> {
369        self.map_status(unsafe { (self.sim_tick)() }, None, None)
370    }
371
372    pub(crate) fn can_rx(&self, bus_id: u32, frames: &[SimCanFrame]) -> Result<(), SimError> {
373        let Some(can_api) = &self.can_api else {
374            return Ok(());
375        };
376        if frames.is_empty() {
377            return Ok(());
378        }
379        let raw_frames = frames.iter().map(SimCanFrame::to_raw).collect::<Vec<_>>();
380        let status =
381            unsafe { (can_api.sim_can_rx)(bus_id, raw_frames.as_ptr(), raw_frames.len() as u32) };
382        self.map_status(status, None, None)
383    }
384
385    pub(crate) fn can_tx(&self, bus_id: u32) -> Result<Vec<SimCanFrame>, SimError> {
386        let Some(can_api) = &self.can_api else {
387            return Ok(Vec::new());
388        };
389        let mut out = Vec::new();
390        let mut capacity = 32_u32;
391        loop {
392            let mut raw_frames = vec![
393                SimCanFrameRaw {
394                    arb_id: 0,
395                    len: 0,
396                    flags: 0,
397                    _pad: [0, 0],
398                    data: [0; 64],
399                };
400                capacity as usize
401            ];
402            let mut written = 0_u32;
403            let status = unsafe {
404                (can_api.sim_can_tx)(
405                    bus_id,
406                    raw_frames.as_mut_ptr(),
407                    capacity,
408                    &mut written as *mut u32,
409                )
410            };
411            if status != STATUS_OK && status != STATUS_BUFFER_TOO_SMALL {
412                self.map_status(status, None, None)?;
413                break;
414            }
415            raw_frames.truncate(
416                validate_written(written, capacity, "sim_can_tx").map_err(SimError::FfiContract)?,
417            );
418            out.extend(raw_frames.into_iter().map(SimCanFrame::from_raw));
419            if status == STATUS_BUFFER_TOO_SMALL {
420                capacity = next_capacity(capacity, "sim_can_tx").map_err(SimError::FfiContract)?;
421                continue;
422            }
423            break;
424        }
425        Ok(out)
426    }
427
428    pub(crate) fn shared_read(
429        &self,
430        channel_id: u32,
431        slots: &[SimSharedSlot],
432    ) -> Result<(), SimError> {
433        let Some(shared_api) = &self.shared_api else {
434            return Ok(());
435        };
436        let expected_slot_count = self
437            .shared_channel_by_id(channel_id)
438            .ok_or_else(|| {
439                SimError::FfiContract(format!("unknown shared channel id {channel_id}"))
440            })?
441            .slot_count as usize;
442        validate_dense_shared_snapshot(slots, expected_slot_count, "sim_shared_read")?;
443        let raw_slots = slots.iter().map(SimSharedSlot::to_raw).collect::<Vec<_>>();
444        let status = unsafe {
445            (shared_api.sim_shared_read)(channel_id, raw_slots.as_ptr(), raw_slots.len() as u32)
446        };
447        self.map_status(status, None, None)
448    }
449
450    pub(crate) fn shared_write(&self, channel_id: u32) -> Result<Vec<SimSharedSlot>, SimError> {
451        let Some(shared_api) = &self.shared_api else {
452            return Ok(Vec::new());
453        };
454        let expected_slot_count = self
455            .shared_channel_by_id(channel_id)
456            .ok_or_else(|| {
457                SimError::FfiContract(format!("unknown shared channel id {channel_id}"))
458            })?
459            .slot_count;
460        let capacity = expected_slot_count.max(1);
461        let mut raw_slots = vec![SimSharedSlotRaw::default(); capacity as usize];
462        let mut written = 0_u32;
463        let status = unsafe {
464            (shared_api.sim_shared_write)(
465                channel_id,
466                raw_slots.as_mut_ptr(),
467                capacity,
468                &mut written as *mut u32,
469            )
470        };
471        if status == STATUS_BUFFER_TOO_SMALL {
472            return Err(SimError::FfiContract(format!(
473                "sim_shared_write reported BUFFER_TOO_SMALL for channel {channel_id} with declared slot_count {expected_slot_count}"
474            )));
475        }
476        if status != STATUS_OK {
477            self.map_status(status, None, None)?;
478        }
479
480        raw_slots.truncate(
481            validate_written(written, capacity, "sim_shared_write")
482                .map_err(SimError::FfiContract)?,
483        );
484        let slots = raw_slots
485            .into_iter()
486            .map(|slot| {
487                SimSharedSlot::try_from_raw(slot)
488                    .map_err(|err| SimError::FfiContract(format!("sim_shared_write: {err}")))
489            })
490            .collect::<Result<Vec<_>, _>>()?;
491        validate_dense_shared_snapshot(&slots, expected_slot_count as usize, "sim_shared_write")?;
492        Ok(slots)
493    }
494
495    pub(crate) fn read(&self, signal: &SignalMeta) -> Result<SignalValue, SimError> {
496        let mut raw = SimValueRaw {
497            signal_type: 0,
498            data: crate::sim::types::SimValueDataRaw { u32: 0 },
499        };
500        let status = unsafe { (self.sim_read_val)(signal.id, &mut raw as *mut SimValueRaw) };
501        self.map_status(status, Some(signal), None)?;
502        let value = unsafe { SignalValue::from_raw(raw) }
503            .ok_or_else(|| SimError::InvalidArg("bad read value".to_string()))?;
504        Ok(value)
505    }
506
507    pub(crate) fn write(&self, signal: &SignalMeta, value: &SignalValue) -> Result<(), SimError> {
508        let raw = value.to_raw();
509        let status = unsafe { (self.sim_write_val)(signal.id, &raw as *const SimValueRaw) };
510        self.map_status(status, Some(signal), Some(value.signal_type()))
511    }
512
513    fn map_status(
514        &self,
515        status: u32,
516        signal: Option<&SignalMeta>,
517        actual_type: Option<SignalType>,
518    ) -> Result<(), SimError> {
519        match status {
520            STATUS_OK => Ok(()),
521            STATUS_NOT_INITIALIZED => Err(SimError::NotInitialized),
522            2 => Err(SimError::InvalidArg("invalid ffi argument".to_string())),
523            STATUS_INVALID_SIGNAL => Err(SimError::InvalidSignal(
524                signal
525                    .map(|s| s.name.clone())
526                    .unwrap_or_else(|| "<unknown>".to_string()),
527            )),
528            STATUS_TYPE_MISMATCH => Err(SimError::TypeMismatch {
529                name: signal
530                    .map(|s| s.name.clone())
531                    .unwrap_or_else(|| "<unknown>".to_string()),
532                expected: signal.map(|s| s.signal_type).unwrap_or(SignalType::F64),
533                actual: actual_type.unwrap_or(SignalType::F64),
534            }),
535            STATUS_BUFFER_TOO_SMALL => Err(SimError::BufferTooSmall),
536            255 => Err(SimError::Internal),
537            _ => Err(SimError::UnknownStatus(status)),
538        }
539    }
540
541    fn load_can_buses(
542        sim_can_get_buses: SimCanGetBusesFn,
543    ) -> Result<Vec<SimCanBusDesc>, ProjectError> {
544        let mut capacity = 4_u32;
545        loop {
546            let mut raw = vec![
547                SimCanBusDescRaw {
548                    id: 0,
549                    name: std::ptr::null(),
550                    bitrate: 0,
551                    bitrate_data: 0,
552                    flags: 0,
553                    _pad: [0, 0, 0],
554                };
555                capacity as usize
556            ];
557            let mut written = 0_u32;
558            let status =
559                unsafe { sim_can_get_buses(raw.as_mut_ptr(), capacity, &mut written as *mut u32) };
560            if status == STATUS_BUFFER_TOO_SMALL {
561                capacity = next_capacity(capacity, "sim_can_get_buses")
562                    .map_err(ProjectError::FfiContract)?;
563                continue;
564            }
565            if status != STATUS_OK {
566                return Err(ProjectError::LibraryLoad(format!(
567                    "sim_can_get_buses failed with status {status}"
568                )));
569            }
570            raw.truncate(
571                validate_written(written, capacity, "sim_can_get_buses")
572                    .map_err(ProjectError::FfiContract)?,
573            );
574            return raw
575                .into_iter()
576                .map(|entry| {
577                    let name = decode_owned_cstr(entry.name, "CAN bus name")
578                        .map_err(ProjectError::InvalidCanMetadata)?;
579                    Ok(SimCanBusDesc {
580                        id: entry.id,
581                        name,
582                        bitrate: entry.bitrate,
583                        bitrate_data: entry.bitrate_data,
584                        fd_capable: (entry.flags & 0x01) != 0,
585                    })
586                })
587                .collect();
588        }
589    }
590
591    fn load_shared_channels(
592        sim_shared_get_channels: SimSharedGetChannelsFn,
593    ) -> Result<Vec<SimSharedDesc>, ProjectError> {
594        let mut capacity = 4_u32;
595        loop {
596            let mut raw = vec![
597                SimSharedDescRaw {
598                    id: 0,
599                    name: std::ptr::null(),
600                    slot_count: 0,
601                };
602                capacity as usize
603            ];
604            let mut written = 0_u32;
605            let status = unsafe {
606                sim_shared_get_channels(raw.as_mut_ptr(), capacity, &mut written as *mut u32)
607            };
608            if status == STATUS_BUFFER_TOO_SMALL {
609                capacity = next_capacity(capacity, "sim_shared_get_channels")
610                    .map_err(ProjectError::FfiContract)?;
611                continue;
612            }
613            if status != STATUS_OK {
614                return Err(ProjectError::LibraryLoad(format!(
615                    "sim_shared_get_channels failed with status {status}"
616                )));
617            }
618            raw.truncate(
619                validate_written(written, capacity, "sim_shared_get_channels")
620                    .map_err(ProjectError::FfiContract)?,
621            );
622            return raw
623                .into_iter()
624                .map(|entry| {
625                    let name = decode_owned_cstr(entry.name, "shared channel name")
626                        .map_err(ProjectError::InvalidSharedMetadata)?;
627                    Ok(SimSharedDesc {
628                        id: entry.id,
629                        name,
630                        slot_count: entry.slot_count,
631                    })
632                })
633                .collect();
634        }
635    }
636}
637
638fn validate_dense_shared_snapshot(
639    slots: &[SimSharedSlot],
640    expected_slot_count: usize,
641    context: &str,
642) -> Result<(), SimError> {
643    if slots.len() != expected_slot_count {
644        return Err(SimError::FfiContract(format!(
645            "{context} returned {} slots, expected {expected_slot_count}",
646            slots.len()
647        )));
648    }
649
650    for (expected_slot_id, slot) in slots.iter().enumerate() {
651        if slot.slot_id as usize != expected_slot_id {
652            return Err(SimError::FfiContract(format!(
653                "{context} returned slot id {} at dense index {}; expected slot id {}",
654                slot.slot_id, expected_slot_id, expected_slot_id
655            )));
656        }
657    }
658
659    Ok(())
660}