blob: 06fcee8b699ef8e3637c9dc1636bd63b4ca37264 [file] [log] [blame]
use std::{
marker::PhantomData,
mem::ManuallyDrop,
ops::{Deref, DerefMut},
pin::Pin,
ptr,
};
use autocxx::WithinBox;
use cxx::UniquePtr;
use futures::{future::pending, never::Never};
pub use aos_configuration::{Channel, Configuration, ConfigurationExt, Node};
use aos_configuration_fbs::aos::Configuration as RustConfiguration;
pub use aos_events_event_loop_runtime::EventLoop;
use aos_events_event_loop_runtime::EventLoopRuntime;
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::ExitHandle")
generate!("aos::SimulatedEventLoopFactoryForRust")
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<'new_config: 'config>(
config: &'new_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 is `'static` because it's wrapped in `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.
pub fn make_runtime(&mut self, name: &str, node: Option<&Node>) -> SimulatedEventLoopRuntime {
SimulatedEventLoopRuntime::new(self.make_event_loop(name, node))
}
pub fn make_exit_handle(&mut self) -> ExitHandle {
ExitHandle(self.as_mut().MakeExitHandle())
}
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());
}
// TODO(Brian): Move this and the `generate!` somewhere else once we wrap ShmEventLoop, which also
// uses it.
pub struct ExitHandle(UniquePtr<ffi::aos::ExitHandle>);
impl ExitHandle {
/// Exits the EventLoops represented by this handle. You probably want to immediately return
/// from the context this is called in. Awaiting [`exit`] instead of using this function is an
/// easy way to do that.
pub fn exit_sync(mut self) {
self.0.as_mut().unwrap().Exit();
}
/// Exits the EventLoops represented by this handle, and never returns. Immediately awaiting
/// this from a [`EventLoopRuntime::spawn`]ed task is usually what you want, it will ensure
/// that no more code from that task runs.
pub async fn exit(self) -> Never {
self.exit_sync();
pending().await
}
}
pub struct SimulatedEventLoopRuntime(ManuallyDrop<EventLoopRuntime<'static>>);
impl SimulatedEventLoopRuntime {
pub fn new(event_loop: UniquePtr<EventLoop>) -> Self {
// SAFETY: We own the underlying EventLoop, so `'static` is the correct lifetime. Anything
// using this `EventLoopRuntime` will need to borrow the object we're returning, which will
// ensure it stays alive.
let runtime = unsafe { EventLoopRuntime::<'static>::new(event_loop.into_raw()) };
Self(ManuallyDrop::new(runtime))
}
}
impl Drop for SimulatedEventLoopRuntime {
fn drop(&mut self) {
let event_loop = self.raw_event_loop();
// SAFETY: We're not going to touch this field again.
unsafe { ManuallyDrop::drop(&mut self.0) };
// SAFETY: `new` created this from `into_raw`. We just dropped the only Rust reference to
// it.
unsafe { UniquePtr::from_raw(event_loop) };
}
}
impl Deref for SimulatedEventLoopRuntime {
type Target = EventLoopRuntime<'static>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SimulatedEventLoopRuntime {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[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_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 mut runtime1 = event_loop_factory.make_runtime("runtime1", pi1);
let channel = runtime1
.configuration()
.get_channel("/test", "aos.examples.Ping", "test", pi1)
.unwrap();
let mut runtime2 = event_loop_factory.make_runtime("runtime2", pi1);
{
let mut watcher = runtime1.make_raw_watcher(channel);
let exit_handle = event_loop_factory.make_exit_handle();
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 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);
});
}
}
}