diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-allowlist-file.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-allowlist-file.rs new file mode 100644 index 0000000000..667aa9be33 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-allowlist-file.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn ALLOWED_MACRO(x: i64) -> i64 { + ((x) + 1) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-allowlist-type.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-allowlist-type.rs new file mode 100644 index 0000000000..8d7ca5dff3 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-allowlist-type.rs @@ -0,0 +1,16 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct Foo { + pub x: ::std::os::raw::c_int, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of Foo"][::std::mem::size_of::() - 4usize]; + ["Alignment of Foo"][::std::mem::align_of::() - 4usize]; + ["Offset of field: Foo::x"][::std::mem::offset_of!(Foo, x) - 0usize]; +}; +#[allow(non_snake_case, unused_parens)] +pub const fn ADD(x: i64, y: i64) -> i64 { + ((x) + (y)) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-allowlist.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-allowlist.rs new file mode 100644 index 0000000000..d1ce81c08c --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-allowlist.rs @@ -0,0 +1,9 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn KEEP_ADD(x: i64, y: i64) -> i64 { + ((x) + (y)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn KEEP_FLAG(n: i64) -> i64 { + (1 << (n)) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-arity-mismatch.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-arity-mismatch.rs new file mode 100644 index 0000000000..3e13c0bfc5 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-arity-mismatch.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn B(x: i64) -> i64 { + ((x) + 1) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-bad-turbofish.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-bad-turbofish.rs new file mode 100644 index 0000000000..5eb94964c9 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-bad-turbofish.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn ID(x: i64) -> i64 { + (x) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-blocklist-var.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-blocklist-var.rs new file mode 100644 index 0000000000..175ceaa295 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-blocklist-var.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn KEEP_ME(x: i64) -> i64 { + ((x) + 2) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-blocklist.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-blocklist.rs new file mode 100644 index 0000000000..a2148bef2f --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-blocklist.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn ALLOWED(x: i64) -> i64 { + ((x) + 1) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-comparison.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-comparison.rs new file mode 100644 index 0000000000..40d1e31c3c --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-comparison.rs @@ -0,0 +1,45 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn GT(x: i64, y: i64) -> i64 { + (((x) > (y)) as i64) +} +#[allow(non_snake_case, unused_parens)] +pub const fn LT(x: i64, y: i64) -> i64 { + (((x) < (y)) as i64) +} +#[allow(non_snake_case, unused_parens)] +pub const fn EQ(x: i64, y: i64) -> i64 { + (((x) == (y)) as i64) +} +#[allow(non_snake_case, unused_parens)] +pub const fn NE(x: i64, y: i64) -> i64 { + (((x) != (y)) as i64) +} +#[allow(non_snake_case, unused_parens)] +pub const fn GTE(x: i64, y: i64) -> i64 { + (((x) >= (y)) as i64) +} +#[allow(non_snake_case, unused_parens)] +pub const fn LTE(x: i64, y: i64) -> i64 { + (((x) <= (y)) as i64) +} +#[allow(non_snake_case, unused_parens)] +pub const fn GT_BARE(x: i64, y: i64) -> i64 { + ((x) > (y)) as i64 +} +#[allow(non_snake_case, unused_parens)] +pub const fn NESTED(x: i64, y: i64) -> i64 { + ((((((x) > (y)) as i64)))) +} +#[allow(non_snake_case, unused_parens)] +pub const fn CMP_ADD(x: i64, y: i64) -> i64 { + (((x) > (y)) as i64) + (((x) < (y)) as i64) +} +#[allow(non_snake_case, unused_parens)] +pub const fn CMP_MUL(x: i64, y: i64) -> i64 { + ((((x) > (y)) as i64) * 2) +} +#[allow(non_snake_case, non_camel_case_types, unused_parens)] +pub const fn IS_BIG() -> i64 { + ((core::mem::size_of::() as i64 > 4) as i64) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-edge-cases.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-edge-cases.rs new file mode 100644 index 0000000000..1bf25a8628 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-edge-cases.rs @@ -0,0 +1,74 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +pub const A: u32 = 1; +pub const B: i32 = -1; +unsafe extern "C" { + pub static mut ext_var: ::std::os::raw::c_int; +} +#[allow(non_snake_case, unused_parens)] +pub const fn NEG() -> i64 { + (((-1) as i32 as i64)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn NEGX(x: i64) -> i64 { + (((-(x)) as i32 as i64)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn POS(x: f64) -> f64 { + (if (((x) > (0 as f64)) as i64 as f64) != 0.0 { 1.0 } else { 0.0 }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn MIX(x: u32) -> u32 { + ((x) + A + (B as u32)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ALL_BITS() -> u32 { + (!0) +} +#[allow(non_snake_case, unused_parens)] +pub const fn SMALL_U(x: u32) -> u32 { + ((x) + 1) +} +#[allow(non_snake_case, unused_parens)] +pub const fn BIG_U() -> u64 { + 0x100000000 +} +#[allow(non_snake_case, unused_parens)] +pub const fn BIG_OCT_U() -> u64 { + 0o40000000000 +} +#[allow(non_snake_case, unused_parens)] +pub const fn HUGE(x: u64) -> u64 { + ((x) + 0xFFFFFFFFFFFFFFFF) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ALL_BITS_UL() -> u64 { + (!0) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ALL_BITS_LU() -> u64 { + (!0) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ALL_BITS_LLU() -> u64 { + (!0) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ALL_BITS_UI64() -> u64 { + (!0) +} +#[allow(non_snake_case, unused_parens)] +pub const fn HUGE_MIXED(x: u64) -> u64 { + ((x) + (A as u64) + (B as u64) + 0xFFFFFFFFFFFFFFFF) +} +#[allow(non_snake_case, unused_parens)] +pub const fn PERMS() -> i64 { + 0o644 +} +#[allow(non_snake_case, unused_parens)] +pub const fn OCTAL(x: i64) -> i64 { + ((x) + 0o77) +} +#[allow(non_snake_case, unused_parens)] +pub const fn GOOD(x: i64) -> i64 { + ((x) + 1) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-float-suffix.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-float-suffix.rs new file mode 100644 index 0000000000..b44cea5f3b --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-float-suffix.rs @@ -0,0 +1,48 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn ADD_HALF(x: f64) -> f64 { + ((x) + 0.5) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ADD_QUARTER(x: f64) -> f64 { + ((x) + 0.25) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ADD_EXP(x: f64) -> f64 { + ((x) + 1e3) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ADD_EXPF(x: f64) -> f64 { + ((x) + 1e3) +} +#[allow(non_snake_case, unused_parens)] +pub const fn HEX_MASK(x: i64) -> i64 { + ((x) & 0xFF) +} +#[allow(non_snake_case, unused_parens)] +pub const fn HEX_COMBO(x: i64) -> i64 { + ((x) | 0xABCDEF) +} +#[allow(non_snake_case, unused_parens)] +pub const fn NOT_HALF() -> f64 { + (((0.5) == 0.0) as i64 as f64) +} +#[allow(non_snake_case, unused_parens)] +pub const fn NOT_PARAM(x: f64) -> f64 { + (if ((((x) == 0.0) as i64 as f64)) != 0.0 { 1.0 } else { 0.0 }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn FLOAT_BOTH() -> f64 { + (if (((((0.5) != 0.0) && ((2.0) != 0.0)) as i64 as f64)) != 0.0 { 1.0 } else { 0.0 }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn FLOAT_CHAIN() -> f64 { + (if (((((0.5) != 0.0) + && ((((((2.0) != 0.0) && ((3.0) != 0.0)) as i64 as f64)) != 0.0)) as i64 as f64)) + != 0.0 + { + 1.0 + } else { + 0.0 + }) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-forward-ref.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-forward-ref.rs new file mode 100644 index 0000000000..9b4b4d56b8 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-forward-ref.rs @@ -0,0 +1,9 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn B(x: i64) -> i64 { + ((x) + 1) +} +#[allow(non_snake_case, unused_parens)] +pub const fn A(x: i64) -> i64 { + B(x) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-generate-types.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-generate-types.rs new file mode 100644 index 0000000000..fe64295a68 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-generate-types.rs @@ -0,0 +1 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] \ No newline at end of file diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-invalid-expr.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-invalid-expr.rs new file mode 100644 index 0000000000..12b25f6c1f --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-invalid-expr.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn GOOD(x: i64) -> i64 { + ((x) + 1) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-ioctl.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-ioctl.rs new file mode 100644 index 0000000000..834a8ad0bc --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-ioctl.rs @@ -0,0 +1,24 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +pub const _IOC_NRSHIFT: u32 = 0; +pub const _IOC_TYPESHIFT: u32 = 8; +pub const _IOC_SIZESHIFT: u32 = 16; +pub const _IOC_DIRSHIFT: u32 = 30; +pub const _IOC_NONE: u32 = 0; +pub const _IOC_READ: u32 = 2; +#[allow(non_snake_case, unused_parens)] +pub const fn _IOC(dir: u32, r#type: u32, nr: u32, size: u32) -> u32 { + (((dir) << _IOC_DIRSHIFT) | ((r#type) << _IOC_TYPESHIFT) | ((nr) << _IOC_NRSHIFT) + | ((size) << _IOC_SIZESHIFT)) +} +#[allow(non_snake_case, non_camel_case_types, unused_parens)] +pub const fn _IOC_TYPECHECK() -> i64 { + (core::mem::size_of::() as i64) +} +#[allow(non_snake_case, unused_parens)] +pub const fn _IO(r#type: u32, nr: u32) -> u32 { + _IOC(_IOC_NONE, (r#type), (nr), 0) +} +#[allow(non_snake_case, non_camel_case_types, unused_parens)] +pub const fn _IOR(r#type: u32, nr: u32) -> u32 { + _IOC(_IOC_READ, (r#type), (nr), ((_IOC_TYPECHECK::() as u32))) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-keyword-name.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-keyword-name.rs new file mode 100644 index 0000000000..a3db56cf1d --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-keyword-name.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn r#type(x: i64) -> i64 { + ((x) + 1) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-late-const.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-late-const.rs new file mode 100644 index 0000000000..082ff2fab6 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-late-const.rs @@ -0,0 +1,6 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +pub const SHIFT: u32 = 8; +#[allow(non_snake_case, unused_parens)] +pub const fn USE_SHIFT(x: u32) -> u32 { + ((x) << SHIFT) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-logical-not.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-logical-not.rs new file mode 100644 index 0000000000..54c1a93b8f --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-logical-not.rs @@ -0,0 +1,45 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn NOT(x: i64) -> i64 { + ((((x) as i64 == 0) as i64)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn TOBOOL(x: i64) -> i64 { + (((((((x) as i64 == 0) as i64)) as i64 == 0) as i64)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ID(x: i64) -> i64 { + (x) +} +#[allow(non_snake_case, unused_parens)] +pub const fn NOT_ZERO() -> u32 { + (((0) as i64 == 0) as u32) +} +#[allow(non_snake_case, non_camel_case_types, unused_parens)] +pub const fn NOT_SIZEOF() -> i64 { + ((((core::mem::size_of::() as i64) as i64 == 0) as i64)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn AND(x: i64, y: i64) -> i64 { + (((((((x)) as i64) != 0) && ((((y)) as i64) != 0)) as i64)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn OR(x: i64, y: i64) -> i64 { + (((((((x)) as i64) != 0) || ((((y)) as i64) != 0)) as i64)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn CHAIN3(a: i64, b: i64, c: i64) -> i64 { + (((((((a)) as i64) != 0) + && (((((((((b)) as i64) != 0) && ((((c)) as i64) != 0)) as i64)) as i64) != 0)) + as i64)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn MIXED_CHAIN(a: i64, b: i64, c: i64) -> i64 { + (((((((a)) as i64) != 0) + || (((((((((b)) as i64) != 0) && ((((c)) as i64) != 0)) as i64)) as i64) != 0)) + as i64)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn NOT_ID(x: i64) -> i64 { + (((ID(x)) as i64 == 0) as i64) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-mixed-generic.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-mixed-generic.rs new file mode 100644 index 0000000000..8b10ce52e2 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-mixed-generic.rs @@ -0,0 +1,9 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, non_camel_case_types, unused_parens)] +pub const fn M(x: i64) -> i64 { + (core::mem::size_of::() as i64 + (x)) +} +#[allow(non_snake_case, non_camel_case_types, unused_parens)] +pub const fn N(y: i64) -> i64 { + (M::(y) as i64) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-new-skips.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-new-skips.rs new file mode 100644 index 0000000000..12b25f6c1f --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-new-skips.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn GOOD(x: i64) -> i64 { + ((x) + 1) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-paren-call.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-paren-call.rs new file mode 100644 index 0000000000..a8d4970452 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-paren-call.rs @@ -0,0 +1,4 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +unsafe extern "C" { + pub fn foo(arg1: ::std::os::raw::c_int) -> ::std::os::raw::c_int; +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-redefine.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-redefine.rs new file mode 100644 index 0000000000..359a4883e7 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-redefine.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn F(x: i64) -> i64 { + ((x) + 2) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-skipped.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-skipped.rs new file mode 100644 index 0000000000..12b25f6c1f --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-skipped.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn GOOD(x: i64) -> i64 { + ((x) + 1) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-stringify.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-stringify.rs new file mode 100644 index 0000000000..fe64295a68 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-stringify.rs @@ -0,0 +1 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] \ No newline at end of file diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-ternary-bool.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-ternary-bool.rs new file mode 100644 index 0000000000..ee02382673 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-ternary-bool.rs @@ -0,0 +1,68 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +#[allow(non_snake_case, unused_parens)] +pub const fn BOOL_NOT(x: i64) -> i64 { + (if (((((x) as i64 == 0) as i64)) as i64) != 0 { 1 } else { 0 }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn BOOL_AND(x: i64, y: i64) -> i64 { + (if ((((((((x)) as i64) != 0) && ((((y)) as i64) != 0)) as i64)) as i64) != 0 { + 1 + } else { + 0 + }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn BOOL_OR(x: i64, y: i64) -> i64 { + (if ((((((((x)) as i64) != 0) || ((((y)) as i64) != 0)) as i64)) as i64) != 0 { + 1 + } else { + 0 + }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn CMP_GT(x: i64, y: i64) -> i64 { + (if ((((x) > (y)) as i64) as i64) != 0 { (x) } else { (y) }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn RANGE(x: i64) -> i64 { + (if (((((((((x) > 0) as i64) as i64) != 0) && (((((x) < 10) as i64) as i64) != 0)) + as i64)) as i64) != 0 + { + 1 + } else { + 0 + }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ALL3(x: i64, y: i64, z: i64) -> i64 { + (if (((((((((x) > 0) as i64) as i64) != 0) + && ((((((((((y) > 0) as i64) as i64) != 0) && (((((z) > 0) as i64) as i64) != 0)) + as i64)) as i64) != 0)) as i64)) as i64) != 0 + { + 1 + } else { + 0 + }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ANY3(x: i64, y: i64, z: i64) -> i64 { + (if (((((((((x) > 0) as i64) as i64) != 0) + || ((((((((((y) > 0) as i64) as i64) != 0) || (((((z) > 0) as i64) as i64) != 0)) + as i64)) as i64) != 0)) as i64)) as i64) != 0 + { + 1 + } else { + 0 + }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn MIXED3(x: i64, y: i64, z: i64) -> i64 { + (if (((((((((x) > 0) as i64) as i64) != 0) + || ((((((((((y) > 0) as i64) as i64) != 0) && (((((z) > 0) as i64) as i64) != 0)) + as i64)) as i64) != 0)) as i64)) as i64) != 0 + { + 1 + } else { + 0 + }) +} diff --git a/bindgen-tests/tests/expectations/tests/translate-func-macro-typedef-cast.rs b/bindgen-tests/tests/expectations/tests/translate-func-macro-typedef-cast.rs new file mode 100644 index 0000000000..8940f000f5 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-func-macro-typedef-cast.rs @@ -0,0 +1,2 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +pub type my_t = ::std::os::raw::c_uint; diff --git a/bindgen-tests/tests/expectations/tests/translate-function-macros.rs b/bindgen-tests/tests/expectations/tests/translate-function-macros.rs new file mode 100644 index 0000000000..9cf17d02f2 --- /dev/null +++ b/bindgen-tests/tests/expectations/tests/translate-function-macros.rs @@ -0,0 +1,50 @@ +#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)] +pub const BIG: u32 = 42; +#[allow(non_snake_case, unused_parens)] +pub const fn ADD(x: i64, y: i64) -> i64 { + ((x) + (y)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn DOUBLE(x: i64) -> i64 { + ((x) * 2) +} +#[allow(non_snake_case, unused_parens)] +pub const fn FLAG(n: i64) -> i64 { + (1 << (n)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn COMPLEMENT(x: i64) -> i64 { + (!(x)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn MAX(a: i64, b: i64) -> i64 { + (if ((((a) > (b)) as i64) as i64) != 0 { (a) } else { (b) }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn ABS(x: i64) -> i64 { + (if ((((x) >= 0) as i64) as i64) != 0 { (x) } else { -(x) }) +} +#[allow(non_snake_case, unused_parens)] +pub const fn TO_UNSIGNED(x: i64) -> i64 { + (((x) as u32 as i64)) +} +#[allow(non_snake_case, non_camel_case_types, unused_parens)] +pub const fn SIZEOF_T() -> i64 { + core::mem::size_of::() as i64 +} +#[allow(non_snake_case, unused_parens)] +pub const fn IOC(dir: i64, r#type: i64, nr: i64, size: i64) -> i64 { + (((dir) << 30) | ((r#type) << 8) | (nr) | ((size) << 16)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn SHIFT(r#type: i64, n: i64) -> i64 { + ((r#type) << (n)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn MASK(n: u64) -> u64 { + (0xFF << (n)) +} +#[allow(non_snake_case, unused_parens)] +pub const fn IO(r#type: i64, nr: i64) -> i64 { + IOC(0, (r#type), (nr), 0) +} diff --git a/bindgen-tests/tests/headers/translate-func-macro-allowlist-file-includes/allowed.h b/bindgen-tests/tests/headers/translate-func-macro-allowlist-file-includes/allowed.h new file mode 100644 index 0000000000..5a05fbe6ed --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-allowlist-file-includes/allowed.h @@ -0,0 +1 @@ +#define ALLOWED_MACRO(x) ((x) + 1) diff --git a/bindgen-tests/tests/headers/translate-func-macro-allowlist-file-includes/blocked.h b/bindgen-tests/tests/headers/translate-func-macro-allowlist-file-includes/blocked.h new file mode 100644 index 0000000000..0bef91056b --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-allowlist-file-includes/blocked.h @@ -0,0 +1 @@ +#define BLOCKED_MACRO(x) ((x) + 2) diff --git a/bindgen-tests/tests/headers/translate-func-macro-allowlist-file.h b/bindgen-tests/tests/headers/translate-func-macro-allowlist-file.h new file mode 100644 index 0000000000..dcaaf4647d --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-allowlist-file.h @@ -0,0 +1,4 @@ +// bindgen-flags: --translate-function-macros --allowlist-file .*allowed\.h -- -Itests/headers/translate-func-macro-allowlist-file-includes + +#include "allowed.h" +#include "blocked.h" diff --git a/bindgen-tests/tests/headers/translate-func-macro-allowlist-type.h b/bindgen-tests/tests/headers/translate-func-macro-allowlist-type.h new file mode 100644 index 0000000000..262fb881e1 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-allowlist-type.h @@ -0,0 +1,6 @@ +// bindgen-flags: --translate-function-macros --allowlist-type Foo + +struct Foo { + int x; +}; +#define ADD(x, y) ((x) + (y)) diff --git a/bindgen-tests/tests/headers/translate-func-macro-allowlist.h b/bindgen-tests/tests/headers/translate-func-macro-allowlist.h new file mode 100644 index 0000000000..2bd7c6250f --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-allowlist.h @@ -0,0 +1,5 @@ +// bindgen-flags: --translate-function-macros --allowlist-item "KEEP.*" + +#define KEEP_ADD(x, y) ((x) + (y)) +#define KEEP_FLAG(n) (1 << (n)) +#define EXCLUDED(x) ((x) * 2) diff --git a/bindgen-tests/tests/headers/translate-func-macro-arity-mismatch.h b/bindgen-tests/tests/headers/translate-func-macro-arity-mismatch.h new file mode 100644 index 0000000000..7d5742de72 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-arity-mismatch.h @@ -0,0 +1,12 @@ +// bindgen-flags: --translate-function-macros + +// Wrong-arity calls should cause the caller macro to be skipped, +// not emitted as uncompilable Rust (E0061). + +#define B(x) ((x) + 1) + +// A() calls B() with 0 args, but B expects 1. +#define A() B() + +// C(x) calls B(x, x) with 2 args, but B expects 1. +#define C(x) B(x, x) diff --git a/bindgen-tests/tests/headers/translate-func-macro-bad-turbofish.h b/bindgen-tests/tests/headers/translate-func-macro-bad-turbofish.h new file mode 100644 index 0000000000..238899e13a --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-bad-turbofish.h @@ -0,0 +1,6 @@ +// bindgen-flags: --translate-function-macros + +// ID takes a value param. WRAP passes sizeof type param T to ID. +// WRAP should be skipped (T is a type but ID expects a value). +#define ID(x) (x) +#define WRAP(T) (ID(T) + sizeof(T)) diff --git a/bindgen-tests/tests/headers/translate-func-macro-blocklist-var.h b/bindgen-tests/tests/headers/translate-func-macro-blocklist-var.h new file mode 100644 index 0000000000..4ed51e3d5c --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-blocklist-var.h @@ -0,0 +1,4 @@ +// bindgen-flags: --translate-function-macros --blocklist-var BLOCK_ME + +#define BLOCK_ME(x) ((x) + 1) +#define KEEP_ME(x) ((x) + 2) diff --git a/bindgen-tests/tests/headers/translate-func-macro-blocklist.h b/bindgen-tests/tests/headers/translate-func-macro-blocklist.h new file mode 100644 index 0000000000..d3d48ad534 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-blocklist.h @@ -0,0 +1,5 @@ +// bindgen-flags: --translate-function-macros --blocklist-item "BLOCKED.*" + +#define ALLOWED(x) ((x) + 1) +#define BLOCKED_ADD(x, y) ((x) + (y)) +#define BLOCKED_MUL(x, y) ((x) * (y)) diff --git a/bindgen-tests/tests/headers/translate-func-macro-comparison.h b/bindgen-tests/tests/headers/translate-func-macro-comparison.h new file mode 100644 index 0000000000..ff4e8e5660 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-comparison.h @@ -0,0 +1,23 @@ +// bindgen-flags: --translate-function-macros + +// Bare comparisons: C returns int (0 or 1), Rust returns bool. +// Must cast to the value type so the result is integer-typed. +#define GT(x, y) ((x) > (y)) +#define LT(x, y) ((x) < (y)) +#define EQ(x, y) ((x) == (y)) +#define NE(x, y) ((x) != (y)) +#define GTE(x, y) ((x) >= (y)) +#define LTE(x, y) ((x) <= (y)) + +// Without outer parens. +#define GT_BARE(x, y) (x) > (y) + +// Deeply nested parens around comparison. +#define NESTED(x, y) (((((x) > (y))))) + +// Comparison result used in arithmetic (bool + bool would fail). +#define CMP_ADD(x, y) ((x) > (y)) + ((x) < (y)) +#define CMP_MUL(x, y) (((x) > (y)) * 2) + +// Comparison with sizeof. +#define IS_BIG(T) (sizeof(T) > 4) diff --git a/bindgen-tests/tests/headers/translate-func-macro-edge-cases.h b/bindgen-tests/tests/headers/translate-func-macro-edge-cases.h new file mode 100644 index 0000000000..14c55f09a2 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-edge-cases.h @@ -0,0 +1,45 @@ +// bindgen-flags: --translate-function-macros + +// (type)-literal and (type)-(expr): valid C casts with unary prefix. +// Previously panicked on make_ident("-"). +#define NEG() ((int)-1) +#define NEGX(x) ((int)-(x)) + +// Float ternary with integer literal comparison: the `0` must be +// cast to f64 when value type is float. +#define POS(x) ((x) > 0 ? 1.0f : 0.0f) + +// Deterministic type inference: when tied, result should be +// consistent (not depend on HashMap iteration order). +// Both A (u32) and B (i32) are referenced once each. +#define A 1U +#define B -1 +#define MIX(x) ((x) + A + B) + +// Unsigned suffix inference: +// U (small value) → u32 +#define ALL_BITS() (~0U) +#define SMALL_U(x) ((x) + 1U) +// U (value > u32::MAX) → promoted to u64 +#define BIG_U() 0x100000000U +#define BIG_OCT_U() 040000000000U +// UL/LU/ULL/LLU/ui64/UI64 → all u64 regardless of order/case +#define HUGE(x) ((x) + 0xFFFFFFFFFFFFFFFFULL) +#define ALL_BITS_UL() (~0UL) +#define ALL_BITS_LU() (~0LU) +#define ALL_BITS_LLU() (~0LLU) +#define ALL_BITS_UI64() (~0ui64) +// ULL with competing signed constants → still u64 +#define HUGE_MIXED(x) ((x) + A + B + 0xFFFFFFFFFFFFFFFFULL) + +// Octal literals: C 0644 must become Rust 0o644 (not decimal 644). +#define PERMS() 0644 +#define OCTAL(x) ((x) + 077) + +// non_const_vars bypass: !x and (type)x must also reject extern vars. +extern int ext_var; +#define NOT_EXT() (!ext_var) +#define CAST_EXT() ((int)ext_var) + +// These should still work normally. +#define GOOD(x) ((x) + 1) diff --git a/bindgen-tests/tests/headers/translate-func-macro-float-suffix.h b/bindgen-tests/tests/headers/translate-func-macro-float-suffix.h new file mode 100644 index 0000000000..f2f794f8cc --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-float-suffix.h @@ -0,0 +1,23 @@ +// bindgen-flags: --translate-function-macros + +// Float suffix f/F is stripped; float literals infer f64. +#define ADD_HALF(x) ((x) + 0.5f) +#define ADD_QUARTER(x) ((x) + 0.25F) + +// Exponent notation also infers f64. +#define ADD_EXP(x) ((x) + 1e3) +#define ADD_EXPF(x) ((x) + 1e3f) + +// Hex literals: f/F are hex digits, NOT suffixes. These stay i64. +#define HEX_MASK(x) ((x) & 0xFF) +#define HEX_COMBO(x) ((x) | 0xABCDEF) + +// Float logical NOT: uses == 0.0 instead of as i64. +#define NOT_HALF() !0.5f + +// Float ternary with logical NOT. +#define NOT_PARAM(x) (!(x) ? 1.0f : 0.0f) + +// Float logical ops: operands wrapped with != 0.0. +#define FLOAT_BOTH() (0.5f && 2.0f ? 1.0f : 0.0f) +#define FLOAT_CHAIN() (0.5f && 2.0f && 3.0f ? 1.0f : 0.0f) diff --git a/bindgen-tests/tests/headers/translate-func-macro-forward-ref.h b/bindgen-tests/tests/headers/translate-func-macro-forward-ref.h new file mode 100644 index 0000000000..628470ad14 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-forward-ref.h @@ -0,0 +1,4 @@ +// bindgen-flags: --translate-function-macros + +#define A(x) B(x) +#define B(x) ((x) + 1) diff --git a/bindgen-tests/tests/headers/translate-func-macro-generate-types.h b/bindgen-tests/tests/headers/translate-func-macro-generate-types.h new file mode 100644 index 0000000000..c9d58709d9 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-generate-types.h @@ -0,0 +1,3 @@ +// bindgen-flags: --translate-function-macros --generate types + +#define ADD(x, y) ((x) + (y)) diff --git a/bindgen-tests/tests/headers/translate-func-macro-invalid-expr.h b/bindgen-tests/tests/headers/translate-func-macro-invalid-expr.h new file mode 100644 index 0000000000..5c9252ac6f --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-invalid-expr.h @@ -0,0 +1,10 @@ +// bindgen-flags: --translate-function-macros + +// These produce token sequences that aren't valid Rust expressions. +// The syn validation should reject them. +#define ATTR_MACRO(f) __attribute__((nothrow)) f +#define TWO_IDENTS(a, b) a b +#define ASMNAME(cname) __asm__(cname) + +// This should still work. +#define GOOD(x) ((x) + 1) diff --git a/bindgen-tests/tests/headers/translate-func-macro-ioctl.h b/bindgen-tests/tests/headers/translate-func-macro-ioctl.h new file mode 100644 index 0000000000..63b263ba26 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-ioctl.h @@ -0,0 +1,23 @@ +// bindgen-flags: --translate-function-macros + +// Simulates the real linux ioctl pattern where _IOC_TYPECHECK wraps sizeof +// and _IOR/_IOW pass type params through it. + +#define _IOC_NRSHIFT 0 +#define _IOC_TYPESHIFT 8 +#define _IOC_SIZESHIFT 16 +#define _IOC_DIRSHIFT 30 + +#define _IOC_NONE 0U +#define _IOC_READ 2U + +#define _IOC(dir,type,nr,size) \ + (((dir) << _IOC_DIRSHIFT) | \ + ((type) << _IOC_TYPESHIFT) | \ + ((nr) << _IOC_NRSHIFT) | \ + ((size) << _IOC_SIZESHIFT)) + +#define _IOC_TYPECHECK(t) (sizeof(t)) + +#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) +#define _IOR(type,nr,argtype) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(argtype))) diff --git a/bindgen-tests/tests/headers/translate-func-macro-keyword-name.h b/bindgen-tests/tests/headers/translate-func-macro-keyword-name.h new file mode 100644 index 0000000000..ffe4da5f83 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-keyword-name.h @@ -0,0 +1,3 @@ +// bindgen-flags: --translate-function-macros + +#define type(x) ((x) + 1) diff --git a/bindgen-tests/tests/headers/translate-func-macro-late-const.h b/bindgen-tests/tests/headers/translate-func-macro-late-const.h new file mode 100644 index 0000000000..7d2da4dadc --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-late-const.h @@ -0,0 +1,4 @@ +// bindgen-flags: --translate-function-macros + +#define USE_SHIFT(x) ((x) << SHIFT) +#define SHIFT 8U diff --git a/bindgen-tests/tests/headers/translate-func-macro-logical-not.h b/bindgen-tests/tests/headers/translate-func-macro-logical-not.h new file mode 100644 index 0000000000..684c3bacce --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-logical-not.h @@ -0,0 +1,23 @@ +// bindgen-flags: --translate-function-macros + +// C logical NOT: !0 = 1, !nonzero = 0. +// Translated to ((operand) as i64 == 0) as vt. + +#define NOT(x) (!(x)) +#define TOBOOL(x) (!!(x)) + +// !call(x) — the call must be fully consumed. +#define ID(x) (x) +#define NOT_ID(x) !ID(x) + +// !literal with C suffix — suffix must be stripped. +#define NOT_ZERO() !0U + +// !sizeof(T) — sizeof must be handled before the call-identifier path. +#define NOT_SIZEOF(T) (!sizeof(T)) + +// Standalone && / || chains (not in ternary). +#define AND(x, y) ((x) && (y)) +#define OR(x, y) ((x) || (y)) +#define CHAIN3(a, b, c) ((a) && (b) && (c)) +#define MIXED_CHAIN(a, b, c) ((a) || (b) && (c)) diff --git a/bindgen-tests/tests/headers/translate-func-macro-mixed-generic.h b/bindgen-tests/tests/headers/translate-func-macro-mixed-generic.h new file mode 100644 index 0000000000..b37404f570 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-mixed-generic.h @@ -0,0 +1,6 @@ +// bindgen-flags: --translate-function-macros + +// M takes a type param T (via sizeof) and a value param x. +// N forwards T and y to M, so N also gets T as a type param. +#define M(T, x) (sizeof(T) + (x)) +#define N(T, y) M(T, y) diff --git a/bindgen-tests/tests/headers/translate-func-macro-new-skips.h b/bindgen-tests/tests/headers/translate-func-macro-new-skips.h new file mode 100644 index 0000000000..6dc606b553 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-new-skips.h @@ -0,0 +1,18 @@ +// bindgen-flags: --translate-function-macros + +// These should all be skipped (not appear in output). + +// String literal body (Finding 5). +#define STRING_MACRO(x) "hello" + +// Assignment operators (Finding 9). +#define ASSIGN_LIKE(x) ((x) += 1) + +// Comma operator (Finding 4). +#define COMMA(a, b) ((a), (b)) + +// Compiler builtins (Finding 1). +#define ASMNAME(cname) __asm__(cname) + +// This should still appear. +#define GOOD(x) ((x) + 1) diff --git a/bindgen-tests/tests/headers/translate-func-macro-paren-call.h b/bindgen-tests/tests/headers/translate-func-macro-paren-call.h new file mode 100644 index 0000000000..56fbdbd620 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-paren-call.h @@ -0,0 +1,4 @@ +// bindgen-flags: --translate-function-macros + +int foo(int); +#define CALL_FOO(x) (foo)(x) diff --git a/bindgen-tests/tests/headers/translate-func-macro-redefine.h b/bindgen-tests/tests/headers/translate-func-macro-redefine.h new file mode 100644 index 0000000000..b5acc6e705 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-redefine.h @@ -0,0 +1,6 @@ +// bindgen-flags: --translate-function-macros + +// Redefined macro: the last definition should win. +#define F(x) ((x) + 1) +#undef F +#define F(x) ((x) + 2) diff --git a/bindgen-tests/tests/headers/translate-func-macro-skipped.h b/bindgen-tests/tests/headers/translate-func-macro-skipped.h new file mode 100644 index 0000000000..86c57ca9c8 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-skipped.h @@ -0,0 +1,11 @@ +// bindgen-flags: --translate-function-macros + +// These should all be skipped (not appear in output). +#define VARIADIC(fmt, ...) fmt +#define HAS_VOID(x) ((void*)(x)) +#define EMPTY_BODY() +#define STMT_MACRO(x) do { x; } while(0) +#define CALL_PTR(f, x) (f)(x) + +// This should still appear. +#define GOOD(x) ((x) + 1) diff --git a/bindgen-tests/tests/headers/translate-func-macro-stringify.h b/bindgen-tests/tests/headers/translate-func-macro-stringify.h new file mode 100644 index 0000000000..9986674f2b --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-stringify.h @@ -0,0 +1,3 @@ +// bindgen-flags: --translate-function-macros + +#define STR(x) #x diff --git a/bindgen-tests/tests/headers/translate-func-macro-ternary-bool.h b/bindgen-tests/tests/headers/translate-func-macro-ternary-bool.h new file mode 100644 index 0000000000..2d98042871 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-ternary-bool.h @@ -0,0 +1,22 @@ +// bindgen-flags: --translate-function-macros + +// `!x` in ternary condition: logical NOT, gets (x == 0) translation. +#define BOOL_NOT(x) (!x ? 1 : 0) + +// &&/|| in ternary condition: operands wrapped with != 0. +#define BOOL_AND(x, y) ((x) && (y) ? 1 : 0) +#define BOOL_OR(x, y) ((x) || (y) ? 1 : 0) + +// Comparison operators produce bool directly (no wrapper). +#define CMP_GT(x, y) ((x) > (y) ? (x) : (y)) + +// Comparisons + logical ops: comparison sides are already bool. +#define RANGE(x) ((x) > 0 && (x) < 10 ? 1 : 0) + +// Chained logical ops with comparisons — each sub-expression with +// both a comparison and a logical op is integer-typed, not bool. +#define ALL3(x, y, z) ((x) > 0 && (y) > 0 && (z) > 0 ? 1 : 0) +#define ANY3(x, y, z) ((x) > 0 || (y) > 0 || (z) > 0 ? 1 : 0) + +// Mixed precedence: || splits first, && stays in right sub-expression. +#define MIXED3(x, y, z) ((x) > 0 || (y) > 0 && (z) > 0 ? 1 : 0) diff --git a/bindgen-tests/tests/headers/translate-func-macro-typedef-cast.h b/bindgen-tests/tests/headers/translate-func-macro-typedef-cast.h new file mode 100644 index 0000000000..267417fec4 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-func-macro-typedef-cast.h @@ -0,0 +1,4 @@ +// bindgen-flags: --translate-function-macros + +typedef unsigned int my_t; +#define CAST_MY_T(x) ((my_t)(x)) diff --git a/bindgen-tests/tests/headers/translate-function-macros.h b/bindgen-tests/tests/headers/translate-function-macros.h new file mode 100644 index 0000000000..f9e56ddc23 --- /dev/null +++ b/bindgen-tests/tests/headers/translate-function-macros.h @@ -0,0 +1,30 @@ +// bindgen-flags: --translate-function-macros + +// Simple arithmetic +#define ADD(x, y) ((x) + (y)) +#define DOUBLE(x) ((x) * 2) +#define FLAG(n) (1 << (n)) + +// Bitwise NOT (~ → !) +#define COMPLEMENT(x) (~(x)) + +// Ternary +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define ABS(x) ((x) >= 0 ? (x) : -(x)) + +// C casts +#define TO_UNSIGNED(x) ((unsigned int)(x)) + +// sizeof with generic type parameter +#define SIZEOF_T(T) sizeof(T) + +// Composing macros +#define IOC(dir, type, nr, size) (((dir) << 30) | ((type) << 8) | (nr) | ((size) << 16)) +#define IO(type, nr) IOC(0, (type), (nr), 0) + +// Keyword parameter escaping +#define SHIFT(type, n) ((type) << (n)) + +// Number literal suffix translation +#define BIG 42UL +#define MASK(n) (0xFFUL << (n)) diff --git a/bindgen/callbacks.rs b/bindgen/callbacks.rs index 630a306aec..e230b64a42 100644 --- a/bindgen/callbacks.rs +++ b/bindgen/callbacks.rs @@ -73,6 +73,23 @@ pub trait ParseCallbacks: fmt::Debug { /// the expansion of the macro as a sequence of tokens. fn func_macro(&self, _name: &str, _value: &[&[u8]]) {} + /// The Rust type that a translated function-like macro's value parameters + /// and return type should have, or `None` to use the inferred default. + /// + /// This is only called when `--translate-function-macros` is enabled. + /// The `inferred_type` parameter contains the type that would be used + /// by default (e.g., "u32" if all referenced constants are `u32`, or + /// "i64" as a fallback). + /// + /// Return a Rust type string like `"u32"`, `"i64"`, `"u16"`, etc. + fn func_macro_type( + &self, + _name: &str, + _inferred_type: &str, + ) -> Option { + None + } + /// This function should return whether, given an enum variant /// name, and value, this enum variant will forcibly be a constant. fn enum_variant_behavior( diff --git a/bindgen/codegen/mod.rs b/bindgen/codegen/mod.rs index 7a998c8fac..af7d93b62d 100644 --- a/bindgen/codegen/mod.rs +++ b/bindgen/codegen/mod.rs @@ -5299,6 +5299,95 @@ pub(crate) fn codegen( result.push(dynamic_items_tokens); } + // Emit translated function-like macros as `const fn` items. + // These originate from `#define`s, so they follow var semantics + // for filtering: gated on `--generate vars`, filtered by + // `--blocklist-var`/`--blocklist-item`, and allowed by + // `--allowlist-var`/`--allowlist-item`/`--allowlist-file`. + // `--allowlist-type` and `--allowlist-function` do NOT activate + // the allowlist gate for function macros — they are orthogonal + // (function macros are macro-derived, not C type/function + // declarations). + if context.options().codegen_config.vars() { + let has_any_allowlist = + !context.options().allowlisted_vars.is_empty() || + !context.options().allowlisted_items.is_empty() || + !context.options().allowlisted_files.is_empty(); + for fm in context.function_macros() { + if context.options().blocklisted_items.matches(&fm.name) + || context.options().blocklisted_vars.matches(&fm.name) + { + continue; + } + if has_any_allowlist { + // Check var/item allowlists by name. + let name_allowed = + context.options().allowlisted_vars.matches(&fm.name) + || context + .options() + .allowlisted_items + .matches(&fm.name); + // Check file allowlist by source location. + let file_allowed = fm.source_file.as_ref().is_some_and( + |f| context.options().allowlisted_files.matches(f), + ); + if !name_allowed && !file_allowed { + continue; + } + } + let name = if crate::ir::func_macro::RUST_KEYWORDS + .contains(&fm.name.as_str()) + { + Ident::new_raw(&fm.name, Span::call_site()) + } else { + Ident::new(&fm.name, Span::call_site()) + }; + let vt: proc_macro2::TokenStream = + fm.value_type.parse().unwrap_or_else(|_| quote! { i64 }); + let value_params: Vec = fm + .params + .iter() + .map(|p| { + let ident = if let Some(raw) = p.strip_prefix("r#") { + Ident::new_raw(raw, Span::call_site()) + } else { + Ident::new(p, Span::call_site()) + }; + quote! { #ident : #vt } + }) + .collect(); + let type_params: Vec = fm + .type_params + .iter() + .map(|p| { + let ident = if let Some(raw) = p.strip_prefix("r#") { + Ident::new_raw(raw, Span::call_site()) + } else { + Ident::new(p, Span::call_site()) + }; + quote! { #ident } + }) + .collect(); + let body = &fm.body; + + if type_params.is_empty() { + result.push(quote! { + #[allow(non_snake_case, unused_parens)] + pub const fn #name ( #( #value_params ),* ) -> #vt { + #body + } + }); + } else { + result.push(quote! { + #[allow(non_snake_case, non_camel_case_types, unused_parens)] + pub const fn #name < #( #type_params ),* > ( #( #value_params ),* ) -> #vt { + #body + } + }); + } + } + } // codegen_config.vars() + utils::serialize_items(&result, context)?; Ok(postprocessing::postprocessing( diff --git a/bindgen/ir/context.rs b/bindgen/ir/context.rs index 8e4163df5e..c135dbe045 100644 --- a/bindgen/ir/context.rs +++ b/bindgen/ir/context.rs @@ -356,6 +356,13 @@ pub(crate) struct BindgenContext { /// This needs to be an `std::HashMap` because the `cexpr` API requires it. parsed_macros: StdHashMap, cexpr::expr::EvalResult>, + /// Raw function-like macro definitions collected during parse, awaiting + /// translation in the post-parse fixpoint pass. + raw_function_macros: Vec, + + /// Function-like macros translated to Rust `const fn` declarations. + function_macros: Vec, + /// A map with all include locations. /// /// This is needed so that items are created in the order they are defined in. @@ -587,6 +594,8 @@ If you encounter an error missing from this list, please file an issue or a PR!" semantic_parents: Default::default(), currently_parsed_types: vec![], parsed_macros: Default::default(), + raw_function_macros: Default::default(), + function_macros: Default::default(), replacements: Default::default(), collected_typerefs: false, in_codegen: false, @@ -1178,6 +1187,12 @@ If you encounter an error missing from this list, please file an issue or a PR!" where F: FnOnce(&Self) -> Result, { + // Translate function-like macros before entering codegen phase, + // since translation needs access to parsed_macros. + if self.options.translate_function_macros { + self.translate_function_macros(); + } + self.in_codegen = true; self.resolve_typerefs(); @@ -2147,6 +2162,155 @@ If you encounter an error missing from this list, please file an issue or a PR!" self.parsed_macros.insert(id, value); } + /// Add a raw function-like macro definition for deferred translation. + /// If a macro with the same name already exists, it is replaced — this + /// handles `#undef` / `#define` redefinitions correctly (the last + /// definition wins, matching the preprocessor's semantics). + pub(crate) fn add_raw_function_macro( + &mut self, + raw: super::func_macro::RawFunctionMacro, + ) { + if let Some(existing) = self + .raw_function_macros + .iter_mut() + .find(|existing| existing.name == raw.name) + { + *existing = raw; + } else { + self.raw_function_macros.push(raw); + } + } + + /// Translate raw function-like macros in a fixpoint pass. + /// + /// Runs repeated waves until no more progress. Each wave attempts to + /// translate all unresolved macros using the current set of already- + /// translated macros as context. Forward references between macros + /// are resolved naturally: wave 1 translates leaf macros, wave 2 + /// translates macros that call them, etc. + fn translate_function_macros(&mut self) { + use super::func_macro::{ + FunctionMacro, PriorFunctionMacro, SkipReason, + }; + use super::var::build_constant_type_map; + + let constant_types = build_constant_type_map(self); + + // Build a set of non-const variable names. These are extern + // variables that become `static mut` in Rust and can't be + // referenced from `const fn`. + let non_const_vars: std::collections::HashSet = self + .items() + .filter_map(|(_, item)| { + item.kind().as_var().and_then(|var| { + if var.is_const() { + None + } else { + Some(var.name().to_owned()) + } + }) + }) + .collect(); + + let mut pending = mem::take(&mut self.raw_function_macros); + let mut wave = 0; + + loop { + wave += 1; + let mut progress = false; + let mut still_pending = Vec::new(); + + let priors: Vec = self + .function_macros + .iter() + .map(|fm| PriorFunctionMacro { + name: fm.name.clone(), + all_param_names: fm.all_param_names.clone(), + type_param_names: fm.type_params.clone(), + value_param_count: fm.params.len(), + }) + .collect(); + + for raw in pending { + match FunctionMacro::parse( + &raw.tokens, + &constant_types, + None, + &priors, + &non_const_vars, + self.target_pointer_size(), + ) { + Ok(mut fm) => { + fm.source_file = raw.source_file; + // Apply callback override. + if let Some(override_type) = + self.options.last_callback(|c| { + c.func_macro_type(&fm.name, &fm.value_type) + }) + { + match FunctionMacro::parse( + &raw.tokens, + &constant_types, + Some(&override_type), + &priors, + &non_const_vars, + self.target_pointer_size(), + ) { + Ok(mut overridden) => { + overridden.source_file = fm.source_file; + self.function_macros.push(overridden); + } + Err(reason) => { + warn!( + "Skipping function-like macro `{}` \ + (type override to `{}`): {}", + fm.name, override_type, reason, + ); + } + } + } else { + self.function_macros.push(fm); + } + progress = true; + } + Err(SkipReason::UnknownCallee(_)) => { + // Unknown callee (forward reference). Keep for + // next wave — the callee may be translated then. + still_pending.push(raw); + } + Err(reason) => { + warn!( + "Skipping function-like macro `{}`: {}", + raw.name, reason, + ); + } + } + } + + pending = still_pending; + + if !progress || pending.is_empty() { + break; + } + } + + // Anything still pending after fixpoint is genuinely unresolvable. + for raw in &pending { + warn!( + "Skipping function-like macro `{}`: \ + unresolved after {} wave(s)", + raw.name, wave, + ); + } + } + + /// Get all translated function-like macros. + pub(crate) fn function_macros( + &self, + ) -> &[super::func_macro::FunctionMacro] { + &self.function_macros + } + /// Are we in the codegen phase? pub(crate) fn in_codegen_phase(&self) -> bool { self.in_codegen diff --git a/bindgen/ir/func_macro.rs b/bindgen/ir/func_macro.rs new file mode 100644 index 0000000000..582eaa82b6 --- /dev/null +++ b/bindgen/ir/func_macro.rs @@ -0,0 +1,1838 @@ +//! Function-like C macro to Rust `const fn` translation. +//! +//! This module translates function-like C preprocessor macros into Rust +//! `const fn` declarations. For example: +//! +//! ```c +//! #define ADD(x, y) ((x) + (y)) +//! ``` +//! +//! becomes: +//! +//! ```rust,ignore +//! pub const fn ADD(x: i64, y: i64) -> i64 { +//! ((x) + (y)) +//! } +//! ``` +//! +//! Parameters used as `sizeof` arguments become generic type parameters: +//! +//! ```c +//! #define _IOR(type, nr, size) _IOC(_IOC_READ, (type), (nr), sizeof(size)) +//! ``` +//! +//! becomes: +//! +//! ```rust,ignore +//! pub const fn _IOR(r#type: u32, nr: u32) -> u32 { +//! _IOC(_IOC_READ, (r#type), (nr), core::mem::size_of::() as u32) +//! } +//! ``` + +use crate::clang::ClangToken; +use clang_sys::*; +use proc_macro2::{Delimiter, Group, Ident, Span, TokenStream}; +use quote::quote; +use std::collections::{HashMap, HashSet}; +use std::fmt; + +/// An owned copy of a C token's kind and spelling, for deferred translation. +/// +/// `ClangToken` borrows the translation unit and can't outlive the parse +/// phase. This struct captures the data we need so function-like macros +/// can be translated in a post-parse fixpoint pass. +#[derive(Debug, Clone)] +pub(crate) struct OwnedToken { + /// The token kind (`CXToken_Punctuation`, `CXToken_Identifier`, etc.). + pub kind: CXTokenKind, + /// The token spelling as owned bytes. + pub spelling: Vec, +} + +impl OwnedToken { + /// Create from a `ClangToken`. + pub fn from_clang(t: &ClangToken) -> Self { + Self { + kind: t.kind, + spelling: t.spelling().to_vec(), + } + } + + /// Get the spelling as a byte slice (matches `ClangToken::spelling()`). + pub fn spelling(&self) -> &[u8] { + &self.spelling + } +} + +/// A raw function-like macro definition collected during parse, before +/// translation. Stored on `BindgenContext` for deferred fixpoint translation. +#[derive(Debug, Clone)] +pub(crate) struct RawFunctionMacro { + /// The macro name. + pub name: String, + /// All tokens of the macro definition (including name and param list). + pub tokens: Vec, + /// The source file this macro was defined in. + pub source_file: Option, +} + +/// Default value type when inference finds nothing. +pub(crate) const DEFAULT_VALUE_TYPE: &str = "i64"; + +/// Rust keywords that must be escaped with `r#` when used as identifiers. +pub(crate) const RUST_KEYWORDS: &[&str] = &[ + "as", "break", "const", "continue", "crate", "else", "enum", "extern", + "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", + "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", + "super", "trait", "true", "type", "unsafe", "use", "where", "while", + "async", "await", "dyn", "abstract", "become", "box", "do", "final", + "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", + "try", +]; + +/// Create a `proc_macro2::Ident`, using raw identifier syntax for Rust +/// keywords. +fn make_ident(name: &str) -> Ident { + if RUST_KEYWORDS.contains(&name) { + Ident::new_raw(name, Span::call_site()) + } else { + Ident::new(name, Span::call_site()) + } +} + +/// Escape a name if it's a Rust keyword (for string-level param tracking). +fn escape_keyword(name: &str) -> String { + if RUST_KEYWORDS.contains(&name) { + format!("r#{name}") + } else { + name.to_owned() + } +} + +/// Parse a string as a `TokenStream`. Panics on invalid input — only use for +/// known-valid fragments like type names from our own maps. +fn parse_ts(s: &str) -> TokenStream { + s.parse().expect("internal: invalid token fragment") +} + +/// Reason a function-like macro was skipped during translation. +#[derive(Debug)] +pub(crate) enum SkipReason { + /// Empty token list or no name. + Empty, + /// Macro uses variadic arguments. + Variadic, + /// Macro body contains untranslatable C type keywords. + TypeKeywordInBody(String), + /// Macro body contains `sizeof` on an unresolvable argument. + UntranslatableSizeof, + /// Macro body uses `#` (stringification) or `##` (token pasting). + /// These are preprocessor-level operations with no `const fn` equivalent. + TokenPasting, + /// Macro body calls a function not yet translated (forward reference). + /// The fixpoint pass will retry. + UnknownCallee(String), + /// Macro body contains `(ident)(expr)` which is either a typedef cast + /// or a parenthesized function call — neither is expressible in + /// `const fn`. + FunctionCallOrCast(String), + /// Translated body is not a valid Rust expression (e.g., contains + /// compiler-internal constructs, adjacent identifiers, etc.). + InvalidExpression, + /// Macro calls another macro with the wrong number of arguments. + ArityMismatch(String), + /// Macro body references a non-const variable (e.g., `extern int x`), + /// which becomes `static mut` in Rust and can't be used in `const fn`. + MutableStaticReference(String), + /// Translated body is empty. + EmptyBody, +} + +impl fmt::Display for SkipReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Empty => write!(f, "empty or unnamed macro"), + Self::Variadic => write!(f, "variadic macro"), + Self::TypeKeywordInBody(kw) => { + write!(f, "untranslatable C keyword `{kw}` in body") + } + Self::UntranslatableSizeof => { + write!(f, "sizeof with unresolvable argument") + } + Self::TokenPasting => { + write!(f, "uses # or ## (stringification/token pasting)") + } + Self::UnknownCallee(name) => { + write!(f, "calls unknown function `{name}` (may be forward reference)") + } + Self::FunctionCallOrCast(name) => { + write!( + f, + "contains ({name})(...) which is a typedef cast or \ + function call, neither expressible in const fn" + ) + } + Self::InvalidExpression => { + write!(f, "translated body is not a valid Rust expression") + } + Self::ArityMismatch(detail) => { + write!(f, "argument count mismatch: {detail}") + } + Self::MutableStaticReference(name) => { + write!( + f, + "references non-const variable `{name}` (static mut \ + in Rust, not usable in const fn)" + ) + } + Self::EmptyBody => write!(f, "empty body after translation"), + } + } +} + +/// Summary of a previously-translated function macro, used to inform +/// translation of later macros in the fixpoint pass. +#[derive(Debug, Clone)] +pub(crate) struct PriorFunctionMacro { + /// The macro name. + pub name: String, + /// All parameter names in original C declaration order. + pub all_param_names: Vec, + /// Names of type parameters. Empty if no generics. + pub type_param_names: Vec, + /// Number of value parameters. + pub value_param_count: usize, +} + +/// A parsed function-like C macro that can be translated to a Rust `const fn`. +#[derive(Debug, Clone)] +pub(crate) struct FunctionMacro { + /// The macro name. + pub name: String, + /// All parameter names in original C declaration order. + pub all_param_names: Vec, + /// Value parameters (typed arguments). + pub params: Vec, + /// Type parameters (used in `sizeof` — become generics). + pub type_params: Vec, + /// The macro body as a `proc_macro2::TokenStream`. + pub body: TokenStream, + /// The Rust type for value parameters and return (e.g., `"u32"`, `"i64"`). + pub value_type: String, + /// The source file this macro was defined in, if known. + pub source_file: Option, +} + +impl FunctionMacro { + /// Try to parse a function-like macro from clang tokens. + /// + /// `constant_types` maps known constant names to their Rust type strings + /// (e.g., `"u32"`, `"i64"`). This is used to infer the function's value + /// type and avoid unnecessary casts. + /// + /// If `type_override` is `Some`, that type is used instead of inferring. + /// + /// Returns `Err(SkipReason)` if the macro can't be translated. + /// `prior_macros`: info about previously-parsed function macros, used to + /// detect sizeof-like calls and reject calls to unknown functions. + pub fn parse( + tokens: &[OwnedToken], + constant_types: &HashMap, + type_override: Option<&str>, + prior_macros: &[PriorFunctionMacro], + non_const_vars: &HashSet, + target_pointer_size: usize, + ) -> Result { + if tokens.is_empty() { + return Err(SkipReason::Empty); + } + + let name_token = &tokens[0]; + if name_token.kind != CXToken_Identifier { + return Err(SkipReason::Empty); + } + let name = std::str::from_utf8(name_token.spelling()) + .map_err(|_| SkipReason::Empty)? + .to_owned(); + + let lparen = tokens + .iter() + .position(|t| t.kind == CXToken_Punctuation && t.spelling() == b"(") + .ok_or(SkipReason::Empty)?; + + let rparen = + find_matching_paren(tokens, lparen).ok_or(SkipReason::Empty)?; + + let mut all_params = Vec::new(); + for token in &tokens[lparen + 1..rparen] { + if token.kind == CXToken_Identifier { + let param = std::str::from_utf8(token.spelling()) + .map_err(|_| SkipReason::Empty)? + .to_owned(); + if param != "__VA_ARGS__" { + all_params.push(param); + } + } + if token.kind == CXToken_Punctuation && token.spelling() == b"..." { + return Err(SkipReason::Variadic); + } + } + + let body_tokens = &tokens[rparen + 1..]; + if body_tokens.is_empty() { + return Err(SkipReason::EmptyBody); + } + + // Sizeof-like: takes only type params, no value params. + let sizeof_fn_names: Vec = prior_macros + .iter() + .filter(|m| { + !m.type_param_names.is_empty() && m.value_param_count == 0 + }) + .map(|m| m.name.clone()) + .collect(); + let sizeof_params = find_sizeof_params( + body_tokens, + &all_params, + &sizeof_fn_names, + prior_macros, + ); + + let mut params = Vec::new(); + let mut type_params = Vec::new(); + for p in &all_params { + let escaped = escape_keyword(p); + if sizeof_params.contains(p) { + type_params.push(escaped); + } else { + params.push(escaped); + } + } + + let value_type = type_override.map_or_else( + || infer_value_type(body_tokens, &all_params, constant_types), + |s| s.to_owned(), + ); + + // Validate value_type is a parseable TokenStream. This catches + // bad input from func_macro_type callbacks without panicking. + if value_type.parse::().is_err() { + return Err(SkipReason::InvalidExpression); + } + + let ctx = TranslateCtx { + params: ¶ms, + type_params: &type_params, + value_type: &value_type, + constant_types, + prior_macros, + non_const_vars, + target_pointer_size, + }; + let body = translate_expr(body_tokens, &ctx)?; + + if body.is_empty() { + return Err(SkipReason::EmptyBody); + } + + // Validate the body is a syntactically valid Rust expression. + // This catches glibc/compiler-internal macros whose bodies + // contain attribute annotations, token pasting results, or + // other non-expression constructs that slipped through the + // token-level checks above. + match syn::parse2::(body.clone()) { + Err(_) => return Err(SkipReason::InvalidExpression), + Ok(ref expr) if expr_has_unsupported_construct(expr) => { + return Err(SkipReason::InvalidExpression); + } + Ok(_) => {} + } + + let all_param_names = + all_params.iter().map(|p| escape_keyword(p)).collect(); + + Ok(FunctionMacro { + name, + all_param_names, + params, + type_params, + body, + value_type, + source_file: None, + }) + } +} + +/// Context passed through the recursive translator. +struct TranslateCtx<'a> { + /// Value parameter names. + params: &'a [String], + /// Type parameter names (for `sizeof`). + type_params: &'a [String], + /// The inferred Rust type (e.g., `"u32"`, `"i64"`). + value_type: &'a str, + /// Map of known constant names to Rust type strings. + constant_types: &'a HashMap, + /// Previously-translated function macros (for callee lookups). + prior_macros: &'a [PriorFunctionMacro], + /// Names of non-const variables (extern vars → `static mut` in Rust). + non_const_vars: &'a HashSet, + /// Target pointer size in bytes (4 for 32-bit, 8 for 64-bit). + /// Used for target-aware `long`/`unsigned long` mapping. + target_pointer_size: usize, +} + +impl TranslateCtx<'_> { + /// Get the value type as a `TokenStream`. + fn vt(&self) -> TokenStream { + parse_ts(self.value_type) + } +} + +/// Infer the value type from constants and literals in the body. +/// +/// Priority: if float literals are present, returns `"f64"` (since mixing +/// floats with integer types produces type errors in Rust). Otherwise +/// uses the most common type among referenced constants, defaulting to +/// `"i64"`. +fn infer_value_type( + tokens: &[OwnedToken], + params: &[String], + constant_types: &HashMap, +) -> String { + let mut type_counts: HashMap<&str, usize> = HashMap::new(); + let mut has_float_literal = false; + // Track the minimum unsigned type implied by C literal suffixes: + // U → u32, UL/ULL → u64. None if no unsigned suffix seen. + let mut unsigned_hint: Option<&str> = None; + + for token in tokens { + // Check literal suffixes for type hints. + if token.kind == CXToken_Literal { + if let Ok(spelling) = std::str::from_utf8(token.spelling()) { + let num = strip_c_suffix(spelling); + if is_float_literal(num) { + has_float_literal = true; + } + // C unsigned suffix determines minimum unsigned type: + // ULL → u64, UL → u64, U → u32 (but u64 if value + // exceeds u32::MAX, matching C's promotion rules). + let lower = spelling.to_ascii_lowercase(); + if lower.contains('u') { + let has_long = + lower.contains('l') || lower.ends_with("i64"); + let suffix_type = if has_long { + // UL, ULL, LU, LLU, and MSVC ui64/UI64 → u64. + "u64" + } else if literal_exceeds_u32(num) { + // U without L/LL but value > u32::MAX → + // C promotes to unsigned long/long long. + "u64" + } else { + "u32" + }; + if unsigned_hint.is_none() || unsigned_hint == Some("u32") { + unsigned_hint = Some(suffix_type); + } + } + } + continue; + } + + if token.kind != CXToken_Identifier { + continue; + } + let Ok(name) = std::str::from_utf8(token.spelling()) else { + continue; + }; + if params.iter().any(|p| p == name) { + continue; + } + if let Some(ty) = constant_types.get(name) { + *type_counts.entry(ty.as_str()).or_insert(0) += 1; + } + } + + // Float literals force f64 — integer arithmetic ops on floats + // are validated during translation (bitwise/modulo/shift rejected). + if has_float_literal { + return "f64".to_owned(); + } + + // Infer from constant references. + let has_constants = !type_counts.is_empty(); + let inferred = type_counts + .into_iter() + // Break ties deterministically by lexical type name order + // (so output doesn't depend on HashMap iteration order). + .max_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(b.0))) + .map_or(DEFAULT_VALUE_TYPE, |(ty, _)| ty); + + // Apply unsigned suffix hint: U → u32, UL/ULL → u64. + if let Some(min_unsigned) = unsigned_hint { + if !has_constants { + // No constants referenced — suffix determines the type. + return min_unsigned.to_owned(); + } + // Constants referenced — flip signed→unsigned, ensure at + // least as wide as the suffix minimum. + let flipped = match inferred { + "i8" => "u8", + "i16" => "u16", + "i32" => "u32", + "i64" => "u64", + other => other, + }; + let width = |t: &str| -> u8 { + match t { + "u8" => 1, + "u16" => 2, + "u32" => 4, + "u64" => 8, + _ => 4, + } + }; + return if width(flipped) >= width(min_unsigned) { + flipped.to_owned() + } else { + min_unsigned.to_owned() + }; + } + + inferred.to_owned() +} + +// --------------------------------------------------------------------------- +// Expression translator (recursive descent for ternary) +// --------------------------------------------------------------------------- + +/// Translate a C token slice to a Rust `TokenStream`. +fn translate_expr( + tokens: &[OwnedToken], + ctx: &TranslateCtx, +) -> Result { + if let Some((q_pos, c_pos)) = find_ternary(tokens) { + let cond = translate_expr(&tokens[..q_pos], ctx)?; + let then_ = translate_expr(&tokens[q_pos + 1..c_pos], ctx)?; + let else_ = translate_expr(&tokens[c_pos + 1..], ctx)?; + + // Condition is always converted to bool via != 0. + // Comparisons already return integer via `as vt` below, + // so the != 0 check is redundant but harmless in that case. + return if matches!(ctx.value_type, "f64" | "f32") { + Ok(quote! { if (#cond) != 0.0 { #then_ } else { #else_ } }) + } else { + Ok(quote! { if ((#cond) as i64) != 0 { #then_ } else { #else_ } }) + }; + } + + // Logical operators: split on || (lowest precedence) or &&. + // Each operand is wrapped with != 0 to convert to bool, and + // the result is cast back to the value type (C's && and || + // return 0 or 1 as int). + if let Some((op_pos, is_or)) = find_top_level_logical_op(tokens) { + let left = translate_expr(&tokens[..op_pos], ctx)?; + let right = translate_expr(&tokens[op_pos + 1..], ctx)?; + + let is_float = matches!(ctx.value_type, "f64" | "f32"); + let left_bool = if is_float { + quote! { ((#left) != 0.0) } + } else { + quote! { (((#left) as i64) != 0) } + }; + let right_bool = if is_float { + quote! { ((#right) != 0.0) } + } else { + quote! { (((#right) as i64) != 0) } + }; + + let vt = ctx.vt(); + // Can't cast bool directly to f64 — go through i64. + let cast = if matches!(ctx.value_type, "f64" | "f32") { + quote! { as i64 as #vt } + } else { + quote! { as #vt } + }; + return if is_or { + Ok(quote! { ((#left_bool || #right_bool) #cast) }) + } else { + Ok(quote! { ((#left_bool && #right_bool) #cast) }) + }; + } + + // Bare comparisons produce `bool` in Rust but `int` (0 or 1) in C. + // Cast to the value type so the result can be used in arithmetic. + if expr_tokens_are_boolean(tokens) { + let body = translate_tokens(tokens, ctx)?; + let vt = ctx.vt(); + return if matches!(ctx.value_type, "f64" | "f32") { + Ok(quote! { (#body) as i64 as #vt }) + } else { + Ok(quote! { (#body) as #vt }) + }; + } + + translate_tokens(tokens, ctx) +} + +/// Find a top-level ternary `?` and its matching `:`. +/// +/// Returns `Some((question_pos, colon_pos))` if found. +fn find_ternary(tokens: &[OwnedToken]) -> Option<(usize, usize)> { + let mut depth = 0i32; + let mut question_pos = None; + let mut ternary_depth = 0i32; + + for (i, t) in tokens.iter().enumerate() { + if t.kind != CXToken_Punctuation { + continue; + } + match t.spelling() { + b"(" => depth += 1, + b")" => depth -= 1, + b"?" if depth == 0 => { + if question_pos.is_none() { + question_pos = Some(i); + } else { + ternary_depth += 1; + } + } + b":" if depth == 0 && question_pos.is_some() => { + if ternary_depth == 0 { + return Some((question_pos.unwrap(), i)); + } + ternary_depth -= 1; + } + _ => {} + } + } + + None +} + +/// Check if tokens contain a top-level comparison operator, meaning the +/// expression evaluates to `bool` in Rust. +/// +/// Only actual comparison operators produce `bool` in Rust. `&&` and `||` +/// require `bool` operands (which integer params aren't), and `!` on +/// integers is bitwise NOT (returns integer, not bool). So we only check +/// for `>`, `<`, `>=`, `<=`, `==`, `!=`. +fn has_top_level_comparison(tokens: &[OwnedToken]) -> bool { + let mut depth = 0i32; + + for t in tokens { + if t.kind != CXToken_Punctuation { + continue; + } + match t.spelling() { + b"(" => depth += 1, + b")" => depth -= 1, + b">" | b"<" | b">=" | b"<=" | b"==" | b"!=" if depth == 0 => { + return true; + } + _ => {} + } + } + + false +} + +/// Find the first top-level `||` or `&&` operator in tokens. +/// +/// Prefers `||` (lower precedence) over `&&`. Returns `(position, is_or)`. +/// Parenthesized sub-expressions are skipped. +fn find_top_level_logical_op(tokens: &[OwnedToken]) -> Option<(usize, bool)> { + let mut depth = 0i32; + let mut first_or = None; + let mut first_and = None; + + for (i, t) in tokens.iter().enumerate() { + if t.kind != CXToken_Punctuation { + continue; + } + match t.spelling() { + b"(" => depth += 1, + b")" => depth -= 1, + b"||" if depth == 0 && first_or.is_none() => { + first_or = Some(i); + } + b"&&" if depth == 0 && first_and.is_none() => { + first_and = Some(i); + } + _ => {} + } + } + + // Split on || first (lower precedence), then &&. + if let Some(pos) = first_or { + Some((pos, true)) + } else { + first_and.map(|pos| (pos, false)) + } +} + +/// Check if a token slice produces a boolean result when translated. +/// +/// Only raw comparison operators WITHOUT logical ops produce `bool`. +/// If the tokens also contain `&&`/`||`, the logical op handler wraps +/// the result with `as vt`, making it integer-typed even when +/// sub-expressions are comparisons (e.g., `x > 0 && y > 0` → +/// `((bool && bool) as i64)`). +fn expr_tokens_are_boolean(tokens: &[OwnedToken]) -> bool { + has_top_level_comparison(tokens) && + find_top_level_logical_op(tokens).is_none() && + find_ternary(tokens).is_none() +} + +// --------------------------------------------------------------------------- +// Flat token-by-token translator +// --------------------------------------------------------------------------- + +/// Translate tokens to a Rust `TokenStream`, handling casts, `sizeof`, and +/// operators. +fn translate_tokens( + tokens: &[OwnedToken], + ctx: &TranslateCtx, +) -> Result { + let mut out = TokenStream::new(); + let mut i = 0; + // Track whether the last emitted token was a "value" (identifier, + // literal, or close-paren). Used to distinguish binary & (bitwise + // AND, after a value) from unary & (address-of, after an operator). + let mut prev_was_value = false; + + while i < tokens.len() { + let token = &tokens[i]; + let spelling = std::str::from_utf8(token.spelling()) + .map_err(|_| SkipReason::Empty)?; + + match token.kind { + CXToken_Identifier => { + prev_was_value = true; + // Reject compiler builtins that pass syn validation + // (they parse as function calls) but produce + // uncompilable code. + if spelling == "__asm__" || + spelling == "__asm" || + spelling.starts_with("__builtin_") + { + return Err(SkipReason::TypeKeywordInBody( + spelling.to_owned(), + )); + } + + if spelling == "sizeof" { + let (ts, consumed) = + translate_sizeof(&tokens[i + 1..], ctx)?; + out.extend(ts); + prev_was_value = true; + i += 1 + consumed; + continue; + } + + let escaped = escape_keyword(spelling); + let ident = make_ident(spelling); + + if ctx.params.contains(&escaped) { + out.extend(quote! { #ident }); + } else { + let next_is_call = tokens.get(i + 1).is_some_and(|t| { + t.kind == CXToken_Punctuation && t.spelling() == b"(" + }); + if next_is_call { + // Consult callee metadata for turbofish / type + // param routing. Returns None if callee unknown + // (forward reference — fixpoint will retry). + if let Some((call_ts, consumed)) = + try_translate_call(spelling, &tokens[i + 1..], ctx)? + { + out.extend(call_ts); + i += 1 + consumed; + continue; + } + // Unknown callee or no special handling needed. + out.extend(quote! { #ident }); + } else { + // Reject references to non-const variables + // (extern → static mut in Rust, not usable + // in const fn). + if ctx.non_const_vars.contains(spelling) { + return Err(SkipReason::MutableStaticReference( + spelling.to_owned(), + )); + } + + let needs_cast = ctx + .constant_types + .get(spelling) + .map_or(true, |ty| ty != ctx.value_type); + if needs_cast { + let vt = ctx.vt(); + out.extend(quote! { (#ident as #vt) }); + } else { + out.extend(quote! { #ident }); + } + } + } + } + + CXToken_Literal => { + prev_was_value = true; + out.extend(translate_literal(spelling, ctx)?); + } + + CXToken_Punctuation => match spelling { + // Stringification and token pasting — no const fn + // equivalent. + "#" | "##" => { + return Err(SkipReason::TokenPasting); + } + // Assignment operators — produce `()` in Rust, not a + // value, so the return type would mismatch. + "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | + "^=" | "<<=" | ">>=" => { + return Err(SkipReason::InvalidExpression); + } + // Note: && and || are handled at the translate_expr + // level (recursive descent), so they should not reach + // here. If they do, it's a bug — reject to be safe. + "&&" | "||" => { + return Err(SkipReason::InvalidExpression); + } + // Bitwise, modulo, and shift operators are invalid on + // floats. Reject when value type is f64/f32. + "%" | "|" | "^" | "<<" | ">>" + if matches!(ctx.value_type, "f64" | "f32") => + { + return Err(SkipReason::InvalidExpression); + } + "&" if matches!(ctx.value_type, "f64" | "f32") => { + return Err(SkipReason::InvalidExpression); + } + // Unary & (address-of) produces a reference type, not + // an integer. Detect by checking if the previous token + // was a value — if not, & is unary (not binary AND). + "&" if !prev_was_value => { + return Err(SkipReason::InvalidExpression); + } + "~" if matches!(ctx.value_type, "f64" | "f32") => { + return Err(SkipReason::InvalidExpression); + } + "~" => { + out.extend(quote! { ! }); + } + "!" => { + // C's `!` is logical NOT (0→1, nonzero→0). + // Rust's `!` on integers is bitwise NOT. + // Translate: !operand → (((operand) as i64 == 0) as vt) + let (operand_ts, consumed) = + translate_logical_not(&tokens[i + 1..], ctx)?; + out.extend(operand_ts); + i += 1 + consumed; + prev_was_value = true; + continue; + } + "(" => { + // Try keyword cast first: (int)(x), (unsigned long)(x) + if let Some((cast_ts, consumed)) = + try_parse_cast(&tokens[i..], ctx)? + { + out.extend(cast_ts); + i += consumed; + prev_was_value = true; + continue; + } + let close = find_matching_paren(tokens, i) + .ok_or(SkipReason::Empty)?; + let inner_tokens = &tokens[i + 1..close]; + + // (ident)( — either a typedef cast like (my_t)(x), + // a parenthesized function call like (foo)(x), or + // a call-through-param like (f)(x). None are + // expressible in const fn: typedef casts produce + // invalid Rust, and function/param calls aren't + // allowed in const context (i64 isn't callable). + if inner_tokens.len() == 1 && + inner_tokens[0].kind == CXToken_Identifier && + tokens.get(close + 1).is_some_and(|t| { + t.kind == CXToken_Punctuation && + t.spelling() == b"(" + }) + { + let name = + std::str::from_utf8(inner_tokens[0].spelling()) + .unwrap_or("?"); + return Err(SkipReason::FunctionCallOrCast( + name.to_owned(), + )); + } + + let inner = translate_expr(inner_tokens, ctx)?; + out.extend(std::iter::once(proc_macro2::TokenTree::Group( + Group::new(Delimiter::Parenthesis, inner), + ))); + i = close + 1; + prev_was_value = true; + continue; + } + _ => { + let op: TokenStream = + spelling.parse().map_err(|_| SkipReason::Empty)?; + out.extend(op); + prev_was_value = false; + } + }, + + CXToken_Keyword => match spelling { + "void" | "struct" | "union" | "enum" | "typeof" | "int" | + "unsigned" | "signed" | "long" | "short" | "char" | + "float" | "double" | "__asm__" | "__asm" | "asm" => { + return Err(SkipReason::TypeKeywordInBody( + spelling.to_owned(), + )); + } + "sizeof" => { + let (ts, consumed) = + translate_sizeof(&tokens[i + 1..], ctx)?; + out.extend(ts); + prev_was_value = true; + i += 1 + consumed; + continue; + } + _ => { + // Unknown keyword — emit as identifier. The syn + // validation will catch invalid constructs. + let ident = Ident::new(spelling, Span::call_site()); + out.extend(quote! { #ident }); + } + }, + + CXToken_Comment => {} + _ => {} + } + + i += 1; + } + + Ok(out) +} + +// --------------------------------------------------------------------------- +// sizeof translation +// --------------------------------------------------------------------------- + +/// Scan body tokens for `sizeof(param_name)` patterns and return the set of +/// parameter names used as `sizeof` arguments (these become generic type +/// params). +fn find_sizeof_params( + tokens: &[OwnedToken], + params: &[String], + sizeof_fns: &[String], + prior_macros: &[PriorFunctionMacro], +) -> Vec { + let mut sizeof_params = Vec::new(); + let mut i = 0; + while i < tokens.len() { + let spelling = std::str::from_utf8(tokens[i].spelling()).unwrap_or(""); + + // Direct sizeof(param). + let is_sizeof = (tokens[i].kind == CXToken_Identifier || + tokens[i].kind == CXToken_Keyword) && + spelling == "sizeof"; + + // Call to a sizeof-like function: FUNC(param) where FUNC + // takes only type params (e.g., _IOC_TYPECHECK). + let is_sizeof_fn = tokens[i].kind == CXToken_Identifier && + sizeof_fns.iter().any(|f| f == spelling); + + if is_sizeof || is_sizeof_fn { + if let Some(arg) = extract_sizeof_arg(&tokens[i + 1..], params) { + if !sizeof_params.contains(&arg) { + sizeof_params.push(arg); + } + } + } + + // Call to a known macro with mixed type/value params: + // CALLEE(arg1, arg2, ...). If any arg position corresponds + // to a callee type param AND the arg is one of our params, + // that param becomes a type param. + if tokens[i].kind == CXToken_Identifier { + if let Some(callee) = + prior_macros.iter().find(|m| m.name == spelling) + { + if !callee.type_param_names.is_empty() { + if let Some(args) = extract_call_args(&tokens[i + 1..]) { + for (pos, arg) in args.iter().enumerate() { + if let Some(param_name) = + callee.all_param_names.get(pos) + { + if callee.type_param_names.contains(param_name) && + params.contains(arg) && + !sizeof_params.contains(arg) + { + sizeof_params.push(arg.clone()); + } + } + } + } + } + } + } + + i += 1; + } + sizeof_params +} + +/// Extract single-identifier arguments from a `(arg1, arg2, ...)` call. +/// Returns a list of arg names if ALL args are single identifiers that +/// appear in `params`. Returns None if the call can't be parsed or has +/// complex args. +fn extract_call_args(tokens: &[OwnedToken]) -> Option> { + if tokens.is_empty() || + tokens[0].kind != CXToken_Punctuation || + tokens[0].spelling() != b"(" + { + return None; + } + let close = find_matching_paren(tokens, 0)?; + let arg_tokens = &tokens[1..close]; + let ranges = split_arg_ranges(arg_tokens); + let mut args = Vec::new(); + for &(start, end) in &ranges { + let arg = &arg_tokens[start..end]; + if arg.len() == 1 && arg[0].kind == CXToken_Identifier { + let name = std::str::from_utf8(arg[0].spelling()).ok()?.to_owned(); + args.push(name); + } else { + // Complex arg — can't determine type param propagation. + args.push(String::new()); + } + } + Some(args) +} + +/// Try to translate a function call using callee metadata to decide +/// whether turbofish syntax is needed. +/// +/// Looks up the callee in `ctx.prior_macros`. If the callee has type params, +/// the call arguments are matched positionally: callee type-param positions +/// become generic args, callee value-param positions become regular args. +/// +/// If a caller passes a type param where the callee expects a value, the +/// caller macro is unskippable (returns `Err`). +/// +/// `fn_name` is the raw callee name, `tokens` starts at `(`. +fn try_translate_call( + fn_name: &str, + tokens: &[OwnedToken], + ctx: &TranslateCtx, +) -> Result, SkipReason> { + if tokens.is_empty() || + tokens[0].kind != CXToken_Punctuation || + tokens[0].spelling() != b"(" + { + return Ok(None); + } + + // Look up callee in prior macros. + let callee = ctx.prior_macros.iter().find(|m| m.name == fn_name); + let Some(callee) = callee else { + // Unknown callee — forward reference. Return error so the + // fixpoint pass defers this macro to a later wave. + return Err(SkipReason::UnknownCallee(fn_name.to_owned())); + }; + + let Some(close) = find_matching_paren(tokens, 0) else { + return Ok(None); + }; + let arg_tokens = &tokens[1..close]; + let arg_ranges = split_arg_ranges(arg_tokens); + let total_callee_params = + callee.type_param_names.len() + callee.value_param_count; + + // If arg count doesn't match callee param count, the generated + // call would fail with E0061. Skip the macro rather than emit + // uncompilable code. + if arg_ranges.len() != total_callee_params { + return Err(SkipReason::ArityMismatch(format!( + "`{fn_name}` expects {} args, got {}", + total_callee_params, + arg_ranges.len(), + ))); + } + + // No type params on callee → regular call, no turbofish needed. + if callee.type_param_names.is_empty() { + // But check: is the caller passing a type param as a value arg? + for &(start, end) in &arg_ranges { + let arg = &arg_tokens[start..end]; + if arg.len() == 1 && arg[0].kind == CXToken_Identifier { + let name = std::str::from_utf8(arg[0].spelling()).unwrap_or(""); + let escaped = escape_keyword(name); + if ctx.type_params.contains(&escaped) { + // Passing a type param to a value-only callee — can't + // express this in const fn. + return Err(SkipReason::FunctionCallOrCast(format!( + "type param `{name}` passed as value to `{fn_name}`" + ))); + } + } + } + return Ok(None); + } + + // Callee has type params — build turbofish. + // Use POSITIONAL matching: check whether the callee's parameter at + // each position is a type param, rather than comparing names (which + // breaks when caller/callee use different param names, e.g., + // _IOR(type,nr,argtype) calling _IOC_TYPECHECK(t)). + let mut type_args = Vec::new(); + let mut value_args = Vec::new(); + + for (arg_idx, &(start, end)) in arg_ranges.iter().enumerate() { + let arg = &arg_tokens[start..end]; + + // Is this position a type-param position in the callee? + let callee_param_is_type = callee + .all_param_names + .get(arg_idx) + .is_some_and(|p| callee.type_param_names.contains(p)); + + if arg.len() == 1 && arg[0].kind == CXToken_Identifier { + let name = std::str::from_utf8(arg[0].spelling()).unwrap_or(""); + let escaped = escape_keyword(name); + + if ctx.type_params.contains(&escaped) { + if callee_param_is_type { + // Caller type param → callee type param: turbofish. + type_args.push(make_ident(name)); + continue; + } + // Caller passes a type param but callee expects a value. + return Err(SkipReason::FunctionCallOrCast(format!( + "type param `{name}` passed as value to `{fn_name}`" + ))); + } + + if callee_param_is_type { + // Callee expects type at this position, caller passes a + // non-type-param identifier — treat as concrete type. + type_args.push(make_ident(name)); + continue; + } + } else if callee_param_is_type { + // Complex expression in a type param position — can't + // express as turbofish generic argument. + return Err(SkipReason::FunctionCallOrCast(format!( + "complex expression in type param position for \ + `{fn_name}`" + ))); + } + + // Value argument. + value_args.push(translate_expr(arg, ctx)?); + } + + let fn_ident = make_ident(fn_name); + let vt = ctx.vt(); + let ts = if value_args.is_empty() { + quote! { (#fn_ident ::< #(#type_args),* >() as #vt) } + } else { + quote! { (#fn_ident ::< #(#type_args),* >( #(#value_args),* ) as #vt) } + }; + + Ok(Some((ts, close + 1))) +} + +/// Split tokens into argument ranges by top-level commas. +/// Returns `(start, end)` index pairs into the token slice. +/// An empty token slice (from `()`) returns an empty vec (0 arguments). +fn split_arg_ranges(tokens: &[OwnedToken]) -> Vec<(usize, usize)> { + if tokens.is_empty() { + return Vec::new(); + } + + let mut ranges = Vec::new(); + let mut start = 0; + let mut depth = 0i32; + + for (i, t) in tokens.iter().enumerate() { + if t.kind == CXToken_Punctuation { + match t.spelling() { + b"(" => depth += 1, + b")" => depth -= 1, + b"," if depth == 0 => { + ranges.push((start, i)); + start = i + 1; + } + _ => {} + } + } + } + if start <= tokens.len() { + ranges.push((start, tokens.len())); + } + ranges +} + +/// Extract the parameter name from a `sizeof(param)` expression. +/// +/// `tokens` starts after the `sizeof` keyword. +fn extract_sizeof_arg( + tokens: &[OwnedToken], + params: &[String], +) -> Option { + if tokens.is_empty() { + return None; + } + if tokens[0].kind == CXToken_Punctuation && tokens[0].spelling() == b"(" { + let close = find_matching_paren(tokens, 0)?; + let inner = &tokens[1..close]; + if inner.len() == 1 && inner[0].kind == CXToken_Identifier { + let name = + std::str::from_utf8(inner[0].spelling()).ok()?.to_owned(); + if params.contains(&name) { + return Some(name); + } + } + } + None +} + +/// Translate a `sizeof(...)` expression to `core::mem::size_of`. +/// +/// Returns `(TokenStream, tokens_consumed)`. +fn translate_sizeof( + tokens: &[OwnedToken], + ctx: &TranslateCtx, +) -> Result<(TokenStream, usize), SkipReason> { + if tokens.is_empty() { + return Err(SkipReason::UntranslatableSizeof); + } + + if tokens[0].kind == CXToken_Punctuation && tokens[0].spelling() == b"(" { + let close = find_matching_paren(tokens, 0) + .ok_or(SkipReason::UntranslatableSizeof)?; + let inner = &tokens[1..close]; + + // Type parameter: sizeof(T) -> core::mem::size_of::() as vt + if inner.len() == 1 && inner[0].kind == CXToken_Identifier { + let name = std::str::from_utf8(inner[0].spelling()) + .map_err(|_| SkipReason::UntranslatableSizeof)?; + let escaped = escape_keyword(name); + if ctx.type_params.contains(&escaped) { + let type_ident = make_ident(name); + let vt = ctx.vt(); + return Ok(( + quote! { core::mem::size_of::<#type_ident>() as #vt }, + close + 1, + )); + } + } + + // Concrete C type: sizeof(int) -> core::mem::size_of::() as vt + if let Some(rust_type) = + try_translate_c_type(inner, ctx.target_pointer_size) + { + let rt = parse_ts(&rust_type); + let vt = ctx.vt(); + return Ok(( + quote! { core::mem::size_of::<#rt>() as #vt }, + close + 1, + )); + } + } + + Err(SkipReason::UntranslatableSizeof) +} + +// --------------------------------------------------------------------------- +// Logical NOT translation +// --------------------------------------------------------------------------- + +/// Build a logical NOT expression: `(((operand) == 0) as vt)` for integers, +/// `((operand == 0.0) as vt)` for floats. +fn logical_not_expr(operand: &TokenStream, ctx: &TranslateCtx) -> TokenStream { + let vt = ctx.vt(); + if matches!(ctx.value_type, "f64" | "f32") { + // Float truthiness: 0.0 is false, anything else is true. + // Can't cast bool directly to f64 — go through i64. + quote! { (((#operand) == 0.0) as i64 as #vt) } + } else { + // Integer truthiness via cast to i64. + quote! { (((#operand) as i64 == 0) as #vt) } + } +} + +/// Translate C's logical NOT `!operand` to Rust. +/// +/// C's `!` is logical NOT: `!0` = 1, `!nonzero` = 0. +/// Rust's `!` on integers is bitwise NOT (completely different). +/// +/// For integer types: `!operand` → `(((operand) as i64 == 0) as vt)` +/// For float types: `!operand` → `(((operand) == 0.0) as vt)` +/// +/// `tokens` starts after the `!`. Returns `(TokenStream, tokens_consumed)`. +fn translate_logical_not( + tokens: &[OwnedToken], + ctx: &TranslateCtx, +) -> Result<(TokenStream, usize), SkipReason> { + if tokens.is_empty() { + return Err(SkipReason::Empty); + } + + // Parenthesized operand: !(expr) + if tokens[0].kind == CXToken_Punctuation && tokens[0].spelling() == b"(" { + let close = find_matching_paren(tokens, 0).ok_or(SkipReason::Empty)?; + let inner = translate_expr(&tokens[1..close], ctx)?; + return Ok((logical_not_expr(&inner, ctx), close + 1)); + } + + let operand_spelling = std::str::from_utf8(tokens[0].spelling()) + .map_err(|_| SkipReason::Empty)?; + + // Chained NOT: !!x → recurse. + if tokens[0].kind == CXToken_Punctuation && operand_spelling == "!" { + let (inner, consumed) = translate_logical_not(&tokens[1..], ctx)?; + return Ok((logical_not_expr(&inner, ctx), 1 + consumed)); + } + + // sizeof operand: !sizeof(T) → NOT of size_of. + if (tokens[0].kind == CXToken_Identifier || + tokens[0].kind == CXToken_Keyword) && + operand_spelling == "sizeof" + { + let (sizeof_ts, consumed) = translate_sizeof(&tokens[1..], ctx)?; + return Ok((logical_not_expr(&sizeof_ts, ctx), 1 + consumed)); + } + + // Identifier operand — but check if it's a function call: !ID(x). + if tokens[0].kind == CXToken_Identifier { + // Reject non-const variables (same check as translate_tokens). + if ctx.non_const_vars.contains(operand_spelling) { + return Err(SkipReason::MutableStaticReference( + operand_spelling.to_owned(), + )); + } + + if tokens.get(1).is_some_and(|t| { + t.kind == CXToken_Punctuation && t.spelling() == b"(" + }) { + let close = + find_matching_paren(tokens, 1).ok_or(SkipReason::Empty)?; + let call_tokens = &tokens[..=close]; + let call_ts = translate_tokens(call_tokens, ctx)?; + return Ok((logical_not_expr(&call_ts, ctx), close + 1)); + } + + let ident = make_ident(operand_spelling); + return Ok((logical_not_expr("e! { #ident }, ctx), 1)); + } + + // Literal operand — use translate_literal for C suffix stripping. + if tokens[0].kind == CXToken_Literal { + let lit_ts = translate_literal(operand_spelling, ctx)?; + return Ok((logical_not_expr(&lit_ts, ctx), 1)); + } + + // Other single-token operand (unlikely but safe fallback). + let operand: TokenStream = + operand_spelling.parse().map_err(|_| SkipReason::Empty)?; + Ok((logical_not_expr(&operand, ctx), 1)) +} + +// --------------------------------------------------------------------------- +// C cast detection and translation +// --------------------------------------------------------------------------- + +/// Try to parse a C-style cast at the current position. +/// +/// Detects `(type_keywords)expr` and returns the translated Rust `as` +/// expression with the number of tokens consumed. +fn try_parse_cast( + tokens: &[OwnedToken], + ctx: &TranslateCtx, +) -> Result, SkipReason> { + if tokens.is_empty() || + tokens[0].kind != CXToken_Punctuation || + tokens[0].spelling() != b"(" + { + return Ok(None); + } + + let Some(close) = find_matching_paren(tokens, 0) else { + return Ok(None); + }; + let inner = &tokens[1..close]; + + if inner.is_empty() { + return Ok(None); + } + + let all_type_keywords = inner.iter().all(|t| { + t.kind == CXToken_Keyword && + matches!( + std::str::from_utf8(t.spelling()).unwrap_or(""), + "int" | + "unsigned" | + "signed" | + "long" | + "short" | + "char" | + "float" | + "double" + ) + }); + + if !all_type_keywords { + return Ok(None); + } + + let Some(rust_type) = try_translate_c_type(inner, ctx.target_pointer_size) + else { + return Ok(None); + }; + + let rest = &tokens[close + 1..]; + if rest.is_empty() { + return Ok(None); + } + + let rt = parse_ts(&rust_type); + let vt = ctx.vt(); + + if rest[0].kind == CXToken_Punctuation && rest[0].spelling() == b"(" { + // (type)(expr) — parenthesized operand. + let expr_close = + find_matching_paren(rest, 0).ok_or(SkipReason::Empty)?; + let expr = translate_expr(&rest[1..expr_close], ctx)?; + let consumed = close + 1 + expr_close + 1; + Ok(Some((quote! { ((#expr) as #rt as #vt) }, consumed))) + } else if rest[0].kind == CXToken_Identifier || + rest[0].kind == CXToken_Literal + { + // (type)operand — single token operand (identifier or literal). + let operand = std::str::from_utf8(rest[0].spelling()) + .map_err(|_| SkipReason::Empty)?; + // Reject non-const variables (same check as translate_tokens). + if rest[0].kind == CXToken_Identifier && + ctx.non_const_vars.contains(operand) + { + return Err(SkipReason::MutableStaticReference(operand.to_owned())); + } + let operand_ts = translate_cast_operand(operand); + let consumed = close + 1 + 1; + Ok(Some((quote! { ((#operand_ts) as #rt as #vt) }, consumed))) + } else if rest[0].kind == CXToken_Punctuation && + (rest[0].spelling() == b"-" || rest[0].spelling() == b"+") && + rest.len() > 1 + { + // (type)-operand or (type)+operand — unary prefix after cast. + let prefix: TokenStream = std::str::from_utf8(rest[0].spelling()) + .map_err(|_| SkipReason::Empty)? + .parse() + .map_err(|_| SkipReason::Empty)?; + + if rest[1].kind == CXToken_Punctuation && rest[1].spelling() == b"(" { + // (type)-(expr) — prefix + parenthesized expression. + let expr_close = + find_matching_paren(rest, 1).ok_or(SkipReason::Empty)?; + let inner = translate_expr(&rest[2..expr_close], ctx)?; + let consumed = close + 1 + expr_close + 1; + Ok(Some(( + quote! { ((#prefix (#inner)) as #rt as #vt) }, + consumed, + ))) + } else if rest[1].kind == CXToken_Identifier || + rest[1].kind == CXToken_Literal + { + // (type)-token — prefix + single identifier or literal. + let operand = std::str::from_utf8(rest[1].spelling()) + .map_err(|_| SkipReason::Empty)?; + if rest[1].kind == CXToken_Identifier && + ctx.non_const_vars.contains(operand) + { + return Err(SkipReason::MutableStaticReference( + operand.to_owned(), + )); + } + let operand_ts = translate_cast_operand(operand); + let consumed = close + 1 + 2; + Ok(Some(( + quote! { ((#prefix #operand_ts) as #rt as #vt) }, + consumed, + ))) + } else { + Ok(None) + } + } else { + // Operand we can't handle. Fall back to normal paren + // processing which will reject the type keywords. + Ok(None) + } +} + +/// Translate a single-token cast operand. Handles both identifiers (including +/// keywords) and numeric/char literals. +fn translate_cast_operand(operand: &str) -> TokenStream { + // Try as a number literal first. + if operand.starts_with(|c: char| c.is_ascii_digit()) { + let num = strip_c_suffix(operand); + let num_str = + try_convert_c_octal(num).unwrap_or_else(|| num.to_owned()); + return num_str.parse().unwrap_or_default(); + } + // Character literal. + if operand.starts_with('\'') { + return operand.parse().unwrap_or_default(); + } + // Identifier (possibly a keyword). + let ident = make_ident(operand); + quote! { #ident } +} + +/// Translate a sequence of C type keyword tokens to a Rust type name. +/// +/// `target_pointer_size` is in bytes (4 for 32-bit, 8 for 64-bit) and +/// controls the mapping of `long` / `unsigned long` which are +/// pointer-width on most platforms. +fn try_translate_c_type( + tokens: &[OwnedToken], + target_pointer_size: usize, +) -> Option { + let words: Vec<&str> = tokens + .iter() + .filter_map(|t| std::str::from_utf8(t.spelling()).ok()) + .collect(); + + let type_str = words.join(" "); + + // long/unsigned long are target-dependent: 32-bit on ILP32, 64-bit + // on LP64. long long is always 64-bit. + let (long_signed, long_unsigned) = if target_pointer_size >= 8 { + ("i64", "u64") + } else { + ("i32", "u32") + }; + + match type_str.as_str() { + "unsigned long long" => Some("u64".to_owned()), + "long long" | "signed long long" => Some("i64".to_owned()), + "unsigned long" => Some(long_unsigned.to_owned()), + "long" | "signed long" => Some(long_signed.to_owned()), + "unsigned int" | "unsigned" => Some("u32".to_owned()), + "int" | "signed int" | "signed" => Some("i32".to_owned()), + "unsigned short" => Some("u16".to_owned()), + "short" | "signed short" => Some("i16".to_owned()), + "unsigned char" => Some("u8".to_owned()), + "char" | "signed char" => Some("i8".to_owned()), + "float" => Some("f32".to_owned()), + "double" => Some("f64".to_owned()), + _ if words.len() == 2 && + matches!(words[0], "struct" | "union" | "enum") => + { + Some(words[1].to_owned()) + } + _ => None, + } +} + +// --------------------------------------------------------------------------- +// Literal translation +// --------------------------------------------------------------------------- + +/// Translate a C literal to a `TokenStream`. +fn translate_literal( + lit: &str, + ctx: &TranslateCtx, +) -> Result { + // String literals can't match integer/float return types. + if lit.starts_with('"') { + return Err(SkipReason::InvalidExpression); + } + // Character literal — cast to value type. + if lit.starts_with('\'') { + let char_ts: TokenStream = lit.parse().unwrap_or_default(); + let vt = ctx.vt(); + return Ok(quote! { (#char_ts as #vt) }); + } + // Number literal — strip C suffixes, parse as bare number. + let num = strip_c_suffix(lit); + // Float literals with an integer value type produce type mismatches. + // If the value type is f64/f32, the literal is allowed through. + if is_float_literal(num) && !matches!(ctx.value_type, "f64" | "f32") { + return Err(SkipReason::InvalidExpression); + } + // Convert C octal (0644) to Rust octal (0o644). + let num_str = try_convert_c_octal(num).unwrap_or_else(|| num.to_owned()); + let num_ts: TokenStream = num_str.parse().unwrap_or_default(); + // When value type is float but the literal is integer (e.g., `0` + // in a macro that also has `1.0f`), cast so Rust doesn't infer + // it as an integer in comparison/arithmetic context. + if matches!(ctx.value_type, "f64" | "f32") && !is_float_literal(num) { + let vt = ctx.vt(); + return Ok(quote! { (#num_ts as #vt) }); + } + Ok(num_ts) +} + +/// Convert a C octal literal to Rust syntax. +/// +/// C uses `0644` for octal; Rust uses `0o644`. Returns `None` if not octal. +/// A literal is C octal if it starts with `0`, has more than one digit, and +/// doesn't have a `0x`/`0X`/`0b`/`0B` prefix. +fn try_convert_c_octal(num: &str) -> Option { + if num.len() > 1 && + num.starts_with('0') && + !num.starts_with("0x") && + !num.starts_with("0X") && + !num.starts_with("0b") && + !num.starts_with("0B") && + !num.starts_with("0o") && + !num.starts_with("0O") && + num[1..].chars().all(|c| c.is_ascii_digit()) + { + Some(format!("0o{}", &num[1..])) + } else { + None + } +} + +/// Check if a (suffix-stripped) numeric literal exceeds `u32::MAX`. +fn literal_exceeds_u32(num: &str) -> bool { + let val = if num.starts_with("0x") || num.starts_with("0X") { + u64::from_str_radix(&num[2..], 16).ok() + } else if num.len() > 1 && + num.starts_with('0') && + num[1..].chars().all(|c| c.is_ascii_digit()) + { + // C octal (leading zero). + u64::from_str_radix(&num[1..], 8).ok() + } else { + num.parse::().ok() + }; + val.is_some_and(|v| v > u64::from(u32::MAX)) +} + +/// Check if a (suffix-stripped) numeric literal is a float. +/// Detects decimal points (`1.5`) and exponent notation (`1e3`, `1E-3`). +fn is_float_literal(num: &str) -> bool { + // Must not be hex (0x prefix) — 'e'/'E' are hex digits there. + if num.starts_with("0x") || num.starts_with("0X") { + return false; + } + num.contains('.') || num.contains('e') || num.contains('E') +} + +/// Strip C type suffixes (`U`, `L`, `UL`, `ULL`, `f`, `F`, etc.) from a +/// number literal. Also handles MSVC-specific suffixes (`i8`, `i16`, +/// `i32`, `i64`, `ui8`, `ui16`, `ui32`, `ui64`). +fn strip_c_suffix(lit: &str) -> &str { + // MSVC-specific suffixes: i8, i16, i32, i64, ui8, ui16, ui32, ui64 + // in any case combination (Clang in MSVC mode accepts mixed case). + // Check these first since they contain digits that would confuse + // the simpler u/U/l/L scan below. + { + let lower = lit.to_ascii_lowercase(); + for suffix in + &["ui64", "ui32", "ui16", "ui8", "i64", "i32", "i16", "i8"] + { + if lower.ends_with(suffix) { + let stripped = &lit[..lit.len() - suffix.len()]; + if !stripped.is_empty() { + return stripped; + } + } + } + } + + // Strip standard C integer suffixes: u/U/l/L. + let end = lit.find(['u', 'U', 'l', 'L']).unwrap_or(lit.len()); + let num = &lit[..end]; + let num = if num.is_empty() { lit } else { num }; + + // Strip float suffix f/F from the end, but NOT for hex literals + // (where f/F are valid hex digits, e.g., 0xFF). + if !num.starts_with("0x") && !num.starts_with("0X") { + if let Some(stripped) = + num.strip_suffix('f').or_else(|| num.strip_suffix('F')) + { + if !stripped.is_empty() { + return stripped; + } + } + } + + num +} + +// --------------------------------------------------------------------------- +// Post-syn semantic checks +// --------------------------------------------------------------------------- + +/// Recursively check a parsed expression for constructs that are +/// syntactically valid Rust but produce type mismatches in our generated +/// `const fn` (which returns an integer type). +fn expr_has_unsupported_construct(expr: &syn::Expr) -> bool { + match expr { + // C comma operator becomes a Rust tuple — type mismatch. + syn::Expr::Tuple(_) => true, + // Recurse into common wrapper expressions. + syn::Expr::Paren(p) => expr_has_unsupported_construct(&p.expr), + syn::Expr::Group(g) => expr_has_unsupported_construct(&g.expr), + syn::Expr::Binary(b) => { + expr_has_unsupported_construct(&b.left) || + expr_has_unsupported_construct(&b.right) + } + syn::Expr::Unary(u) => expr_has_unsupported_construct(&u.expr), + syn::Expr::Cast(c) => expr_has_unsupported_construct(&c.expr), + syn::Expr::Call(c) => { + c.args.iter().any(expr_has_unsupported_construct) + } + syn::Expr::If(i) => { + expr_has_unsupported_construct(&i.cond) || + i.then_branch.stmts.iter().any(|s| { + matches!(s, syn::Stmt::Expr(e, _) if expr_has_unsupported_construct(e)) + }) || + i.else_branch + .as_ref() + .is_some_and(|(_, e)| { + expr_has_unsupported_construct(e) + }) + } + _ => false, + } +} + +// --------------------------------------------------------------------------- +// Utility +// --------------------------------------------------------------------------- + +/// Find the matching close paren for the open paren at `tokens[pos]`. +fn find_matching_paren(tokens: &[OwnedToken], pos: usize) -> Option { + let mut depth = 0i32; + for (i, t) in tokens[pos..].iter().enumerate() { + if t.kind == CXToken_Punctuation { + match t.spelling() { + b"(" => depth += 1, + b")" => { + depth -= 1; + if depth == 0 { + return Some(pos + i); + } + } + _ => {} + } + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strip_c_suffix() { + assert_eq!(strip_c_suffix("42"), "42"); + assert_eq!(strip_c_suffix("42U"), "42"); + assert_eq!(strip_c_suffix("42UL"), "42"); + assert_eq!(strip_c_suffix("42ULL"), "42"); + assert_eq!(strip_c_suffix("42L"), "42"); + assert_eq!(strip_c_suffix("42LL"), "42"); + assert_eq!(strip_c_suffix("0xFF"), "0xFF"); + assert_eq!(strip_c_suffix("0xFFU"), "0xFF"); + assert_eq!(strip_c_suffix("0xFFUL"), "0xFF"); + // Float suffix f/F. + assert_eq!(strip_c_suffix("1.5f"), "1.5"); + assert_eq!(strip_c_suffix("1.5F"), "1.5"); + assert_eq!(strip_c_suffix("2.0f"), "2.0"); + // Hex literals: f/F are hex digits, NOT suffixes. + assert_eq!(strip_c_suffix("0xFf"), "0xFf"); + assert_eq!(strip_c_suffix("0xABCDEF"), "0xABCDEF"); + // MSVC suffixes. + assert_eq!(strip_c_suffix("42i64"), "42"); + assert_eq!(strip_c_suffix("42i32"), "42"); + assert_eq!(strip_c_suffix("42ui64"), "42"); + assert_eq!(strip_c_suffix("42ui32"), "42"); + assert_eq!(strip_c_suffix("42i8"), "42"); + assert_eq!(strip_c_suffix("0xFFui64"), "0xFF"); + // MSVC suffixes: any case combination. + assert_eq!(strip_c_suffix("42I64"), "42"); + assert_eq!(strip_c_suffix("42UI64"), "42"); + assert_eq!(strip_c_suffix("42I32"), "42"); + assert_eq!(strip_c_suffix("42Ui64"), "42"); + assert_eq!(strip_c_suffix("42uI64"), "42"); + } + + #[test] + fn test_try_convert_c_octal() { + // C octal → Rust octal. + assert_eq!(try_convert_c_octal("0644"), Some("0o644".to_owned())); + assert_eq!(try_convert_c_octal("077"), Some("0o77".to_owned())); + assert_eq!(try_convert_c_octal("00"), Some("0o0".to_owned())); + // NOT octal: hex, binary, single zero, decimal. + assert_eq!(try_convert_c_octal("0xFF"), None); + assert_eq!(try_convert_c_octal("0b101"), None); + assert_eq!(try_convert_c_octal("0"), None); + assert_eq!(try_convert_c_octal("42"), None); + // Already Rust octal. + assert_eq!(try_convert_c_octal("0o77"), None); + } + + #[test] + fn test_literal_exceeds_u32() { + // Fits in u32. + assert!(!literal_exceeds_u32("0")); + assert!(!literal_exceeds_u32("1")); + assert!(!literal_exceeds_u32("4294967295")); // u32::MAX + assert!(!literal_exceeds_u32("0xFFFFFFFF")); + assert!(!literal_exceeds_u32("037777777777")); // octal u32::MAX + // Exceeds u32. + assert!(literal_exceeds_u32("4294967296")); // u32::MAX + 1 + assert!(literal_exceeds_u32("0x100000000")); + assert!(literal_exceeds_u32("040000000000")); // octal u32::MAX + 1 + assert!(literal_exceeds_u32("0xFFFFFFFFFFFFFFFF")); + } + + #[test] + fn test_make_ident_keyword() { + let ident = make_ident("type"); + assert_eq!(ident.to_string(), "r#type"); + } + + #[test] + fn test_make_ident_normal() { + let ident = make_ident("foo"); + assert_eq!(ident.to_string(), "foo"); + } + + #[test] + fn test_skip_reason_display() { + assert_eq!(SkipReason::Variadic.to_string(), "variadic macro"); + assert_eq!( + SkipReason::TypeKeywordInBody("void".into()).to_string(), + "untranslatable C keyword `void` in body" + ); + } + + #[test] + fn test_infer_value_type_empty() { + let types = HashMap::new(); + assert_eq!(infer_value_type(&[], &[], &types), "i64"); + } + + #[test] + fn test_translate_cast_operand_literal() { + let ts = translate_cast_operand("42"); + assert_eq!(ts.to_string(), "42"); + } + + #[test] + fn test_translate_cast_operand_ident() { + let ts = translate_cast_operand("foo"); + assert_eq!(ts.to_string(), "foo"); + } +} diff --git a/bindgen/ir/mod.rs b/bindgen/ir/mod.rs index acdb4896cd..bd1ac6c087 100644 --- a/bindgen/ir/mod.rs +++ b/bindgen/ir/mod.rs @@ -12,6 +12,7 @@ pub(crate) mod context; pub(crate) mod derive; pub(crate) mod dot; pub(crate) mod enum_ty; +pub(crate) mod func_macro; pub(crate) mod function; pub(crate) mod int; pub(crate) mod item; diff --git a/bindgen/ir/var.rs b/bindgen/ir/var.rs index 9d72dcf06e..fd5d4a9587 100644 --- a/bindgen/ir/var.rs +++ b/bindgen/ir/var.rs @@ -12,6 +12,7 @@ use crate::clang; use crate::clang::ClangToken; use crate::parse::{ClangSubItemParser, ParseError, ParseResult}; +use std::collections::HashMap; use std::io; use std::num::Wrapping; @@ -149,6 +150,62 @@ fn default_macro_constant_type(ctx: &BindgenContext, value: i64) -> IntKind { } } +/// Build a map of macro constant names to their Rust type strings by +/// examining the `parsed_macros` in the context. +pub(crate) fn build_constant_type_map( + ctx: &BindgenContext, +) -> HashMap { + let mut map = HashMap::new(); + for (name_bytes, eval_result) in ctx.parsed_macros() { + if let cexpr::expr::EvalResult::Int(Wrapping(value)) = eval_result { + let kind = ctx + .options() + .last_callback(|c| { + let name = String::from_utf8_lossy(name_bytes); + c.int_macro(&name, *value) + }) + .unwrap_or_else(|| default_macro_constant_type(ctx, *value)); + let type_str = match kind { + IntKind::Bool => "bool", + IntKind::I8 => "i8", + IntKind::U8 => "u8", + IntKind::I16 => "i16", + IntKind::U16 => "u16", + IntKind::I32 => "i32", + IntKind::U32 => "u32", + IntKind::I64 => "i64", + IntKind::U64 => "u64", + _ => "i64", + }; + if let Ok(name) = String::from_utf8(name_bytes.clone()) { + map.insert(name, type_str.to_owned()); + } + } + } + map +} + +/// Collect a raw function-like macro definition for deferred translation. +fn collect_raw_function_macro( + ctx: &mut BindgenContext, + cursor: &clang::Cursor, + tokens: &[ClangToken], +) { + let source_file = { + let (file, _, _, _) = cursor.location().location(); + file.name() + }; + let owned_tokens = tokens + .iter() + .map(super::func_macro::OwnedToken::from_clang) + .collect(); + ctx.add_raw_function_macro(super::func_macro::RawFunctionMacro { + name: cursor.spelling(), + tokens: owned_tokens, + source_file, + }); +} + /// Parses tokens from a `CXCursor_MacroDefinition` pointing into a function-like /// macro, and calls the `func_macro` callback. fn handle_function_macro( @@ -192,11 +249,25 @@ impl ClangSubItemParser for Var { if cursor.is_macro_function_like() { handle_function_macro(&cursor, callbacks.as_ref()); - // We handled the macro, skip macro processing below. + if ctx.options().translate_function_macros { + let tokens: Vec<_> = + cursor.tokens().iter().collect(); + collect_raw_function_macro(ctx, &cursor, &tokens); + } return Err(ParseError::Continue); } } + // If this is a function-like macro and we have no callbacks + // (the loop above didn't run), handle it here. + if cursor.is_macro_function_like() { + if ctx.options().translate_function_macros { + let tokens: Vec<_> = cursor.tokens().iter().collect(); + collect_raw_function_macro(ctx, &cursor, &tokens); + } + return Err(ParseError::Continue); + } + let value = parse_macro(ctx, &cursor); let Some((id, value)) = value else { diff --git a/bindgen/options/cli.rs b/bindgen/options/cli.rs index 5304862584..431a8aa2a3 100644 --- a/bindgen/options/cli.rs +++ b/bindgen/options/cli.rs @@ -496,6 +496,9 @@ struct BindgenCommand { /// Wrap unsafe operations in unsafe blocks. #[arg(long)] wrap_unsafe_ops: bool, + /// Translate function-like C macros to Rust const fn declarations. + #[arg(long)] + translate_function_macros: bool, /// Enable fallback for clang macro parsing. #[arg(long)] clang_macro_fallback: bool, @@ -694,6 +697,7 @@ where merge_extern_blocks, override_abi, wrap_unsafe_ops, + translate_function_macros, clang_macro_fallback, clang_macro_fallback_build_dir, flexarray_dst, @@ -1000,6 +1004,7 @@ where merge_extern_blocks, override_abi => |b, (abi, regex)| b.override_abi(abi, regex), wrap_unsafe_ops, + translate_function_macros => |b, _| b.translate_function_macros(), clang_macro_fallback => |b, _| b.clang_macro_fallback(), clang_macro_fallback_build_dir, flexarray_dst, diff --git a/bindgen/options/mod.rs b/bindgen/options/mod.rs index baa541c5ac..63f1f99345 100644 --- a/bindgen/options/mod.rs +++ b/bindgen/options/mod.rs @@ -2242,6 +2242,24 @@ options! { }, as_args: "--emit-diagnostics", }, + /// Whether to translate function-like C macros to Rust `const fn` declarations. + translate_function_macros: bool { + methods: { + /// Translate function-like C preprocessor macros into Rust `const fn` declarations. + /// + /// For example, `#define ADD(x, y) ((x) + (y))` becomes + /// `pub const fn ADD(x: i64, y: i64) -> i64 { ((x) + (y)) }`. + /// + /// Supports arithmetic, casts, `sizeof`, ternary, logical operators, + /// and cross-macro calls. Variadic macros, statement macros, and other + /// untranslatable constructs are silently skipped. + pub fn translate_function_macros(mut self) -> Self { + self.options.translate_function_macros = true; + self + } + }, + as_args: "--translate-function-macros", + } /// Whether to use Clang evaluation on temporary files as a fallback for macros that fail to /// parse. clang_macro_fallback: bool { diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index ac57aaad64..7fcd4a2a89 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -28,4 +28,5 @@ - [Using Unions](./using-unions.md) - [Using Bitfields](./using-bitfields.md) - [Using Flexible Array Members](./using-fam.md) +- [Translating Function-Like Macros](./function-like-macros.md) - [FAQ](./faq.md) diff --git a/book/src/function-like-macros.md b/book/src/function-like-macros.md new file mode 100644 index 0000000000..0eda501077 --- /dev/null +++ b/book/src/function-like-macros.md @@ -0,0 +1,208 @@ +# Translating Function-Like Macros + +By default, `bindgen` can only generate Rust constants for simple object-like C +macros (e.g., `#define FOO 42`). Function-like macros (macros with parameters) +are silently skipped. + +The `--translate-function-macros` flag enables translating function-like C macros +into Rust `const fn` declarations. + +## Basic Usage + +**CLI:** + +```console +bindgen input.h --translate-function-macros +``` + +**`build.rs`:** + +```rust,ignore +let bindings = bindgen::Builder::default() + .header("input.h") + .translate_function_macros() + .generate() + .expect("Unable to generate bindings"); +``` + +## What Gets Translated + +Given: + +```c +#define ADD(x, y) ((x) + (y)) +#define FLAG(n) (1 << (n)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +``` + +`bindgen` generates: + +```rust,ignore +pub const fn ADD(x: i64, y: i64) -> i64 { ((x) + (y)) } +pub const fn FLAG(n: i64) -> i64 { (1 << (n)) } +pub const fn MAX(a: i64, b: i64) -> i64 { + (if (a) > (b) { (a) } else { (b) }) +} +``` + +## Type Inference + +When a macro body references object-like macro constants, `bindgen` infers the +value type from those constants. For example, if all referenced constants are +`u32`, the function uses `u32` for its parameters and return type. + +Type inference runs after the entire header is parsed, so the order of `#define`s +does not matter. Forward references between function-like macros are also +supported — `bindgen` translates them in multiple passes until all dependencies +are resolved. + +```c +#define _IOC_DIRSHIFT 30 // bindgen emits: pub const _IOC_DIRSHIFT: u32 = 30; +#define _IOC_TYPESHIFT 8 +#define _IOC(dir, type, nr, size) \ + (((dir) << _IOC_DIRSHIFT) | ((type) << _IOC_TYPESHIFT) | (nr) | ((size) << 16)) +``` + +Generates: + +```rust,ignore +pub const fn _IOC(dir: u32, r#type: u32, nr: u32, size: u32) -> u32 { + ((dir) << _IOC_DIRSHIFT) | ((r#type) << _IOC_TYPESHIFT) | (nr) | ((size) << 16) +} +``` + +When no constants are referenced, the type defaults to `i64`. + +### Overriding the Type + +Use the `func_macro_type` callback to override the inferred type: + +```rust,ignore +impl bindgen::callbacks::ParseCallbacks for MyCallbacks { + fn func_macro_type(&self, name: &str, _inferred: &str) -> Option { + if name == "MY_MACRO" { + Some("u16".into()) + } else { + None + } + } +} +``` + +## sizeof and Generic Type Parameters + +When a macro parameter is used as a `sizeof` argument, it becomes a generic type +parameter: + +```c +#define _IOR(type, nr, size) _IOC(2, (type), (nr), sizeof(size)) +``` + +Generates: + +```rust,ignore +pub const fn _IOR(r#type: u32, nr: u32) -> u32 { + _IOC(2, (r#type), (nr), core::mem::size_of::() as u32) +} +``` + +Called as `_IOR::(b'M' as u32, 1)`. + +Concrete types in `sizeof` are also supported: + +```c +#define OFFSET(n) ((n) + sizeof(int)) +``` + +Generates `(n) + core::mem::size_of::() as i64`. + +## C Casts + +C-style casts like `(unsigned long)(x)` are translated to Rust `as` expressions: + +```c +#define TO_U32(x) ((unsigned int)(x)) +``` + +Generates: `((x) as u32 as i64)` + +The intermediate cast preserves truncation semantics, and the final `as` converts +back to the function's value type. + +## Logical Operators + +C's logical NOT `!`, AND `&&`, and OR `||` are translated with correct +integer truthiness semantics: + +```c +#define NOT(x) (!(x)) +#define BOTH(x,y) ((x) && (y)) +#define EITHER(x,y) ((x) || (y)) +``` + +Generates: + +```rust,ignore +pub const fn NOT(x: i64) -> i64 { (((x) as i64 == 0) as i64) } +pub const fn BOTH(x: i64, y: i64) -> i64 { + (((((x) as i64) != 0) && (((y) as i64) != 0)) as i64) +} +``` + +C's `!` is logical NOT (0→1, nonzero→0), not Rust's bitwise NOT. +Each `&&`/`||` operand is wrapped with `!= 0` to convert integers to `bool`, +and the result is cast back to the value type (0 or 1, matching C semantics). + +## Float Literals + +When a macro body contains float literals (including exponent notation like +`1e3`), the value type is automatically inferred as `f64`: + +```c +#define ADD_HALF(x) ((x) + 0.5f) +``` + +Generates: `pub const fn ADD_HALF(x: f64) -> f64 { ((x) + 0.5) }` + +Macros that mix float literals with integer-only operators (`%`, `&`, `|`, +`^`, `<<`, `>>`) are skipped since those operators don't work on floats. + +## Octal Literals + +C octal literals (leading zero, e.g., `0644`) are converted to Rust octal +syntax (`0o644`). Without this conversion, `0644` would be interpreted as +decimal 644 in Rust instead of octal 644 (decimal 420). + +## Composing with `--clang-macro-fallback` + +Both flags can be used together: + +- `--translate-function-macros` emits the function-like macro **definitions** as + `const fn` +- `--clang-macro-fallback` evaluates object-like macros that **invoke** + function-like macros to concrete values + +```console +bindgen input.h --translate-function-macros --clang-macro-fallback +``` + +This gives you both the callable `const fn _IO(...)` and the evaluated +`pub const UI_DEV_CREATE: u32 = 21761`. + +## What Gets Skipped + +The following macros are silently skipped (a warning is logged at `RUST_LOG=warn`): + +| Pattern | Reason | +|---------|--------| +| Variadic macros (`...`, `__VA_ARGS__`) | No `const fn` equivalent | +| `#` stringification / `##` token pasting | Preprocessor operations | +| Macros with `void*`, pointer types | Return type is integer-based | +| Statement macros (`do { } while(0)`) | Not expressions | +| `typeof` | No Rust equivalent | +| Typedef casts `(my_type)(x)` | Not expressible in `const fn` | +| Compiler builtins (`__asm__`, `__builtin_*`) | Not real functions | +| String literal bodies | Return type mismatch | +| Assignment operators (`+=`, `=`, etc.) | Produces `()`, not a value | +| C comma operator `(a, b)` | Becomes a Rust tuple | +| References to non-const globals | `static mut` requires `unsafe` |