From 3b8da2818e9d41665d25cb567eb48ceae5f5ca20 Mon Sep 17 00:00:00 2001 From: fly1d <309400591+fly1d@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:57:31 +0800 Subject: [PATCH] Handle C++23 explicit object member functions Detect explicit object parameters from the first parameter token because libclang does not expose this distinction directly. Represent those members separately from static methods so their declared object parameter is preserved without injecting an implicit receiver. Move the regression into the standard header expectation infrastructure with a minimum libclang version marker, covering value and reference receivers while leaving ordinary member functions unchanged. Fixes #3441 Developed with assistance from OpenAI Codex. --- .../tests/expectations/tests/issue-3441.rs | 40 +++++++++++++++++++ bindgen-tests/tests/headers/issue-3441.hpp | 8 ++++ bindgen-tests/tests/tests.rs | 26 ++++++++++++ bindgen/clang.rs | 14 +++++++ bindgen/codegen/mod.rs | 8 +++- bindgen/ir/comp.rs | 11 ++++- bindgen/ir/function.rs | 8 +++- bindgen/ir/item.rs | 1 + 8 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 bindgen-tests/tests/expectations/tests/issue-3441.rs create mode 100644 bindgen-tests/tests/headers/issue-3441.hpp diff --git a/bindgen-tests/tests/expectations/tests/issue-3441.rs b/bindgen-tests/tests/expectations/tests/issue-3441.rs new file mode 100644 index 0000000000..747ec3d2ef --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/issue-3441.rs @@ -0,0 +1,40 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct S { + pub _address: u8, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of S"][::std::mem::size_of::() - 1usize]; + ["Alignment of S"][::std::mem::align_of::() - 1usize]; +}; +unsafe extern "C" { + #[link_name = "\u{1}_ZNH1S5valueES_i"] + pub fn S_value(self_: S, y: ::std::os::raw::c_int) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[link_name = "\u{1}_ZNH1S9referenceERS_i"] + pub fn S_reference(self_: *mut S, y: ::std::os::raw::c_int) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[link_name = "\u{1}_ZN1S7regularEi"] + pub fn S_regular(this: *mut S, y: ::std::os::raw::c_int) -> ::std::os::raw::c_int; +} +impl S { + #[inline] + pub unsafe fn value(self_: S, y: ::std::os::raw::c_int) -> ::std::os::raw::c_int { + S_value(self_, y) + } + #[inline] + pub unsafe fn reference( + self_: *mut S, + y: ::std::os::raw::c_int, + ) -> ::std::os::raw::c_int { + S_reference(self_, y) + } + #[inline] + pub unsafe fn regular(&mut self, y: ::std::os::raw::c_int) -> ::std::os::raw::c_int { + S_regular(self, y) + } +} diff --git a/bindgen-tests/tests/headers/issue-3441.hpp b/bindgen-tests/tests/headers/issue-3441.hpp new file mode 100644 index 0000000000..4938bb5c0b --- /dev/null +++ b/bindgen-tests/tests/headers/issue-3441.hpp @@ -0,0 +1,8 @@ +// bindgen-flags: -- -std=c++23 +// bindgen-min-clang-version: 18 + +struct S { + int value(this S self, int y); + int reference([[maybe_unused]] this S& self, int y); + int regular(int y); +}; diff --git a/bindgen-tests/tests/tests.rs b/bindgen-tests/tests/tests.rs index 77d86bd45a..2cde8c0441 100644 --- a/bindgen-tests/tests/tests.rs +++ b/bindgen-tests/tests/tests.rs @@ -124,6 +124,17 @@ fn compare_generated_header( builder: BuilderState, check_roundtrip: bool, ) -> Result<(), Error> { + let skip_for_clang_version = if let Some(minimum) = + builder.minimum_clang_version + { + matches!(clang_version().parsed, Some((major, _)) if major < minimum) + } else { + false + }; + if skip_for_clang_version { + return Ok(()); + } + let file_name = header.file_name().ok_or_else(|| { Error::new(ErrorKind::Other, "compare_generated_header expects a file") })?; @@ -241,6 +252,7 @@ fn builder() -> Builder { struct BuilderState { builder: Builder, parse_callbacks: Option, + minimum_clang_version: Option, } impl BuilderState { @@ -259,6 +271,7 @@ impl BuilderState { Some(BuilderState { builder, parse_callbacks: self.parse_callbacks, + minimum_clang_version: self.minimum_clang_version, }) } else { None @@ -277,6 +290,7 @@ fn create_bindgen_builder(header: &Path) -> Result { // Scoop up bindgen-flags from test header let mut flags = Vec::with_capacity(2); let mut parse_callbacks = None; + let mut minimum_clang_version = None; for line in reader.lines() { let line = line?; @@ -302,6 +316,17 @@ fn create_bindgen_builder(header: &Path) -> Result { let parse_cb = line.split("bindgen-parse-callbacks: ").last().unwrap(); parse_callbacks = Some(parse_cb.to_owned()); + } else if line.contains("bindgen-min-clang-version: ") { + let version = + line.split("bindgen-min-clang-version: ").last().unwrap(); + minimum_clang_version = Some(version.parse().map_err(|err| { + Error::new( + ErrorKind::InvalidData, + format!( + "Invalid minimum libclang version '{version}': {err}" + ), + ) + })?); } } @@ -349,6 +374,7 @@ fn create_bindgen_builder(header: &Path) -> Result { Ok(BuilderState { builder, parse_callbacks, + minimum_clang_version, }) } diff --git a/bindgen/clang.rs b/bindgen/clang.rs index 9e614da9f8..3960d2d416 100644 --- a/bindgen/clang.rs +++ b/bindgen/clang.rs @@ -907,6 +907,20 @@ impl Cursor { unsafe { clang_CXXMethod_isStatic(self.x) != 0 } } + /// Is this cursor's referent a C++23 explicit-object member function? + pub(crate) fn method_is_explicit_object(&self) -> bool { + debug_assert_eq!(self.kind(), CXCursor_CXXMethod); + + // libclang has no direct API for this. The first parameter's extent + // starts at `this`, even when attributes precede the parameter. + self.args() + .and_then(|args| args.into_iter().next()) + .and_then(|arg| arg.tokens().iter().next()) + .is_some_and(|token| { + token.kind == CXToken_Keyword && token.spelling() == b"this" + }) + } + /// Is this cursor's referent a member function that is declared `const`? pub(crate) fn method_is_const(&self) -> bool { unsafe { clang_CXXMethod_isConst(self.x) != 0 } diff --git a/bindgen/codegen/mod.rs b/bindgen/codegen/mod.rs index 11bb26a021..277b45e5fc 100644 --- a/bindgen/codegen/mod.rs +++ b/bindgen/codegen/mod.rs @@ -3164,6 +3164,7 @@ impl Method { MethodKind::Destructor | MethodKind::VirtualDestructor { .. } => cc.destructors(), MethodKind::Static | + MethodKind::ExplicitObject | MethodKind::Normal | MethodKind::Virtual { .. } => cc.methods(), } @@ -3237,7 +3238,10 @@ impl Method { let mut args = utils::fnsig_arguments(ctx, signature); let mut ret = utils::fnsig_return_ty(ctx, signature); - if !self.is_static() && !self.is_constructor() { + if !self.is_static() && + !self.is_explicit_object() && + !self.is_constructor() + { args[0] = if self.is_const() { quote! { &self } } else { @@ -3271,7 +3275,7 @@ impl Method { let mut __bindgen_tmp = ::#prefix::mem::MaybeUninit::uninit() }; stmts.push(tmp_variable_decl); - } else if !self.is_static() { + } else if !self.is_static() && !self.is_explicit_object() { assert!(!exprs.is_empty()); exprs[0] = quote! { self diff --git a/bindgen/ir/comp.rs b/bindgen/ir/comp.rs index 5aae745131..17750098f3 100644 --- a/bindgen/ir/comp.rs +++ b/bindgen/ir/comp.rs @@ -44,6 +44,8 @@ pub(crate) enum MethodKind { }, /// A static method. Static, + /// A method with an explicit object parameter. + ExplicitObject, /// A normal method. Normal, /// A virtual method. @@ -72,7 +74,7 @@ impl MethodKind { } } -/// A struct representing a C++ method, either static, normal, or virtual. +/// A struct representing a C++ method. #[derive(Debug)] pub(crate) struct Method { kind: MethodKind, @@ -121,6 +123,11 @@ impl Method { self.kind == MethodKind::Static } + /// Does this method have an explicit object parameter? + pub(crate) fn is_explicit_object(&self) -> bool { + self.kind == MethodKind::ExplicitObject + } + /// Get the ID for the `Function` signature for this method. pub(crate) fn signature(&self) -> FunctionId { self.signature @@ -1581,6 +1588,8 @@ impl CompInfo { MethodKind::Virtual { pure_virtual: cur.method_is_pure_virtual(), } + } else if cur.method_is_explicit_object() { + MethodKind::ExplicitObject } else { MethodKind::Normal }; diff --git a/bindgen/ir/function.rs b/bindgen/ir/function.rs index f329c07d0d..7cc274563c 100644 --- a/bindgen/ir/function.rs +++ b/bindgen/ir/function.rs @@ -52,6 +52,8 @@ impl FunctionKind { }) } else if cursor.method_is_static() { FunctionKind::Method(MethodKind::Static) + } else if cursor.method_is_explicit_object() { + FunctionKind::Method(MethodKind::ExplicitObject) } else { FunctionKind::Method(MethodKind::Normal) } @@ -528,8 +530,10 @@ impl FunctionSig { if is_method || is_constructor || is_destructor { let is_const = is_method && cursor.method_is_const(); let is_virtual = is_method && cursor.method_is_virtual(); - let is_static = is_method && cursor.method_is_static(); - if !is_static && + let has_implicit_object = is_method && + !cursor.method_is_static() && + !cursor.method_is_explicit_object(); + if (!is_method || has_implicit_object) && (!is_virtual || ctx.options().use_specific_virtual_function_receiver) { diff --git a/bindgen/ir/item.rs b/bindgen/ir/item.rs index 4ce8c1b7ca..6e26c937db 100644 --- a/bindgen/ir/item.rs +++ b/bindgen/ir/item.rs @@ -1039,6 +1039,7 @@ impl Item { ) => cc.destructors(), FunctionKind::Method( MethodKind::Static | + MethodKind::ExplicitObject | MethodKind::Normal | MethodKind::Virtual { .. }, ) => cc.methods(),