Base opentitantool application. 1. Establish the basic `opentitantool` framework. 2. Create a procedural macro for deriving `CommandDispatch` for interior nodes in the command hierarchy. 3. Create some sample `hello` commands in `hello.rs`. Signed-off-by: Chris Frantz <cfrantz@google.com>
diff --git a/sw/host/opentitanlib/Cargo.toml b/sw/host/opentitanlib/Cargo.toml index 3309988..85b7870 100644 --- a/sw/host/opentitanlib/Cargo.toml +++ b/sw/host/opentitanlib/Cargo.toml
@@ -15,3 +15,6 @@ nix = "0.17.0" bitflags = "1.0" log = "0.4" + +erased-serde = "0.3.12" +opentitantool_derive = {path = "opentitantool_derive"}
diff --git a/sw/host/opentitanlib/opentitantool_derive/Cargo.toml b/sw/host/opentitanlib/opentitantool_derive/Cargo.toml new file mode 100644 index 0000000..39f5f9d --- /dev/null +++ b/sw/host/opentitanlib/opentitantool_derive/Cargo.toml
@@ -0,0 +1,18 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "opentitantool_derive" +version = "0.1.0" +authors = ["lowRISC contributors"] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +proc-macro-error = "1.0" +quote = "1.0" +syn = "1.0"
diff --git a/sw/host/opentitanlib/opentitantool_derive/src/lib.rs b/sw/host/opentitanlib/opentitantool_derive/src/lib.rs new file mode 100644 index 0000000..3c000c6 --- /dev/null +++ b/sw/host/opentitanlib/opentitantool_derive/src/lib.rs
@@ -0,0 +1,105 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use proc_macro2::TokenStream; +use proc_macro_error::{abort, proc_macro_error}; +use quote::quote; +use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident, Variant}; + +/// Derives the `CommandDispatch` trait for a NewType enum. +/// +/// Derive this on purely interior nodes in the opentitantool command hierarchy +/// (that is: nodes whose only purpose is to dispatch down to the next layer +/// in the command heirarchy). The automatic derivation simply calls the +/// `run` method on the next layer in the command hierarchy. +/// +/// Imagine that you have structs `World` and `People` which implement +/// "Hello World" and "Hello People" commands. You would create Newtype +/// variants inside of a `Hello` enum and derive `CommandDispatch` to +/// generate the dispatch layer for this interior node in the command hierarchy: +/// +/// ``` +/// #[derive(StructOpt, CommandDispatch)] +/// pub enum Hello { +/// World(World), +/// People(People), +/// } +/// +/// // The derived code is as follows: +/// impl opentitanlib::app::command::CommandDispatch for Hello { +/// fn run( +/// &self, +/// backend: &mut dyn opentitanlib::transport::Transport +/// ) -> anyhow::Result<Option<Box<dyn erased_serde::Serialize>>> { +/// match self { +/// Hello::World(ref __field) => __field.run(backend), +/// Hello::People(ref __field) => __field.run(backend), +/// } +/// } +/// } +/// ``` +#[proc_macro_derive(CommandDispatch)] +#[proc_macro_error] +pub fn derive_command_dispatch(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + match &input.data { + Data::Enum(e) => dispatch_enum(&input.ident, e).into(), + _ => abort!( + input.ident.span(), + "CommandDispatch is only implemented for enums" + ), + } +} + +fn dispatch_enum(name: &Ident, e: &DataEnum) -> TokenStream { + let arms = e + .variants + .iter() + .map(|variant| dispatch_variant(name, variant)) + .collect::<Vec<_>>(); + + // We wrap the derived code inside an anonymous const block to give the + // `extern crate` references a local namespace that wont pollute the + // global namespace. + quote! { + const _: () = { + extern crate opentitanlib; + extern crate anyhow; + extern crate erased_serde; + + impl opentitanlib::app::command::CommandDispatch for #name { + fn run( + &self, + backend: &mut dyn opentitanlib::transport::Transport + ) -> anyhow::Result<Option<Box<dyn erased_serde::Serialize>>> { + match self { + #(#arms),* + } + } + } + }; + } +} + +fn dispatch_variant(name: &Ident, variant: &Variant) -> TokenStream { + let ident = &variant.ident; + let unnamed_len = match &variant.fields { + Fields::Unnamed(u) => u.unnamed.len(), + _ => abort!( + variant.ident.span(), + "CommandDispatch is only implemented for Newtype variants" + ), + }; + if unnamed_len != 1 { + abort!( + variant.ident.span(), + "CommandDispatch is only implemented for Newtype variants" + ); + } + quote! { + #name::#ident(ref __field) => + opentitanlib::app::command::CommandDispatch::run(__field, backend) + } +}
diff --git a/sw/host/opentitanlib/src/app/command.rs b/sw/host/opentitanlib/src/app/command.rs new file mode 100644 index 0000000..b275cce --- /dev/null +++ b/sw/host/opentitanlib/src/app/command.rs
@@ -0,0 +1,16 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use crate::transport::Transport; +use anyhow::Result; +use erased_serde::Serialize; +pub use opentitantool_derive::*; + +/// The `CommandDispatch` trait should be implemented for all leaf structures +/// in the application's command hierarchy. It can be automatically derived +/// on internal nodes in the heirarchy. See the documentation for +/// [`opentitantool_derive`]. +pub trait CommandDispatch { + fn run(&self, transport: &mut dyn Transport) -> Result<Option<Box<dyn Serialize>>>; +}
diff --git a/sw/host/opentitanlib/src/app/mod.rs b/sw/host/opentitanlib/src/app/mod.rs new file mode 100644 index 0000000..a371aaa --- /dev/null +++ b/sw/host/opentitanlib/src/app/mod.rs
@@ -0,0 +1,6 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +//! Useful modules for OpenTitanTool application development. +pub mod command;
diff --git a/sw/host/opentitanlib/src/lib.rs b/sw/host/opentitanlib/src/lib.rs index 3825007..ab50890 100644 --- a/sw/host/opentitanlib/src/lib.rs +++ b/sw/host/opentitanlib/src/lib.rs
@@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 +pub mod app; pub mod io; pub mod transport; pub mod util;
diff --git a/sw/host/opentitantool/Cargo.toml b/sw/host/opentitantool/Cargo.toml new file mode 100644 index 0000000..7345c24 --- /dev/null +++ b/sw/host/opentitantool/Cargo.toml
@@ -0,0 +1,24 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "opentitantool" +version = "0.1.0" +authors = ["lowRISC contributors"] +edition = "2018" + +[dependencies] +anyhow = "1.0" +thiserror = "1.0" +opentitanlib = {path="../opentitanlib"} +structopt = "0.3" +log = "0.4" +env_logger = "0.8.3" +raw_tty = "0.1.0" +regex = "1" +nix = "0.17.0" + +serde = {version="1", features=["serde_derive"]} +serde_json = "1" +erased-serde = "0.3.12"
diff --git a/sw/host/opentitantool/src/backend/mod.rs b/sw/host/opentitantool/src/backend/mod.rs new file mode 100644 index 0000000..544be3c --- /dev/null +++ b/sw/host/opentitantool/src/backend/mod.rs
@@ -0,0 +1,29 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Result; +use structopt::StructOpt; +use thiserror::Error; + +use opentitanlib::transport::{EmptyTransport, Transport}; + +#[derive(Debug, StructOpt)] +pub struct BackendOpts { + #[structopt(long, default_value)] + interface: String, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("unknown interface {0}")] + UnknownInterface(String), +} + +/// Creates the requested backend interface according to [`BackendOpts`]. +pub fn create(args: &BackendOpts) -> Result<Box<dyn Transport>> { + match args.interface.as_str() { + "" => Ok(Box::new(EmptyTransport)), + _ => Err(Error::UnknownInterface(args.interface.clone()).into()), + } +}
diff --git a/sw/host/opentitantool/src/command/hello.rs b/sw/host/opentitantool/src/command/hello.rs new file mode 100644 index 0000000..4df9860 --- /dev/null +++ b/sw/host/opentitantool/src/command/hello.rs
@@ -0,0 +1,92 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +//! Sample command heirarchy. +//! +//! This module implements a sample command heirarchy to demonstrate +//! the command framework for opentitantool. + +use anyhow::Result; +use erased_serde::Serialize; +use structopt::StructOpt; + +use opentitanlib::app::command::CommandDispatch; +use opentitanlib::transport::Transport; + +#[derive(Debug, StructOpt)] +/// The `hello world` command accepts an command option of `--cruel`. +pub struct HelloWorld { + #[structopt(short, long)] + cruel: bool, +} + +#[derive(serde::Serialize)] +/// The [`HelloMessage`] is the result of the `hello world` command. +pub struct HelloMessage { + pub greeting: String, +} + +impl CommandDispatch for HelloWorld { + fn run(&self, _transport: &mut dyn Transport) -> Result<Option<Box<dyn Serialize>>> { + // Is the world cruel or not? + let msg = if self.cruel { + "Hello cruel World!" + } else { + "Hello World!" + }; + Ok(Some(Box::new(HelloMessage { + greeting: msg.to_owned(), + }))) + } +} + +#[derive(Debug, StructOpt)] +/// The `hello people` command takes no additional switches or arguments. +pub struct HelloPeople { + // Unit structs not allowed by StructOpt +} + +impl CommandDispatch for HelloPeople { + fn run(&self, _transport: &mut dyn Transport) -> Result<Option<Box<dyn Serialize>>> { + // The `hello people` command produces no result. + Ok(None) + } +} + +#[derive(Debug, StructOpt, CommandDispatch)] +/// There are two types of `hello` subcommand; this enum binds them together +/// so they can both be under the `hello` command. This enum also derives +/// `CommandDispatch` which automatically builds a `run` method for this +/// type which dispatches to the subcommands. +pub enum HelloTypes { + World(HelloWorld), + People(HelloPeople), +} + +#[derive(Debug, StructOpt)] +/// The `goodbye` command takes no options or arguments. +pub struct GoodbyeCommand {} + +#[derive(serde::Serialize)] +/// The [`GoodbyeMessage`] is the result of the `goodbye` command. +pub struct GoodbyeMessage { + pub message: String, +} + +impl CommandDispatch for GoodbyeCommand { + fn run(&self, _transport: &mut dyn Transport) -> Result<Option<Box<dyn Serialize>>> { + Ok(Some(Box::new(GoodbyeMessage { + message: "Goodbye!".to_owned(), + }))) + } +} + +#[derive(Debug, StructOpt, CommandDispatch)] +/// The [`Greetings`] enum binds the `hello` and `goodbye` command hirarchies +/// in this module together into a single type which can be included into +/// the root of the command heirarchy in `main.rs`. +pub enum Greetings { + Hello(HelloTypes), + Goodbye(GoodbyeCommand), +}
diff --git a/sw/host/opentitantool/src/command/mod.rs b/sw/host/opentitantool/src/command/mod.rs new file mode 100644 index 0000000..950ba35 --- /dev/null +++ b/sw/host/opentitantool/src/command/mod.rs
@@ -0,0 +1,5 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +pub mod hello;
diff --git a/sw/host/opentitantool/src/main.rs b/sw/host/opentitantool/src/main.rs new file mode 100644 index 0000000..3dbbb2b --- /dev/null +++ b/sw/host/opentitantool/src/main.rs
@@ -0,0 +1,44 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Result; +use log::LevelFilter; +use structopt::StructOpt; + +mod backend; +mod command; +use opentitanlib::app::command::CommandDispatch; + +#[derive(Debug, StructOpt, CommandDispatch)] +enum RootCommandHierarchy { + // Flattened because `Greetings` is a subcommand hierarchy. + #[structopt(flatten)] + Greetings(command::hello::Greetings), +} + +#[derive(Debug, StructOpt)] +struct Opts { + #[structopt(long, default_value = "off")] + logging: LevelFilter, + + #[structopt(flatten)] + backend_opts: backend::BackendOpts, + + #[structopt(subcommand)] + command: RootCommandHierarchy, +} + +fn main() -> Result<()> { + let opts = Opts::from_args(); + env_logger::Builder::from_default_env() + .filter(None, opts.logging) + .init(); + + let mut interface = backend::create(&opts.backend_opts)?; + + if let Some(value) = opts.command.run(&mut *interface)? { + println!("{}", serde_json::to_string_pretty(&value)?); + } + Ok(()) +}