Skip to content

Commit cb5eb97

Browse files
committed
feat: 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: /// <div rustbindgen attribute="serde(rename = x_coord)"></div> int x; 2. ParseCallbacks::field_attributes() method for programmatic control: fn field_attributes(&self, info: &FieldAttributeInfo) -> Vec<String> 3. CLI flag and Builder API for pattern-based attributes: --field-attr "Point::x=serde(rename = x_coord)" .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.
1 parent d23fcd2 commit cb5eb97

File tree

8 files changed

+323
-3
lines changed

8 files changed

+323
-3
lines changed

bindgen-tests/tests/expectations/tests/field_attr_annotation.rs

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindgen-tests/tests/expectations/tests/field_attr_cli.rs

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <div rustbindgen deriveDebug></div>
2+
struct Point {
3+
/// <div rustbindgen attribute="serde(rename = x_coord)"></div>
4+
int x;
5+
/// <div rustbindgen attribute="serde(rename = y_coord)"></div>
6+
int y;
7+
};
8+
9+
/// <div rustbindgen deriveDebug></div>
10+
union Data {
11+
/// <div rustbindgen attribute="serde(skip)"></div>
12+
int i;
13+
float f;
14+
};
15+
16+
/// <div rustbindgen attribute="serde(skip)"></div>
17+
typedef int Handle;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// bindgen-flags: --field-attr "Point::x=serde(rename = x_coord)" --field-attr "Point::y=serde(rename = y_coord)" --field-attr "Data::i=serde(skip)" --field-attr "Handle::0=serde(skip)" --new-type-alias "Handle"
2+
3+
struct Point {
4+
int x;
5+
int y;
6+
};
7+
8+
union Data {
9+
int i;
10+
float f;
11+
};
12+
13+
typedef int Handle;

bindgen/callbacks.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,35 @@ pub trait ParseCallbacks: fmt::Debug {
142142
vec![]
143143
}
144144

145+
/// Provide a list of custom attributes for struct/union fields.
146+
///
147+
/// These attributes will be applied to the field in the generated Rust code.
148+
/// If no additional attributes are wanted, this function should return an
149+
/// empty `Vec`.
150+
///
151+
/// # Example
152+
///
153+
/// ```
154+
/// # use bindgen::callbacks::{ParseCallbacks, FieldAttributeInfo};
155+
/// # #[derive(Debug)]
156+
/// # struct MyCallbacks;
157+
/// # impl ParseCallbacks for MyCallbacks {
158+
/// fn field_attributes(&self, info: &FieldAttributeInfo<'_>) -> Vec<String> {
159+
/// if info.field_name == "internal" {
160+
/// vec!["serde(skip)".to_string()]
161+
/// } else if info.field_name == "0" {
162+
/// // Newtype tuple field
163+
/// vec!["serde(transparent)".to_string()]
164+
/// } else {
165+
/// vec![]
166+
/// }
167+
/// }
168+
/// # }
169+
/// ```
170+
fn field_attributes(&self, _info: &FieldAttributeInfo<'_>) -> Vec<String> {
171+
vec![]
172+
}
173+
145174
/// Process a source code comment.
146175
fn process_comment(&self, _comment: &str) -> Option<String> {
147176
None
@@ -334,6 +363,27 @@ pub struct FieldInfo<'a> {
334363
pub field_type_name: Option<&'a str>,
335364
}
336365

366+
/// Relevant information about a field to which new attributes will be added using
367+
/// [`ParseCallbacks::field_attributes`].
368+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369+
#[non_exhaustive]
370+
pub struct FieldAttributeInfo<'a> {
371+
/// The name of the containing type (struct/union).
372+
pub type_name: &'a str,
373+
374+
/// The kind of the containing type.
375+
pub type_kind: TypeKind,
376+
377+
/// The name of the field.
378+
///
379+
/// For newtype tuple structs (when using `--default-alias-style=new_type`),
380+
/// this will be `"0"` for the inner field.
381+
pub field_name: &'a str,
382+
383+
/// The name of the field's type, if available.
384+
pub field_type_name: Option<&'a str>,
385+
}
386+
337387
/// Location in the source code. Roughly equivalent to the same type
338388
/// within `clang_sys`.
339389
#[derive(Clone, Debug, PartialEq, Eq)]

bindgen/codegen/mod.rs

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ use self::struct_layout::StructLayoutTracker;
2121
use super::BindgenOptions;
2222

2323
use crate::callbacks::{
24-
AttributeInfo, DeriveInfo, DiscoveredItem, DiscoveredItemId, FieldInfo,
25-
TypeKind as DeriveTypeKind,
24+
AttributeInfo, DeriveInfo, DiscoveredItem, DiscoveredItemId,
25+
FieldAttributeInfo, FieldInfo, TypeKind as DeriveTypeKind,
2626
};
2727
use crate::codegen::error::Error;
2828
use crate::ir::analysis::{HasVtable, Sizedness};
@@ -1138,8 +1138,48 @@ impl CodeGenerator for Type {
11381138
})
11391139
.unwrap_or(ctx.options().default_visibility);
11401140
let access_spec = access_specifier(visibility);
1141+
1142+
// Collect field attributes from multiple sources for newtype tuple field
1143+
let mut all_field_attributes = Vec::new();
1144+
1145+
// 1. Get attributes from typedef annotations (if any)
1146+
all_field_attributes.extend(
1147+
item.annotations().attributes().iter().cloned()
1148+
);
1149+
1150+
// 2. Get custom attributes from callbacks
1151+
all_field_attributes.extend(ctx.options().all_callbacks(|cb| {
1152+
cb.field_attributes(&FieldAttributeInfo {
1153+
type_name: &item.canonical_name(ctx),
1154+
type_kind: DeriveTypeKind::Struct,
1155+
field_name: "0",
1156+
field_type_name: inner_item.expect_type().name(),
1157+
})
1158+
}));
1159+
1160+
// 3. Get attributes from CLI/Builder patterns
1161+
let type_name = item.canonical_name(ctx);
1162+
for (type_pat, field_pat, attr) in &ctx.options().field_attr_patterns {
1163+
if type_pat.as_ref() == type_name && field_pat.as_ref() == "0" {
1164+
all_field_attributes.push(attr.to_string());
1165+
}
1166+
}
1167+
1168+
// Build the field with attributes
1169+
let mut field_tokens = quote! {};
1170+
for attr in &all_field_attributes {
1171+
let attr_tokens: proc_macro2::TokenStream =
1172+
attr.parse().expect("Invalid field attribute");
1173+
field_tokens.append_all(quote! {
1174+
#[#attr_tokens]
1175+
});
1176+
}
1177+
field_tokens.append_all(quote! {
1178+
#access_spec #inner_rust_type
1179+
});
1180+
11411181
quote! {
1142-
(#access_spec #inner_rust_type) ;
1182+
(#field_tokens) ;
11431183
}
11441184
}
11451185
});
@@ -1569,6 +1609,45 @@ impl FieldCodegen<'_> for FieldData {
15691609
let accessor_kind =
15701610
self.annotations().accessor_kind().unwrap_or(accessor_kind);
15711611

1612+
// Collect field attributes from multiple sources
1613+
let mut all_field_attributes = Vec::new();
1614+
1615+
// 1. Get attributes from field annotations (/// <div rustbindgen attribute="..."></div>)
1616+
all_field_attributes.extend(
1617+
self.annotations().attributes().iter().cloned()
1618+
);
1619+
1620+
// 2. Get custom attributes from callbacks
1621+
all_field_attributes.extend(ctx.options().all_callbacks(|cb| {
1622+
cb.field_attributes(&FieldAttributeInfo {
1623+
type_name: &parent_item.canonical_name(ctx),
1624+
type_kind: if parent.is_union() {
1625+
DeriveTypeKind::Union
1626+
} else {
1627+
DeriveTypeKind::Struct
1628+
},
1629+
field_name,
1630+
field_type_name: field_ty.name(),
1631+
})
1632+
}));
1633+
1634+
// 3. Get attributes from CLI/Builder patterns
1635+
let type_name = parent_item.canonical_name(ctx);
1636+
for (type_pat, field_pat, attr) in &ctx.options().field_attr_patterns {
1637+
if type_pat.as_ref() == type_name && field_pat.as_ref() == field_name {
1638+
all_field_attributes.push(attr.to_string());
1639+
}
1640+
}
1641+
1642+
// Apply all custom attributes to the field
1643+
for attr in &all_field_attributes {
1644+
let attr_tokens: proc_macro2::TokenStream =
1645+
attr.parse().expect("Invalid field attribute");
1646+
field.append_all(quote! {
1647+
#[#attr_tokens]
1648+
});
1649+
}
1650+
15721651
match visibility {
15731652
FieldVisibilityKind::Private => {
15741653
field.append_all(quote! {

bindgen/options/cli.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,27 @@ fn parse_custom_attribute(
137137
Ok((attributes, regex.to_owned()))
138138
}
139139

140+
fn parse_field_attr(
141+
field_attr: &str,
142+
) -> Result<(String, String, String), Error> {
143+
// Parse format: TYPE::FIELD=ATTR
144+
// We need to split on the first '=' after finding the '::'
145+
let (type_field, attr) = field_attr
146+
.split_once('=')
147+
.ok_or_else(|| Error::raw(ErrorKind::InvalidValue, "Missing `=` in field-attr"))?;
148+
149+
let (type_name, field_name) = type_field
150+
.rsplit_once("::")
151+
.ok_or_else(|| Error::raw(ErrorKind::InvalidValue, "Missing `::` in field-attr. Expected format: TYPE::FIELD=ATTR"))?;
152+
153+
// Validate the attribute is valid Rust syntax
154+
if let Err(err) = TokenStream::from_str(attr) {
155+
return Err(Error::raw(ErrorKind::InvalidValue, err));
156+
}
157+
158+
Ok((type_name.to_owned(), field_name.to_owned(), attr.to_owned()))
159+
}
160+
140161
#[derive(Parser, Debug)]
141162
#[clap(
142163
about = "Generates Rust bindings from C/C++ headers.",
@@ -531,6 +552,9 @@ struct BindgenCommand {
531552
/// be called.
532553
#[arg(long)]
533554
generate_private_functions: bool,
555+
/// Add a custom attribute to a field. The SPEC value must be of the shape TYPE::FIELD=ATTR.
556+
#[arg(long, value_name = "SPEC", value_parser = parse_field_attr)]
557+
field_attr: Vec<(String, String, String)>,
534558
/// Whether to emit diagnostics or not.
535559
#[cfg(feature = "experimental")]
536560
#[arg(long, requires = "experimental")]
@@ -684,6 +708,7 @@ where
684708
generate_deleted_functions,
685709
generate_pure_virtual_functions,
686710
generate_private_functions,
711+
field_attr,
687712
#[cfg(feature = "experimental")]
688713
emit_diagnostics,
689714
generate_shell_completions,
@@ -981,6 +1006,7 @@ where
9811006
generate_deleted_functions,
9821007
generate_pure_virtual_functions,
9831008
generate_private_functions,
1009+
field_attr => |b, (type_name, field_name, attr)| b.field_attribute(type_name, field_name, attr),
9841010
}
9851011
);
9861012

bindgen/options/mod.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2329,4 +2329,51 @@ options! {
23292329
},
23302330
as_args: "--generate-private-functions",
23312331
},
2332+
/// Field attribute patterns for adding custom attributes to struct/union fields.
2333+
field_attr_patterns: Vec<(Box<str>, Box<str>, Box<str>)> {
2334+
methods: {
2335+
/// Add a custom attribute to a specific field.
2336+
///
2337+
/// # Arguments
2338+
///
2339+
/// * `type_name` - The name of the struct or union containing the field
2340+
/// * `field_name` - The name of the field (use "0" for newtype tuple fields)
2341+
/// * `attribute` - The attribute to add (e.g., "serde(skip)")
2342+
///
2343+
/// # Example
2344+
///
2345+
/// ```ignore
2346+
/// bindgen::Builder::default()
2347+
/// .header("input.h")
2348+
/// .field_attribute("MyStruct", "data", r#"serde(rename = "myData")"#)
2349+
/// .field_attribute("MyStruct", "secret", "serde(skip)")
2350+
/// .generate()
2351+
/// .unwrap();
2352+
/// ```
2353+
pub fn field_attribute<T, F, A>(
2354+
mut self,
2355+
type_name: T,
2356+
field_name: F,
2357+
attribute: A,
2358+
) -> Self
2359+
where
2360+
T: Into<String>,
2361+
F: Into<String>,
2362+
A: Into<String>,
2363+
{
2364+
self.options.field_attr_patterns.push((
2365+
type_name.into().into_boxed_str(),
2366+
field_name.into().into_boxed_str(),
2367+
attribute.into().into_boxed_str(),
2368+
));
2369+
self
2370+
}
2371+
},
2372+
as_args: |patterns, args| {
2373+
for (type_pat, field_pat, attr) in patterns {
2374+
args.push("--field-attr".to_owned());
2375+
args.push(format!("{type_pat}::{field_pat}={attr}"));
2376+
}
2377+
},
2378+
},
23322379
}

0 commit comments

Comments
 (0)