From 9a8e5ca2f4089e21300fbea6b73e82efd7f18882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kr=C3=B6ning?= Date: Mon, 8 Jul 2024 16:47:07 +0200 Subject: [PATCH] Add support for custom attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Martin Kröning --- CHANGELOG.md | 1 + Cargo.lock | 2 + bindgen-cli/Cargo.toml | 1 + bindgen-cli/options.rs | 131 ++++++++++++++++++ bindgen-integration/build.rs | 12 ++ bindgen-integration/src/lib.rs | 9 ++ bindgen-tests/Cargo.toml | 1 + .../tests/attribute-custom-cli.rs | 48 +++++++ .../expectations/tests/attribute-custom.rs | 22 +++ .../tests/headers/attribute-custom-cli.h | 14 ++ .../tests/headers/attribute-custom.h | 28 ++++ bindgen/callbacks.rs | 19 +++ bindgen/codegen/mod.rs | 53 ++++++- bindgen/ir/annotations.rs | 8 ++ 14 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 bindgen-tests/tests/expectations/tests/attribute-custom-cli.rs create mode 100644 bindgen-tests/tests/expectations/tests/attribute-custom.rs create mode 100644 bindgen-tests/tests/headers/attribute-custom-cli.h create mode 100644 bindgen-tests/tests/headers/attribute-custom.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e18d5c1b..308ed7675e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -235,6 +235,7 @@ - Add option to use DST structs for flexible arrays (--flexarray-dst, #2772). - Add option to dynamically load variables (#2812). - Add option in CLI to use rustified non-exhaustive enums (--rustified-non-exhaustive-enum, #2847). +- Add support for custom attributes (--with-attribute-custom, #2866) ## Changed - Remove which and lazy-static dependencies (#2809, #2817). - Generate compile-time layout tests (#2787). diff --git a/Cargo.lock b/Cargo.lock index 9ba69946ec..4786857205 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,7 @@ dependencies = [ "clap_complete", "env_logger 0.10.0", "log", + "proc-macro2", "shlex", ] @@ -75,6 +76,7 @@ dependencies = [ "clap_complete", "owo-colors", "prettyplease", + "proc-macro2", "shlex", "similar", "syn 2.0.18", diff --git a/bindgen-cli/Cargo.toml b/bindgen-cli/Cargo.toml index 6fc02f3a55..d75867b76b 100644 --- a/bindgen-cli/Cargo.toml +++ b/bindgen-cli/Cargo.toml @@ -25,6 +25,7 @@ clap = { version = "4", features = ["derive"] } clap_complete = "4" env_logger = { version = "0.10.0", optional = true } log = { version = "0.4", optional = true } +proc-macro2 = { version = "1", default-features = false } shlex = "1" [features] diff --git a/bindgen-cli/options.rs b/bindgen-cli/options.rs index cd5e9bb127..5311d87aa7 100644 --- a/bindgen-cli/options.rs +++ b/bindgen-cli/options.rs @@ -6,10 +6,12 @@ use bindgen::{ }; use clap::error::{Error, ErrorKind}; use clap::{CommandFactory, Parser}; +use proc_macro2::TokenStream; use std::fs::File; use std::io; use std::path::{Path, PathBuf}; use std::process::exit; +use std::str::FromStr; fn rust_target_help() -> String { format!( @@ -87,6 +89,43 @@ fn parse_custom_derive( Ok((derives, regex.to_owned())) } +fn parse_custom_attribute( + custom_attribute: &str, +) -> Result<(Vec, String), Error> { + let mut brace_level = 0; + let (regex, attributes) = custom_attribute + .rsplit_once(|c| { + match c { + ']' => brace_level += 1, + '[' => brace_level -= 1, + _ => {} + } + c == '=' && brace_level == 0 + }) + .ok_or_else(|| Error::raw(ErrorKind::InvalidValue, "Missing `=`"))?; + + let mut brace_level = 0; + let attributes = attributes + .split(|c| { + match c { + ']' => brace_level += 1, + '[' => brace_level -= 1, + _ => {} + } + c == ',' && brace_level == 0 + }) + .map(|s| s.to_owned()) + .collect::>(); + + for attribute in &attributes { + if let Err(err) = TokenStream::from_str(attribute) { + return Err(Error::raw(ErrorKind::InvalidValue, err)); + } + } + + Ok((attributes, regex.to_owned())) +} + #[derive(Parser, Debug)] #[clap( about = "Generates Rust bindings from C/C++ headers.", @@ -424,6 +463,18 @@ struct BindgenCommand { /// 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", value_parser = parse_custom_derive)] with_derive_custom_union: Vec<(Vec, String)>, + /// Add custom attributes on any kind of type. The CUSTOM value must be of the shape REGEX=ATTRIBUTE where ATTRIBUTE is a coma-separated list of attributes. + #[arg(long, value_name = "CUSTOM", value_parser = parse_custom_attribute)] + with_attribute_custom: Vec<(Vec, String)>, + /// Add custom attributes on a `struct`. The CUSTOM value must be of the shape REGEX=ATTRIBUTE where ATTRIBUTE is a coma-separated list of attributes. + #[arg(long, value_name = "CUSTOM", value_parser = parse_custom_attribute)] + with_attribute_custom_struct: Vec<(Vec, String)>, + /// Add custom attributes on an `enum. The CUSTOM value must be of the shape REGEX=ATTRIBUTE where ATTRIBUTE is a coma-separated list of attributes. + #[arg(long, value_name = "CUSTOM", value_parser = parse_custom_attribute)] + with_attribute_custom_enum: Vec<(Vec, String)>, + /// Add custom attributes on a `union`. The CUSTOM value must be of the shape REGEX=ATTRIBUTE where ATTRIBUTE is a coma-separated list of attributes. + #[arg(long, value_name = "CUSTOM", value_parser = parse_custom_attribute)] + with_attribute_custom_union: Vec<(Vec, String)>, /// Generate wrappers for `static` and `static inline` functions. #[arg(long, requires = "experimental")] wrap_static_fns: bool, @@ -574,6 +625,10 @@ where with_derive_custom_struct, with_derive_custom_enum, with_derive_custom_union, + with_attribute_custom, + with_attribute_custom_struct, + with_attribute_custom_enum, + with_attribute_custom_union, wrap_static_fns, wrap_static_fns_path, wrap_static_fns_suffix, @@ -1130,6 +1185,82 @@ where } } + #[derive(Debug)] + struct CustomAttributeCallback { + attributes: Vec, + kind: Option, + regex_set: bindgen::RegexSet, + } + + impl bindgen::callbacks::ParseCallbacks for CustomAttributeCallback { + fn cli_args(&self) -> Vec { + let mut args = vec![]; + + let flag = match &self.kind { + None => "--with-attribute-custom", + Some(TypeKind::Struct) => "--with-attribute-custom-struct", + Some(TypeKind::Enum) => "--with-attribute-custom-enum", + Some(TypeKind::Union) => "--with-attribute-custom-union", + }; + + let attributes = self.attributes.join(","); + + for item in self.regex_set.get_items() { + args.extend_from_slice(&[ + flag.to_owned(), + format!("{}={}", item, attributes), + ]); + } + + args + } + + fn add_attributes( + &self, + info: &bindgen::callbacks::AttributeInfo<'_>, + ) -> Vec { + if self.kind.map(|kind| kind == info.kind).unwrap_or(true) && + self.regex_set.matches(info.name) + { + return self.attributes.clone(); + } + vec![] + } + } + + for (custom_attributes, kind, name) in [ + (with_attribute_custom, None, "--with-attribute-custom"), + ( + with_attribute_custom_struct, + Some(TypeKind::Struct), + "--with-attribute-custom-struct", + ), + ( + with_attribute_custom_enum, + Some(TypeKind::Enum), + "--with-attribute-custom-enum", + ), + ( + with_attribute_custom_union, + Some(TypeKind::Union), + "--with-attribute-custom-union", + ), + ] { + let name = emit_diagnostics.then_some(name); + for (attributes, regex) in custom_attributes { + let mut regex_set = RegexSet::new(); + regex_set.insert(regex); + regex_set.build_with_diagnostics(false, name); + + builder = + builder.parse_callbacks(Box::new(CustomAttributeCallback { + attributes, + kind, + regex_set, + })); + } + } + if wrap_static_fns { builder = builder.wrap_static_fns(true); } diff --git a/bindgen-integration/build.rs b/bindgen-integration/build.rs index 6b06c91bc3..88ba945366 100644 --- a/bindgen-integration/build.rs +++ b/bindgen-integration/build.rs @@ -133,6 +133,18 @@ impl ParseCallbacks for MacroCallback { vec![] } } + + // Test the "custom attributes" capability. + fn add_attributes( + &self, + info: &bindgen::callbacks::AttributeInfo<'_>, + ) -> Vec { + if info.name == "Test" { + vec!["#[cfg_attr(test, derive(PartialOrd))]".into()] + } else { + vec![] + } + } } impl Drop for MacroCallback { diff --git a/bindgen-integration/src/lib.rs b/bindgen-integration/src/lib.rs index c37055ee7d..48cfe092d2 100755 --- a/bindgen-integration/src/lib.rs +++ b/bindgen-integration/src/lib.rs @@ -297,6 +297,15 @@ fn test_custom_derive() { assert!(!(test1 > test2)); } +#[test] +fn test_custom_attributes() { + // The `add_attributes` callback should have added `#[cfg_attr(test, derive(PartialOrd))])` + // to the `Test` struct. If it didn't, this will fail to compile. + let test1 = unsafe { bindings::Test::new(5) }; + let test2 = unsafe { bindings::Test::new(6) }; + assert!(test1 < test2); +} + #[test] fn test_wrap_static_fns() { // GH-1090: https://github.com/rust-lang/rust-bindgen/issues/1090 diff --git a/bindgen-tests/Cargo.toml b/bindgen-tests/Cargo.toml index a253b349b9..47fc0b8ca0 100644 --- a/bindgen-tests/Cargo.toml +++ b/bindgen-tests/Cargo.toml @@ -10,6 +10,7 @@ clap = { version = "4", features = ["derive"] } clap_complete = "4" shlex = "1" prettyplease = { version = "0.2.7", features = ["verbatim"] } +proc-macro2 = { version = "1", default-features = false } syn = { version = "2.0" } tempfile = "3" similar = { version = "2.2.1", features = ["inline"] } diff --git a/bindgen-tests/tests/expectations/tests/attribute-custom-cli.rs b/bindgen-tests/tests/expectations/tests/attribute-custom-cli.rs new file mode 100644 index 0000000000..55353116d3 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/attribute-custom-cli.rs @@ -0,0 +1,48 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[repr(C)] +#[doc(hidden)] +#[derive(Default)] +pub struct foo_struct { + pub inner: ::std::os::raw::c_int, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of foo_struct"][::std::mem::size_of::() - 4usize]; + ["Alignment of foo_struct"][::std::mem::align_of::() - 4usize]; + [ + "Offset of field: foo_struct::inner", + ][::std::mem::offset_of!(foo_struct, inner) - 0usize]; +}; +#[repr(u32)] +#[cfg_attr(test, derive(PartialOrd, Copy))] +#[derive(Clone, Hash, PartialEq, Eq)] +pub enum foo_enum { + inner = 0, +} +#[repr(C)] +#[doc(hidden)] +#[derive(Clone)] +#[derive(Copy)] +pub union foo_union { + pub fst: ::std::mem::ManuallyDrop<::std::os::raw::c_int>, + pub snd: ::std::mem::ManuallyDrop, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of foo_union"][::std::mem::size_of::() - 4usize]; + ["Alignment of foo_union"][::std::mem::align_of::() - 4usize]; + ["Offset of field: foo_union::fst"][::std::mem::offset_of!(foo_union, fst) - 0usize]; + ["Offset of field: foo_union::snd"][::std::mem::offset_of!(foo_union, snd) - 0usize]; +}; +#[repr(C)] +pub struct non_matching { + pub inner: ::std::os::raw::c_int, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of non_matching"][::std::mem::size_of::() - 4usize]; + ["Alignment of non_matching"][::std::mem::align_of::() - 4usize]; + [ + "Offset of field: non_matching::inner", + ][::std::mem::offset_of!(non_matching, inner) - 0usize]; +}; diff --git a/bindgen-tests/tests/expectations/tests/attribute-custom.rs b/bindgen-tests/tests/expectations/tests/attribute-custom.rs new file mode 100644 index 0000000000..6d616d3f3e --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/attribute-custom.rs @@ -0,0 +1,22 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +///
+#[repr(C)] +#[derive(Debug)] +pub struct my_type { + pub a: ::std::os::raw::c_int, +} +/**
+
*/ +#[repr(C)] +#[derive(Debug)] +#[derive(Clone)] +pub struct my_type2 { + pub a: ::std::os::raw::c_uint, +} +///
+#[repr(C)] +#[derive(Debug)] +#[derive(Clone)] +pub struct my_type3 { + pub a: ::std::os::raw::c_ulong, +} diff --git a/bindgen-tests/tests/headers/attribute-custom-cli.h b/bindgen-tests/tests/headers/attribute-custom-cli.h new file mode 100644 index 0000000000..a5f73c78e5 --- /dev/null +++ b/bindgen-tests/tests/headers/attribute-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-attribute-custom="foo_[^e].*=#[doc(hidden)]" --with-attribute-custom-struct="foo.*=#[derive(Default)]" --with-attribute-custom-enum="foo.*=#[cfg_attr(test, derive(PartialOrd, Copy))]" --with-attribute-custom-union="foo.*=#[derive(Clone)],#[derive(Copy)]" +struct foo_struct { + int inner; +}; +enum foo_enum { + inner = 0 +}; +union foo_union { + int fst; + float snd; +}; +struct non_matching { + int inner; +}; diff --git a/bindgen-tests/tests/headers/attribute-custom.h b/bindgen-tests/tests/headers/attribute-custom.h new file mode 100644 index 0000000000..dd382bf8cd --- /dev/null +++ b/bindgen-tests/tests/headers/attribute-custom.h @@ -0,0 +1,28 @@ +// bindgen-flags: --no-derive-debug --no-derive-copy --no-derive-default --default-enum-style rust --no-layout-tests + +/**
*/ +struct my_type; + +/**
*/ +struct my_type; + +struct my_type { + int a; +}; + +/** + *
+ *
+ */ +struct my_type2; + +struct my_type2 { + unsigned a; +}; + +/** + *
+ */ +struct my_type3 { + unsigned long a; +}; diff --git a/bindgen/callbacks.rs b/bindgen/callbacks.rs index 0f16c4c0bf..43dc37d595 100644 --- a/bindgen/callbacks.rs +++ b/bindgen/callbacks.rs @@ -129,6 +129,14 @@ pub trait ParseCallbacks: fmt::Debug { vec![] } + /// Provide a list of custom attributes. + /// + /// If no additional attributes are wanted, this function should return an + /// empty `Vec`. + fn add_attributes(&self, _info: &AttributeInfo<'_>) -> Vec { + vec![] + } + /// Process a source code comment. fn process_comment(&self, _comment: &str) -> Option { None @@ -167,6 +175,17 @@ pub struct DeriveInfo<'a> { pub kind: TypeKind, } +/// Relevant information about a type to which new attributes will be added using +/// [`ParseCallbacks::add_attributes`]. +#[derive(Debug)] +#[non_exhaustive] +pub struct AttributeInfo<'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 { diff --git a/bindgen/codegen/mod.rs b/bindgen/codegen/mod.rs index 1cccac6761..b92f5e127a 100644 --- a/bindgen/codegen/mod.rs +++ b/bindgen/codegen/mod.rs @@ -20,7 +20,9 @@ use self::struct_layout::StructLayoutTracker; use super::BindgenOptions; -use crate::callbacks::{DeriveInfo, FieldInfo, TypeKind as DeriveTypeKind}; +use crate::callbacks::{ + AttributeInfo, DeriveInfo, FieldInfo, TypeKind as DeriveTypeKind, +}; use crate::codegen::error::Error; use crate::ir::analysis::{HasVtable, Sizedness}; use crate::ir::annotations::{ @@ -1047,6 +1049,19 @@ impl CodeGenerator for Type { .extend(custom_derives.iter().map(|s| s.as_str())); attributes.push(attributes::derives(&derives)); + let custom_attributes = + ctx.options().all_callbacks(|cb| { + cb.add_attributes(&AttributeInfo { + name: &name, + kind: DeriveTypeKind::Struct, + }) + }); + attributes.extend( + custom_attributes + .iter() + .map(|s| s.parse().unwrap()), + ); + quote! { #( #attributes )* pub struct #rust_name @@ -2378,6 +2393,25 @@ impl CodeGenerator for CompInfo { attributes.push(attributes::derives(&derives)) } + attributes.extend( + item.annotations() + .attributes() + .iter() + .map(|s| s.parse().unwrap()), + ); + + let custom_attributes = ctx.options().all_callbacks(|cb| { + cb.add_attributes(&AttributeInfo { + name: &canonical_name, + kind: if is_rust_union { + DeriveTypeKind::Union + } else { + DeriveTypeKind::Struct + }, + }) + }); + attributes.extend(custom_attributes.iter().map(|s| s.parse().unwrap())); + if item.must_use(ctx) { attributes.push(attributes::must_use()); } @@ -3570,6 +3604,23 @@ impl CodeGenerator for 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())); + attrs.extend( + item.annotations() + .attributes() + .iter() + .map(|s| s.parse().unwrap()), + ); + + // The custom attribute callback may return a list of attributes; + // add them to the end of the list. + let custom_attributes = ctx.options().all_callbacks(|cb| { + cb.add_attributes(&AttributeInfo { + name: &name, + kind: DeriveTypeKind::Enum, + }) + }); + attrs.extend(custom_attributes.iter().map(|s| s.parse().unwrap())); + attrs.push(attributes::derives(&derives)); } diff --git a/bindgen/ir/annotations.rs b/bindgen/ir/annotations.rs index fc9cc0ffe7..12295288c1 100644 --- a/bindgen/ir/annotations.rs +++ b/bindgen/ir/annotations.rs @@ -102,6 +102,8 @@ pub(crate) struct Annotations { constify_enum_variant: bool, /// List of explicit derives for this type. derives: Vec, + /// List of explicit attributes for this type. + attributes: Vec, } fn parse_accessor(s: &str) -> FieldAccessorKind { @@ -169,6 +171,11 @@ impl Annotations { &self.derives } + /// The list of attributes that have been specified in this annotation. + pub(crate) fn attributes(&self) -> &[String] { + &self.attributes + } + /// Should we avoid implementing the `Copy` trait? pub(crate) fn disallow_copy(&self) -> bool { self.disallow_copy @@ -223,6 +230,7 @@ impl Annotations { ) } "derive" => self.derives.push(attr.value), + "attribute" => self.attributes.push(attr.value), "private" => { self.visibility_kind = if attr.value != "false" { Some(FieldVisibilityKind::Private)