Skip to main content

pamoja_ffi/
can_bus.rs

1//! The C ABI for a node on a CAN bus.
2//!
3//! A node is a SocketCAN socket on a kernel interface such as `can0`, on a Linux board, or a
4//! node on a bus simulated inside the program. [`pamoja_can_bus_join`] puts another node on the
5//! same bus, and a node hears every frame the others send and none of its own. Frames cross as
6//! the [`PamojaCanFrame`] handles of the framing calls; a filter is only scalars, so it crosses
7//! by value as [`PamojaCanFilter`].
8//!
9//! A receive on a simulated bus never waits: with nothing there it returns at once and counts
10//! its timeout in [`pamoja_can_bus_waited_micros`].
11
12use std::os::raw::c_char;
13use std::panic::{catch_unwind, AssertUnwindSafe};
14use std::ptr;
15use std::time::Duration;
16
17use pamoja_can::bus::{BusError, CanBus, CanBusKind, Filter, OpenError};
18use pamoja_can::CanId;
19
20use crate::can::PamojaCanFrame;
21use crate::{read_str, set_last_error, PamojaStatus};
22
23/// A bus kind: a kernel CAN interface reached through SocketCAN.
24pub const PAMOJA_CAN_BUS_DEVICE: u8 = 0;
25
26/// A bus kind: a bus inside the program.
27pub const PAMOJA_CAN_BUS_SIMULATED: u8 = 1;
28
29/// A node on a CAN bus. Opaque; release it with [`pamoja_can_bus_free`].
30pub struct PamojaCanBus {
31    bus: CanBus,
32}
33
34/// A frame a node keeps: one whose identifier, masked, equals `id`, masked, and whose format
35/// is the filter's.
36#[repr(C)]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct PamojaCanFilter {
39    /// The identifier to match.
40    pub id: u32,
41    /// The identifier bits that have to match.
42    pub mask: u32,
43    /// `1` for an extended 29-bit identifier, `0` for a standard 11-bit one.
44    pub extended: u8,
45}
46
47impl From<Filter> for PamojaCanFilter {
48    fn from(filter: Filter) -> Self {
49        PamojaCanFilter {
50            id: filter.id().raw(),
51            mask: filter.mask(),
52            extended: u8::from(filter.id().is_extended()),
53        }
54    }
55}
56
57impl PamojaCanFilter {
58    fn filter(self) -> Filter {
59        Filter::new(identifier(self.id, self.extended != 0), self.mask)
60    }
61}
62
63fn identifier(id: u32, extended: bool) -> CanId {
64    if extended {
65        CanId::extended(id)
66    } else {
67        CanId::standard(id as u16)
68    }
69}
70
71fn into_raw(bus: CanBus) -> *mut PamojaCanBus {
72    Box::into_raw(Box::new(PamojaCanBus { bus }))
73}
74
75fn opened(result: Result<CanBus, OpenError>, out_bus: &mut *mut PamojaCanBus) -> PamojaStatus {
76    match result {
77        Ok(bus) => {
78            *out_bus = into_raw(bus);
79            PamojaStatus::Ok
80        }
81        Err(error) => {
82            let status = match error {
83                OpenError::Unsupported => PamojaStatus::Unsupported,
84                OpenError::Device { .. } => PamojaStatus::Io,
85            };
86            set_last_error(error.to_string());
87            status
88        }
89    }
90}
91
92/// Runs a call on a node, turning a null handle, a failure, or a panic into a status.
93///
94/// # Safety
95///
96/// `bus` must be a live handle or null.
97unsafe fn on_bus(
98    bus: *const PamojaCanBus,
99    call: impl FnOnce(&CanBus) -> Result<(), BusError>,
100) -> PamojaStatus {
101    let Some(bus) = bus.as_ref() else {
102        set_last_error("bus must not be null".to_owned());
103        return PamojaStatus::InvalidArgument;
104    };
105    match catch_unwind(AssertUnwindSafe(|| call(&bus.bus))) {
106        Ok(Ok(())) => PamojaStatus::Ok,
107        Ok(Err(error)) => {
108            set_last_error(error.to_string());
109            PamojaStatus::Io
110        }
111        Err(_) => {
112            set_last_error("panic at the FFI boundary".to_owned());
113            PamojaStatus::Panic
114        }
115    }
116}
117
118/// Makes a new bus inside the program, with the returned node the first on it.
119///
120/// # Returns
121///
122/// The node, which the caller releases with [`pamoja_can_bus_free`].
123#[no_mangle]
124pub extern "C" fn pamoja_can_bus_simulated() -> *mut PamojaCanBus {
125    into_raw(CanBus::simulated())
126}
127
128/// Opens a kernel CAN interface through SocketCAN, as one node on its bus.
129///
130/// # Arguments
131///
132/// * `interface` - the interface, such as `can0` or `vcan0`, as UTF-8.
133/// * `out_bus` - receives the node.
134///
135/// # Returns
136///
137/// [`PamojaStatus::Ok`]; [`PamojaStatus::Unsupported`] anywhere but Linux;
138/// [`PamojaStatus::Io`] when the interface does not exist or cannot be bound; or
139/// [`PamojaStatus::InvalidArgument`] for a null argument. The reason is in the last error
140/// message.
141///
142/// # Safety
143///
144/// `interface` must be a NUL-terminated string or null, and `out_bus` a writable pointer or
145/// null.
146#[no_mangle]
147pub unsafe extern "C" fn pamoja_can_bus_open(
148    interface: *const c_char,
149    out_bus: *mut *mut PamojaCanBus,
150) -> PamojaStatus {
151    let Some(out_bus) = out_bus.as_mut() else {
152        set_last_error("out_bus must not be null".to_owned());
153        return PamojaStatus::InvalidArgument;
154    };
155    *out_bus = ptr::null_mut();
156    let Some(interface) = read_str(interface, "interface") else {
157        return PamojaStatus::InvalidArgument;
158    };
159    opened(CanBus::open(interface), out_bus)
160}
161
162/// Puts another node on the same bus: another socket on the same interface, or another node on
163/// the same simulated bus.
164///
165/// # Arguments
166///
167/// * `bus` - a node on the bus.
168/// * `out_bus` - receives the new node.
169///
170/// # Returns
171///
172/// As [`pamoja_can_bus_open`].
173///
174/// # Safety
175///
176/// `bus` must be a live handle or null, and `out_bus` a writable pointer or null.
177#[no_mangle]
178pub unsafe extern "C" fn pamoja_can_bus_join(
179    bus: *const PamojaCanBus,
180    out_bus: *mut *mut PamojaCanBus,
181) -> PamojaStatus {
182    let Some(out_bus) = out_bus.as_mut() else {
183        set_last_error("out_bus must not be null".to_owned());
184        return PamojaStatus::InvalidArgument;
185    };
186    *out_bus = ptr::null_mut();
187    let Some(bus) = bus.as_ref() else {
188        set_last_error("bus must not be null".to_owned());
189        return PamojaStatus::InvalidArgument;
190    };
191    opened(bus.bus.join(), out_bus)
192}
193
194/// Returns what a node's bus is: [`PAMOJA_CAN_BUS_DEVICE`] or [`PAMOJA_CAN_BUS_SIMULATED`],
195/// which a null handle also reports.
196///
197/// # Safety
198///
199/// `bus` must be a live handle or null.
200#[no_mangle]
201pub unsafe extern "C" fn pamoja_can_bus_kind(bus: *const PamojaCanBus) -> u8 {
202    match bus.as_ref().map(|bus| bus.bus.kind()) {
203        Some(CanBusKind::Device) => PAMOJA_CAN_BUS_DEVICE,
204        _ => PAMOJA_CAN_BUS_SIMULATED,
205    }
206}
207
208/// Sends a frame to every other node on the bus.
209///
210/// # Arguments
211///
212/// * `bus` - the node.
213/// * `frame` - the frame, which stays the caller's.
214///
215/// # Returns
216///
217/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null argument; or
218/// [`PamojaStatus::Io`] when the kernel refuses the frame, with the reason in the last error
219/// message.
220///
221/// # Safety
222///
223/// `bus` and `frame` must be live handles or null.
224#[no_mangle]
225pub unsafe extern "C" fn pamoja_can_bus_send(
226    bus: *const PamojaCanBus,
227    frame: *const PamojaCanFrame,
228) -> PamojaStatus {
229    let Some(frame) = frame.as_ref() else {
230        set_last_error("frame must not be null".to_owned());
231        return PamojaStatus::InvalidArgument;
232    };
233    let frame = frame.frame;
234    on_bus(bus, |bus| bus.send(&frame))
235}
236
237/// Takes the next frame the node keeps, waiting up to a timeout for one.
238///
239/// # Arguments
240///
241/// * `bus` - the node.
242/// * `timeout_micros` - how long to wait.
243/// * `out_frame` - receives a new frame handle, which the caller releases with
244///   [`pamoja_can_frame_free`](crate::can::pamoja_can_frame_free), or null when the timeout
245///   passed with nothing.
246///
247/// # Returns
248///
249/// As [`pamoja_can_bus_send`].
250///
251/// # Safety
252///
253/// `bus` must be a live handle or null, and `out_frame` a writable pointer or null.
254#[no_mangle]
255pub unsafe extern "C" fn pamoja_can_bus_receive(
256    bus: *const PamojaCanBus,
257    timeout_micros: u64,
258    out_frame: *mut *mut PamojaCanFrame,
259) -> PamojaStatus {
260    let Some(out_frame) = out_frame.as_mut() else {
261        set_last_error("out_frame must not be null".to_owned());
262        return PamojaStatus::InvalidArgument;
263    };
264    *out_frame = ptr::null_mut();
265    on_bus(bus, |bus| {
266        if let Some(frame) = bus.receive(Duration::from_micros(timeout_micros))? {
267            *out_frame = Box::into_raw(Box::new(PamojaCanFrame { frame }));
268        }
269        Ok(())
270    })
271}
272
273/// Keeps only the frames that pass at least one of the filters, from now on. An empty list
274/// keeps nothing.
275///
276/// # Arguments
277///
278/// * `bus` - the node.
279/// * `filters` - the filters.
280/// * `len` - how many, at most 512.
281///
282/// # Returns
283///
284/// As [`pamoja_can_bus_send`].
285///
286/// # Safety
287///
288/// `bus` must be a live handle or null, and `filters` must point to `len` readable filters.
289#[no_mangle]
290pub unsafe extern "C" fn pamoja_can_bus_set_filters(
291    bus: *const PamojaCanBus,
292    filters: *const PamojaCanFilter,
293    len: usize,
294) -> PamojaStatus {
295    let filters: Vec<Filter> = if len == 0 {
296        Vec::new()
297    } else if filters.is_null() {
298        set_last_error("filters must not be null when len is non-zero".to_owned());
299        return PamojaStatus::InvalidArgument;
300    } else {
301        std::slice::from_raw_parts(filters, len)
302            .iter()
303            .map(|filter| filter.filter())
304            .collect()
305    };
306    on_bus(bus, |bus| bus.set_filters(&filters))
307}
308
309/// Keeps every frame again, as a node does when it joins.
310///
311/// # Returns
312///
313/// As [`pamoja_can_bus_send`].
314///
315/// # Safety
316///
317/// `bus` must be a live handle or null.
318#[no_mangle]
319pub unsafe extern "C" fn pamoja_can_bus_clear_filters(bus: *const PamojaCanBus) -> PamojaStatus {
320    on_bus(bus, CanBus::clear_filters)
321}
322
323/// Returns how many frames a node has sent, or 0 for a null handle.
324///
325/// # Safety
326///
327/// `bus` must be a live handle or null.
328#[no_mangle]
329pub unsafe extern "C" fn pamoja_can_bus_sent(bus: *const PamojaCanBus) -> usize {
330    bus.as_ref().map_or(0, |bus| bus.bus.sent())
331}
332
333/// Returns how many frames a node has received, or 0 for a null handle.
334///
335/// # Safety
336///
337/// `bus` must be a live handle or null.
338#[no_mangle]
339pub unsafe extern "C" fn pamoja_can_bus_received(bus: *const PamojaCanBus) -> usize {
340    bus.as_ref().map_or(0, |bus| bus.bus.received())
341}
342
343/// Returns how long receives on a node have waited without a frame, in microseconds, whether
344/// or not the process slept through it, or 0 for a null handle.
345///
346/// # Safety
347///
348/// `bus` must be a live handle or null.
349#[no_mangle]
350pub unsafe extern "C" fn pamoja_can_bus_waited_micros(bus: *const PamojaCanBus) -> u64 {
351    bus.as_ref().map_or(0, |bus| bus.bus.waited_micros())
352}
353
354/// Releases a node; it leaves the bus. A null pointer is ignored.
355///
356/// # Safety
357///
358/// `bus` must be a handle that has not been freed, or null.
359#[no_mangle]
360pub unsafe extern "C" fn pamoja_can_bus_free(bus: *mut PamojaCanBus) {
361    if !bus.is_null() {
362        drop(Box::from_raw(bus));
363    }
364}
365
366/// A filter that passes one identifier and nothing else.
367///
368/// # Arguments
369///
370/// * `id` - the identifier.
371/// * `extended` - whether it is a 29-bit extended identifier.
372///
373/// # Returns
374///
375/// The filter.
376#[no_mangle]
377pub extern "C" fn pamoja_can_filter_exact(id: u32, extended: bool) -> PamojaCanFilter {
378    Filter::exact(identifier(id, extended)).into()
379}
380
381/// A filter that passes one J1939 parameter group at any priority, from any source, and for
382/// an addressed group, to any destination.
383///
384/// # Arguments
385///
386/// * `pgn` - the parameter group number.
387///
388/// # Returns
389///
390/// The filter.
391#[no_mangle]
392pub extern "C" fn pamoja_can_filter_pgn(pgn: u32) -> PamojaCanFilter {
393    Filter::pgn(pgn).into()
394}
395
396/// Reports whether a frame with an identifier passes a filter.
397///
398/// # Arguments
399///
400/// * `filter` - the filter.
401/// * `id` - the frame's identifier.
402/// * `extended` - whether it is a 29-bit extended identifier.
403///
404/// # Returns
405///
406/// `true` when the frame formats agree and the masked bits are equal.
407#[no_mangle]
408pub extern "C" fn pamoja_can_filter_matches(
409    filter: PamojaCanFilter,
410    id: u32,
411    extended: bool,
412) -> bool {
413    filter.filter().matches(identifier(id, extended))
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use crate::can::{pamoja_can_frame_data_len, pamoja_can_frame_free, pamoja_can_frame_new};
420
421    #[test]
422    fn two_nodes_trade_a_frame_through_the_c_abi() {
423        unsafe {
424            let engine = pamoja_can_bus_simulated();
425            let mut gateway = ptr::null_mut();
426            assert_eq!(pamoja_can_bus_join(engine, &mut gateway), PamojaStatus::Ok);
427            assert_eq!(pamoja_can_bus_kind(gateway), PAMOJA_CAN_BUS_SIMULATED);
428            let filter = pamoja_can_filter_pgn(61_444);
429            assert_eq!(
430                pamoja_can_bus_set_filters(gateway, &filter, 1),
431                PamojaStatus::Ok
432            );
433
434            let speed = pamoja_can::J1939Id::broadcast(3, 61_444, 0).to_id().raw();
435            let data = [0xFFu8; 8];
436            let mut frame = ptr::null_mut();
437            assert_eq!(
438                pamoja_can_frame_new(speed, true, data.as_ptr(), 8, &mut frame),
439                PamojaStatus::Ok
440            );
441            assert_eq!(pamoja_can_bus_send(engine, frame), PamojaStatus::Ok);
442            pamoja_can_frame_free(frame);
443
444            let mut heard = ptr::null_mut();
445            assert_eq!(
446                pamoja_can_bus_receive(gateway, 1_000, &mut heard),
447                PamojaStatus::Ok
448            );
449            assert!(!heard.is_null());
450            assert_eq!(pamoja_can_frame_data_len(heard), 8);
451            pamoja_can_frame_free(heard);
452
453            assert_eq!(
454                pamoja_can_bus_receive(gateway, 250_000, &mut heard),
455                PamojaStatus::Ok
456            );
457            assert!(heard.is_null());
458            assert_eq!(pamoja_can_bus_waited_micros(gateway), 250_000);
459            assert_eq!(
460                (
461                    pamoja_can_bus_sent(engine),
462                    pamoja_can_bus_received(gateway)
463                ),
464                (1, 1)
465            );
466            assert!(pamoja_can_filter_matches(filter, speed, true));
467            assert!(!pamoja_can_filter_matches(filter, speed, false));
468
469            pamoja_can_bus_free(gateway);
470            pamoja_can_bus_free(engine);
471        }
472    }
473
474    #[test]
475    fn opening_an_interface_off_linux_is_unsupported() {
476        if cfg!(not(target_os = "linux")) {
477            let mut bus = ptr::null_mut();
478            let name = c"can0";
479            assert_eq!(
480                unsafe { pamoja_can_bus_open(name.as_ptr(), &mut bus) },
481                PamojaStatus::Unsupported
482            );
483            assert!(bus.is_null());
484        }
485    }
486}