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//!
14//! Every call on an endpoint takes it by shared pointer, and an endpoint may be
15//! used from several threads at once: a publish or a subscribe runs while a next
16//! waits on another thread, and two nexts on one endpoint take turns.
17
18use std::ptr;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::time::Duration;
21
22use pamoja_bus::{BroadcastBus, EventPublisher};
23use pamoja_core::EventBus;
24use tokio::sync::Mutex;
25use tokio::time::Instant;
26
27use crate::{read_bytes, runtime, set_last_error, PamojaBuffer, PamojaStatus};
28
29/// An opaque handle to one endpoint on an event bus.
30///
31/// A handle both publishes and receives, and it receives what it publishes
32/// itself. Each subscriber needs its own, taken with
33/// [`pamoja_event_bus_subscribe`], because a handle only sees events published
34/// after it existed.
35pub struct PamojaEventBus {
36    publisher: EventPublisher<Vec<u8>>,
37    receiver: Mutex<BroadcastBus<Vec<u8>>>,
38    missed: AtomicU64,
39}
40
41impl PamojaEventBus {
42    /// Boxes an endpoint around one subscription.
43    fn into_raw(receiver: BroadcastBus<Vec<u8>>) -> *mut PamojaEventBus {
44        Box::into_raw(Box::new(PamojaEventBus {
45            publisher: receiver.publisher(),
46            receiver: Mutex::new(receiver),
47            missed: AtomicU64::new(0),
48        }))
49    }
50}
51
52/// An opaque handle that publishes to a bus and has no queue of its own.
53///
54/// A part that only announces holds one of these rather than an endpoint, so it
55/// never fills a buffer it does not read.
56pub struct PamojaEventPublisher {
57    inner: EventPublisher<Vec<u8>>,
58}
59
60/// Creates an event bus.
61///
62/// # Arguments
63///
64/// * `capacity` - how many events a slow subscriber may fall behind before it
65///   starts missing them, rounded up to the next power of two and at most
66///   1048576.
67///
68/// # Returns
69///
70/// A handle the caller must release with [`pamoja_event_bus_free`].
71#[no_mangle]
72pub extern "C" fn pamoja_event_bus_new(capacity: usize) -> *mut PamojaEventBus {
73    PamojaEventBus::into_raw(BroadcastBus::new(capacity))
74}
75
76/// Takes another endpoint on the same bus.
77///
78/// The new endpoint sees events published from now on, not those already sent,
79/// so subscribe before publishing anything the subscriber needs to see.
80///
81/// # Arguments
82///
83/// * `bus` - an existing endpoint on the bus to join.
84///
85/// # Returns
86///
87/// A handle the caller must release with [`pamoja_event_bus_free`], or null if
88/// `bus` is null.
89///
90/// # Safety
91///
92/// `bus` must be a live handle from [`pamoja_event_bus_new`] or this function,
93/// or null.
94#[no_mangle]
95pub unsafe extern "C" fn pamoja_event_bus_subscribe(
96    bus: *const PamojaEventBus,
97) -> *mut PamojaEventBus {
98    match endpoint(bus) {
99        Some(bus) => PamojaEventBus::into_raw(bus.publisher.subscribe()),
100        None => ptr::null_mut(),
101    }
102}
103
104/// Takes a publish-only handle on the same bus.
105///
106/// # Arguments
107///
108/// * `bus` - an existing endpoint on the bus.
109///
110/// # Returns
111///
112/// A handle the caller must release with [`pamoja_event_publisher_free`], or
113/// null if `bus` is null.
114///
115/// # Safety
116///
117/// `bus` must be a live handle, or null.
118#[no_mangle]
119pub unsafe extern "C" fn pamoja_event_bus_publisher(
120    bus: *const PamojaEventBus,
121) -> *mut PamojaEventPublisher {
122    match endpoint(bus) {
123        Some(bus) => Box::into_raw(Box::new(PamojaEventPublisher {
124            inner: bus.publisher.clone(),
125        })),
126        None => ptr::null_mut(),
127    }
128}
129
130/// Publishes an event to every subscriber, this endpoint included.
131///
132/// The call never waits: a subscriber that has fallen behind loses its oldest
133/// event rather than holding up the publisher.
134///
135/// # Arguments
136///
137/// * `bus` - the endpoint to publish from.
138/// * `payload` - the event bytes.
139/// * `payload_len` - the length of `payload`.
140///
141/// # Returns
142///
143/// [`PamojaStatus::Ok`] once every subscriber has been handed the event.
144///
145/// # Safety
146///
147/// `bus` must be a live handle, and `payload` must point to at least
148/// `payload_len` readable bytes or be null when that length is 0.
149#[no_mangle]
150pub unsafe extern "C" fn pamoja_event_bus_publish(
151    bus: *const PamojaEventBus,
152    payload: *const u8,
153    payload_len: usize,
154) -> PamojaStatus {
155    let Some(bus) = endpoint(bus) else {
156        return PamojaStatus::InvalidArgument;
157    };
158    match read_bytes(payload, payload_len) {
159        Ok(payload) => {
160            bus.publisher.publish(payload);
161            PamojaStatus::Ok
162        }
163        Err(status) => status,
164    }
165}
166
167/// Waits for the next event on this endpoint.
168///
169/// The endpoint holds a publisher of its own, so the bus stays open while it
170/// exists and the call waits until an event arrives. Two calls on one endpoint
171/// take turns.
172///
173/// # Arguments
174///
175/// * `bus` - the endpoint to receive on.
176/// * `out_event` - receives a buffer handle the caller must release with
177///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
178///
179/// # Returns
180///
181/// [`PamojaStatus::Ok`] on success.
182///
183/// # Safety
184///
185/// `bus` must be a live handle and `out_event` must be writable.
186#[no_mangle]
187pub unsafe extern "C" fn pamoja_event_bus_next(
188    bus: *const PamojaEventBus,
189    out_event: *mut *mut PamojaBuffer,
190) -> PamojaStatus {
191    let Some(bus) = endpoint(bus) else {
192        return PamojaStatus::InvalidArgument;
193    };
194    if out_event.is_null() {
195        set_last_error("out_event must not be null".to_owned());
196        return PamojaStatus::InvalidArgument;
197    }
198    *out_event = ptr::null_mut();
199    match runtime().block_on(next_on(bus, None)) {
200        Some(event) => deliver(event, out_event),
201        None => PamojaStatus::Ok,
202    }
203}
204
205/// Waits for the next event on this endpoint, giving up after a time limit.
206///
207/// A wait that runs out takes no event, so the next one published is left for
208/// the next call.
209///
210/// # Arguments
211///
212/// * `bus` - the endpoint to receive on.
213/// * `timeout_ms` - how long to wait, in milliseconds, including any wait for
214///   another call on this endpoint to finish. A limit too far off to schedule
215///   waits as [`pamoja_event_bus_next`] does.
216/// * `out_event` - receives a buffer handle the caller must release with
217///   [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null when the time
218///   ran out.
219/// * `out_timed_out` - receives `true` when the time ran out.
220///
221/// # Returns
222///
223/// [`PamojaStatus::Ok`] whether an event arrived or the time ran out.
224///
225/// # Safety
226///
227/// `bus` must be a live handle, and `out_event` and `out_timed_out` must be
228/// writable.
229#[no_mangle]
230pub unsafe extern "C" fn pamoja_event_bus_next_within(
231    bus: *const PamojaEventBus,
232    timeout_ms: u64,
233    out_event: *mut *mut PamojaBuffer,
234    out_timed_out: *mut bool,
235) -> PamojaStatus {
236    let Some(bus) = endpoint(bus) else {
237        return PamojaStatus::InvalidArgument;
238    };
239    if out_event.is_null() || out_timed_out.is_null() {
240        set_last_error("out_event and out_timed_out must not be null".to_owned());
241        return PamojaStatus::InvalidArgument;
242    }
243    *out_event = ptr::null_mut();
244    *out_timed_out = false;
245    let limit = Duration::from_millis(timeout_ms);
246    match runtime().block_on(async { next_on(bus, Instant::now().checked_add(limit)).await }) {
247        Some(event) => deliver(event, out_event),
248        None => {
249            *out_timed_out = true;
250            PamojaStatus::Ok
251        }
252    }
253}
254
255/// Counts the events this endpoint lost by falling behind.
256///
257/// # Arguments
258///
259/// * `bus` - the endpoint to ask.
260///
261/// # Returns
262///
263/// How many events were dropped from this endpoint's buffer before it read
264/// them, as of its last completed wait, or 0 if `bus` is null.
265///
266/// # Safety
267///
268/// `bus` must be a live handle, or null.
269#[no_mangle]
270pub unsafe extern "C" fn pamoja_event_bus_missed(bus: *const PamojaEventBus) -> u64 {
271    endpoint(bus).map_or(0, |bus| bus.missed.load(Ordering::Relaxed))
272}
273
274/// Releases an event bus endpoint.
275///
276/// Other endpoints on the same bus keep working.
277///
278/// Passing null is a no-op.
279///
280/// # Safety
281///
282/// `bus` must be a handle from [`pamoja_event_bus_new`],
283/// [`pamoja_event_bus_subscribe`], or [`pamoja_event_publisher_subscribe`] that
284/// has not already been freed, or null. After this call it must not be used
285/// again.
286#[no_mangle]
287pub unsafe extern "C" fn pamoja_event_bus_free(bus: *mut PamojaEventBus) {
288    if !bus.is_null() {
289        drop(Box::from_raw(bus));
290    }
291}
292
293/// Creates a bus with no subscribers yet, and a publisher on it.
294///
295/// # Arguments
296///
297/// * `capacity` - how many events a slow subscriber may fall behind before it
298///   starts missing them, rounded up to the next power of two and at most
299///   1048576.
300///
301/// # Returns
302///
303/// A handle the caller must release with [`pamoja_event_publisher_free`].
304#[no_mangle]
305pub extern "C" fn pamoja_event_publisher_new(capacity: usize) -> *mut PamojaEventPublisher {
306    Box::into_raw(Box::new(PamojaEventPublisher {
307        inner: EventPublisher::new(capacity),
308    }))
309}
310
311/// Hands an event to every current subscriber.
312///
313/// The call never waits, and it may be made from any thread.
314///
315/// # Arguments
316///
317/// * `publisher` - the publisher.
318/// * `payload` - the event bytes.
319/// * `payload_len` - the length of `payload`.
320/// * `out_reached` - receives how many subscribers the event was handed to; an
321///   event published while no one is subscribed is dropped, and this is 0.
322///
323/// # Returns
324///
325/// [`PamojaStatus::Ok`] on success.
326///
327/// # Safety
328///
329/// `publisher` must be a live handle, `payload` must point to at least
330/// `payload_len` readable bytes or be null when that length is 0, and
331/// `out_reached` must be writable.
332#[no_mangle]
333pub unsafe extern "C" fn pamoja_event_publisher_publish(
334    publisher: *const PamojaEventPublisher,
335    payload: *const u8,
336    payload_len: usize,
337    out_reached: *mut usize,
338) -> PamojaStatus {
339    if publisher.is_null() || out_reached.is_null() {
340        set_last_error("publisher and out_reached must not be null".to_owned());
341        return PamojaStatus::InvalidArgument;
342    }
343    match read_bytes(payload, payload_len) {
344        Ok(payload) => {
345            *out_reached = (*publisher).inner.publish(payload);
346            PamojaStatus::Ok
347        }
348        Err(status) => status,
349    }
350}
351
352/// Subscribes to the bus a publisher publishes on.
353///
354/// # Arguments
355///
356/// * `publisher` - the publisher.
357///
358/// # Returns
359///
360/// An endpoint that receives events published from now on, which the caller must
361/// release with [`pamoja_event_bus_free`], or null if `publisher` is null.
362///
363/// # Safety
364///
365/// `publisher` must be a live handle, or null.
366#[no_mangle]
367pub unsafe extern "C" fn pamoja_event_publisher_subscribe(
368    publisher: *const PamojaEventPublisher,
369) -> *mut PamojaEventBus {
370    if publisher.is_null() {
371        set_last_error("publisher must not be null".to_owned());
372        return ptr::null_mut();
373    }
374    PamojaEventBus::into_raw((*publisher).inner.subscribe())
375}
376
377/// Takes another publisher on the same bus, for another part that announces.
378///
379/// # Arguments
380///
381/// * `publisher` - an existing publisher on the bus.
382///
383/// # Returns
384///
385/// A handle with a lifetime of its own, which the caller must release with
386/// [`pamoja_event_publisher_free`], or null if `publisher` is null.
387///
388/// # Safety
389///
390/// `publisher` must be a live handle, or null.
391#[no_mangle]
392pub unsafe extern "C" fn pamoja_event_publisher_clone(
393    publisher: *const PamojaEventPublisher,
394) -> *mut PamojaEventPublisher {
395    if publisher.is_null() {
396        set_last_error("publisher must not be null".to_owned());
397        return ptr::null_mut();
398    }
399    Box::into_raw(Box::new(PamojaEventPublisher {
400        inner: (*publisher).inner.publisher(),
401    }))
402}
403
404/// Releases a publisher.
405///
406/// The bus stays open for the endpoints still on it.
407///
408/// Passing null is a no-op.
409///
410/// # Safety
411///
412/// `publisher` must be a handle from [`pamoja_event_publisher_new`],
413/// [`pamoja_event_publisher_clone`], or [`pamoja_event_bus_publisher`] that has not
414/// already been freed, or null. After this call it must not be used again.
415#[no_mangle]
416pub unsafe extern "C" fn pamoja_event_publisher_free(publisher: *mut PamojaEventPublisher) {
417    if !publisher.is_null() {
418        drop(Box::from_raw(publisher));
419    }
420}
421
422/// Borrows an endpoint, rejecting a null pointer.
423///
424/// # Safety
425///
426/// `bus` must be a live handle, or null.
427unsafe fn endpoint<'a>(bus: *const PamojaEventBus) -> Option<&'a PamojaEventBus> {
428    if bus.is_null() {
429        set_last_error("bus must not be null".to_owned());
430        return None;
431    }
432    Some(&*bus)
433}
434
435/// Waits for this endpoint's next event, until `deadline` when one is given.
436///
437/// Returns `None` when the deadline passed, whether waiting for another call on
438/// the endpoint or for an event, and records the endpoint's missed count after
439/// every wait that reached its buffer.
440async fn next_on(bus: &PamojaEventBus, deadline: Option<Instant>) -> Option<Option<Vec<u8>>> {
441    let mut receiver = match deadline {
442        Some(deadline) => tokio::time::timeout_at(deadline, bus.receiver.lock())
443            .await
444            .ok()?,
445        None => bus.receiver.lock().await,
446    };
447    let event = match deadline {
448        Some(deadline) => tokio::time::timeout_at(deadline, receiver.next_event()).await,
449        None => Ok(receiver.next_event().await),
450    };
451    bus.missed.store(receiver.missed(), Ordering::Relaxed);
452    event.ok().map(|event| event.ok().flatten())
453}
454
455/// Writes an event through `out_event`.
456///
457/// # Safety
458///
459/// `out_event` must be writable.
460unsafe fn deliver(event: Option<Vec<u8>>, out_event: *mut *mut PamojaBuffer) -> PamojaStatus {
461    if let Some(event) = event {
462        *out_event = PamojaBuffer::into_raw(event);
463    }
464    PamojaStatus::Ok
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
471
472    unsafe fn take(event: *mut PamojaBuffer) -> Vec<u8> {
473        assert!(!event.is_null());
474        let bytes = std::slice::from_raw_parts(pamoja_buffer_data(event), pamoja_buffer_len(event))
475            .to_vec();
476        pamoja_buffer_free(event);
477        bytes
478    }
479
480    #[test]
481    fn every_subscriber_sees_a_published_event() {
482        unsafe {
483            let bus = pamoja_event_bus_new(8);
484            let first = pamoja_event_bus_subscribe(bus);
485            let second = pamoja_event_bus_subscribe(bus);
486
487            assert_eq!(
488                pamoja_event_bus_publish(bus, b"battery.low".as_ptr(), 11),
489                PamojaStatus::Ok
490            );
491
492            for subscriber in [first, second] {
493                let mut event = ptr::null_mut();
494                assert_eq!(
495                    pamoja_event_bus_next(subscriber, &mut event),
496                    PamojaStatus::Ok
497                );
498                assert_eq!(take(event), b"battery.low");
499                pamoja_event_bus_free(subscriber);
500            }
501
502            pamoja_event_bus_free(bus);
503        }
504    }
505
506    #[test]
507    fn an_endpoint_publishes_while_its_own_next_waits() {
508        unsafe {
509            let bus = pamoja_event_bus_new(8);
510            let address = bus as usize;
511            let waiting = std::thread::spawn(move || {
512                let mut event = ptr::null_mut();
513                let status = pamoja_event_bus_next(address as *const PamojaEventBus, &mut event);
514                (status, event as usize)
515            });
516            std::thread::sleep(Duration::from_millis(50));
517
518            assert_eq!(
519                pamoja_event_bus_publish(bus, b"heater.off".as_ptr(), 10),
520                PamojaStatus::Ok
521            );
522            let (status, event) = waiting.join().expect("the waiting thread");
523            assert_eq!(status, PamojaStatus::Ok);
524            assert_eq!(take(event as *mut PamojaBuffer), b"heater.off");
525
526            pamoja_event_bus_free(bus);
527        }
528    }
529
530    #[test]
531    fn a_timed_next_runs_out_and_leaves_the_next_event() {
532        unsafe {
533            let bus = pamoja_event_bus_new(8);
534            let mut event = ptr::null_mut();
535            let mut timed_out = false;
536            assert_eq!(
537                pamoja_event_bus_next_within(bus, 20, &mut event, &mut timed_out),
538                PamojaStatus::Ok
539            );
540            assert!(timed_out);
541            assert!(event.is_null());
542
543            pamoja_event_bus_publish(bus, b"link.up".as_ptr(), 7);
544            assert_eq!(
545                pamoja_event_bus_next_within(bus, 1_000, &mut event, &mut timed_out),
546                PamojaStatus::Ok
547            );
548            assert!(!timed_out);
549            assert_eq!(take(event), b"link.up");
550
551            pamoja_event_bus_publish(bus, b"link.down".as_ptr(), 9);
552            assert_eq!(
553                pamoja_event_bus_next_within(bus, u64::MAX, &mut event, &mut timed_out),
554                PamojaStatus::Ok
555            );
556            assert!(!timed_out);
557            assert_eq!(take(event), b"link.down");
558
559            pamoja_event_bus_free(bus);
560        }
561    }
562
563    #[test]
564    fn a_publisher_counts_its_subscribers_and_an_endpoint_counts_what_it_missed() {
565        unsafe {
566            let power = pamoja_event_publisher_new(2);
567            let mut reached = usize::MAX;
568            assert_eq!(
569                pamoja_event_publisher_publish(power, b"0".as_ptr(), 1, &mut reached),
570                PamojaStatus::Ok
571            );
572            assert_eq!(reached, 0);
573
574            let logger = pamoja_event_publisher_subscribe(power);
575            let sampler = pamoja_event_publisher_clone(power);
576            for sample in [b"1", b"2", b"3", b"4", b"5"] {
577                pamoja_event_publisher_publish(sampler, sample.as_ptr(), 1, &mut reached);
578                assert_eq!(reached, 1);
579            }
580            pamoja_event_publisher_free(sampler);
581
582            let mut event = ptr::null_mut();
583            assert_eq!(pamoja_event_bus_next(logger, &mut event), PamojaStatus::Ok);
584            assert_eq!(take(event), b"4");
585            assert_eq!(pamoja_event_bus_missed(logger), 3);
586
587            pamoja_event_bus_free(logger);
588            pamoja_event_publisher_free(power);
589        }
590    }
591
592    #[test]
593    fn a_null_handle_is_refused_rather_than_dereferenced() {
594        unsafe {
595            assert!(pamoja_event_bus_subscribe(ptr::null()).is_null());
596            assert!(pamoja_event_bus_publisher(ptr::null()).is_null());
597            assert!(pamoja_event_publisher_subscribe(ptr::null()).is_null());
598            assert!(pamoja_event_publisher_clone(ptr::null()).is_null());
599            assert_eq!(
600                pamoja_event_bus_publish(ptr::null(), b"x".as_ptr(), 1),
601                PamojaStatus::InvalidArgument
602            );
603            let mut reached = 0;
604            assert_eq!(
605                pamoja_event_publisher_publish(ptr::null(), b"x".as_ptr(), 1, &mut reached),
606                PamojaStatus::InvalidArgument
607            );
608            assert_eq!(pamoja_event_bus_missed(ptr::null()), 0);
609            pamoja_event_bus_free(ptr::null_mut());
610            pamoja_event_publisher_free(ptr::null_mut());
611        }
612    }
613}