Repository: rust-lang/rust-bindgen (rust)

Issue:
CLI support for custom derives
I'd like to continue the discussion started here: https://github.com/rust-lang/rust-bindgen/pull/2059#issuecomment-1012880503

Support for adding custom derives exists in the Builder API. I would like to support this in the CLI as well.

I believe that the CLI syntax would need to accept a regex, and not just apply the custom derive to all types. The derive support that is built-in (Copy, Debug, Default, etc) benefits from knowing when they can be applied to a type, based on its fields. Custom derives do not have access to this knowledge, as their requirements are unknown to bindgen.

Therefore the user of the CLI must supply a regex to determine which types get the custom derive.

I was thinking something like: `--with-custom-derive zerocopy::AsBytes,zerocopy::FromBytes/Foo.*` where the `/` separator can be anything else we decide on, it just can't be any valid symbol in a type (including `:`) or a special shell character (`;|`).

The regex is useful because it also captures anonymous nested structs, whose names are generated from the outer struct.

Example:
```
struct Foo {
  struct {
    int a;
  } nested;
};
```

generates both `Foo` and `Foo__bindgen_ty_1`. The regex `Foo.*` would capture both types.

Pull request description:
Implement cli option for custom derive
closes #2170

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e70b1187..9850539f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -156,13 +156,18 @@
 ## Changed
  * Fixed name collisions when having a C `enum` and a `typedef` with the same
    name.
- * The `ParseCallbacks::generated_name_override` now receives `ItemInfo<'_>` as
+ * The `ParseCallbacks::generated_name_override` method now receives `ItemInfo<'_>` as
    argument instead of a `&str`.
  * Updated the `clang-sys` crate version to 1.4.0 to support clang 15.
  * The return type is now ommited in signatures of functions returning `void`.
  * Updated the `clap` dependency for `bindgen-cli` to 4.
  * Rewrote the `bindgen-cli` argument parser which could introduce unexpected
    behavior changes. 
+ * The `ParseCallbacks::add_derives` method now receives `DeriveInfo<'_>` as
+   argument instead of a `&str`. This type also includes the kind of target type.
+ * Added a new set of flags `--with-derive-custom`,
+   `--with-derive-custom-struct`, `--with-derive-custom-enum` and
+   `--with-derive-custom-enum` to add custom derives from the CLI.
 
 ## Removed
 
diff --git a/bindgen-cli/Cargo.toml b/bindgen-cli/Cargo.toml
index 76099eb0..b900d940 100644
--- a/bindgen-cli/Cargo.toml
+++ b/bindgen-cli/Cargo.toml
@@ -21,7 +21,7 @@ path = "main.rs"
 name = "bindgen"
 
 [dependencies]
-bindgen = { path = "../bindgen", version = "=0.63.0" }
+bindgen = { path = "../bindgen", version = "=0.63.0", features = ["cli"] }
 shlex = "1"
 clap = { version = "4", features = ["derive"] }
 env_logger = { version = "0.9.0", optional = true }
diff --git a/bindgen-cli/options.rs b/bindgen-cli/options.rs
index c536ed62..2b09d85f 100644
--- a/bindgen-cli/options.rs
+++ b/bindgen-cli/options.rs
@@ -1,6 +1,7 @@
+use bindgen::callbacks::TypeKind;
 use bindgen::{
     builder, AliasVariation, Builder, CodegenConfig, EnumVariation,
-    MacroTypeVariation, NonCopyUnionStyle, RustTarget,
+    MacroTypeVariation, NonCopyUnionStyle, RegexSet, RustTarget,
     DEFAULT_ANON_FIELDS_PREFIX, RUST_TARGET_STRINGS,
 };
 use clap::Parser;
@@ -340,6 +341,18 @@ struct BindgenCommand {
     /// Wrap unsafe operations in unsafe blocks.
     #[arg(long)]
     wrap_unsafe_ops: bool,
+    /// Derive custom traits on any kind of type. The <CUSTOM> value must be of the shape <REGEX>=<DERIVE> where <DERIVE> is a coma-separated list of derive macros.
+    #[arg(long, value_name = "CUSTOM")]
+    with_derive_custom: Vec<String>,
+    /// Derive custom traits on a `struct`. The <CUSTOM> value must be of the shape <REGEX>=<DERIVE> where <DERIVE> is a coma-separated list of derive macros.
+    #[arg(long, value_name = "CUSTOM")]
+    with_derive_custom_struct: Vec<String>,
+    /// Derive custom traits on an `enum. The <CUSTOM> value must be of the shape <REGEX>=<DERIVE> where <DERIVE> is a coma-separated list of derive macros.
+    #[arg(long, value_name = "CUSTOM")]
+    with_derive_custom_enum: Vec<String>,
+    /// Derive custom traits on a `union`. The <CUSTOM> value must be of the shape <REGEX>=<DERIVE> where <DERIVE> is a coma-separated list of derive macros.
+    #[arg(long, value_name = "CUSTOM")]
+    with_derive_custom_union: Vec<String>,
     /// Prints the version, and exits
     #[arg(short = 'V', long)]
     version: bool,
@@ -456,6 +469,10 @@ where
         merge_extern_blocks,
         override_abi,
         wrap_unsafe_ops,
+        with_derive_custom,
+        with_derive_custom_struct,
+        with_derive_custom_enum,
+        with_derive_custom_union,
         version,
         clang_args,
     } = command;
@@ -894,5 +911,72 @@ where
         builder = builder.wrap_unsafe_ops(true);
     }
 
+    #[derive(Debug)]
+    struct CustomDeriveCallback {
+        derives: Vec<String>,
+        kind: Option<TypeKind>,
+        regex_set: bindgen::RegexSet,
+    }
+
+    impl bindgen::callbacks::ParseCallbacks for CustomDeriveCallback {
+        fn cli_args(&self) -> Vec<String> {
+            let mut args = vec![];
+
+            let flag = match &self.kind {
+                None => "--with-derive-custom",
+                Some(TypeKind::Struct) => "--with-derive-custom-struct",
+                Some(TypeKind::Enum) => "--with-derive-custom-enum",
+                Some(TypeKind::Union) => "--with-derive-custom-union",
+            };
+
+            let derives = self.derives.join(",");
+
+            for item in self.regex_set.get_items() {
+                args.extend_from_slice(&[
+                    flag.to_owned(),
+                    format!("{}={}", item, derives),
+                ]);
+            }
+
+            args
+        }
+
+        fn add_derives(
+            &self,
+            info: &bindgen::callbacks::DeriveInfo<'_>,
+        ) -> Vec<String> {
+            if self.kind.map(|kind| kind == info.kind).unwrap_or(true) &&
+                self.regex_set.matches(info.name)
+            {
+                return self.derives.clone();
+            }
+            vec![]
+        }
+    }
+
+    for (custom_derives, kind) in [
+        (with_derive_custom, None),
+        (with_derive_custom_struct, Some(TypeKind::Struct)),
+        (with_derive_custom_enum, Some(TypeKind::Enum)),
+        (with_derive_custom_union, Some(TypeKind::Union)),
+    ] {
+        for custom_derive in custom_derives {
+            let (regex, derives) = custom_derive
+                .rsplit_once('=')
+                .expect("Invalid custom derive argument: Missing `=`");
+            let derives = derives.split(',').map(|s| s.to_owned()).collect();
+
+            let mut regex_set = RegexSet::new();
+            regex_set.insert(regex);
+            regex_set.build(false);
+
+            builder = builder.parse_callbacks(Box::new(CustomDeriveCallback {
+                derives,
+                kind,
+                regex_set,
+            }));
+        }
+    }
+
     Ok((builder, output, verbose))
 }
diff --git a/bindgen/Cargo.toml b/bindgen/Cargo.toml
index 5a03d248..2292a5ef 100644
--- a/bindgen/Cargo.toml
+++ b/bindgen/Cargo.toml
@@ -47,6 +47,7 @@ static = ["clang-sys/static"]
 runtime = ["clang-sys/runtime"]
 # Dynamically discover a `rustfmt` binary using the `which` crate
 which-rustfmt = ["which"]
+cli = []
 
 # These features only exist for CI testing -- don't use them if you're not hacking
 # on bindgen!
diff --git a/bindgen/callbacks.rs b/bindgen/callbacks.rs
index cba406cb..dc20e258 100644
--- a/bindgen/callbacks.rs
+++ b/bindgen/callbacks.rs
@@ -25,6 +25,12 @@ impl Default for MacroParsingBehavior {
 /// A trait to allow configuring different kinds of types in different
 /// situations.
 pub trait ParseCallbacks: fmt::Debug {
+    #[cfg(feature = "cli")]
+    #[doc(hidden)]
+    fn cli_args(&self) -> Vec<String> {
+        vec![]
+    }
+
     /// This function will be run on every macro that is identified.
     fn will_parse_macro(&self, _name: &str) -> MacroParsingBehavior {
         MacroParsingBehavior::Default
@@ -120,10 +126,24 @@ pub trait ParseCallbacks: fmt::Debug {
 
 /// Relevant information about a type to which new derive attributes will be added using
 /// [`ParseCallbacks::add_derives`].
+#[derive(Debug)]
 #[non_exhaustive]
 pub struct DeriveInfo<'a> {
     /// The name of the type.
     pub name: &'a str,
+    /// The kind of the type.
+    pub kind: TypeKind,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+/// The kind of the current type.
+pub enum TypeKind {
+    /// The type is a Rust `struct`.
+    Struct,
+    /// The type is a Rust `enum`.
+    Enum,
+    /// The type is a Rust `union`.
+    Union,
 }
 
 /// An struct providing information about the item being passed to `ParseCallbacks::generated_name_override`.
diff --git a/bindgen/codegen/mod.rs b/bindgen/codegen/mod.rs
index 041d3669..6b24ae1b 100644
--- a/bindgen/codegen/mod.rs
+++ b/bindgen/codegen/mod.rs
@@ -18,6 +18,7 @@ use self::struct_layout::StructLayoutTracker;
 
 use super::BindgenOptions;
 
+use crate::callbacks::{DeriveInfo, TypeKind as DeriveTypeKind};
 use crate::ir::analysis::{HasVtable, Sizedness};
 use crate::ir::annotations::FieldAccessorKind;
 use crate::ir::comp::{
@@ -2100,11 +2101,18 @@ impl CodeGenerator for CompInfo {
         let mut derives: Vec<_> = derivable_traits.into();
         derives.extend(item.annotations().derives().iter().map(String::as_str));
 
+        let is_rust_union = is_union && struct_layout.is_rust_union();
+
         // The custom derives callback may return a list of derive attributes;
         // add them to the end of the list.
         let custom_derives = ctx.options().all_callbacks(|cb| {
-            cb.add_derives(&crate::callbacks::DeriveInfo {
+            cb.add_derives(&DeriveInfo {
                 name: &canonical_name,
+                kind: if is_rust_union {
+                    DeriveTypeKind::Union
+                } else {
+                    DeriveTypeKind::Struct
+                },
             })
         });
         // In most cases this will be a no-op, since custom_derives will be empty.
@@ -2118,7 +2126,7 @@ impl CodeGenerator for CompInfo {
             attributes.push(attributes::must_use());
         }
 
-        let mut tokens = if is_union && struct_layout.is_rust_union() {
+        let mut tokens = if is_rust_union {
             quote! {
                 #( #attributes )*
                 pub union #canonical_ident
@@ -3112,7 +3120,10 @@ impl CodeGenerator for Enum {
             // The custom derives callback may return a list of derive attributes;
             // add them to the end of the list.
             let custom_derives = ctx.options().all_callbacks(|cb| {
-                cb.add_derives(&crate::callbacks::DeriveInfo { name: &name })
+                cb.add_derives(&DeriveInfo {
+                    name: &name,
+                    kind: DeriveTypeKind::Enum,
+                })
             });
             // In most cases this will be a no-op, since custom_derives will be empty.
             derives.extend(custom_derives.iter().map(|s| s.as_str()));
diff --git a/bindgen/lib.rs b/bindgen/lib.rs
index 46444a93..cf1486c2 100644
--- a/bindgen/lib.rs
+++ b/bindgen/lib.rs
@@ -65,7 +65,7 @@ mod clang;
 mod codegen;
 mod deps;
 mod features;
-mod ir;
+pub mod ir;
 mod parse;
 mod regex_set;
 mod time;
@@ -91,7 +91,7 @@ use crate::ir::context::{BindgenContext, ItemId};
 pub use crate::ir::function::Abi;
 use crate::ir::item::Item;
 use crate::parse::ParseError;
-use crate::regex_set::RegexSet;
+pub use crate::regex_set::RegexSet;
 
 use std::borrow::Cow;
 use std::env;
@@ -653,6 +653,11 @@ impl Builder {
             output_vector.push("--wrap-unsafe-ops".into());
         }
 
+        #[cfg(feature = "cli")]
+        for callbacks in &self.options.parse_callbacks {
+            output_vector.extend(callbacks.cli_args());
+        }
+
         // Add clang arguments
 
         output_vector.push("--".into());
diff --git a/bindgen/regex_set.rs b/bindgen/regex_set.rs
index 9f1e2251..6246dd25 100644
--- a/bindgen/regex_set.rs
+++ b/bindgen/regex_set.rs
@@ -16,6 +16,13 @@ pub struct RegexSet {
 }
 
 impl RegexSet {
+    /// Create a new RegexSet
+    pub fn new() -> RegexSet {
+        RegexSet {
+            ..Default::default()
+        }
+    }
+
     /// Is this set empty?
     pub fn is_empty(&self) -> bool {
         self.items.is_empty()


Test patch:
diff --git a/bindgen-tests/Cargo.toml b/bindgen-tests/Cargo.toml
index 32694dad..0678274f 100644
--- a/bindgen-tests/Cargo.toml
+++ b/bindgen-tests/Cargo.toml
@@ -5,7 +5,7 @@ version = "0.1.0"
 publish = false
 
 [dev-dependencies]
-bindgen = { path = "../bindgen" }
+bindgen = { path = "../bindgen", features = ["cli"] }
 diff = "0.1"
 shlex = "1"
 clap = { version = "4", features = ["derive"] }
diff --git a/bindgen-tests/tests/expectations/tests/derive-custom-cli.rs b/bindgen-tests/tests/expectations/tests/derive-custom-cli.rs
new file mode 100644
index 00000000..36f84c7d
--- /dev/null
+++ b/bindgen-tests/tests/expectations/tests/derive-custom-cli.rs
@@ -0,0 +1,115 @@
+#![allow(
+    dead_code,
+    non_snake_case,
+    non_camel_case_types,
+    non_upper_case_globals
+)]
+
+#[repr(C)]
+#[derive(Clone, Default)]
+pub struct foo_struct {
+    pub inner: ::std::os::raw::c_int,
+}
+#[test]
+fn bindgen_test_layout_foo_struct() {
+    const UNINIT: ::std::mem::MaybeUninit<foo_struct> =
+        ::std::mem::MaybeUninit::uninit();
+    let ptr = UNINIT.as_ptr();
+    assert_eq!(
+        ::std::mem::size_of::<foo_struct>(),
+        4usize,
+        concat!("Size of: ", stringify!(foo_struct))
+    );
+    assert_eq!(
+        ::std::mem::align_of::<foo_struct>(),
+        4usize,
+        concat!("Alignment of ", stringify!(foo_struct))
+    );
+    assert_eq!(
+        unsafe { ::std::ptr::addr_of!((*ptr).inner) as usize - ptr as usize },
+        0usize,
+        concat!(
+            "Offset of field: ",
+            stringify!(foo_struct),
+            "::",
+            stringify!(inner)
+        )
+    );
+}
+#[repr(u32)]
+#[derive(Clone, Hash, PartialEq, Eq, Copy)]
+pub enum foo_enum {
+    inner = 0,
+}
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub union foo_union {
+    pub fst: ::std::mem::ManuallyDrop<::std::os::raw::c_int>,
+    pub snd: ::std::mem::ManuallyDrop<f32>,
+}
+#[test]
+fn bindgen_test_layout_foo_union() {
+    const UNINIT: ::std::mem::MaybeUninit<foo_union> =
+        ::std::mem::MaybeUninit::uninit();
+    let ptr = UNINIT.as_ptr();
+    assert_eq!(
+        ::std::mem::size_of::<foo_union>(),
+        4usize,
+        concat!("Size of: ", stringify!(foo_union))
+    );
+    assert_eq!(
+        ::std::mem::align_of::<foo_union>(),
+        4usize,
+        concat!("Alignment of ", stringify!(foo_union))
+    );
+    assert_eq!(
+        unsafe { ::std::ptr::addr_of!((*ptr).fst) as usize - ptr as usize },
+        0usize,
+        concat!(
+            "Offset of field: ",
+            stringify!(foo_union),
+            "::",
+            stringify!(fst)
+        )
+    );
+    assert_eq!(
+        unsafe { ::std::ptr::addr_of!((*ptr).snd) as usize - ptr as usize },
+        0usize,
+        concat!(
+            "Offset of field: ",
+            stringify!(foo_union),
+            "::",
+            stringify!(snd)
+        )
+    );
+}
+#[repr(C)]
+pub struct non_matching {
+    pub inner: ::std::os::raw::c_int,
+}
+#[test]
+fn bindgen_test_layout_non_matching() {
+    const UNINIT: ::std::mem::MaybeUninit<non_matching> =
+        ::std::mem::MaybeUninit::uninit();
+    let ptr = UNINIT.as_ptr();
+    assert_eq!(
+        ::std::mem::size_of::<non_matching>(),
+        4usize,
+        concat!("Size of: ", stringify!(non_matching))
+    );
+    assert_eq!(
+        ::std::mem::align_of::<non_matching>(),
+        4usize,
+        concat!("Alignment of ", stringify!(non_matching))
+    );
+    assert_eq!(
+        unsafe { ::std::ptr::addr_of!((*ptr).inner) as usize - ptr as usize },
+        0usize,
+        concat!(
+            "Offset of field: ",
+            stringify!(non_matching),
+            "::",
+            stringify!(inner)
+        )
+    );
+}
diff --git a/bindgen-tests/tests/headers/derive-custom-cli.h b/bindgen-tests/tests/headers/derive-custom-cli.h
new file mode 100644
index 00000000..9b65536f
--- /dev/null
+++ b/bindgen-tests/tests/headers/derive-custom-cli.h
@@ -0,0 +1,14 @@
+// bindgen-flags: --default-enum-style rust --default-non-copy-union-style manually_drop --no-default=".*" --no-hash=".*" --no-partialeq=".*" --no-debug=".*" --no-copy=".*" --with-derive-custom="foo_[^e].*=Clone" --with-derive-custom-struct="foo.*=Default" --with-derive-custom-enum="foo.*=Copy" --with-derive-custom-union="foo.*=Copy"
+struct foo_struct {
+    int inner;
+};
+enum foo_enum {
+    inner = 0
+};
+union foo_union {
+    int fst;
+    float snd;
+};
+struct non_matching {
+    int inner;
+};