From 6ecc7dc5578843440f242f805ac56f4e6d787623 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Mon, 17 Apr 2023 15:39:12 +0800 Subject: [PATCH] Add crate int-to-c-enum --- Cargo.lock | 16 +++ Cargo.toml | 1 + services/libs/int-to-c-enum/.gitignore | 2 + services/libs/int-to-c-enum/Cargo.toml | 13 +++ services/libs/int-to-c-enum/README.md | 35 ++++++ services/libs/int-to-c-enum/derive/Cargo.toml | 14 +++ services/libs/int-to-c-enum/derive/src/lib.rs | 106 ++++++++++++++++++ services/libs/int-to-c-enum/src/lib.rs | 47 ++++++++ .../libs/int-to-c-enum/tests/regression.rs | 22 ++++ 9 files changed, 256 insertions(+) create mode 100644 services/libs/int-to-c-enum/.gitignore create mode 100644 services/libs/int-to-c-enum/Cargo.toml create mode 100644 services/libs/int-to-c-enum/README.md create mode 100644 services/libs/int-to-c-enum/derive/Cargo.toml create mode 100644 services/libs/int-to-c-enum/derive/src/lib.rs create mode 100644 services/libs/int-to-c-enum/src/lib.rs create mode 100644 services/libs/int-to-c-enum/tests/regression.rs diff --git a/Cargo.lock b/Cargo.lock index b4e50c715..88e48ddbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -758,6 +758,22 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +[[package]] +name = "value-enum" +version = "0.1.0" +dependencies = [ + "value-enum-derive", +] + +[[package]] +name = "value-enum-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 143ce953a..66fb01821 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "services/libs/typeflags-util", "services/libs/jinux-util", "services/libs/cpio-decoder", + "services/libs/int-to-c-enum", ] exclude = ["services/libs/comp-sys/controlled", "services/libs/comp-sys/cargo-component"] diff --git a/services/libs/int-to-c-enum/.gitignore b/services/libs/int-to-c-enum/.gitignore new file mode 100644 index 000000000..4fffb2f89 --- /dev/null +++ b/services/libs/int-to-c-enum/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/services/libs/int-to-c-enum/Cargo.toml b/services/libs/int-to-c-enum/Cargo.toml new file mode 100644 index 000000000..09656d0c6 --- /dev/null +++ b/services/libs/int-to-c-enum/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "int-to-c-enum" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +int-to-c-enum-derive = { path = "derive", optional = true } + +[features] +default = ["derive"] +derive = ["int-to-c-enum-derive"] diff --git a/services/libs/int-to-c-enum/README.md b/services/libs/int-to-c-enum/README.md new file mode 100644 index 000000000..f79694d31 --- /dev/null +++ b/services/libs/int-to-c-enum/README.md @@ -0,0 +1,35 @@ +# TryFromInt - A convenient derive macro for converting an integer to an enum + +## Quick Start +To use this crate, first add this crate to your `Cargo.toml`. + +```toml +[dependencies] +int-to-c-enum = "0.1.0" +``` + +You can use this macro for a [C-like enum](https://doc.rust-lang.org/stable/rust-by-example/custom_types/enum/c_like.html). + +```rust +use int_to_c_enum::TryFromInt; +#[repr(u8)] +#[derive(TryFromInt, Debug)] +pub enum Color { + Red = 1, + Blue = 2, + Green = 3, +} +``` + +Then, you can use `try_from` function for this enum. +```rust +fn main() { + let color = Color::try_from(1).unwrap(); + println!("color = {color:?}"); // color = Red; +} +``` + +## Introduction +This crate provides a derive procedural macro named `TryFromInt`. This macro will automatically implement [TryFrom](https://doc.rust-lang.org/core/convert/trait.TryFrom.html) trait for enums that meet the following requirements: +1. The enum must have a primitive repr, i.e., the enum should have attribute like #[repr(u8)], #[repr(u32)], etc. The type parameter of TryFrom will be the repr, e.g., in the `QuickStart` example, the macro will implment `TryFrom` for `Color`. +2. The enum must consist solely of unit variants, which is called [units only enum](https://doc.rust-lang.org/reference/items/enumerations.html#unit-only-enum). Each field should have an **explicit discriminant**. diff --git a/services/libs/int-to-c-enum/derive/Cargo.toml b/services/libs/int-to-c-enum/derive/Cargo.toml new file mode 100644 index 000000000..c235a57aa --- /dev/null +++ b/services/libs/int-to-c-enum/derive/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "int-to-c-enum-derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0.15", features = ["parsing"] } diff --git a/services/libs/int-to-c-enum/derive/src/lib.rs b/services/libs/int-to-c-enum/derive/src/lib.rs new file mode 100644 index 000000000..317f7dfb3 --- /dev/null +++ b/services/libs/int-to-c-enum/derive/src/lib.rs @@ -0,0 +1,106 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote, TokenStreamExt}; +use syn::{parse_macro_input, Attribute, Data, DataEnum, DeriveInput, Generics}; + +const ALLOWED_REPRS: &[&str] = &[ + "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "usize", "isize", +]; +const VALUE_NAME: &str = "value"; +const REPR_PATH: &str = "repr"; + +#[proc_macro_derive(TryFromInt)] +pub fn derive_try_from_num(input_token: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input_token as DeriveInput); + expand_derive_try_from_num(input).into() +} + +fn expand_derive_try_from_num(input: DeriveInput) -> TokenStream { + let attrs = input.attrs; + let ident = input.ident; + let generics = input.generics; + if let Data::Enum(data_enum) = input.data { + impl_try_from(data_enum, attrs, generics, ident) + } else { + panic!("cannot derive TryFromInt for structs or union.") + } +} + +fn impl_try_from( + data_enum: DataEnum, + attrs: Vec, + generics: Generics, + ident: Ident, +) -> TokenStream { + let valid_repr = if let Some(valid_repr) = has_valid_repr(attrs) { + format_ident!("{}", valid_repr) + } else { + panic!( + "{} does not have invalid repr to implement TryFromInt.", + ident.to_string() + ); + }; + + for variant in &data_enum.variants { + if variant.discriminant.is_none() { + panic!("Enum can only have fields like Variant=1"); + } + } + + let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); + let fn_body = fn_body_tokens(VALUE_NAME, &data_enum, ident.clone()); + let param = format_ident!("{}", VALUE_NAME); + quote!( + #[automatically_derived] + impl #impl_generics ::core::convert::TryFrom<#valid_repr> #type_generics for #ident #where_clause { + type Error = ::int_to_c_enum::TryFromIntError; + fn try_from(#param: #valid_repr) -> ::core::result::Result { + #fn_body + } + } + ) +} + +fn fn_body_tokens(value_name: &str, data_enum: &DataEnum, ident: Ident) -> TokenStream { + let mut match_bodys = quote!(); + for variant in &data_enum.variants { + let (_, value) = variant + .discriminant + .as_ref() + .expect("Each field must be assigned a discriminant value explicitly"); + let vairant_ident = &variant.ident; + let statement = quote!(#value => ::core::result::Result::Ok(#ident::#vairant_ident),); + match_bodys.append_all(statement); + } + match_bodys.append_all(quote!(_ => core::result::Result::Err(::int_to_c_enum::TryFromIntError::InvalidValue),)); + let param = format_ident!("{}", value_name); + quote!(match #param { + #match_bodys + }) +} + +fn has_valid_repr(attrs: Vec) -> Option<&'static str> { + for attr in attrs { + if let Some(repr) = parse_repr(attr) { + return Some(repr); + } + } + None +} + +fn parse_repr(attr: Attribute) -> Option<&'static str> { + if !attr.path().is_ident(REPR_PATH) { + return None; + } + let mut repr = None; + attr.parse_nested_meta(|meta| { + for allowed_repr in ALLOWED_REPRS { + if meta.path.is_ident(*allowed_repr) { + repr = Some(*allowed_repr); + break; + } + } + Ok(()) + }) + .ok()?; + repr +} diff --git a/services/libs/int-to-c-enum/src/lib.rs b/services/libs/int-to-c-enum/src/lib.rs new file mode 100644 index 000000000..58f86f2a8 --- /dev/null +++ b/services/libs/int-to-c-enum/src/lib.rs @@ -0,0 +1,47 @@ +//! This crate provides a derive macro named TryFromInt. This macro can be used to automatically implement TryFrom trait +//! for [C-like enums](https://doc.rust-lang.org/stable/rust-by-example/custom_types/enum/c_like.html). +//! +//! Currently, this macro only supports enums with [explicit discriminants](https://doc.rust-lang.org/reference/items/enumerations.html#explicit-discriminants). +//! +//! Below is a simple example. We derive macro `TryFromInt` for an enum `Color`. +//! ```rust +//! use int_to_c_enum::TryFromInt; +//! #[repr(u8)] +//! #[derive(TryFromInt, Eq, PartialEq)] +//! pub enum Color { +//! Red = 1, +//! Yellow = 2, +//! Blue = 3, +//! } +//! // Then, we can use method `try_from` for `Color`. +//! let color = Color::try_from(1).unwrap(); +//! assert!(color == Color::Red); +//! ``` +//! +//! The `TryFromInt` macro will automatically implement trait `TryFrom` for `Color`. +//! After macro expansion, the generated code looks like as follows: +//! ```ignore +//! impl TryFrom for Color { +//! type Error = TryFromIntError; +//! fn try_from(value: u8) -> Result { +//! match value { +//! 1 => Ok(Color::Red), +//! 2 => Ok(Color::Yellow), +//! 3 => Ok(Color::Blue), +//! _ => Err(TryFromIntError::InvalidValue), +//! } +//! } +//! } +//! ``` +//! + +#![no_std] + +/// Error type for TryFromInt derive macro +#[derive(Debug, Clone, Copy)] +pub enum TryFromIntError { + InvalidValue, +} + +#[cfg(feature = "derive")] +pub use int_to_c_enum_derive::TryFromInt; \ No newline at end of file diff --git a/services/libs/int-to-c-enum/tests/regression.rs b/services/libs/int-to-c-enum/tests/regression.rs new file mode 100644 index 000000000..b3ede6712 --- /dev/null +++ b/services/libs/int-to-c-enum/tests/regression.rs @@ -0,0 +1,22 @@ +use int_to_c_enum::TryFromInt; + +#[derive(TryFromInt, Debug, PartialEq, Eq)] +#[repr(u8)] +enum Color { + Red = 1, + Blue = 2, + Green = 3, +} + +#[test] +fn conversion() { + let color = Color::try_from(1).unwrap(); + println!("color = {color:?}"); + assert!(color == Color::Red); +} + +#[test] +fn invalid_value() { + let color = Color::try_from(4); + assert!(color.is_err()); +}