From 5b9816fd16f1ee5c2795f12abea1e3de5e78b1ad Mon Sep 17 00:00:00 2001 From: Jeremy Fitzhardinge Date: Tue, 21 Oct 2025 18:31:42 -0700 Subject: [PATCH 1/2] Add support for custom per-field attributes This adds the ability to apply custom Rust attributes to individual struct, union, and newtype fields in generated bindings through three mechanisms: 1. HTML annotations in C/C++ comments: ```c ///
int x; ``` 2. ParseCallbacks::field_attributes() method for programmatic control: ```rust fn field_attributes(&self, info: &FieldAttributeInfo) -> Vec ``` 3. CLI flag and Builder API for pattern-based attributes: ```bash --field-attr "Point::x=serde(rename = x_coord)" ``` ```rust .field_attribute("Point", "x", "serde(rename = x_coord)") ``` All three mechanisms can be used together, with attributes merged in order: annotations, callbacks, then CLI/Builder patterns. This is useful for adding serde attributes, documentation, or other derive-related metadata to specific fields. --- .../tests/field_attr_annotation.rs | 46 +++++++++ .../expectations/tests/field_attr_cli.rs | 42 +++++++++ .../tests/headers/field_attr_annotation.h | 17 ++++ bindgen-tests/tests/headers/field_attr_cli.h | 13 +++ bindgen/callbacks.rs | 50 ++++++++++ bindgen/codegen/mod.rs | 94 ++++++++++++++++++- bindgen/options/cli.rs | 30 ++++++ bindgen/options/mod.rs | 47 ++++++++++ 8 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 bindgen-tests/tests/expectations/tests/field_attr_annotation.rs create mode 100644 bindgen-tests/tests/expectations/tests/field_attr_cli.rs create mode 100644 bindgen-tests/tests/headers/field_attr_annotation.h create mode 100644 bindgen-tests/tests/headers/field_attr_cli.h diff --git a/bindgen-tests/tests/expectations/tests/field_attr_annotation.rs b/bindgen-tests/tests/expectations/tests/field_attr_annotation.rs new file mode 100644 index 0000000000..8db20d3ab3 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/field_attr_annotation.rs @@ -0,0 +1,46 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +///
+#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct Point { + ///
+ #[cfg(test)] + pub x: ::std::os::raw::c_int, + ///
+ #[allow(dead_code)] + pub y: ::std::os::raw::c_int, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of Point"][::std::mem::size_of::() - 8usize]; + ["Alignment of Point"][::std::mem::align_of::() - 4usize]; + ["Offset of field: Point::x"][::std::mem::offset_of!(Point, x) - 0usize]; + ["Offset of field: Point::y"][::std::mem::offset_of!(Point, y) - 4usize]; +}; +///
+#[repr(C)] +#[derive(Copy, Clone)] +pub union Data { + ///
+ #[allow(dead_code)] + pub i: ::std::os::raw::c_int, + pub f: f32, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of Data"][::std::mem::size_of::() - 4usize]; + ["Alignment of Data"][::std::mem::align_of::() - 4usize]; + ["Offset of field: Data::i"][::std::mem::offset_of!(Data, i) - 0usize]; + ["Offset of field: Data::f"][::std::mem::offset_of!(Data, f) - 0usize]; +}; +impl Default for Data { + fn default() -> Self { + let mut s = ::std::mem::MaybeUninit::::uninit(); + unsafe { + ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +///
+pub type Handle = ::std::os::raw::c_int; diff --git a/bindgen-tests/tests/expectations/tests/field_attr_cli.rs b/bindgen-tests/tests/expectations/tests/field_attr_cli.rs new file mode 100644 index 0000000000..04b91b027d --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/field_attr_cli.rs @@ -0,0 +1,42 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct Point { + #[cfg(test)] + pub x: ::std::os::raw::c_int, + #[allow(dead_code)] + pub y: ::std::os::raw::c_int, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of Point"][::std::mem::size_of::() - 8usize]; + ["Alignment of Point"][::std::mem::align_of::() - 4usize]; + ["Offset of field: Point::x"][::std::mem::offset_of!(Point, x) - 0usize]; + ["Offset of field: Point::y"][::std::mem::offset_of!(Point, y) - 4usize]; +}; +#[repr(C)] +#[derive(Copy, Clone)] +pub union Data { + #[allow(dead_code)] + pub i: ::std::os::raw::c_int, + pub f: f32, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of Data"][::std::mem::size_of::() - 4usize]; + ["Alignment of Data"][::std::mem::align_of::() - 4usize]; + ["Offset of field: Data::i"][::std::mem::offset_of!(Data, i) - 0usize]; + ["Offset of field: Data::f"][::std::mem::offset_of!(Data, f) - 0usize]; +}; +impl Default for Data { + fn default() -> Self { + let mut s = ::std::mem::MaybeUninit::::uninit(); + unsafe { + ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +#[repr(transparent)] +#[derive(Debug, Default, Copy, Clone)] +pub struct Handle(#[cfg(test)] pub ::std::os::raw::c_int); diff --git a/bindgen-tests/tests/headers/field_attr_annotation.h b/bindgen-tests/tests/headers/field_attr_annotation.h new file mode 100644 index 0000000000..d6196b9642 --- /dev/null +++ b/bindgen-tests/tests/headers/field_attr_annotation.h @@ -0,0 +1,17 @@ +///
+struct Point { + ///
+ int x; + ///
+ int y; +}; + +///
+union Data { + ///
+ int i; + float f; +}; + +///
+typedef int Handle; diff --git a/bindgen-tests/tests/headers/field_attr_cli.h b/bindgen-tests/tests/headers/field_attr_cli.h new file mode 100644 index 0000000000..5fa6d6c196 --- /dev/null +++ b/bindgen-tests/tests/headers/field_attr_cli.h @@ -0,0 +1,13 @@ +// bindgen-flags: --field-attr "Point::x=cfg(test)" --field-attr "Point::y=allow(dead_code)" --field-attr "Data::i=allow(dead_code)" --field-attr "Handle::0=cfg(test)" --new-type-alias "Handle" + +struct Point { + int x; + int y; +}; + +union Data { + int i; + float f; +}; + +typedef int Handle; diff --git a/bindgen/callbacks.rs b/bindgen/callbacks.rs index 7967912930..f923544e27 100644 --- a/bindgen/callbacks.rs +++ b/bindgen/callbacks.rs @@ -142,6 +142,35 @@ pub trait ParseCallbacks: fmt::Debug { vec![] } + /// Provide a list of custom attributes for struct/union fields. + /// + /// These attributes will be applied to the field in the generated Rust code. + /// If no additional attributes are wanted, this function should return an + /// empty `Vec`. + /// + /// # Example + /// + /// ``` + /// # use bindgen::callbacks::{ParseCallbacks, FieldAttributeInfo}; + /// # #[derive(Debug)] + /// # struct MyCallbacks; + /// # impl ParseCallbacks for MyCallbacks { + /// fn field_attributes(&self, info: &FieldAttributeInfo<'_>) -> Vec { + /// if info.field_name == "internal" { + /// vec!["serde(skip)".to_string()] + /// } else if info.field_name == "0" { + /// // Newtype tuple field + /// vec!["serde(transparent)".to_string()] + /// } else { + /// vec![] + /// } + /// } + /// # } + /// ``` + fn field_attributes(&self, _info: &FieldAttributeInfo<'_>) -> Vec { + vec![] + } + /// Process a source code comment. fn process_comment(&self, _comment: &str) -> Option { None @@ -334,6 +363,27 @@ pub struct FieldInfo<'a> { pub field_type_name: Option<&'a str>, } +/// Relevant information about a field to which new attributes will be added using +/// [`ParseCallbacks::field_attributes`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub struct FieldAttributeInfo<'a> { + /// The name of the containing type (struct/union). + pub type_name: &'a str, + + /// The kind of the containing type. + pub type_kind: TypeKind, + + /// The name of the field. + /// + /// For newtype tuple structs (when using `--default-alias-style=new_type`), + /// this will be `"0"` for the inner field. + pub field_name: &'a str, + + /// The name of the field's type, if available. + pub field_type_name: Option<&'a str>, +} + /// Location in the source code. Roughly equivalent to the same type /// within `clang_sys`. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/bindgen/codegen/mod.rs b/bindgen/codegen/mod.rs index b6615a1600..73816e1707 100644 --- a/bindgen/codegen/mod.rs +++ b/bindgen/codegen/mod.rs @@ -21,8 +21,8 @@ use self::struct_layout::StructLayoutTracker; use super::BindgenOptions; use crate::callbacks::{ - AttributeInfo, DeriveInfo, DiscoveredItem, DiscoveredItemId, FieldInfo, - TypeKind as DeriveTypeKind, + AttributeInfo, DeriveInfo, DiscoveredItem, DiscoveredItemId, + FieldAttributeInfo, FieldInfo, TypeKind as DeriveTypeKind, }; use crate::codegen::error::Error; use crate::ir::analysis::{HasVtable, Sizedness}; @@ -1138,8 +1138,56 @@ impl CodeGenerator for Type { }) .unwrap_or(ctx.options().default_visibility); let access_spec = access_specifier(visibility); + + // Collect field attributes from multiple sources for newtype tuple field + let mut all_field_attributes = Vec::new(); + + // 1. Get attributes from typedef annotations (if any) + all_field_attributes.extend( + item.annotations().attributes().iter().cloned(), + ); + + // 2. Get custom attributes from callbacks + all_field_attributes.extend( + ctx.options().all_callbacks(|cb| { + cb.field_attributes(&FieldAttributeInfo { + type_name: &item.canonical_name(ctx), + type_kind: DeriveTypeKind::Struct, + field_name: "0", + field_type_name: inner_item + .expect_type() + .name(), + }) + }), + ); + + // 3. Get attributes from CLI/Builder patterns + let type_name = item.canonical_name(ctx); + for (type_pat, field_pat, attr) in + &ctx.options().field_attr_patterns + { + if type_pat.as_ref() == type_name && + field_pat.as_ref() == "0" + { + all_field_attributes.push(attr.to_string()); + } + } + + // Build the field with attributes + let mut field_tokens = quote! {}; + for attr in &all_field_attributes { + let attr_tokens: proc_macro2::TokenStream = + attr.parse().expect("Invalid field attribute"); + field_tokens.append_all(quote! { + #[#attr_tokens] + }); + } + field_tokens.append_all(quote! { + #access_spec #inner_rust_type + }); + quote! { - (#access_spec #inner_rust_type) ; + (#field_tokens) ; } } }); @@ -1569,6 +1617,46 @@ impl FieldCodegen<'_> for FieldData { let accessor_kind = self.annotations().accessor_kind().unwrap_or(accessor_kind); + // Collect field attributes from multiple sources + let mut all_field_attributes = Vec::new(); + + // 1. Get attributes from field annotations (///
) + all_field_attributes + .extend(self.annotations().attributes().iter().cloned()); + + // 2. Get custom attributes from callbacks + all_field_attributes.extend(ctx.options().all_callbacks(|cb| { + cb.field_attributes(&FieldAttributeInfo { + type_name: &parent_item.canonical_name(ctx), + type_kind: if parent.is_union() { + DeriveTypeKind::Union + } else { + DeriveTypeKind::Struct + }, + field_name, + field_type_name: field_ty.name(), + }) + })); + + // 3. Get attributes from CLI/Builder patterns + let type_name = parent_item.canonical_name(ctx); + for (type_pat, field_pat, attr) in &ctx.options().field_attr_patterns { + if type_pat.as_ref() == type_name && + field_pat.as_ref() == field_name + { + all_field_attributes.push(attr.to_string()); + } + } + + // Apply all custom attributes to the field + for attr in &all_field_attributes { + let attr_tokens: proc_macro2::TokenStream = + attr.parse().expect("Invalid field attribute"); + field.append_all(quote! { + #[#attr_tokens] + }); + } + match visibility { FieldVisibilityKind::Private => { field.append_all(quote! { diff --git a/bindgen/options/cli.rs b/bindgen/options/cli.rs index 972b487c25..5304862584 100644 --- a/bindgen/options/cli.rs +++ b/bindgen/options/cli.rs @@ -137,6 +137,31 @@ fn parse_custom_attribute( Ok((attributes, regex.to_owned())) } +fn parse_field_attr( + field_attr: &str, +) -> Result<(String, String, String), Error> { + // Parse format: TYPE::FIELD=ATTR + // We need to split on the first '=' after finding the '::' + let (type_field, attr) = field_attr.split_once('=').ok_or_else(|| { + Error::raw(ErrorKind::InvalidValue, "Missing `=` in field-attr") + })?; + + let (type_name, field_name) = + type_field.rsplit_once("::").ok_or_else(|| { + Error::raw( + ErrorKind::InvalidValue, + "Missing `::` in field-attr. Expected format: TYPE::FIELD=ATTR", + ) + })?; + + // Validate the attribute is valid Rust syntax + if let Err(err) = TokenStream::from_str(attr) { + return Err(Error::raw(ErrorKind::InvalidValue, err)); + } + + Ok((type_name.to_owned(), field_name.to_owned(), attr.to_owned())) +} + #[derive(Parser, Debug)] #[clap( about = "Generates Rust bindings from C/C++ headers.", @@ -531,6 +556,9 @@ struct BindgenCommand { /// be called. #[arg(long)] generate_private_functions: bool, + /// Add a custom attribute to a field. The SPEC value must be of the shape TYPE::FIELD=ATTR. + #[arg(long, value_name = "SPEC", value_parser = parse_field_attr)] + field_attr: Vec<(String, String, String)>, /// Whether to emit diagnostics or not. #[cfg(feature = "experimental")] #[arg(long, requires = "experimental")] @@ -684,6 +712,7 @@ where generate_deleted_functions, generate_pure_virtual_functions, generate_private_functions, + field_attr, #[cfg(feature = "experimental")] emit_diagnostics, generate_shell_completions, @@ -981,6 +1010,7 @@ where generate_deleted_functions, generate_pure_virtual_functions, generate_private_functions, + field_attr => |b, (type_name, field_name, attr)| b.field_attribute(type_name, field_name, attr), } ); diff --git a/bindgen/options/mod.rs b/bindgen/options/mod.rs index b9b33a850b..baa541c5ac 100644 --- a/bindgen/options/mod.rs +++ b/bindgen/options/mod.rs @@ -2329,4 +2329,51 @@ options! { }, as_args: "--generate-private-functions", }, + /// Field attribute patterns for adding custom attributes to struct/union fields. + field_attr_patterns: Vec<(Box, Box, Box)> { + methods: { + /// Add a custom attribute to a specific field. + /// + /// # Arguments + /// + /// * `type_name` - The name of the struct or union containing the field + /// * `field_name` - The name of the field (use "0" for newtype tuple fields) + /// * `attribute` - The attribute to add (e.g., "serde(skip)") + /// + /// # Example + /// + /// ```ignore + /// bindgen::Builder::default() + /// .header("input.h") + /// .field_attribute("MyStruct", "data", r#"serde(rename = "myData")"#) + /// .field_attribute("MyStruct", "secret", "serde(skip)") + /// .generate() + /// .unwrap(); + /// ``` + pub fn field_attribute( + mut self, + type_name: T, + field_name: F, + attribute: A, + ) -> Self + where + T: Into, + F: Into, + A: Into, + { + self.options.field_attr_patterns.push(( + type_name.into().into_boxed_str(), + field_name.into().into_boxed_str(), + attribute.into().into_boxed_str(), + )); + self + } + }, + as_args: |patterns, args| { + for (type_pat, field_pat, attr) in patterns { + args.push("--field-attr".to_owned()); + args.push(format!("{type_pat}::{field_pat}={attr}")); + } + }, + }, } From 943d01a843de2715b28fe5379a8640632a622445 Mon Sep 17 00:00:00 2001 From: Jeremy Fitzhardinge Date: Fri, 24 Oct 2025 11:33:18 -0700 Subject: [PATCH 2/2] Dedup code to collect custom field attributes A single function gathers custom field attributes from cli, API and annotations, and then use it for both newtype and regular struct/union fields. --- bindgen/codegen/mod.rs | 116 ++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 60 deletions(-) diff --git a/bindgen/codegen/mod.rs b/bindgen/codegen/mod.rs index 73816e1707..2fcc4e3e1f 100644 --- a/bindgen/codegen/mod.rs +++ b/bindgen/codegen/mod.rs @@ -212,6 +212,40 @@ fn append_custom_derives<'a>( } } +/// Collects field attributes from multiple sources (annotations, callbacks, and CLI/Builder patterns). +fn collect_field_attributes( + ctx: &BindgenContext, + annotations: &Annotations, + type_name: &str, + type_kind: DeriveTypeKind, + field_name: &str, + field_type_name: Option<&str>, +) -> Vec { + let mut all_field_attributes = Vec::new(); + + // 1. Get attributes from annotations + all_field_attributes.extend(annotations.attributes().iter().cloned()); + + // 2. Get custom attributes from callbacks + all_field_attributes.extend(ctx.options().all_callbacks(|cb| { + cb.field_attributes(&FieldAttributeInfo { + type_name, + type_kind, + field_name, + field_type_name, + }) + })); + + // 3. Get attributes from CLI/Builder patterns + for (type_pat, field_pat, attr) in &ctx.options().field_attr_patterns { + if type_pat.as_ref() == type_name && field_pat.as_ref() == field_name { + all_field_attributes.push(attr.to_string()); + } + } + + all_field_attributes +} + impl From for Vec<&'static str> { fn from(derivable_traits: DerivableTraits) -> Vec<&'static str> { [ @@ -1139,39 +1173,16 @@ impl CodeGenerator for Type { .unwrap_or(ctx.options().default_visibility); let access_spec = access_specifier(visibility); - // Collect field attributes from multiple sources for newtype tuple field - let mut all_field_attributes = Vec::new(); - - // 1. Get attributes from typedef annotations (if any) - all_field_attributes.extend( - item.annotations().attributes().iter().cloned(), - ); - - // 2. Get custom attributes from callbacks - all_field_attributes.extend( - ctx.options().all_callbacks(|cb| { - cb.field_attributes(&FieldAttributeInfo { - type_name: &item.canonical_name(ctx), - type_kind: DeriveTypeKind::Struct, - field_name: "0", - field_type_name: inner_item - .expect_type() - .name(), - }) - }), - ); - - // 3. Get attributes from CLI/Builder patterns + // Collect field attributes for newtype tuple field let type_name = item.canonical_name(ctx); - for (type_pat, field_pat, attr) in - &ctx.options().field_attr_patterns - { - if type_pat.as_ref() == type_name && - field_pat.as_ref() == "0" - { - all_field_attributes.push(attr.to_string()); - } - } + let all_field_attributes = collect_field_attributes( + ctx, + item.annotations(), + &type_name, + DeriveTypeKind::Struct, + "0", + inner_item.expect_type().name(), + ); // Build the field with attributes let mut field_tokens = quote! {}; @@ -1618,35 +1629,20 @@ impl FieldCodegen<'_> for FieldData { self.annotations().accessor_kind().unwrap_or(accessor_kind); // Collect field attributes from multiple sources - let mut all_field_attributes = Vec::new(); - - // 1. Get attributes from field annotations (///
) - all_field_attributes - .extend(self.annotations().attributes().iter().cloned()); - - // 2. Get custom attributes from callbacks - all_field_attributes.extend(ctx.options().all_callbacks(|cb| { - cb.field_attributes(&FieldAttributeInfo { - type_name: &parent_item.canonical_name(ctx), - type_kind: if parent.is_union() { - DeriveTypeKind::Union - } else { - DeriveTypeKind::Struct - }, - field_name, - field_type_name: field_ty.name(), - }) - })); - - // 3. Get attributes from CLI/Builder patterns let type_name = parent_item.canonical_name(ctx); - for (type_pat, field_pat, attr) in &ctx.options().field_attr_patterns { - if type_pat.as_ref() == type_name && - field_pat.as_ref() == field_name - { - all_field_attributes.push(attr.to_string()); - } - } + let type_kind = if parent.is_union() { + DeriveTypeKind::Union + } else { + DeriveTypeKind::Struct + }; + let all_field_attributes = collect_field_attributes( + ctx, + self.annotations(), + &type_name, + type_kind, + field_name, + field_ty.name(), + ); // Apply all custom attributes to the field for attr in &all_field_attributes {