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}