rust-bindgen icon indicating copy to clipboard operation
rust-bindgen copied to clipboard

impl_debug(true) and default_alias_style(NewTypeDeref) broken for C unions with typedef aliases

Open ivmaykov opened this issue 4 months ago • 1 comments

When using impl_debug(true) together with default_alias_style(bindgen::AliasVariation::NewTypeDeref) and generating bindings to a C union which has a typedef alias, the generated Rust code has a Debug implementation for the C union but does NOT have a Debug implementation for the newtype alias of the C union. If there are any structs defined which contain the aliased type, then the generated bindings won't even compile! I know it's a weird case, but this is actually preventing me from using default_alias_style(bindgen::AliasVariation::NewTypeDeref) in a real project.

Very simple example:

// foo.h
union Union {
  uint8_t bytes[4];
  uint32_t word;
};

typedef union Union UnionAlias;

struct StructContainingUnionAlias {
  UnionAlias ua;
};
// build.rs
use std::env;
use std::path::PathBuf;

fn main() {
    let crate_dir = &env::var("CARGO_MANIFEST_DIR").unwrap();
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    let bindings = bindgen::Builder::default()
        .derive_default(true)
        .impl_debug(true)
        .default_alias_style(bindgen::AliasVariation::NewTypeDeref)
        .header(format!("{crate_dir}/src/foo.h"))
        .allowlist_file(".*foo.h.*")
        .generate()
        .expect("Unable to generate bindings");

    bindings
        .write_to_file(out_path.join("foo_bindings.rs"))
        .expect("Couldn't write bindings");
}
// foo_bindings.rs, generated by bindgen
/* automatically generated by rust-bindgen 0.72.0 */

#[repr(C)]
#[derive(Copy, Clone)]
pub union Union {
    pub bytes: [u8; 4usize],
    pub word: u32,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
    ["Size of Union"][::std::mem::size_of::<Union>() - 4usize];
    ["Alignment of Union"][::std::mem::align_of::<Union>() - 4usize];
    ["Offset of field: Union::bytes"][::std::mem::offset_of!(Union, bytes) - 0usize];
    ["Offset of field: Union::word"][::std::mem::offset_of!(Union, word) - 0usize];
};
impl Default for Union {
    fn default() -> Self {
        let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
        unsafe {
            ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
            s.assume_init()
        }
    }
}
impl ::std::fmt::Debug for Union {
    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
        write!(f, "Union {{ union }}")
    }
}
#[repr(transparent)]
#[derive(Copy, Clone)]
pub struct UnionAlias(pub Union);
impl ::std::ops::Deref for UnionAlias {
    type Target = Union;
    #[inline]
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
impl ::std::ops::DerefMut for UnionAlias {
    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}
#[repr(C)]
#[derive(Copy, Clone)]
pub struct StructContainingUnionAlias {
    pub ua: UnionAlias,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
    ["Size of StructContainingUnionAlias"]
        [::std::mem::size_of::<StructContainingUnionAlias>() - 4usize];
    ["Alignment of StructContainingUnionAlias"]
        [::std::mem::align_of::<StructContainingUnionAlias>() - 4usize];
    ["Offset of field: StructContainingUnionAlias::ua"]
        [::std::mem::offset_of!(StructContainingUnionAlias, ua) - 0usize];
};
impl Default for StructContainingUnionAlias {
    fn default() -> Self {
        let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
        unsafe {
            ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
            s.assume_init()
        }
    }
}
impl ::std::fmt::Debug for StructContainingUnionAlias {
    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
        write!(f, "StructContainingUnionAlias {{ ua: {:?} }}", self.ua)
    }
}

The generated file does not compile, since it's trying to derive Debug for StructContainingUnionAlias, but the field self.ua does not derive or implement Debug. Compilation error is:

$ cargo build
   Compiling bindgen-bug v0.1.0 (/Users/ivmaykov/Development/experiments/bindgen-bug)
error[E0277]: `UnionAlias` doesn't implement `Debug`
  --> /Users/ivmaykov/Development/experiments/bindgen-bug/target/debug/build/bindgen-bug-d43f6c4a8531d2b1/out/foo_bindings.rs:71:64
   |
71 |         write!(f, "StructContainingUnionAlias {{ ua: {:?} }}", self.ua)
   |                                                      ----      ^^^^^^^ `UnionAlias` cannot be formatted using `{:?}` because it doesn't implement `Debug`
   |                                                      |
   |                                                      required by this formatting parameter
   |
   = help: the trait `Debug` is not implemented for `UnionAlias`
   = note: add `#[derive(Debug)]` to `UnionAlias` or manually `impl Debug for UnionAlias`
   = note: this error originates in the macro `$crate::format_args` which comes from the expansion of the macro `write` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `UnionAlias` with `#[derive(Debug)]`
   |
32 + #[derive(Debug)]
33 | pub struct UnionAlias(pub Union);
   |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `bindgen-bug` (lib) due to 1 previous error

I'm using bindgen 0.72.0 and rustc 1.90.0-nightly on MacOS.

See https://github.com/ivmaykov/bindgen-bug for a minimal project which reproduces the issue.

ivmaykov avatar Aug 16 '25 00:08 ivmaykov

Found another, possibly related bug: struct fields of aliased types are omitted by impl_debug.

For example:

union IPv4Address {
  unsigned char bytes[4];
  uint32_t words[1];
};

struct IPv4AddressAndPort {
  union IPv4Address addr;
  uint32_t port;
};

Will generate the following implementation for IPv4AddressAndPort:

#[repr(C)]
#[derive(Copy, Clone)]
pub struct IPv4AddressAndPort {
    pub addr: IPv4Address,
    pub port: u32,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
    ["Size of IPv4AddressAndPort"][::std::mem::size_of::<IPv4AddressAndPort>() - 8usize];
    ["Alignment of IPv4AddressAndPort"][::std::mem::align_of::<IPv4AddressAndPort>() - 4usize];
    ["Offset of field: IPv4AddressAndPort::addr"]
        [::std::mem::offset_of!(IPv4AddressAndPort, addr) - 0usize];
    ["Offset of field: IPv4AddressAndPort::port"]
        [::std::mem::offset_of!(IPv4AddressAndPort, port) - 4usize];
};
impl Default for IPv4AddressAndPort {
    fn default() -> Self {
        let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
        unsafe {
            ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
            s.assume_init()
        }
    }
}
impl ::std::fmt::Debug for IPv4AddressAndPort {
    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
        write!(f, "IPv4AddressAndPort {{ addr: {:?} }}", self.addr)
    }
}

Notice how the port field is missing from the fields output by impl_debug, presumably because uint32_t is a typedef alias for a built-in type. If we change the type of port to unsigned int, the impl_debug implementation prints the field:

#[repr(C)]
#[derive(Copy, Clone)]
pub struct IPv4AddressAndPort {
    pub addr: IPv4Address,
    pub port: ::std::os::raw::c_uint,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
    ["Size of IPv4AddressAndPort"][::std::mem::size_of::<IPv4AddressAndPort>() - 8usize];
    ["Alignment of IPv4AddressAndPort"][::std::mem::align_of::<IPv4AddressAndPort>() - 4usize];
    ["Offset of field: IPv4AddressAndPort::addr"]
        [::std::mem::offset_of!(IPv4AddressAndPort, addr) - 0usize];
    ["Offset of field: IPv4AddressAndPort::port"]
        [::std::mem::offset_of!(IPv4AddressAndPort, port) - 4usize];
};
impl Default for IPv4AddressAndPort {
    fn default() -> Self {
        let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
        unsafe {
            ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
            s.assume_init()
        }
    }
}
impl ::std::fmt::Debug for IPv4AddressAndPort {
    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
        write!(
            f,
            "IPv4AddressAndPort {{ addr: {:?}, port: {:?} }}",
            self.addr, self.port
        )
    }
}

This is not a problem when C unions are not involved, because bindgen will just derive Debug in that case.

ivmaykov avatar Sep 02 '25 01:09 ivmaykov