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..2fcc4e3e1f 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};
@@ -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> {
[
@@ -1138,8 +1172,33 @@ impl CodeGenerator for Type {
})
.unwrap_or(ctx.options().default_visibility);
let access_spec = access_specifier(visibility);
+
+ // Collect field attributes for newtype tuple field
+ let type_name = item.canonical_name(ctx);
+ 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! {};
+ 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 +1628,31 @@ impl FieldCodegen<'_> for FieldData {
let accessor_kind =
self.annotations().accessor_kind().unwrap_or(accessor_kind);
+ // Collect field attributes from multiple sources
+ let type_name = parent_item.canonical_name(ctx);
+ 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 {
+ 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}"));
+ }
+ },
+ },
}