1use 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
29pub struct PamojaEventBus {
36 publisher: EventPublisher<Vec<u8>>,
37 receiver: Mutex<BroadcastBus<Vec<u8>>>,
38 missed: AtomicU64,
39}
40
41impl PamojaEventBus {
42 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
52pub struct PamojaEventPublisher {
57 inner: EventPublisher<Vec<u8>>,
58}
59
60#[no_mangle]
72pub extern "C" fn pamoja_event_bus_new(capacity: usize) -> *mut PamojaEventBus {
73 PamojaEventBus::into_raw(BroadcastBus::new(capacity))
74}
75
76#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
422unsafe 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
435async 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
455unsafe 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}