Skip to content

Commit

Permalink
libbpf-cargo: Generate C enums as custom types with const fields
Browse files Browse the repository at this point in the history
Rather than generating Rust enums from C enums, which might not be
correct, as for example, C allows enum variants to share a value while
Rust does not.

This fixes libbpf#982.

Test Plan
=========

Added regression test where a C defined enum with duplicated values is
converted to Rust using BTF and then successfully compiled with Rustc.

Verified that tests pass in CI in my fork and that my project compiles
fine and works well with this change.
  • Loading branch information
javierhonduco committed Nov 8, 2024
1 parent b45de35 commit e31378b
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 30 deletions.
1 change: 1 addition & 0 deletions libbpf-cargo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
0.24.7
------
- Fixed handling of empty unions in BPF types
- Represent C enums with custom types and const fields


0.24.6
Expand Down
41 changes: 25 additions & 16 deletions libbpf-cargo/src/gen/btf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,8 @@ impl<'s> GenBtf<'s> {
_ => bail!("Invalid enum size: {}", t.size()),
};

let enum_name = self.anon_types.type_name_or_anon(&t);

let mut signed = "u";
for value in t.iter() {
if value.value < 0 {
Expand All @@ -805,30 +807,37 @@ impl<'s> GenBtf<'s> {
}
}

writeln!(
def,
r#"#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]"#
)?;
writeln!(def, r#"#[repr({signed}{repr_size})]"#)?;
writeln!(
def,
r#"pub enum {name} {{"#,
name = self.anon_types.type_name_or_anon(&t),
)?;
let mut first_field = None;

writeln!(def, r#"#[derive(Debug, Copy, Clone)]"#)?;
writeln!(def, r#"#[repr(transparent)]"#)?;
writeln!(def, r#"pub struct {enum_name}({signed}{repr_size});"#)?;
writeln!(def, "#[allow(non_upper_case_globals)]")?;
writeln!(def, r#"impl {enum_name} {{"#,)?;

for value in t.iter() {
first_field = first_field.or(Some(value));

for (i, value) in t.iter().enumerate() {
if i == 0 {
writeln!(def, r#" #[default]"#)?;
}
writeln!(
def,
r#" {name} = {value},"#,
r#" pub const {name}: {enum_name} = {enum_name}({value});"#,
name = value.name.unwrap().to_string_lossy(),
value = value.value,
)?;
}

writeln!(def, "}}")?;
writeln!(def, r#"}}"#)?;

if let Some(first_field) = first_field {
writeln!(def, r#"impl Default for {enum_name} {{"#)?;
writeln!(
def,
r#" fn default() -> Self {{ {enum_name}::{name} }}"#,
name = first_field.name.unwrap().to_string_lossy()
)?;
writeln!(def, r#"}}"#)?;
}

Ok(())
}

Expand Down
57 changes: 43 additions & 14 deletions libbpf-cargo/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1817,20 +1817,26 @@ fn test_btf_dump_definition_enum() {
enum Foo {
Zero = 0,
One,
seven = 7,
Seven = 7,
ZeroDup = Zero,
};
enum Foo foo;
"#;

let expected_output = r#"
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
#[repr(u32)]
pub enum Foo {
#[default]
Zero = 0,
One = 1,
seven = 7,
#[derive(Debug, Copy, Clone)]
#[repr(transparent)]
pub struct Foo(u32);
#[allow(non_upper_case_globals)]
impl Foo {
pub const Zero: Foo = Foo(0);
pub const One: Foo = Foo(1);
pub const Seven: Foo = Foo(7);
pub const ZeroDup: Foo = Foo(0);
}
impl Default for Foo {
fn default() -> Self { Foo::Zero }
}
"#;

Expand All @@ -1841,6 +1847,26 @@ pub enum Foo {
let enum_foo = find_type_in_btf!(btf, types::Enum<'_>, "Foo");

assert_definition(&btf, &enum_foo, expected_output);

// Ensure this code is valid Rust. See https://github.com/libbpf/libbpf-rs/issues/982.
let mut rust_code = NamedTempFile::new().unwrap();
let temp_dir = TempDir::new().unwrap();
rust_code.write_all(expected_output.as_bytes()).unwrap();
rust_code.write_all(b"\n").unwrap();
// Add `main` so the code compiles.
rust_code.write_all(b"fn main() {}").unwrap();
let status = Command::new("rustc")
.arg(rust_code.path())
// Crate names can't contain certain symbols that might be used
// by `tempfile` so override its name.
.arg("--crate-name")
.arg("btf_to_rust_test")
// We don't care about the produced object file.
.arg("--out-dir")
.arg(temp_dir.into_path())
.status()
.expect("failed to compile rust code");
assert!(status.success());
}

#[test]
Expand Down Expand Up @@ -2565,13 +2591,16 @@ impl Default for Foo {
}
}
}
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
#[repr(u32)]
pub enum __anon_1 {
#[default]
FOO = 1,
#[derive(Debug, Copy, Clone)]
#[repr(transparent)]
pub struct __anon_1(u32);
#[allow(non_upper_case_globals)]
impl __anon_1 {
pub const FOO: __anon_1 = __anon_1(1);
}
"#;
impl Default for __anon_1 {
fn default() -> Self { __anon_1::FOO }
}"#;

let mmap = build_btf_mmap(prog_text);
let btf = btf_from_mmap(&mmap);
Expand Down

0 comments on commit e31378b

Please sign in to comment.