From f819034c69c6abd669e7ef12080d53546fdb1d3b Mon Sep 17 00:00:00 2001 From: Nicolas Pauss Date: Mon, 7 Jul 2025 12:26:36 +0200 Subject: [PATCH] Add ParseCallbacks::allow_or_block_item(). Like `allowlist_item` and `blocklist_item` options, add new methods to `ParseCallbacks`, `ParseCallbacks::allow_item()` and `ParseCallbacks::block_item()`, to be able to allow and block items from being generated. `allowlist_*` and `blocklist_*` options work with regexes and are inserted in a RegexSet. There are two issues with this approach: 1. In some cases, we want to have more flexibility than just using regexes. If we want to have dynamic conditions to allow or block items, using regexes can be limited. 2. RegexSet scales linearly with the number of elements inserted. This means that if we have a huge number of items that we want to allow or block, regexes and RegexSet are not necessarily the most appropriate data structures. Using a new method in `ParseCallbacks` solves these two issues. We can manually decide the appropriate rules and data structure to match the items. This method takes precedences over the `allowlist_*` options. If at least one of the parse callbacks returns `Block`, the generation of the bindings for the item is blocked. If all the parse callbacks that don't return `None` return `Allow`, the bindings for the item are generated. If all the parse callbacks return `None` (the default implementation), the `allowlist_*` options are used instead. --- .../expectations/tests/allow-item-callback.rs | 65 +++++++++++++++++++ .../expectations/tests/block-item-callback.rs | 52 +++++++++++++++ .../tests/headers/allow-item-callback.h | 38 +++++++++++ .../tests/headers/block-item-callback.h | 38 +++++++++++ bindgen-tests/tests/parse_callbacks/mod.rs | 28 ++++++++ bindgen/callbacks.rs | 30 +++++++++ bindgen/codegen/mod.rs | 35 ++++++++-- bindgen/ir/context.rs | 13 ++++ bindgen/ir/item.rs | 35 +++++++--- bindgen/lib.rs | 32 +++++++++ 10 files changed, 351 insertions(+), 15 deletions(-) create mode 100644 bindgen-tests/tests/expectations/tests/allow-item-callback.rs create mode 100644 bindgen-tests/tests/expectations/tests/block-item-callback.rs create mode 100644 bindgen-tests/tests/headers/allow-item-callback.h create mode 100644 bindgen-tests/tests/headers/block-item-callback.h diff --git a/bindgen-tests/tests/expectations/tests/allow-item-callback.rs b/bindgen-tests/tests/expectations/tests/allow-item-callback.rs new file mode 100644 index 0000000000..aea5999cc3 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/allow-item-callback.rs @@ -0,0 +1,65 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct allowed_my_struct { + pub a: ::std::os::raw::c_int, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of allowed_my_struct"][::std::mem::size_of::() - 4usize]; + [ + "Alignment of allowed_my_struct", + ][::std::mem::align_of::() - 4usize]; + [ + "Offset of field: allowed_my_struct::a", + ][::std::mem::offset_of!(allowed_my_struct, a) - 0usize]; +}; +#[repr(C)] +#[derive(Copy, Clone)] +pub union allowed_my_union { + pub a: ::std::os::raw::c_int, + pub b: f64, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of allowed_my_union"][::std::mem::size_of::() - 8usize]; + [ + "Alignment of allowed_my_union", + ][::std::mem::align_of::() - 8usize]; + [ + "Offset of field: allowed_my_union::a", + ][::std::mem::offset_of!(allowed_my_union, a) - 0usize]; + [ + "Offset of field: allowed_my_union::b", + ][::std::mem::offset_of!(allowed_my_union, b) - 0usize]; +}; +impl Default for allowed_my_union { + 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 const allowed_my_enum_ALLOWED_MY_ENUM_A: allowed_my_enum = 0; +pub const allowed_my_enum_ALLOWED_MY_ENUM_B: allowed_my_enum = 1; +pub type allowed_my_enum = ::std::os::raw::c_uint; +pub const allowed_my_const: ::std::os::raw::c_int = 10; +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct list_allowed_my_struct { + pub a: ::std::os::raw::c_int, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + [ + "Size of list_allowed_my_struct", + ][::std::mem::size_of::() - 4usize]; + [ + "Alignment of list_allowed_my_struct", + ][::std::mem::align_of::() - 4usize]; + [ + "Offset of field: list_allowed_my_struct::a", + ][::std::mem::offset_of!(list_allowed_my_struct, a) - 0usize]; +}; diff --git a/bindgen-tests/tests/expectations/tests/block-item-callback.rs b/bindgen-tests/tests/expectations/tests/block-item-callback.rs new file mode 100644 index 0000000000..fbf2a4350a --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/block-item-callback.rs @@ -0,0 +1,52 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct non_blocked_my_struct { + pub a: ::std::os::raw::c_int, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + [ + "Size of non_blocked_my_struct", + ][::std::mem::size_of::() - 4usize]; + [ + "Alignment of non_blocked_my_struct", + ][::std::mem::align_of::() - 4usize]; + [ + "Offset of field: non_blocked_my_struct::a", + ][::std::mem::offset_of!(non_blocked_my_struct, a) - 0usize]; +}; +#[repr(C)] +#[derive(Copy, Clone)] +pub union non_blocked_my_union { + pub a: ::std::os::raw::c_int, + pub b: f64, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + [ + "Size of non_blocked_my_union", + ][::std::mem::size_of::() - 8usize]; + [ + "Alignment of non_blocked_my_union", + ][::std::mem::align_of::() - 8usize]; + [ + "Offset of field: non_blocked_my_union::a", + ][::std::mem::offset_of!(non_blocked_my_union, a) - 0usize]; + [ + "Offset of field: non_blocked_my_union::b", + ][::std::mem::offset_of!(non_blocked_my_union, b) - 0usize]; +}; +impl Default for non_blocked_my_union { + 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 const non_blocked_my_enum_NON_BLOCKED_MY_ENUM_A: non_blocked_my_enum = 0; +pub const non_blocked_my_enum_NON_BLOCKED_MY_ENUM_B: non_blocked_my_enum = 1; +pub type non_blocked_my_enum = ::std::os::raw::c_uint; +pub const non_blocked_my_const: ::std::os::raw::c_int = 10; diff --git a/bindgen-tests/tests/headers/allow-item-callback.h b/bindgen-tests/tests/headers/allow-item-callback.h new file mode 100644 index 0000000000..b727987398 --- /dev/null +++ b/bindgen-tests/tests/headers/allow-item-callback.h @@ -0,0 +1,38 @@ +// bindgen-flags: --allowlist-item 'list_allowed_.*' +// bindgen-parse-callbacks: allow-item + +struct allowed_my_struct { + int a; +}; + +union allowed_my_union { + int a; + double b; +}; + +enum allowed_my_enum { + ALLOWED_MY_ENUM_A, + ALLOWED_MY_ENUM_B, +}; + +static const int allowed_my_const = 10; + +struct non_allowed_my_struct { + int a; +}; + +union non_allowed_my_union { + int a; + double b; +}; + +enum non_allowed_my_enum { + NON_ALLOWED_MY_ENUM_A, + NON_ALLOWED_MY_ENUM_B, +}; + +static const int non_allowed_my_const = 10; + +struct list_allowed_my_struct { + int a; +}; diff --git a/bindgen-tests/tests/headers/block-item-callback.h b/bindgen-tests/tests/headers/block-item-callback.h new file mode 100644 index 0000000000..046f3475b1 --- /dev/null +++ b/bindgen-tests/tests/headers/block-item-callback.h @@ -0,0 +1,38 @@ +// bindgen-flags: --blocklist-item 'list_blocked_.*' +// bindgen-parse-callbacks: block-item + +struct blocked_my_struct { + int a; +}; + +union blocked_my_union { + int a; + double b; +}; + +enum blocked_my_enum { + BLOCKED_MY_ENUM_A, + BLOCKED_MY_ENUM_B, +}; + +static const int blocked_my_const = 10; + +struct non_blocked_my_struct { + int a; +}; + +union non_blocked_my_union { + int a; + double b; +}; + +enum non_blocked_my_enum { + NON_BLOCKED_MY_ENUM_A, + NON_BLOCKED_MY_ENUM_B, +}; + +static const int non_blocked_my_const = 10; + +struct list_blocked_my_struct { + int a; +}; diff --git a/bindgen-tests/tests/parse_callbacks/mod.rs b/bindgen-tests/tests/parse_callbacks/mod.rs index 02d7fe8316..5bea1f10ef 100644 --- a/bindgen-tests/tests/parse_callbacks/mod.rs +++ b/bindgen-tests/tests/parse_callbacks/mod.rs @@ -160,6 +160,32 @@ impl ParseCallbacks for OperatorRename { } } +#[derive(Debug)] +struct AllowItem; + +impl ParseCallbacks for AllowItem { + fn allow_or_block_item(&self, item: &ItemInfo) -> Option { + if item.name.starts_with("allowed_") { + Some(AllowOrBlockItem::Allow) + } else { + None + } + } +} + +#[derive(Debug)] +struct BlockItem; + +impl ParseCallbacks for BlockItem { + fn allow_or_block_item(&self, item: &ItemInfo) -> Option { + if item.name.starts_with("blocked_") { + Some(AllowOrBlockItem::Block) + } else { + None + } + } +} + pub fn lookup(cb: &str) -> Box { match cb { "enum-variant-rename" => Box::new(EnumVariantRename), @@ -169,6 +195,8 @@ pub fn lookup(cb: &str) -> Box { "wrap-as-variadic-fn" => Box::new(WrapAsVariadicFn), "type-visibility" => Box::new(TypeVisibility), "operator-rename" => Box::new(OperatorRename), + "allow-item" => Box::new(AllowItem), + "block-item" => Box::new(BlockItem), call_back => { if let Some(prefix) = call_back.strip_prefix("remove-function-prefix-") diff --git a/bindgen/callbacks.rs b/bindgen/callbacks.rs index 630a306aec..71086bedef 100644 --- a/bindgen/callbacks.rs +++ b/bindgen/callbacks.rs @@ -19,6 +19,16 @@ pub enum MacroParsingBehavior { Default, } +/// Enum to indicate if the bindings for a given should be generated or blocked. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum AllowOrBlockItem { + /// Generate the bindings for the given item. + Allow, + + /// Block bindings for the given item. + Block, +} + /// A trait to allow configuring different kinds of types in different /// situations. pub trait ParseCallbacks: fmt::Debug { @@ -206,6 +216,26 @@ pub trait ParseCallbacks: fmt::Debug { ) { } + /// Generate or block the bindings for the given item. + /// + /// This method takes precedences over the `allowlist_*` options. + /// + /// If at least one of the parse callbacks returns `Block`, the generation of the bindings + /// for the item is blocked. + /// + /// If all the parse callbacks that don't return `None` return `Allow`, the bindings + /// for the item are generated. + /// + /// If all the parse callbacks return `None` (the default implementation), the `allowlist_*` + /// options are used instead. + /// + fn allow_or_block_item( + &self, + _item: &ItemInfo, + ) -> Option { + None + } + // TODO add callback for ResolvedTypeRef } diff --git a/bindgen/codegen/mod.rs b/bindgen/codegen/mod.rs index a28f5b33e5..4914c821ca 100644 --- a/bindgen/codegen/mod.rs +++ b/bindgen/codegen/mod.rs @@ -22,7 +22,8 @@ use super::BindgenOptions; use crate::callbacks::{ AttributeInfo, DeriveInfo, DiscoveredItem, DiscoveredItemId, - FieldAttributeInfo, FieldInfo, TypeKind as DeriveTypeKind, + FieldAttributeInfo, FieldInfo, ItemInfo, ItemKind as CallbackItemKind, + TypeKind as DeriveTypeKind, }; use crate::codegen::error::Error; use crate::ir::analysis::{HasVtable, Sizedness}; @@ -5008,7 +5009,18 @@ fn objc_method_codegen( // Item::process_before_codegen; however, ObjC methods are not currently // made into function items. let name = format!("{rust_class_name}::{prefix}{}", method.rust_name()); - if ctx.options().blocklisted_items.matches(name) { + + let item_info = ItemInfo { + name: &name, + kind: CallbackItemKind::Function, + }; + + if ctx + .options() + .cb_item_is_blocked(&item_info) + .unwrap_or_else(|| ctx.options().blocklisted_items.matches(&name)) + { + // Item is blocked through the parse callbacks or `blocklisted_items`. return; } @@ -5325,7 +5337,9 @@ pub(crate) mod utils { use super::helpers::BITFIELD_UNIT; use super::serialize::CSerialize; use super::{error, CodegenError, CodegenResult, ToRustTyOrOpaque}; - use crate::callbacks::DiscoveredItemId; + use crate::callbacks::{ + DiscoveredItemId, ItemInfo, ItemKind as CallbackItemKind, + }; use crate::ir::context::BindgenContext; use crate::ir::context::TypeId; use crate::ir::function::{Abi, ClangAbi, FunctionSig}; @@ -5456,9 +5470,20 @@ pub(crate) mod utils { ctx: &BindgenContext, result: &mut Vec, ) { - if ctx.options().blocklisted_items.matches(BITFIELD_UNIT) || - ctx.options().blocklisted_types.matches(BITFIELD_UNIT) + let item_info = ItemInfo { + name: BITFIELD_UNIT, + kind: CallbackItemKind::Type, + }; + + if ctx + .options() + .cb_item_is_blocked(&item_info) + .unwrap_or_else(|| { + ctx.options().blocklisted_items.matches(BITFIELD_UNIT) || + ctx.options().blocklisted_types.matches(BITFIELD_UNIT) + }) { + // Item is blocked through the parse callbacks or blocklists. return; } diff --git a/bindgen/ir/context.rs b/bindgen/ir/context.rs index 8e4163df5e..095ea6e07a 100644 --- a/bindgen/ir/context.rs +++ b/bindgen/ir/context.rs @@ -21,6 +21,7 @@ use super::traversal::{self, Edge, ItemTraversal}; use super::ty::{FloatKind, Type, TypeKind}; use crate::clang::{self, ABIKind, Cursor}; use crate::codegen::CodegenError; +use crate::ir::item::ItemCanonicalName; use crate::BindgenOptions; use crate::{Entry, HashMap, HashSet}; @@ -2406,6 +2407,18 @@ If you encounter an error missing from this list, please file an issue or a PR!" // Only consider roots that are enabled for codegen. .filter(|&(_, item)| item.is_enabled_for_codegen(self)) .filter(|&(_, item)| { + let item_info = crate::callbacks::ItemInfo { + name: &item.canonical_name(self), + kind: item.callback_item_kind(), + }; + + if let Some(is_cb_allow) = + self.options().cb_item_is_allowed(&item_info) + { + // Item is allowed or not with the parse callbacks. + return is_cb_allow; + } + // If nothing is explicitly allowlisted, then everything is fair // game. if self.options().allowlisted_types.is_empty() && diff --git a/bindgen/ir/item.rs b/bindgen/ir/item.rs index eea02cce6c..42233676d3 100644 --- a/bindgen/ir/item.rs +++ b/bindgen/ir/item.rs @@ -641,6 +641,20 @@ impl Item { return true; } + let path = self.path_for_allowlisting(ctx); + let name = path[1..].join("::"); + let item_info = ItemInfo { + name: &name, + kind: self.callback_item_kind(), + }; + + if let Some(is_cb_blocked) = + ctx.options().cb_item_is_blocked(&item_info) + { + // The item is blocked or not with the parse callbacks. + return is_cb_blocked; + } + if !ctx.options().blocklisted_files.is_empty() { if let Some(location) = &self.location { let (file, _, _, _) = location.location(); @@ -652,8 +666,6 @@ impl Item { } } - let path = self.path_for_allowlisting(ctx); - let name = path[1..].join("::"); ctx.options().blocklisted_items.matches(&name) || match self.kind { ItemKind::Type(..) => { @@ -819,6 +831,16 @@ impl Item { } } + /// Get the callback item kind of this item. + pub(crate) fn callback_item_kind(&self) -> crate::callbacks::ItemKind { + match self.kind() { + ItemKind::Module(..) => crate::callbacks::ItemKind::Module, + ItemKind::Type(..) => crate::callbacks::ItemKind::Type, + ItemKind::Function(..) => crate::callbacks::ItemKind::Function, + ItemKind::Var(..) => crate::callbacks::ItemKind::Var, + } + } + /// Get the canonical name without taking into account the replaces /// annotation. /// @@ -926,14 +948,7 @@ impl Item { let name = if opt.user_mangled == UserMangled::Yes { let item_info = ItemInfo { name: &name, - kind: match self.kind() { - ItemKind::Module(..) => crate::callbacks::ItemKind::Module, - ItemKind::Type(..) => crate::callbacks::ItemKind::Type, - ItemKind::Function(..) => { - crate::callbacks::ItemKind::Function - } - ItemKind::Var(..) => crate::callbacks::ItemKind::Var, - }, + kind: self.callback_item_kind(), }; ctx.options() .last_callback(|callbacks| callbacks.item_name(item_info)) diff --git a/bindgen/lib.rs b/bindgen/lib.rs index 0305b5cd7b..9da7a0f8d2 100644 --- a/bindgen/lib.rs +++ b/bindgen/lib.rs @@ -586,6 +586,38 @@ impl BindgenOptions { self.parse_callbacks.iter().for_each(|cb| f(cb.as_ref())); } + fn cb_item_is_allowed( + &self, + item_info: &callbacks::ItemInfo, + ) -> Option { + let mut res = None; + + for cb in &self.parse_callbacks { + if let Some(allow_or_block) = cb.allow_or_block_item(item_info) { + match allow_or_block { + callbacks::AllowOrBlockItem::Allow => { + // Continue to check if some other callbacks returns `Block`. + res = Some(true); + } + callbacks::AllowOrBlockItem::Block => { + // Return `false` immediately to block the bindings generation. + return Some(false); + } + } + } + } + + res + } + + fn cb_item_is_blocked( + &self, + item_info: &callbacks::ItemInfo, + ) -> Option { + self.cb_item_is_allowed(item_info) + .map(|is_allowed| !is_allowed) + } + fn process_comment(&self, comment: &str) -> String { let comment = comment::preprocess(comment); self.last_callback(|cb| cb.process_comment(&comment))