Brian Silverman | 2a60707 | 2022-07-23 16:12:23 -0700 | [diff] [blame^] | 1 | use std::{ |
| 2 | marker::PhantomData, |
| 3 | mem::ManuallyDrop, |
| 4 | ops::{Deref, DerefMut}, |
| 5 | pin::Pin, |
| 6 | ptr, |
| 7 | }; |
| 8 | |
| 9 | use autocxx::WithinBox; |
| 10 | use cxx::UniquePtr; |
| 11 | use futures::{future::pending, never::Never}; |
| 12 | |
| 13 | pub use aos_configuration::{Channel, Configuration, ConfigurationExt, Node}; |
| 14 | use aos_configuration_fbs::aos::Configuration as RustConfiguration; |
| 15 | pub use aos_events_event_loop_runtime::EventLoop; |
| 16 | use aos_events_event_loop_runtime::EventLoopRuntime; |
| 17 | use aos_flatbuffers::{transmute_table_to, Flatbuffer}; |
| 18 | |
| 19 | autocxx::include_cpp! ( |
| 20 | #include "aos/events/simulated_event_loop.h" |
| 21 | #include "aos/events/simulated_event_loop_for_rust.h" |
| 22 | |
| 23 | safety!(unsafe) |
| 24 | |
| 25 | generate!("aos::ExitHandle") |
| 26 | generate!("aos::SimulatedEventLoopFactoryForRust") |
| 27 | |
| 28 | extern_cpp_type!("aos::Configuration", crate::Configuration) |
| 29 | extern_cpp_type!("aos::Node", crate::Node) |
| 30 | extern_cpp_type!("aos::EventLoop", crate::EventLoop) |
| 31 | ); |
| 32 | |
| 33 | /// A Rust-owned C++ `SimulatedEventLoopFactory` object. |
| 34 | /// |
| 35 | /// Owning one of these via a heap allocation makes things a lot simpler, and all the functions |
| 36 | /// that are called repeatedly are heavyweight enough this is not a performance concern. |
| 37 | /// |
| 38 | /// We don't want to own the `SimulatedEventLoopFactory` directly because C++ maintains multiple |
| 39 | /// pointers to it, so we can't create Rust mutable references to it. |
| 40 | pub struct SimulatedEventLoopFactory<'config> { |
| 41 | // SAFETY: This stores a pointer to the configuration, whose lifetime is `'config`. |
| 42 | event_loop_factory: Pin<Box<ffi::aos::SimulatedEventLoopFactoryForRust>>, |
| 43 | // This represents the config pointer C++ is storing. |
| 44 | _marker: PhantomData<&'config Configuration>, |
| 45 | } |
| 46 | |
| 47 | impl<'config> SimulatedEventLoopFactory<'config> { |
| 48 | pub fn new<'new_config: 'config>( |
| 49 | config: &'new_config impl Flatbuffer<RustConfiguration<'static>>, |
| 50 | ) -> Self { |
| 51 | // SAFETY: `_marker` represents the lifetime of this pointer we're handing off to C++ to |
| 52 | // store. |
| 53 | let event_loop_factory = unsafe { |
| 54 | ffi::aos::SimulatedEventLoopFactoryForRust::new(transmute_table_to::<Configuration>( |
| 55 | &config.message()._tab, |
| 56 | )) |
| 57 | } |
| 58 | .within_box(); |
| 59 | Self { |
| 60 | event_loop_factory, |
| 61 | _marker: PhantomData, |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | fn as_mut(&mut self) -> Pin<&mut ffi::aos::SimulatedEventLoopFactoryForRust> { |
| 66 | self.event_loop_factory.as_mut() |
| 67 | } |
| 68 | |
| 69 | /// Creates a Rust-owned EventLoop. |
| 70 | /// |
| 71 | /// You probably don't want to call this directly if you're creating a Rust application. This |
| 72 | /// is intended for creating C++ applications. Use [`make_runtime`] instead when creating Rust |
| 73 | /// applications. |
| 74 | pub fn make_event_loop(&mut self, name: &str, node: Option<&Node>) -> UniquePtr<EventLoop> { |
| 75 | // SAFETY: |
| 76 | // * `self` has a valid C++ object. |
| 77 | // * C++ doesn't need the lifetimes of `name` or `node` to last any longer than this method |
| 78 | // call. |
| 79 | // * The return value is `'static` because it's wrapped in `unique_ptr`. |
| 80 | // |
| 81 | // Note that dropping `self` before the return value will abort from C++, but this is still |
| 82 | // sound. |
| 83 | unsafe { |
| 84 | self.as_mut() |
| 85 | .MakeEventLoop(name, node.map_or(ptr::null(), |p| p)) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | /// Creates an [`EventLoopRuntime`] wrapper which also owns its underlying EventLoop. |
| 90 | pub fn make_runtime(&mut self, name: &str, node: Option<&Node>) -> SimulatedEventLoopRuntime { |
| 91 | SimulatedEventLoopRuntime::new(self.make_event_loop(name, node)) |
| 92 | } |
| 93 | |
| 94 | pub fn make_exit_handle(&mut self) -> ExitHandle { |
| 95 | ExitHandle(self.as_mut().MakeExitHandle()) |
| 96 | } |
| 97 | |
| 98 | pub fn run(&mut self) { |
| 99 | self.as_mut().Run(); |
| 100 | } |
| 101 | |
| 102 | // TODO(Brian): Expose OnStartup. Just take a callback for creating things, and rely on |
| 103 | // dropping the created objects instead of OnShutdown. |
| 104 | // pub fn spawn_on_startup(&mut self, spawner: impl FnMut()); |
| 105 | } |
| 106 | |
| 107 | // TODO(Brian): Move this and the `generate!` somewhere else once we wrap ShmEventLoop, which also |
| 108 | // uses it. |
| 109 | pub struct ExitHandle(UniquePtr<ffi::aos::ExitHandle>); |
| 110 | |
| 111 | impl ExitHandle { |
| 112 | /// Exits the EventLoops represented by this handle. You probably want to immediately return |
| 113 | /// from the context this is called in. Awaiting [`exit`] instead of using this function is an |
| 114 | /// easy way to do that. |
| 115 | pub fn exit_sync(mut self) { |
| 116 | self.0.as_mut().unwrap().Exit(); |
| 117 | } |
| 118 | |
| 119 | /// Exits the EventLoops represented by this handle, and never returns. Immediately awaiting |
| 120 | /// this from a [`EventLoopRuntime::spawn`]ed task is usually what you want, it will ensure |
| 121 | /// that no more code from that task runs. |
| 122 | pub async fn exit(self) -> Never { |
| 123 | self.exit_sync(); |
| 124 | pending().await |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | pub struct SimulatedEventLoopRuntime(ManuallyDrop<EventLoopRuntime<'static>>); |
| 129 | |
| 130 | impl SimulatedEventLoopRuntime { |
| 131 | pub fn new(event_loop: UniquePtr<EventLoop>) -> Self { |
| 132 | // SAFETY: We own the underlying EventLoop, so `'static` is the correct lifetime. Anything |
| 133 | // using this `EventLoopRuntime` will need to borrow the object we're returning, which will |
| 134 | // ensure it stays alive. |
| 135 | let runtime = unsafe { EventLoopRuntime::<'static>::new(event_loop.into_raw()) }; |
| 136 | Self(ManuallyDrop::new(runtime)) |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | impl Drop for SimulatedEventLoopRuntime { |
| 141 | fn drop(&mut self) { |
| 142 | let event_loop = self.raw_event_loop(); |
| 143 | // SAFETY: We're not going to touch this field again. |
| 144 | unsafe { ManuallyDrop::drop(&mut self.0) }; |
| 145 | // SAFETY: `new` created this from `into_raw`. We just dropped the only Rust reference to |
| 146 | // it. |
| 147 | unsafe { UniquePtr::from_raw(event_loop) }; |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | impl Deref for SimulatedEventLoopRuntime { |
| 152 | type Target = EventLoopRuntime<'static>; |
| 153 | fn deref(&self) -> &Self::Target { |
| 154 | &self.0 |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | impl DerefMut for SimulatedEventLoopRuntime { |
| 159 | fn deref_mut(&mut self) -> &mut Self::Target { |
| 160 | &mut self.0 |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | #[cfg(test)] |
| 165 | mod tests { |
| 166 | use super::*; |
| 167 | |
| 168 | use std::cell::RefCell; |
| 169 | |
| 170 | use futures::future::pending; |
| 171 | use runfiles::Runfiles; |
| 172 | |
| 173 | use aos_configuration::read_config_from; |
| 174 | use aos_init::test_init; |
| 175 | use ping_rust_fbs::aos::examples::PingBuilder; |
| 176 | |
| 177 | // A really basic test of the functionality here. |
| 178 | #[test] |
| 179 | fn smoke_test() { |
| 180 | #[derive(Debug, Default)] |
| 181 | struct GlobalState { |
| 182 | watcher_count: u32, |
| 183 | startup_count: u32, |
| 184 | } |
| 185 | |
| 186 | thread_local!(static GLOBAL_STATE: RefCell<GlobalState> = Default::default()); |
| 187 | |
| 188 | test_init(); |
| 189 | let r = Runfiles::create().unwrap(); |
| 190 | let config = read_config_from( |
| 191 | &r.rlocation("org_frc971/aos/events/multinode_pingpong_test_combined_config.json"), |
| 192 | ) |
| 193 | .unwrap(); |
| 194 | let mut event_loop_factory = SimulatedEventLoopFactory::new(&config); |
| 195 | { |
| 196 | let pi1 = Some(config.message().get_node("pi1").unwrap()); |
| 197 | let mut runtime1 = event_loop_factory.make_runtime("runtime1", pi1); |
| 198 | let channel = runtime1 |
| 199 | .configuration() |
| 200 | .get_channel("/test", "aos.examples.Ping", "test", pi1) |
| 201 | .unwrap(); |
| 202 | let mut runtime2 = event_loop_factory.make_runtime("runtime2", pi1); |
| 203 | |
| 204 | { |
| 205 | let mut watcher = runtime1.make_raw_watcher(channel); |
| 206 | let exit_handle = event_loop_factory.make_exit_handle(); |
| 207 | runtime1.spawn(async move { |
| 208 | watcher.next().await; |
| 209 | GLOBAL_STATE.with(|g| { |
| 210 | let g = &mut *g.borrow_mut(); |
| 211 | g.watcher_count = g.watcher_count + 1; |
| 212 | }); |
| 213 | exit_handle.exit().await |
| 214 | }); |
| 215 | } |
| 216 | |
| 217 | { |
| 218 | let mut sender = runtime2.make_raw_sender(channel); |
| 219 | runtime2.spawn(async move { |
| 220 | GLOBAL_STATE.with(|g| { |
| 221 | let g = &mut *g.borrow_mut(); |
| 222 | g.startup_count = g.startup_count + 1; |
| 223 | }); |
| 224 | |
| 225 | let mut builder = sender.make_builder(); |
| 226 | let ping = PingBuilder::new(builder.fbb()).finish(); |
| 227 | // SAFETY: We're using the correct message type. |
| 228 | unsafe { builder.send(ping) }.expect("send should succeed"); |
| 229 | pending().await |
| 230 | }); |
| 231 | } |
| 232 | |
| 233 | GLOBAL_STATE.with(|g| { |
| 234 | let g = g.borrow(); |
| 235 | assert_eq!(0, g.watcher_count); |
| 236 | // TODO(Brian): Use an OnRun wrapper to defer setting this until it actually starts, |
| 237 | // then check it. |
| 238 | //assert_eq!(0, g.startup_count); |
| 239 | }); |
| 240 | event_loop_factory.run(); |
| 241 | GLOBAL_STATE.with(|g| { |
| 242 | let g = g.borrow(); |
| 243 | assert_eq!(1, g.watcher_count); |
| 244 | assert_eq!(1, g.startup_count); |
| 245 | }); |
| 246 | } |
| 247 | } |
| 248 | } |