Skip to content

Commit 5b9816f

Browse files
committed
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 /// <div rustbindgen attribute="serde(rename = x_coord)"></div> int x; ``` 2. ParseCallbacks::field_attributes() method for programmatic control: ```rust fn field_attributes(&self, info: &FieldAttributeInfo) -> Vec<String> ``` 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.
1 parent d23fcd2 commit 5b9816f

File tree

8 files changed

+336
-3
lines changed

8 files changed

+336
-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="cfg(test)"></div>
4+
int x;
5+
/// <div rustbindgen attribute="allow(dead_code)"></div>
6+
int y;
7+
};
8+
9+
/// <div rustbindgen deriveDebug></div>
10+
union Data {
11+
/// <div rustbindgen attribute="allow(dead_code)"></div>
12+
int i;
13+
float f;
14+
};
15+
16+
/// <div rustbindgen attribute="cfg(test)"></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=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"
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: 91 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,56 @@ 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(
1152+
ctx.options().all_callbacks(|cb| {
1153+
cb.field_attributes(&FieldAttributeInfo {
1154+
type_name: &item.canonical_name(ctx),
1155+
type_kind: DeriveTypeKind::Struct,
1156+
field_name: "0",
1157+
field_type_name: inner_item
1158+
.expect_type()
1159+
.name(),
1160+
})
1161+
}),
1162+
);
1163+
1164+
// 3. Get attributes from CLI/Builder patterns
1165+
let type_name = item.canonical_name(ctx);
1166+
for (type_pat, field_pat, attr) in
1167+
&ctx.options().field_attr_patterns
1168+
{
1169+
if type_pat.as_ref() == type_name &&
1170+
field_pat.as_ref() == "0"
1171+
{
1172+
all_field_attributes.push(attr.to_string());
1173+
}
1174+
}
1175+
1176+
// Build the field with attributes
1177+
let mut field_tokens = quote! {};
1178+
for attr in &all_field_attributes {
1179+
let attr_tokens: proc_macro2::TokenStream =
1180+
attr.parse().expect("Invalid field attribute");
1181+
field_tokens.append_all(quote! {
1182+
#[#attr_tokens]
1183+
});
1184+
}
1185+
field_tokens.append_all(quote! {
1186+
#access_spec #inner_rust_type
1187+
});
1188+
11411189
quote! {
1142-
(#access_spec #inner_rust_type) ;
1190+
(#field_tokens) ;
11431191
}
11441192
}
11451193
});
@@ -1569,6 +1617,46 @@ impl FieldCodegen<'_> for FieldData {
15691617
let accessor_kind =
15701618
self.annotations().accessor_kind().unwrap_or(accessor_kind);
15711619

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

bindgen/options/cli.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,31 @@ 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.split_once('=').ok_or_else(|| {
146+
Error::raw(ErrorKind::InvalidValue, "Missing `=` in field-attr")
147+
})?;
148+
149+
let (type_name, field_name) =
150+
type_field.rsplit_once("::").ok_or_else(|| {
151+
Error::raw(
152+
ErrorKind::InvalidValue,
153+
"Missing `::` in field-attr. Expected format: TYPE::FIELD=ATTR",
154+
)
155+
})?;
156+
157+
// Validate the attribute is valid Rust syntax
158+
if let Err(err) = TokenStream::from_str(attr) {
159+
return Err(Error::raw(ErrorKind::InvalidValue, err));
160+
}
161+
162+
Ok((type_name.to_owned(), field_name.to_owned(), attr.to_owned()))
163+
}
164+
140165
#[derive(Parser, Debug)]
141166
#[clap(
142167
about = "Generates Rust bindings from C/C++ headers.",
@@ -531,6 +556,9 @@ struct BindgenCommand {
531556
/// be called.
532557
#[arg(long)]
533558
generate_private_functions: bool,
559+
/// Add a custom attribute to a field. The SPEC value must be of the shape TYPE::FIELD=ATTR.
560+
#[arg(long, value_name = "SPEC", value_parser = parse_field_attr)]
561+
field_attr: Vec<(String, String, String)>,
534562
/// Whether to emit diagnostics or not.
535563
#[cfg(feature = "experimental")]
536564
#[arg(long, requires = "experimental")]
@@ -684,6 +712,7 @@ where
684712
generate_deleted_functions,
685713
generate_pure_virtual_functions,
686714
generate_private_functions,
715+
field_attr,
687716
#[cfg(feature = "experimental")]
688717
emit_diagnostics,
689718
generate_shell_completions,
@@ -981,6 +1010,7 @@ where
9811010
generate_deleted_functions,
9821011
generate_pure_virtual_functions,
9831012
generate_private_functions,
1013+
field_attr => |b, (type_name, field_name, attr)| b.field_attribute(type_name, field_name, attr),
9841014
}
9851015
);
9861016

0 commit comments

Comments
 (0)