blob: fe0408dfb0b5962b895717638fe501451ef2ca00 [file] [log] [blame]
Adam Snaider9121b302023-12-14 15:30:54 -08001//! AOS Initialization
2//!
3//! This module "links" the C++ library and the Rust application together.
4//! In particular it provides the [`Init`] trait which is implemented for
5//! any struct that implements [`clap::Parser`]. The reason for this is that
6//! an important part of initializing the C++ library involves setting the
7//! gFlags which get resolved dynamically thanks to their reflection API.
8//!
9//! # Examples
10//!
11//! ```no_run
12//! use aos_init::Init;
13//! use clap::Parser;
14//!
15//! #[derive(Parser, Debug)]
16//! struct App {
17//! /// Time to sleep between pings.
18//! #[arg(long, default_value_t = 10000, value_name = "MICROS")]
19//! sleep: u64,
20//! }
21//!
22//! fn main() {
23//! // Initializes AOS and returns the App struct with the parsed CLI flags
24//! let app: App = App::init();
25//! // At this point your flags are parsed and AOS is initialized.
26//! }
27//! ```
28//! You can also use [`DefaultApp`] to avoid having to specify your own CLI options if you don't
29//! need them. For example:
30//!
31//! ```no_run
32//! use aos_init::{DefaultApp, Init};
33//! use clap::Parser;
34//!
35//! fn main() {
36//! // Initializes AOS. DefaultApp doesn't have any flags to parse.
37//! let _ = DefaultApp::init();
38//! // At this point AOS is initialized and you can create event loop.
39//! }
40//!```
41
Adam Snaider48a62f32023-10-02 15:49:23 -070042use std::{
43 env,
44 ffi::{CString, OsStr, OsString},
45 os::unix::prelude::OsStrExt,
46 sync::Once,
47};
48
49use clap::{
50 error::{ContextKind, ContextValue},
51 Arg, ArgAction, Error, Parser,
52};
Brian Silvermane4c79ce2022-08-15 05:57:28 -070053
54autocxx::include_cpp! (
Adam Snaider48a62f32023-10-02 15:49:23 -070055#include "aos/init_for_rust.h"
Brian Silvermane4c79ce2022-08-15 05:57:28 -070056
57safety!(unsafe)
58
59generate!("aos::InitFromRust")
Adam Snaider48a62f32023-10-02 15:49:23 -070060generate!("aos::GetCppFlags")
61generate!("aos::FlagInfo")
62generate!("aos::SetCommandLineOption")
63generate!("aos::GetCommandLineOption")
Brian Silvermane4c79ce2022-08-15 05:57:28 -070064);
65
Adam Snaider9121b302023-12-14 15:30:54 -080066// Intended to be used only from here and test_init. Don't use it anywhere else please.
67#[doc(hidden)]
68pub mod internal {
69 use super::*;
70 /// Generic initialization for production and tests.
71 ///
72 /// Sets up the C++ side of things. Notably, it doesn't setup the command line flags.
73 pub fn init() {
74 static ONCE: Once = Once::new();
75 ONCE.call_once(|| {
76 // We leak the `CString` with `into_raw`. It's not sound for C++ to free
77 // it but `InitGoogleLogging` requries that it is long-lived.
78 let argv0 = std::env::args()
79 .map(|arg| CString::new(arg).expect("Arg may not have NUL"))
80 .next()
81 .expect("Missing argv[0]?")
82 .into_raw();
83 // SAFETY: argv0 is a well-defined CString.
84 unsafe {
85 ffi::aos::InitFromRust(argv0);
86 }
87 });
88 }
Brian Silvermane4c79ce2022-08-15 05:57:28 -070089}
Adam Snaider48a62f32023-10-02 15:49:23 -070090
Adam Snaider9121b302023-12-14 15:30:54 -080091/// An application that doesn't need custom command line flags.
92///
93/// If you need your own command line flags, use any struct that derives [`clap::Parser`] instead.
94#[derive(Parser, Debug)]
95pub struct DefaultApp {}
96
Adam Snaider48a62f32023-10-02 15:49:23 -070097/// Trait used to append C++ gFlags to a clap CLI.
Adam Snaider9121b302023-12-14 15:30:54 -080098pub trait Init: Parser {
99 /// Initializes an AOS application.
100 ///
101 /// Parses the command line flags and runs the initialization logic.
102 fn init() -> Self {
103 let this = Self::parse_with_cpp_flags();
104 // Rust logs to stderr by default. Make that true for C++ as that will be easier than
105 // managing one or multiple files across FFI. We can pipe the stderr to a file to get
106 // a log file if we want.
107 CxxFlag::set_option("logtostderr", "true".as_ref())
108 .expect("Error setting C++ flag: logtostderr");
109 internal::init();
110 // Non-test initialization below
111 env_logger::init();
112 this
113 }
114
Adam Snaider48a62f32023-10-02 15:49:23 -0700115 /// Parses the comannd line arguments while also setting the C++ gFlags.
116 fn parse_with_cpp_flags() -> Self {
117 Self::parse_with_cpp_flags_from(env::args_os())
118 }
119
Adam Snaider9121b302023-12-14 15:30:54 -0800120 /// Like [`Init::parse_with_cpp_flags`] but read from an iterator.
Adam Snaider48a62f32023-10-02 15:49:23 -0700121 fn parse_with_cpp_flags_from<I, T>(itr: I) -> Self
122 where
123 I: IntoIterator<Item = T>,
124 T: Into<OsString> + Clone,
125 {
126 let cxxflags = ffi::aos::GetCppFlags();
127 let cxxflags: Vec<CxxFlag> = cxxflags
128 .iter()
129 .map(|flag| CxxFlag::from(flag))
130 .filter(|flag| flag.name != "help" && flag.name != "version")
131 .collect();
132
133 let mut command = Self::command()
134 .next_help_heading("Flags from C++")
135 .args(cxxflags.iter().cloned());
136
137 let matches = command.clone().get_matches_from(itr);
138
139 for cxxflag in cxxflags {
140 let Some(mut value) = matches.get_raw(&cxxflag.name) else {
141 continue;
142 };
143 // We grab the last match as GFlags does.
144 let value = value.next_back().unwrap();
145 cxxflag.set(value).unwrap_or_else(|_| {
146 let mut error = Error::new(clap::error::ErrorKind::InvalidValue);
147
148 // Let user know how they messed up.
149 error.insert(
150 ContextKind::InvalidArg,
151 ContextValue::String(format!("--{}", cxxflag.name)),
152 );
153 error.insert(
154 ContextKind::InvalidValue,
155 ContextValue::String(
156 value
157 .to_owned()
158 .into_string()
159 .expect("Invalid UTF-8 String"),
160 ),
161 );
162 error.format(&mut command).exit()
163 })
164 }
165
166 match Self::from_arg_matches(&matches) {
167 Ok(flags) => flags,
168 Err(e) => e.format(&mut command).exit(),
169 }
170 }
171}
172
Adam Snaider9121b302023-12-14 15:30:54 -0800173impl<T: Parser> Init for T {}
Adam Snaider48a62f32023-10-02 15:49:23 -0700174
175#[derive(Clone)]
176#[allow(unused)]
177struct CxxFlag {
178 name: String,
179 ty: String,
180 description: String,
181 default_value: String,
182 filename: String,
183}
184
Adam Snaider9121b302023-12-14 15:30:54 -0800185#[derive(Debug)]
Adam Snaider48a62f32023-10-02 15:49:23 -0700186struct SetFlagError;
187
188impl CxxFlag {
189 /// Sets the command gFlag to the specified value.
190 fn set(&self, value: &OsStr) -> Result<(), SetFlagError> {
Adam Snaider9121b302023-12-14 15:30:54 -0800191 Self::set_option(&self.name, value)
192 }
193
194 /// Sets the command gFlag to the specified value.
195 fn set_option(name: &str, value: &OsStr) -> Result<(), SetFlagError> {
Adam Snaider48a62f32023-10-02 15:49:23 -0700196 unsafe {
Adam Snaider9121b302023-12-14 15:30:54 -0800197 let name = CString::new(name.clone()).expect("Flag name may not have NUL");
Adam Snaider48a62f32023-10-02 15:49:23 -0700198 let value = CString::new(value.as_bytes()).expect("Arg may not have NUL");
199 if ffi::aos::SetCommandLineOption(name.as_ptr(), value.as_ptr()) {
200 Ok(())
201 } else {
202 Err(SetFlagError)
203 }
204 }
205 }
206
Adam Snaiderb40b72f2023-11-02 19:40:55 -0700207 #[allow(dead_code)]
Adam Snaider48a62f32023-10-02 15:49:23 -0700208 fn get_option(name: &str) -> String {
209 unsafe {
210 let name = CString::new(name).expect("Flag may not have NUL");
211 ffi::aos::GetCommandLineOption(name.as_ptr()).to_string()
212 }
213 }
214}
215
216impl From<&ffi::aos::FlagInfo> for CxxFlag {
217 fn from(value: &ffi::aos::FlagInfo) -> Self {
218 Self {
219 name: value.name().to_string(),
220 ty: value.ty().to_string(),
221 description: value.description().to_string(),
222 default_value: value.default_value().to_string(),
223 filename: value.filename().to_string(),
224 }
225 }
226}
227
228impl From<CxxFlag> for Arg {
229 fn from(value: CxxFlag) -> Self {
230 assert_ne!(&value.name, "help");
231 Arg::new(&value.name)
232 .long(&value.name)
233 .help(&value.description)
234 .default_value(&value.default_value)
235 .action(ArgAction::Set)
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use std::sync::Mutex;
242
243 use super::*;
244
245 #[derive(Parser)]
246 #[command()]
247 struct App {
248 #[arg(long)]
249 myarg: u64,
250 }
251
252 // We are sharing global state through gFlags. Use a mutex to prevent races.
253 static MUTEX: Mutex<()> = Mutex::new(());
254
255 #[test]
256 fn simple_rust() {
257 let _guard = MUTEX.lock();
258 let app = App::parse_with_cpp_flags_from(&["mytest", "--myarg", "23"]);
259 assert_eq!(app.myarg, 23);
260 }
261
262 #[test]
263 fn set_cxx_flag() {
264 let _guard = MUTEX.lock();
265 let app = App::parse_with_cpp_flags_from(&[
266 "mytest",
267 "--alsologtostderr",
268 "true",
269 "--myarg",
270 "23",
271 ]);
272 assert_eq!(app.myarg, 23);
273 assert_eq!(CxxFlag::get_option("alsologtostderr"), "true");
274 }
275}