Repository: rust-lang/rust-bindgen
Pull request #3002 (2024-11-29 21:15:48)

Introduce `--rust-edition`

This PR introduces the concept of Rust editions to bindgen so it can generate appropriate code based on the desired edition.

The motivation behind this PR comes from #2996 which allows bindgen to generate code with C-String literals, a feature that has been available since rust 1.77 but are gated behind the 2021 edition.

The changes in this PR are:

- Introduce the `--rust-edition` CLI argument and `Builder::rust_edition` method.
- Update bindgen internals to filter features based on the set edition. 
- Gate the `literal_cstr` feature behind the 2021 edition.

cc @nyurik @emilio 

nyurik: @pvdrz this is a dup of #3000 ?
pvdrz: > @pvdrz this is a dup of #3000 ?

yes :scream: but I cherry-picked your changes that were not included on mine and tagged you as Co-Author :sweat_smile:. Hopefully I'll coordinate this better next time 
pvdrz: Blocked by #3006

Patch:
diff --git a/bindgen/features.rs b/bindgen/features.rs
index 1b971a4e..b018354e 100644
--- a/bindgen/features.rs
+++ b/bindgen/features.rs
@@ -79,12 +79,93 @@ impl fmt::Display for InvalidRustTarget {
     }
 }
 
+/// This macro defines the Rust editions supported by bindgen.
+macro_rules! define_rust_editions {
+    ($($variant:ident($value:literal) => $minor:literal,)*) => {
+        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+        pub enum RustEdition {
+            $(
+                #[doc = concat!("The ", stringify!($value), " edition of Rust.")]
+                $variant,
+            )*
+        }
+
+        impl FromStr for RustEdition {
+            type Err = InvalidRustEdition;
+
+            fn from_str(s: &str) -> Result<Self, Self::Err> {
+                match s {
+                    $(stringify!($value) => Ok(Self::$variant),)*
+                    _ => Err(InvalidRustEdition(s.to_owned())),
+                }
+            }
+        }
+
+        impl fmt::Display for RustEdition {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                match self {
+                    $(Self::$variant => stringify!($value).fmt(f),)*
+                }
+            }
+        }
+
+        impl RustEdition {
+            pub(crate) const ALL: [Self; [$($value,)*].len()] = [$(Self::$variant,)*];
+
+            pub(crate) fn is_available(self, target: RustTarget) -> bool {
+                let Some(minor) = target.minor() else {
+                    return true;
+                };
+
+                match self {
+                    $(Self::$variant => $minor <= minor,)*
+                }
+            }
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct InvalidRustEdition(String);
+
+impl fmt::Display for InvalidRustEdition {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "\"{}\" is not a valid Rust edition", self.0)
+    }
+}
+
+impl std::error::Error for InvalidRustEdition {}
+
+define_rust_editions! {
+    Edition2018(2018) => 31,
+    Edition2021(2021) => 56,
+    Edition2024(2024) => 85,
+}
+
+impl RustTarget {
+    /// Returns the latest edition supported by this target.
+    pub(crate) fn latest_edition(self) -> RustEdition {
+        RustEdition::ALL
+            .iter()
+            .rev()
+            .find(|edition| edition.is_available(self))
+            .copied()
+            .expect("bindgen should always support at least one edition")
+    }
+}
+
+impl Default for RustEdition {
+    fn default() -> Self {
+        RustTarget::default().latest_edition()
+    }
+}
+
 /// This macro defines the [`RustTarget`] and [`RustFeatures`] types.
 macro_rules! define_rust_targets {
     (
-        Nightly => {$($nightly_feature:ident $(: #$issue:literal)?),* $(,)?} $(,)?
+        Nightly => {$($nightly_feature:ident $(($nightly_edition:literal))|* $(: #$issue:literal)?),* $(,)?} $(,)?
         $(
-            $variant:ident($minor:literal) => {$($feature:ident $(: #$pull:literal)?),* $(,)?},
+            $variant:ident($minor:literal) => {$($feature:ident $(($edition:literal))|* $(: #$pull:literal)?),* $(,)?},
         )*
         $(,)?
     ) => {
@@ -128,23 +209,35 @@ macro_rules! define_rust_targets {
             $(pub(crate) $nightly_feature: bool,)*
         }
 
-        impl From<RustTarget> for RustFeatures {
-            fn from(target: RustTarget) -> Self {
-                if target == RustTarget::Nightly {
-                    return Self {
-                        $($($feature: true,)*)*
-                        $($nightly_feature: true,)*
-                    };
-                }
-
+        impl RustFeatures {
+            /// Compute the features that must be enabled in a specific Rust target with a specific edition.
+            pub(crate) fn new(target: RustTarget, edition: RustEdition) -> Self {
                 let mut features = Self {
                     $($($feature: false,)*)*
                     $($nightly_feature: false,)*
                 };
 
-                $(if target.is_compatible(&RustTarget::$variant) {
-                    $(features.$feature = true;)*
-                })*
+                if target.is_compatible(&RustTarget::nightly()) {
+                    $(
+                        let editions: &[RustEdition] = &[$(stringify!($nightly_edition).parse::<RustEdition>().ok().expect("invalid edition"),)*];
+
+                        if editions.is_empty() || editions.contains(&edition) {
+                            features.$nightly_feature = true;
+                        }
+                    )*
+                }
+
+                $(
+                    if target.is_compatible(&RustTarget::$variant) {
+                        $(
+                            let editions: &[RustEdition] = &[$(stringify!($edition).parse::<RustEdition>().ok().expect("invalid edition"),)*];
+
+                            if editions.is_empty() || editions.contains(&edition) {
+                                features.$feature = true;
+                            }
+                        )*
+                    }
+                )*
 
                 features
             }
@@ -163,7 +256,7 @@ define_rust_targets! {
     },
     Stable_1_77(77) => {
         offset_of: #106655,
-        literal_cstr: #117472,
+        literal_cstr(2021)|(2024): #117472,
     },
     Stable_1_73(73) => { thiscall_abi: #42202 },
     Stable_1_71(71) => { c_unwind_abi: #106075 },
@@ -296,9 +389,17 @@ impl FromStr for RustTarget {
     }
 }
 
+impl RustFeatures {
+    /// Compute the features that must be enabled in a specific Rust target with the latest edition
+    /// available in that target.
+    pub(crate) fn new_with_latest_edition(target: RustTarget) -> Self {
+        Self::new(target, target.latest_edition())
+    }
+}
+
 impl Default for RustFeatures {
     fn default() -> Self {
-        RustTarget::default().into()
+        Self::new_with_latest_edition(RustTarget::default())
     }
 }
 
@@ -306,26 +407,61 @@ impl Default for RustFeatures {
 mod test {
     use super::*;
 
+    #[test]
+    fn release_versions_for_editions() {
+        assert_eq!(
+            "1.33".parse::<RustTarget>().unwrap().latest_edition(),
+            RustEdition::Edition2018
+        );
+        assert_eq!(
+            "1.56".parse::<RustTarget>().unwrap().latest_edition(),
+            RustEdition::Edition2021
+        );
+        assert_eq!(
+            "1.85".parse::<RustTarget>().unwrap().latest_edition(),
+            RustEdition::Edition2024
+        );
+        assert_eq!(
+            "nightly".parse::<RustTarget>().unwrap().latest_edition(),
+            RustEdition::Edition2024
+        );
+    }
+
     #[test]
     fn target_features() {
-        let features = RustFeatures::from(RustTarget::Stable_1_71);
+        let features =
+            RustFeatures::new_with_latest_edition(RustTarget::Stable_1_71);
         assert!(
             features.c_unwind_abi &&
                 features.abi_efiapi &&
                 !features.thiscall_abi
         );
-        let f_nightly = RustFeatures::from(RustTarget::Nightly);
+
+        let features = RustFeatures::new(
+            RustTarget::Stable_1_77,
+            RustEdition::Edition2018,
+        );
+        assert!(!features.literal_cstr);
+
+        let features =
+            RustFeatures::new_with_latest_edition(RustTarget::Stable_1_77);
+        assert!(features.literal_cstr);
+
+        let f_nightly =
+            RustFeatures::new_with_latest_edition(RustTarget::Nightly);
         assert!(
-            f_nightly.maybe_uninit &&
-                f_nightly.thiscall_abi &&
-                f_nightly.vectorcall_abi
+            f_nightly.vectorcall_abi &&
+                f_nightly.ptr_metadata &&
+                f_nightly.layout_for_ptr
         );
     }
 
     fn test_target(input: &str, expected: RustTarget) {
         // Two targets are equivalent if they enable the same set of features
-        let expected = RustFeatures::from(expected);
-        let found = RustFeatures::from(input.parse::<RustTarget>().unwrap());
+        let expected = RustFeatures::new_with_latest_edition(expected);
+        let found = RustFeatures::new_with_latest_edition(
+            input.parse::<RustTarget>().unwrap(),
+        );
         assert_eq!(
             expected,
             found,
diff --git a/bindgen/lib.rs b/bindgen/lib.rs
index 25ba44eb..3c09069e 100644
--- a/bindgen/lib.rs
+++ b/bindgen/lib.rs
@@ -57,7 +57,7 @@ pub use ir::function::Abi;
 pub use options::cli::builder_from_flags;
 
 use codegen::CodegenError;
-use features::RustFeatures;
+use features::{RustEdition, RustFeatures};
 use ir::comment;
 use ir::context::{BindgenContext, ItemId};
 use ir::item::Item;
@@ -316,6 +316,22 @@ fn get_extra_clang_args(
 impl Builder {
     /// Generate the Rust bindings using the options built up thus far.
     pub fn generate(mut self) -> Result<Bindings, BindgenError> {
+        // Keep rust_features synced with rust_target
+        self.options.rust_features = match self.options.rust_edition {
+            Some(edition) => {
+                if !edition.is_available(self.options.rust_target) {
+                    return Err(BindgenError::UnsupportedEdition(
+                        edition,
+                        self.options.rust_target,
+                    ));
+                }
+                RustFeatures::new(self.options.rust_target, edition)
+            }
+            None => {
+                RustFeatures::new_with_latest_edition(self.options.rust_target)
+            }
+        };
+
         // Add any extra arguments from the environment to the clang command line.
         self.options.clang_args.extend(
             get_extra_clang_args(&self.options.parse_callbacks)
@@ -530,9 +546,6 @@ impl BindgenOptions {
     /// Update rust target version
     pub fn set_rust_target(&mut self, rust_target: RustTarget) {
         self.rust_target = rust_target;
-
-        // Keep rust_features synced with rust_target
-        self.rust_features = rust_target.into();
     }
 
     /// Get features supported by target Rust version
@@ -612,6 +625,8 @@ pub enum BindgenError {
     ClangDiagnostic(String),
     /// Code generation reported an error.
     Codegen(CodegenError),
+    /// The passed edition is not available on that Rust target.
+    UnsupportedEdition(RustEdition, RustTarget),
 }
 
 impl std::fmt::Display for BindgenError {
@@ -632,6 +647,9 @@ impl std::fmt::Display for BindgenError {
             BindgenError::Codegen(err) => {
                 write!(f, "codegen error: {err}")
             }
+            BindgenError::UnsupportedEdition(edition, target) => {
+                write!(f, "edition {edition} is not available on Rust {target}")
+            }
         }
     }
 }
diff --git a/bindgen/options/cli.rs b/bindgen/options/cli.rs
index c683de17..e444432d 100644
--- a/bindgen/options/cli.rs
+++ b/bindgen/options/cli.rs
@@ -3,7 +3,7 @@ use crate::{
     callbacks::{
         AttributeInfo, DeriveInfo, ItemInfo, ParseCallbacks, TypeKind,
     },
-    features::EARLIEST_STABLE_RUST,
+    features::{RustEdition, EARLIEST_STABLE_RUST},
     regex_set::RegexSet,
     Abi, AliasVariation, Builder, CodegenConfig, EnumVariation,
     FieldVisibilityKind, Formatter, MacroTypeVariation, NonCopyUnionStyle,
@@ -26,6 +26,10 @@ fn rust_target_help() -> String {
     )
 }
 
+fn rust_edition_help() -> String {
+    format!("Rust edition to target. Defaults to the latest edition supported by the chosen Rust target. Possible values: ({}). ", RustEdition::ALL.map(|e| e.to_string()).join("|"))
+}
+
 fn parse_codegen_config(
     what_to_generate: &str,
 ) -> Result<CodegenConfig, Error> {
@@ -334,6 +338,8 @@ struct BindgenCommand {
     module_raw_line: Vec<String>,
     #[arg(long, help = rust_target_help())]
     rust_target: Option<RustTarget>,
+    #[arg(long, value_name = "EDITION", help = rust_edition_help())]
+    rust_edition: Option<RustEdition>,
     /// Use types from Rust core instead of std.
     #[arg(long)]
     use_core: bool,
@@ -588,6 +594,7 @@ where
         raw_line,
         module_raw_line,
         rust_target,
+        rust_edition,
         use_core,
         conservative_inline_namespaces,
         allowlist_function,
@@ -821,6 +828,7 @@ where
             },
             header,
             rust_target,
+            rust_edition,
             default_enum_style,
             bitfield_enum,
             newtype_enum,
diff --git a/bindgen/options/mod.rs b/bindgen/options/mod.rs
index baac4bce..4e68dfb7 100644
--- a/bindgen/options/mod.rs
+++ b/bindgen/options/mod.rs
@@ -12,7 +12,7 @@ use crate::codegen::{
     AliasVariation, EnumVariation, MacroTypeVariation, NonCopyUnionStyle,
 };
 use crate::deps::DepfileSpec;
-use crate::features::{RustFeatures, RustTarget};
+use crate::features::{RustEdition, RustFeatures, RustTarget};
 use crate::regex_set::RegexSet;
 use crate::Abi;
 use crate::Builder;
@@ -1609,9 +1609,26 @@ options! {
             args.push(rust_target.to_string());
         },
     },
+    /// The Rust edition to use for code generation.
+    rust_edition: Option<RustEdition> {
+        methods: {
+            /// Specify the Rust target edition.
+            ///
+            /// The default edition is the latest edition supported by the chosen Rust target.
+            pub fn rust_edition(mut self, rust_edition: RustEdition) -> Self {
+                self.options.rust_edition = Some(rust_edition);
+                self
+            }
+        }
+        as_args: |edition, args| {
+            if let Some(edition) =  edition {
+                args.push("--rust-edition".to_owned());
+                args.push(edition.to_string());
+            }
+        },
+    },
     /// Features to be enabled. They are derived from `rust_target`.
     rust_features: RustFeatures {
-        default: RustTarget::default().into(),
         methods: {},
         // This field cannot be set from the CLI,
         as_args: ignore,


Test patch:
diff --git a/bindgen-tests/tests/expectations/tests/strings_cstr2_2018.rs b/bindgen-tests/tests/expectations/tests/strings_cstr2_2018.rs
new file mode 100644
index 00000000..ca089cf1
--- /dev/null
+++ b/bindgen-tests/tests/expectations/tests/strings_cstr2_2018.rs
@@ -0,0 +1,13 @@
+#![allow(dead_code, non_snake_case, non_camel_case_types, non_upper_case_globals)]
+#[allow(unsafe_code)]
+pub const MY_STRING_UTF8: &::std::ffi::CStr = unsafe {
+    ::std::ffi::CStr::from_bytes_with_nul_unchecked(b"Hello, world!\0")
+};
+#[allow(unsafe_code)]
+pub const MY_STRING_INTERIOR_NULL: &::std::ffi::CStr = unsafe {
+    ::std::ffi::CStr::from_bytes_with_nul_unchecked(b"Hello,\0")
+};
+#[allow(unsafe_code)]
+pub const MY_STRING_NON_UTF8: &::std::ffi::CStr = unsafe {
+    ::std::ffi::CStr::from_bytes_with_nul_unchecked(b"ABCDE\xFF\0")
+};
diff --git a/bindgen-tests/tests/headers/strings_cstr2_2018.h b/bindgen-tests/tests/headers/strings_cstr2_2018.h
new file mode 100644
index 00000000..67d117a1
--- /dev/null
+++ b/bindgen-tests/tests/headers/strings_cstr2_2018.h
@@ -0,0 +1,5 @@
+// bindgen-flags: --rust-target=1.77 --rust-edition=2018 --generate-cstr
+
+const char* MY_STRING_UTF8 = "Hello, world!";
+const char* MY_STRING_INTERIOR_NULL = "Hello,\0World!";
+const char* MY_STRING_NON_UTF8 = "ABCDE\xFF";