Skip to main content

pamoja_ffi/
bus.rs

1//! The C ABI for the in-process event bus.
2//!
3//! These functions wrap [`pamoja_bus`] for callers that reach the SDK through
4//! the flat C boundary: one publisher, many subscribers, inside a single
5//! process. It is how the parts of a gateway talk to each other without knowing
6//! about each other, so a sampler can announce a reading and whatever cares
7//! about readings picks it up.
8//!
9//! The Rust bus carries any cloneable event; a C ABI has no such parameter, so
10//! this one carries bytes. That is the shape every binding already exchanges,
11//! and a caller who wants structure encodes it with
12//! [`crate::codec`] on the way in.
13
14use std::ptr;
15
16use pamoja_bus::BroadcastBus;
17use pamoja_core::EventBus;
18
19use crate::{read_bytes, runtime, set_last_error, PamojaBuffer, PamojaStatus};
20
21/// An opaque handle to one endpoint on an event bus.
22///
23/// A handle both publishes and receives. Each subscriber needs its own, taken
24/// with [`pamoja_event_bus_subscribe`], because a handle only sees events
25/// published after it existed.
26pub struct PamojaEventBus {
27    inner: BroadcastBus<Vec<u8>>,
28}
29
30/// Creates an event bus.
31///
32/// # Arguments
33///
34/// * `capacity` - how many events a slow subscriber may fall behind before it
35///   starts missing them.
36///
37/// # Returns
38///
39/// A handle the caller must release with [`pamoja_event_bus_free`].
40#[no_mangle]
41pub extern "C" fn pamoja_event_bus_new(capacity: usize) -> *mut PamojaEventBus {
42    Box::into_raw(Box::new(PamojaEventBus {
43        inner: BroadcastBus::new(capacity),
44    }))
45}
46
47/// Takes another endpoint on the same bus.
48///
49/// The new endpoint sees events published from now on, not those already sent,
50/// so subscribe before publishing anything the subscriber needs to see.
51///
52/// # Arguments
53///
54/// * `bus` - an existing endpoint on the bus to join.
55///
56/// # Returns
57///
58/// A handle the caller must release with [`pamoja_event_bus_free`], or null if
59/// `bus` is null.
60///
61/// # Safety
62///
63/// `bus` must be a live handle from [`pamoja_event_bus_new`] or this function,
64/// or null.
65#[no_mangle]
66pub unsafe extern "C" fn pamoja_event_bus_subscribe(
67    bus: *const PamojaEventBus,
68) -> *mut PamojaEventBus {
69    if bus.is_null() {
70        set_last_error("bus must not be null".to_owned());
71        return ptr::null_mut();
72    }
73    Box::into_raw(Box::new(PamojaEventBus {
74        inner: (*bus).inner.subscribe(),
75    }))
76}
77
78/// Publishes an event to every subscriber.
79///
80/// # Arguments
81///
82/// * `bus` - the endpoint to publish from.
83/// * `payload` - the event bytes.
84/// * `payload_len` - the length of `payload`.
85///
86/// # Returns
87///
88/// [`PamojaStatus::Ok`] once every subscriber has been handed the event, or
89/// [`PamojaStatus::Closed`] if the bus has shut down.
90///
91/// # Safety
92///
93/// `bus` must be a live handle, and `payload` must point to at least
94/// `payload_len` readable bytes or be null when that length is 0.
95#[no_mangle]
96pub unsafe extern "C" fn pamoja_event_bus_publish(
97    bus: *const PamojaEventBus,
98    payload: *const u8,
99    payload_len: usize,
100) -> PamojaStatus {
101    if bus.is_null() {
102        set_last_error("bus must not be null".to_owned());
103        return PamojaStatus::InvalidArgument;
104    }
105    let payload = match read_bytes(payload, payload_len) {
106        Ok(payload) => payload,
107        Err(status) => return status,
108    };
109    match runtime().block_on((*bus).inner.publish(payload)) {
110        Ok(()) => PamojaStatus::Ok,
111        Err(error) => fail(error),
112    }
113}
114
115/// Waits for the next event on this endpoint.
116///
117/// # Arguments
118///
119/// * `bus` - the endpoint to receive on.
120/// * `out_event` - receives a buffer handle, or null when the bus has closed.
121///
122/// # Returns
123///
124/// [`PamojaStatus::Ok`] on success. A null `out_event` with an `Ok` status means
125/// the bus closed rather than that anything failed.
126///
127/// # Safety
128///
129/// `bus` must be a live handle and `out_event` must be writable.
130#[no_mangle]
131pub unsafe extern "C" fn pamoja_event_bus_next(
132    bus: *mut PamojaEventBus,
133    out_event: *mut *mut PamojaBuffer,
134) -> PamojaStatus {
135    if bus.is_null() {
136        set_last_error("bus must not be null".to_owned());
137        return PamojaStatus::InvalidArgument;
138    }
139    if out_event.is_null() {
140        set_last_error("out_event must not be null".to_owned());
141        return PamojaStatus::InvalidArgument;
142    }
143    *out_event = ptr::null_mut();
144    match runtime().block_on((*bus).inner.next_event()) {
145        Ok(Some(event)) => {
146            *out_event = PamojaBuffer::into_raw(event);
147            PamojaStatus::Ok
148        }
149        Ok(None) => PamojaStatus::Ok,
150        Err(error) => fail(error),
151    }
152}
153
154/// Releases an event bus endpoint.
155///
156/// Other endpoints on the same bus keep working.
157///
158/// Passing null is a no-op.
159///
160/// # Safety
161///
162/// `bus` must be a handle from [`pamoja_event_bus_new`] or
163/// [`pamoja_event_bus_subscribe`] that has not already been freed, or null.
164/// After this call it must not be used again.
165#[no_mangle]
166pub unsafe extern "C" fn pamoja_event_bus_free(bus: *mut PamojaEventBus) {
167    if !bus.is_null() {
168        drop(Box::from_raw(bus));
169    }
170}
171
172/// Records an error and maps it onto a status.
173fn fail(error: pamoja_core::Error) -> PamojaStatus {
174    let status = PamojaStatus::from_error(&error);
175    set_last_error(error.to_string());
176    status
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
183
184    #[test]
185    fn every_subscriber_sees_a_published_event() {
186        unsafe {
187            let bus = pamoja_event_bus_new(8);
188            let first = pamoja_event_bus_subscribe(bus);
189            let second = pamoja_event_bus_subscribe(bus);
190
191            assert_eq!(
192                pamoja_event_bus_publish(bus, b"battery.low".as_ptr(), 11),
193                PamojaStatus::Ok
194            );
195
196            for subscriber in [first, second] {
197                let mut event = ptr::null_mut();
198                assert_eq!(
199                    pamoja_event_bus_next(subscriber, &mut event),
200                    PamojaStatus::Ok
201                );
202                assert!(!event.is_null());
203                let bytes =
204                    std::slice::from_raw_parts(pamoja_buffer_data(event), pamoja_buffer_len(event))
205                        .to_vec();
206                assert_eq!(bytes, b"battery.low");
207                pamoja_buffer_free(event);
208                pamoja_event_bus_free(subscriber);
209            }
210
211            pamoja_event_bus_free(bus);
212        }
213    }
214
215    #[test]
216    fn a_null_handle_is_refused_rather_than_dereferenced() {
217        unsafe {
218            assert!(pamoja_event_bus_subscribe(ptr::null()).is_null());
219            assert_eq!(
220                pamoja_event_bus_publish(ptr::null(), b"x".as_ptr(), 1),
221                PamojaStatus::InvalidArgument
222            );
223            pamoja_event_bus_free(ptr::null_mut());
224        }
225    }
226}