Make C++ flags available in Rust.
In order to do this, we automagically grab all of C++ gflags and
add them into the Rust Clap command. We then pass through the flags
back to C++ to set them.
This requires different versions of clap to make both our implementation
and autocxx happy.
Change-Id: I36a9584de2ade55390f7109889996bad6e2cd071
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/aos/BUILD b/aos/BUILD
index 7c0ad3d..7724574 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -184,12 +184,29 @@
],
)
+cc_library(
+ name = "init_for_rust",
+ srcs = [
+ "init_for_rust.cc",
+ ],
+ hdrs = [
+ "init_for_rust.h",
+ ],
+ deps = [
+ ":for_rust",
+ ":init",
+ "//aos/logging",
+ "@com_github_gflags_gflags//:gflags",
+ "@crate_index//:cxx_cc",
+ ],
+)
+
autocxx_library(
name = "init_rs",
srcs = ["init.rs"],
crate_name = "aos_init",
libs = [
- ":init",
+ ":init_for_rust",
],
override_cc_toolchain = "@llvm_toolchain//:cc-clang-x86_64-linux",
target_compatible_with = select({
@@ -197,6 +214,19 @@
"//tools:has_msan": ["@platforms//:incompatible"],
}),
visibility = ["//visibility:public"],
+ deps = [
+ "@crate_index//:clap",
+ ],
+)
+
+rust_test(
+ name = "init_rs_test",
+ crate = ":init_rs",
+ rustc_flags = ["-Crelocation-model=static"],
+ target_compatible_with = select({
+ "//conditions:default": ["//tools/platforms/rust:has_support"],
+ "//tools:has_msan": ["@platforms//:incompatible"],
+ }),
)
autocxx_library(
diff --git a/aos/events/BUILD b/aos/events/BUILD
index 9910fc6..1a5d7b2 100644
--- a/aos/events/BUILD
+++ b/aos/events/BUILD
@@ -282,6 +282,7 @@
"//aos:flatbuffers_rs",
"//aos:init_rs",
"@com_github_google_flatbuffers//rust",
+ "@crate_index//:clap",
"@crate_index//:futures",
],
)
@@ -309,6 +310,7 @@
"//aos:flatbuffers_rs",
"//aos:init_rs",
"@com_github_google_flatbuffers//rust",
+ "@crate_index//:clap",
"@crate_index//:futures",
],
)
diff --git a/aos/events/ping.rs b/aos/events/ping.rs
index 3226461..d699d3c 100644
--- a/aos/events/ping.rs
+++ b/aos/events/ping.rs
@@ -1,6 +1,8 @@
use aos_configuration as config;
use aos_events_event_loop_runtime::{EventLoopRuntime, Sender, Watcher};
use aos_events_shm_event_loop::ShmEventLoop;
+use aos_init::WithCppFlags;
+use clap::{CommandFactory, Parser};
use core::cell::Cell;
use core::time::Duration;
use futures::never::Never;
@@ -9,12 +11,22 @@
use ping_rust_fbs::aos::examples as ping;
use pong_rust_fbs::aos::examples as pong;
+/// Ping portion of a ping/pong system.
+#[derive(Parser, Debug)]
+#[command(name = "ping")]
+struct App {
+ /// Time to sleep between pings.
+ #[arg(long, default_value_t = 10000, value_name = "MICROS")]
+ sleep: u64,
+}
+
fn main() {
+ let app = App::parse_with_cpp_flags();
aos_init::init();
let config = config::read_config_from(Path::new("pingpong_config.json")).unwrap();
let ping = PingTask::new();
ShmEventLoop::new(&config).run_with(|runtime| {
- runtime.spawn(ping.tasks(runtime));
+ runtime.spawn(ping.tasks(runtime, app.sleep));
});
}
@@ -31,15 +43,15 @@
}
/// Returns a future with all the tasks for the ping process
- pub async fn tasks(&self, event_loop: &EventLoopRuntime<'_>) -> Never {
- futures::join!(self.ping(event_loop), self.handle_pong(event_loop));
+ pub async fn tasks(&self, event_loop: &EventLoopRuntime<'_>, sleep: u64) -> Never {
+ futures::join!(self.ping(event_loop, sleep), self.handle_pong(event_loop));
unreachable!("Let's hope `never_type` gets stabilized soon :)");
}
- async fn ping(&self, event_loop: &EventLoopRuntime<'_>) -> Never {
+ async fn ping(&self, event_loop: &EventLoopRuntime<'_>, sleep: u64) -> Never {
// The sender is used to send messages back to the pong channel.
let mut ping_sender: Sender<ping::Ping> = event_loop.make_sender("/test").unwrap();
- let mut interval = event_loop.add_interval(Duration::from_secs(1));
+ let mut interval = event_loop.add_interval(Duration::from_micros(sleep));
event_loop.on_run().await;
loop {
diff --git a/aos/events/pong.rs b/aos/events/pong.rs
index d2859a8..01af56f 100644
--- a/aos/events/pong.rs
+++ b/aos/events/pong.rs
@@ -1,13 +1,21 @@
use aos_configuration as config;
use aos_events_event_loop_runtime::{EventLoopRuntime, Sender, Watcher};
use aos_events_shm_event_loop::ShmEventLoop;
+use aos_init::WithCppFlags;
+use clap::Parser;
use futures::never::Never;
use std::path::Path;
use ping_rust_fbs::aos::examples as ping;
use pong_rust_fbs::aos::examples as pong;
+/// Pong portion of a ping/pong system.
+#[derive(Parser, Debug)]
+#[command(name = "pong")]
+struct App {}
+
fn main() {
+ let _app = App::parse_with_cpp_flags();
aos_init::init();
let config = config::read_config_from(Path::new("pingpong_config.json")).unwrap();
ShmEventLoop::new(&config).run_with(|runtime| {
diff --git a/aos/init.cc b/aos/init.cc
index 3ca7314..579ad33 100644
--- a/aos/init.cc
+++ b/aos/init.cc
@@ -45,25 +45,6 @@
initialized = true;
}
-void InitFromRust(const char *argv0) {
- CHECK(!IsInitialized()) << "Only initialize once.";
-
- FLAGS_logtostderr = true;
-
- google::InitGoogleLogging(argv0);
-
- // TODO(Brian): Provide a way for Rust to configure C++ flags.
- char fake_argv0_val[] = "rust";
- char *fake_argv0 = fake_argv0_val;
- char **fake_argv = &fake_argv0;
- int fake_argc = 1;
- gflags::ParseCommandLineFlags(&fake_argc, &fake_argv, true);
-
- // TODO(Brian): Where should Rust binaries be configured to write coredumps?
-
- // TODO(Brian): Figure out what to do with allocator hooks for C++ and Rust.
-
- initialized = true;
-}
+void MarkInitialized() { initialized = true; }
} // namespace aos
diff --git a/aos/init.h b/aos/init.h
index 29480eb..b9270b3 100644
--- a/aos/init.h
+++ b/aos/init.h
@@ -10,10 +10,10 @@
// ShmEventLoop can confirm the world was initialized before running.
bool IsInitialized();
-// A special initialization function that initializes the C++ parts in a way
-// compatible with Rust. This requires careful coordination with `:init_rs`, do
-// not use it from anywhere else.
-void InitFromRust(const char *argv0);
+// Marks the system as initialized. This is only meant to be used from
+// init_for_rust. DO NOT call this from anywhere else, use InitGoogle
+// instead.
+void MarkInitialized();
} // namespace aos
diff --git a/aos/init.rs b/aos/init.rs
index 13066f1..3b4d876 100644
--- a/aos/init.rs
+++ b/aos/init.rs
@@ -1,20 +1,195 @@
-use std::{ffi::CString, sync::Once};
+use std::{
+ env,
+ ffi::{CString, OsStr, OsString},
+ os::unix::prelude::OsStrExt,
+ sync::Once,
+};
+
+use clap::{
+ error::{ContextKind, ContextValue},
+ Arg, ArgAction, Error, Parser,
+};
autocxx::include_cpp! (
-#include "aos/init.h"
+#include "aos/init_for_rust.h"
safety!(unsafe)
generate!("aos::InitFromRust")
+generate!("aos::GetCppFlags")
+generate!("aos::FlagInfo")
+generate!("aos::SetCommandLineOption")
+generate!("aos::GetCommandLineOption")
);
/// Initializes AOS.
pub fn init() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
- let argv0 = std::env::args().next().expect("must have argv[0]");
- let argv0 = CString::new(argv0).expect("argv[0] may not have NUL");
- // SAFETY: argv0 is a NUL-terminated string.
- unsafe { ffi::aos::InitFromRust(argv0.as_ptr()) };
+ let argv0 = std::env::args()
+ .map(|arg| CString::new(arg).expect("Arg may not have NUL"))
+ .next()
+ .expect("Missing argv[0]?");
+ // SAFETY: argv0 is a well-defined CString.
+ unsafe {
+ ffi::aos::InitFromRust(argv0.as_ptr());
+ }
});
}
+
+/// Trait used to append C++ gFlags to a clap CLI.
+pub trait WithCppFlags: Parser {
+ /// Parses the comannd line arguments while also setting the C++ gFlags.
+ fn parse_with_cpp_flags() -> Self {
+ Self::parse_with_cpp_flags_from(env::args_os())
+ }
+
+ /// Like [`parse_with_cpp_flags`] but read from an iterator.
+ fn parse_with_cpp_flags_from<I, T>(itr: I) -> Self
+ where
+ I: IntoIterator<Item = T>,
+ T: Into<OsString> + Clone,
+ {
+ let cxxflags = ffi::aos::GetCppFlags();
+ let cxxflags: Vec<CxxFlag> = cxxflags
+ .iter()
+ .map(|flag| CxxFlag::from(flag))
+ .filter(|flag| flag.name != "help" && flag.name != "version")
+ .collect();
+
+ let mut command = Self::command()
+ .next_help_heading("Flags from C++")
+ .args(cxxflags.iter().cloned());
+
+ let matches = command.clone().get_matches_from(itr);
+
+ for cxxflag in cxxflags {
+ let Some(mut value) = matches.get_raw(&cxxflag.name) else {
+ continue;
+ };
+ // We grab the last match as GFlags does.
+ let value = value.next_back().unwrap();
+ cxxflag.set(value).unwrap_or_else(|_| {
+ let mut error = Error::new(clap::error::ErrorKind::InvalidValue);
+
+ // Let user know how they messed up.
+ error.insert(
+ ContextKind::InvalidArg,
+ ContextValue::String(format!("--{}", cxxflag.name)),
+ );
+ error.insert(
+ ContextKind::InvalidValue,
+ ContextValue::String(
+ value
+ .to_owned()
+ .into_string()
+ .expect("Invalid UTF-8 String"),
+ ),
+ );
+ error.format(&mut command).exit()
+ })
+ }
+
+ match Self::from_arg_matches(&matches) {
+ Ok(flags) => flags,
+ Err(e) => e.format(&mut command).exit(),
+ }
+ }
+}
+
+impl<T: Parser> WithCppFlags for T {}
+
+#[derive(Clone)]
+#[allow(unused)]
+struct CxxFlag {
+ name: String,
+ ty: String,
+ description: String,
+ default_value: String,
+ filename: String,
+}
+
+struct SetFlagError;
+
+impl CxxFlag {
+ /// Sets the command gFlag to the specified value.
+ fn set(&self, value: &OsStr) -> Result<(), SetFlagError> {
+ unsafe {
+ let name = CString::new(self.name.clone()).expect("Flag name may not have NUL");
+ let value = CString::new(value.as_bytes()).expect("Arg may not have NUL");
+ if ffi::aos::SetCommandLineOption(name.as_ptr(), value.as_ptr()) {
+ Ok(())
+ } else {
+ Err(SetFlagError)
+ }
+ }
+ }
+
+ fn get_option(name: &str) -> String {
+ unsafe {
+ let name = CString::new(name).expect("Flag may not have NUL");
+ ffi::aos::GetCommandLineOption(name.as_ptr()).to_string()
+ }
+ }
+}
+
+impl From<&ffi::aos::FlagInfo> for CxxFlag {
+ fn from(value: &ffi::aos::FlagInfo) -> Self {
+ Self {
+ name: value.name().to_string(),
+ ty: value.ty().to_string(),
+ description: value.description().to_string(),
+ default_value: value.default_value().to_string(),
+ filename: value.filename().to_string(),
+ }
+ }
+}
+
+impl From<CxxFlag> for Arg {
+ fn from(value: CxxFlag) -> Self {
+ assert_ne!(&value.name, "help");
+ Arg::new(&value.name)
+ .long(&value.name)
+ .help(&value.description)
+ .default_value(&value.default_value)
+ .action(ArgAction::Set)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::Mutex;
+
+ use super::*;
+
+ #[derive(Parser)]
+ #[command()]
+ struct App {
+ #[arg(long)]
+ myarg: u64,
+ }
+
+ // We are sharing global state through gFlags. Use a mutex to prevent races.
+ static MUTEX: Mutex<()> = Mutex::new(());
+
+ #[test]
+ fn simple_rust() {
+ let _guard = MUTEX.lock();
+ let app = App::parse_with_cpp_flags_from(&["mytest", "--myarg", "23"]);
+ assert_eq!(app.myarg, 23);
+ }
+
+ #[test]
+ fn set_cxx_flag() {
+ let _guard = MUTEX.lock();
+ let app = App::parse_with_cpp_flags_from(&[
+ "mytest",
+ "--alsologtostderr",
+ "true",
+ "--myarg",
+ "23",
+ ]);
+ assert_eq!(app.myarg, 23);
+ assert_eq!(CxxFlag::get_option("alsologtostderr"), "true");
+ }
+}
diff --git a/aos/init_for_rust.cc b/aos/init_for_rust.cc
new file mode 100644
index 0000000..9af4d83
--- /dev/null
+++ b/aos/init_for_rust.cc
@@ -0,0 +1,49 @@
+#include "aos/init_for_rust.h"
+
+#include "gflags/gflags.h"
+#include "glog/logging.h"
+
+#include "aos/init.h"
+
+namespace aos {
+
+void InitFromRust(const char *argv0) {
+ CHECK(!IsInitialized()) << "Only initialize once.";
+
+ google::InitGoogleLogging(argv0);
+
+ // TODO(Brian): Where should Rust binaries be configured to write coredumps?
+
+ // TODO(Brian): Figure out what to do with allocator hooks for C++ and Rust.
+
+ MarkInitialized();
+}
+
+std::vector<FlagInfo> GetCppFlags() {
+ std::vector<gflags::CommandLineFlagInfo> info;
+ gflags::GetAllFlags(&info);
+ std::vector<FlagInfo> out;
+ for (const auto &flag : info) {
+ FlagInfo out_flag = {
+ .name_ = flag.name,
+ .type_ = flag.type,
+ .description_ = flag.description,
+ .default_value_ = flag.default_value,
+ .filename_ = flag.filename,
+ };
+ out.push_back(out_flag);
+ }
+ return out;
+}
+
+bool SetCommandLineOption(const char *name, const char *value) {
+ return !gflags::SetCommandLineOption(name, value).empty();
+}
+
+std::string GetCommandLineOption(const char *name) {
+ std::string out;
+ CHECK(gflags::GetCommandLineOption(name, &out)) << "Unknown flag " << name;
+ return out;
+}
+
+} // namespace aos
diff --git a/aos/init_for_rust.h b/aos/init_for_rust.h
new file mode 100644
index 0000000..86bfd66
--- /dev/null
+++ b/aos/init_for_rust.h
@@ -0,0 +1,47 @@
+#ifndef AOS_INIT_FOR_RUST_H_
+#define AOS_INIT_FOR_RUST_H_
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "aos/for_rust.h"
+#include "cxx.h"
+
+namespace aos {
+
+struct FlagInfo {
+ std::string name_;
+ std::string type_;
+ std::string description_;
+ std::string default_value_;
+ std::string filename_;
+
+ rust::Str name() const {
+ return StringViewToRustStr(std::string_view(name_));
+ }
+ rust::Str ty() const { return StringViewToRustStr(std::string_view(type_)); }
+ rust::Str description() const {
+ return StringViewToRustStr(std::string_view(description_));
+ }
+ rust::Str default_value() const {
+ return StringViewToRustStr(std::string_view(default_value_));
+ }
+ rust::Str filename() const {
+ return StringViewToRustStr(std::string_view(filename_));
+ }
+};
+
+// A special initialization function that initializes the C++ parts in a way
+// compatible with Rust. This requires careful coordination with `:init_rs`, do
+// not use it from anywhere else.
+void InitFromRust(const char *argv0);
+
+std::vector<FlagInfo> GetCppFlags();
+
+bool SetCommandLineOption(const char *name, const char *value);
+std::string GetCommandLineOption(const char *name);
+
+} // namespace aos
+
+#endif // AOS_INIT_FOR_RUST_H_