blob: 90c4c863762fc10833edb93166629ae25e9a80d9 [file] [log] [blame]
use std::{marker::PhantomData, pin::Pin, ptr};
use autocxx::WithinBox;
use cxx::UniquePtr;
pub use aos_configuration::{Channel, Configuration, ConfigurationExt, Node};
use aos_configuration_fbs::aos::Configuration as RustConfiguration;
pub use aos_events_event_loop_runtime::{CppExitHandle, EventLoop, ExitHandle};
use aos_events_event_loop_runtime::{EventLoopHolder, EventLoopRuntime, EventLoopRuntimeHolder};
use aos_flatbuffers::{transmute_table_to, Flatbuffer};
autocxx::include_cpp! (
#include "aos/events/simulated_event_loop.h"
#include "aos/events/simulated_event_loop_for_rust.h"
safety!(unsafe)
generate!("aos::SimulatedEventLoopFactoryForRust")
extern_cpp_type!("aos::ExitHandle", crate::CppExitHandle)
extern_cpp_type!("aos::Configuration", crate::Configuration)
extern_cpp_type!("aos::Node", crate::Node)
extern_cpp_type!("aos::EventLoop", crate::EventLoop)
);
/// A Rust-owned C++ `SimulatedEventLoopFactory` object.
///
/// Owning one of these via a heap allocation makes things a lot simpler, and all the functions
/// that are called repeatedly are heavyweight enough this is not a performance concern.
///
/// We don't want to own the `SimulatedEventLoopFactory` directly because C++ maintains multiple
/// pointers to it, so we can't create Rust mutable references to it.
pub struct SimulatedEventLoopFactory<'config> {
// SAFETY: This stores a pointer to the configuration, whose lifetime is `'config`.
event_loop_factory: Pin<Box<ffi::aos::SimulatedEventLoopFactoryForRust>>,
// This represents the config pointer C++ is storing.
_marker: PhantomData<&'config Configuration>,
}
impl<'config> SimulatedEventLoopFactory<'config> {
pub fn new(config: &'config impl Flatbuffer<RustConfiguration<'static>>) -> Self {
// SAFETY: `_marker` represents the lifetime of this pointer we're handing off to C++ to
// store.
let event_loop_factory = unsafe {
ffi::aos::SimulatedEventLoopFactoryForRust::new(transmute_table_to::<Configuration>(
&config.message()._tab,
))
}
.within_box();
Self {
event_loop_factory,
_marker: PhantomData,
}
}
fn as_mut(&mut self) -> Pin<&mut ffi::aos::SimulatedEventLoopFactoryForRust> {
self.event_loop_factory.as_mut()
}
/// Creates a Rust-owned EventLoop.
///
/// You probably don't want to call this directly if you're creating a Rust application. This
/// is intended for creating C++ applications. Use [`make_runtime`] instead when creating Rust
/// applications.
pub fn make_event_loop(&mut self, name: &str, node: Option<&Node>) -> UniquePtr<EventLoop> {
// SAFETY:
// * `self` has a valid C++ object.
// * C++ doesn't need the lifetimes of `name` or `node` to last any longer than this method
// call.
// * The return value manages its lifetime via `unique_ptr`.
//
// Note that dropping `self` before the return value will abort from C++, but this is still
// sound.
unsafe {
self.as_mut()
.MakeEventLoop(name, node.map_or(ptr::null(), |p| p))
}
}
/// Creates an [`EventLoopRuntime`] wrapper which also owns its underlying [`EventLoop`].
///
/// All setup must be performed with `fun`, which is called before this function returns. `fun`
/// may create further objects to use in async functions via [`EventLoop.spawn`] etc, but it is
/// the only place to set things up before the EventLoop is run.
///
/// Note that dropping the return value will drop this EventLoop.
pub fn make_runtime<F>(
&mut self,
name: &str,
node: Option<&Node>,
fun: F,
) -> EventLoopRuntimeHolder<SimulatedEventLoopHolder>
where
F: for<'event_loop> FnOnce(&mut EventLoopRuntime<'event_loop>),
{
let event_loop = self.make_event_loop(name, node);
// SAFETY: We just created this EventLoop, so we are the exclusive owner of it.
let holder = unsafe { SimulatedEventLoopHolder::new(event_loop) };
EventLoopRuntimeHolder::new(holder, fun)
}
pub fn make_exit_handle(&mut self) -> ExitHandle {
self.as_mut().MakeExitHandle().into()
}
pub fn run(&mut self) {
self.as_mut().Run();
}
// TODO(Brian): Expose OnStartup. Just take a callback for creating things, and rely on
// dropping the created objects instead of OnShutdown.
// pub fn spawn_on_startup(&mut self, spawner: impl FnMut());
}
pub struct SimulatedEventLoopHolder(UniquePtr<EventLoop>);
impl SimulatedEventLoopHolder {
/// SAFETY: `event_loop` must be the exclusive owner of the underlying EventLoop.
pub unsafe fn new(event_loop: UniquePtr<EventLoop>) -> Self {
Self(event_loop)
}
}
// SAFETY: The UniquePtr functions we're using here mirror most of the EventLoopHolder requirements
// exactly. Safety requirements on [`SimulatedEventLoopHolder.new`] take care of the rest.
unsafe impl EventLoopHolder for SimulatedEventLoopHolder {
fn into_raw(self) -> *mut ffi::aos::EventLoop {
self.0.into_raw()
}
unsafe fn from_raw(raw: *mut ffi::aos::EventLoop) -> Self {
Self(UniquePtr::from_raw(raw))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use futures::future::pending;
use runfiles::Runfiles;
use aos_configuration::read_config_from;
use aos_test_init::test_init;
use ping_rust_fbs::aos::examples::PingBuilder;
// A really basic test of the functionality here.
#[test]
fn smoke_test() {
#[derive(Debug, Default)]
struct GlobalState {
watcher_count: u32,
startup_count: u32,
}
thread_local!(static GLOBAL_STATE: RefCell<GlobalState> = Default::default());
test_init();
let r = Runfiles::create().unwrap();
let config = read_config_from(
&r.rlocation("org_frc971/aos/events/multinode_pingpong_test_combined_config.json"),
)
.unwrap();
let mut event_loop_factory = SimulatedEventLoopFactory::new(&config);
{
let pi1 = Some(config.message().get_node("pi1").unwrap());
let exit_handle = event_loop_factory.make_exit_handle();
let _runtime1 = {
let pi1_ref = &pi1;
event_loop_factory.make_runtime("runtime1", pi1, move |runtime1| {
assert!(pi1_ref.unwrap().has_name());
let channel = runtime1
.get_raw_channel("/test", "aos.examples.Ping")
.unwrap();
let mut watcher = runtime1.make_raw_watcher(channel);
runtime1.spawn(async move {
watcher.next().await;
GLOBAL_STATE.with(|g| {
let g = &mut *g.borrow_mut();
g.watcher_count = g.watcher_count + 1;
});
exit_handle.exit().await
});
})
};
let _runtime2 = {
let pi1_ref = &pi1;
event_loop_factory.make_runtime("runtime2", pi1, |runtime2| {
assert!(pi1_ref.unwrap().has_name());
let channel = runtime2
.get_raw_channel("/test", "aos.examples.Ping")
.unwrap();
let mut sender = runtime2.make_raw_sender(channel);
runtime2.spawn(async move {
GLOBAL_STATE.with(|g| {
let g = &mut *g.borrow_mut();
g.startup_count = g.startup_count + 1;
});
let mut builder = sender.make_builder();
let ping = PingBuilder::new(builder.fbb()).finish();
// SAFETY: We're using the correct message type.
unsafe { builder.send(ping) }.expect("send should succeed");
pending().await
});
})
};
GLOBAL_STATE.with(|g| {
let g = g.borrow();
assert_eq!(0, g.watcher_count);
// TODO(Brian): Use an OnRun wrapper to defer setting this until it actually starts,
// then check it.
//assert_eq!(0, g.startup_count);
});
event_loop_factory.run();
GLOBAL_STATE.with(|g| {
let g = g.borrow();
assert_eq!(1, g.watcher_count);
assert_eq!(1, g.startup_count);
});
}
}
}