Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run-make test to check core::ffi::c_* types against clang #133944

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions tests/auxiliary/minicore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,64 @@
#![feature(no_core, lang_items, rustc_attrs, decl_macro, naked_functions, f16, f128)]
#![allow(unused, improper_ctypes_definitions, internal_features)]
#![feature(asm_experimental_arch)]
#![feature(intrinsics)]
#![no_std]
#![no_core]

/// Vendored from the 'cfg_if' crate

macro_rules! cfg_if {
// match if/else chains with a final `else`
(
$(
if #[cfg( $i_meta:meta )] { $( $i_tokens:tt )* }
) else+
else { $( $e_tokens:tt )* }
) => {
cfg_if! {
@__items () ;
$(
(( $i_meta ) ( $( $i_tokens )* )) ,
)+
(() ( $( $e_tokens )* )) ,
}
};

// Internal and recursive macro to emit all the items
//
// Collects all the previous cfgs in a list at the beginning, so they can be
// negated. After the semicolon is all the remaining items.
(@__items ( $( $_:meta , )* ) ; ) => {};
(
@__items ( $( $no:meta , )* ) ;
(( $( $yes:meta )? ) ( $( $tokens:tt )* )) ,
$( $rest:tt , )*
) => {
// Emit all items within one block, applying an appropriate #[cfg]. The
// #[cfg] will require all `$yes` matchers specified and must also negate
// all previous matchers.
#[cfg(all(
$( $yes , )?
not(any( $( $no ),* ))
))]
cfg_if! { @__identity $( $tokens )* }

// Recurse to emit all other items in `$rest`, and when we do so add all
// our `$yes` matchers to the list of `$no` matchers as future emissions
// will have to negate everything we just matched as well.
cfg_if! {
@__items ( $( $no , )* $( $yes , )? ) ;
$( $rest , )*
}
};

// Internal macro to make __apply work out right for different match types,
// because of how macros match/expand stuff.
(@__identity $( $tokens:tt )* ) => {
$( $tokens )*
};
}
Comment on lines +24 to +76
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem: this isn't actually a core macro. If you want cfg_if, can you actually just depend on cfg_if-the-crate in src/tools/run-make-support, then re-export cfg_if via run-make-support? A run-make test will have access to run-make-support. See how object or regex are re-exported in run-make-support.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, you actually want cfg_if for the test file. In that case, can you just put cfg_if in the test file, and not in minicore.rs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately cfg_if is needed for processed_mod.rs. What I could do to remove it from minicore.rs (although this is really ugly code) is put it into the rmake_content string.

        let mut rmake_content = format!(
            r#"
            #![no_std]
            #![no_core]
            #![feature(link_cfg)]
            #![allow(unused)]
            #![crate_type = "rlib"]

            /* begin minicore content */
            {minicore_content}
            /* end minicore content */

            /// Vendored from the 'cfg_if' crate
            macro_rules! cfg_if {{
                // match if/else chains with a final `else`
                (
                    $(
                        if #[cfg( $i_meta:meta )] {{ $( $i_tokens:tt )* }}
                    ) else+
                    else {{ $( $e_tokens:tt )* }}
                ) => {{
                    cfg_if! {{
                        @__items () ;
                        $(
                            (( $i_meta ) ( $( $i_tokens )* )) ,
                        )+
                        (() ( $( $e_tokens )* )) ,
                    }}
                }};
                // Internal and recursive macro to emit all the items
                //
                // Collects all the previous cfgs in a list at the beginning, so they can be
                // negated. After the semicolon is all the remaining items.
                (@__items ( $( $_:meta , )* ) ; ) => {{}};
                (
                    @__items ( $( $no:meta , )* ) ;
                    (( $( $yes:meta )? ) ( $( $tokens:tt )* )) ,
                    $( $rest:tt , )*
                ) => {{
                    // Emit all items within one block, applying an appropriate #[cfg]. The
                    // #[cfg] will require all `$yes` matchers specified and must also negate
                    // all previous matchers.
                    #[cfg(all(
                        $( $yes , )?
                        not(any( $( $no ),* ))
                    ))]
                    cfg_if! {{ @__identity $( $tokens )* }}
                    // Recurse to emit all other items in `$rest`, and when we do so add all
                    // our `$yes` matchers to the list of `$no` matchers as future emissions
                    // will have to negate everything we just matched as well.
                    cfg_if! {{
                        @__items ( $( $no , )* $( $yes , )? ) ;
                        $( $rest , )*
                    }}
                }};
                // Internal macro to make __apply work out right for different match types,
                // because of how macros match/expand stuff.
                (@__identity $( $tokens:tt )* ) => {{
                    $( $tokens )*
                }};
            }}

            #[path = "processed_mod.rs"]
            mod ffi;
            #[path = "tests.rs"]
            mod tests;
            "#
        );


// `core` has some exotic `marker_impls!` macro for handling the with-generics cases, but for our
// purposes, just use a simple macro_rules macro.
macro_rules! impl_marker_trait {
Expand Down Expand Up @@ -101,10 +156,70 @@ macro_rules! concat {
/* compiler built-in */
};
}

#[rustc_builtin_macro]
#[macro_export]
macro_rules! stringify {
($($t:tt)*) => {
/* compiler built-in */
};
}

#[macro_export]
macro_rules! panic {
($msg:literal) => {
$crate::panic(&$msg)
};
}
Comment on lines +168 to +173
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem [PANIC 2/2]: ... which is why this has an extra & on $msg.


#[rustc_intrinsic]
#[rustc_intrinsic_const_stable_indirect]
#[rustc_intrinsic_must_be_overridden]
pub const fn size_of<T>() -> usize {
loop {}
}

#[rustc_intrinsic]
#[rustc_intrinsic_must_be_overridden]
pub const fn abort() -> ! {
loop {}
}

#[lang = "panic"]
#[rustc_const_panic_str]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem: I think we may need to apply #[inline]/#[inline(never)]/#[inline(always)] and #[rustc_nounwind] and match core's usage, because that can influence codegen. Note that panic inline attreibutes are gated behind the panic_immediate_abort cfg, e.g.

https://github.com/rust-lang/rust/blob/c37fbd873a15e7cdc92476f7d7b964f6c05e64cd/library/core/src/panicking.rs#L81C1-L82C55

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused on this to be honest. Would I just apply #[inline(always)] to allow for a panic_immediate_abort? Or do I literally just add all the features you highlighted above the panic function like this for it to behave correctly:

#[lang = "panic"]
#[rustc_const_panic_str]
#[cfg_attr(not(feature = "panic_immediate_abort"), inline(never), cold)]
#[cfg_attr(feature = "panic_immediate_abort", inline)]
#[track_caller]
#[rustc_nounwind]
const fn panic(_expr: &'static str) -> ! {
    abort()
}

Copy link
Member

@jieyouxu jieyouxu Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the time being, I would make sure that the following attrs are included to minimize the difference versus the actual decl:

  • #[lang = "panic"]
  • #[rustc_const_panic_str]
  • #[inline(never)]
  • #[cold]
  • #[track_caller]
  • #[rustc_nounwind]

to line up the attributes. I wouldn't yet bother with panic_immediate_abort because minicore via //@ add-core-stubs isn't built with panic_immediate_abort.

In the long run, it would be very nice if we didn't need a minicore and just use a core API-layer thing, but that's a moonshot so.

const fn panic(_expr: &&'static str) -> ! {
abort();
}
Comment on lines +188 to +192
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem [PANIC 1/2]: AFAICT, this signature is wrong (this signature has one layer of reference too many), because:

#[cfg_attr(not(feature = "panic_immediate_abort"), inline(never), cold)]
#[cfg_attr(feature = "panic_immediate_abort", inline)]
#[track_caller]
#[rustc_const_stable_indirect] // must follow stable const rules since it is exposed to stable
#[lang = "panic"] // used by lints and miri for panics
pub const fn panic(expr: &'static str) -> ! {


#[lang = "eq"]
pub trait PartialEq<Rhs: ?Sized = Self> {
fn eq(&self, other: &Rhs) -> bool;
fn ne(&self, other: &Rhs) -> bool {
!self.eq(other)
}
}

impl PartialEq for usize {
fn eq(&self, other: &usize) -> bool {
(*self) == (*other)
}
}
Comment on lines +202 to +206
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: I think you could use a macro to construct these impl PartialEqs so as long as the shape remains compatible since I think that's also what core does.


impl PartialEq for bool {
fn eq(&self, other: &bool) -> bool {
(*self) == (*other)
}
}

#[lang = "not"]
pub trait Not {
type Output;
fn not(self) -> Self::Output;
}

impl Not for bool {
type Output = bool;
fn not(self) -> Self {
!self
}
}
194 changes: 194 additions & 0 deletions tests/run-make/core-ffi-typecheck-clang/rmake.rs
tgross35 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//@ needs-force-clang-based-tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (style): can you move this directive below, and change the // comment block below to become a //! doc comment block?

// This test checks that the clang defines for each target allign with the core ffi types defined in
// mod.rs. Therefore each rust target is queried and the clang defines for each target are read and
Comment on lines +2 to +3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: (after you finish the core/ffi changes) can you specify an exact path to the file in question?

// compared to the core sizes to verify all types and sizes allign at buildtime.
//
// If this test fails because Rust adds a target that Clang does not support, this target should be
// added to SKIPPED_TARGETS.
Comment on lines +6 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (style):

Suggested change
// If this test fails because Rust adds a target that Clang does not support, this target should be
// added to SKIPPED_TARGETS.
// If this test fails because Rust adds a target that Clang does not support, this target should be
// added to `SKIPPED_TARGETS`.


use run_make_support::{clang, regex, rfs, rustc, serde_json};
use serde_json::Value;

// It is not possible to run the Rust test-suite on these targets.
ricci009 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: could we elaborate why we can't run this test against these targets?

jieyouxu marked this conversation as resolved.
Show resolved Hide resolved
const SKIPPED_TARGETS: &[&str] = &[
"xtensa-esp32-espidf",
"xtensa-esp32-none-elf",
"xtensa-esp32s2-espidf",
"xtensa-esp32s2-none-elf",
"xtensa-esp32s3-espidf",
"xtensa-esp32s3-none-elf",
"csky-unknown-linux-gnuabiv2",
"csky-unknown-linux-gnuabiv2hf",
];

const MAPPED_TARGETS: &[(&str, &str)] = &[
("armv5te-unknown-linux-uclibcgnueabi", "armv5te-unknown-linux"),
("mips-unknown-linux-uclibc", "mips-unknown-linux"),
("mipsel-unknown-linux-uclibc", "mips-unknown-linux"),
("powerpc-unknown-linux-gnuspe", "powerpc-unknown-linux-gnu"),
("powerpc-unknown-linux-muslspe", "powerpc-unknown-linux-musl"),
("x86_64-unknown-l4re-uclibc", "x86_64-unknown-l4re"),
];
Comment on lines +24 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these targets where we have the wrong LLVM string and should fix that, or where LLVM is different from Clang? A note would be good.

Copy link
Contributor Author

@ricci009 ricci009 Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first val in the pair is a viable llvm target but it is not supported on clang. Hence I just map it to the "default" version that is supported on clang.

For reference:

❯ rustc -Z unstable-options --target=mipsel-unknown-linux-uclibc --print target-spec-json
{
  "arch": "mips",
  ...
  "llvm-target": "mipsel-unknown-linux-uclibc",
  ...
  "os": "linux",
  ...
}

clang attempt to build:

clang: error: version 'uclibc' in target triple 'mipsel-unknown-linux-uclibc' is invalid
Compiler returned: 1

I remove 'uclibc' from the target and proceed to compile with clang.

A follow up on this is what should the non-working llvm target map to? Is it viable to assume I can map it to mips-unknown-linux?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine, the libc used shouldn't change the primitive size. A comment mentioning that they work for LLVM but not for clang is sufficient 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: indeed, please document why this mapping is needed (and what purpose it serves)


fn main() {
let minicore_path = run_make_support::source_root().join("tests/auxiliary/minicore.rs");

preprocess_core_ffi();

let targets_json =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let targets_json =
// Print the list of target JSON files, which will be used to match Rust target strings to LLVM
// target strings
let targets_json =

rustc().arg("--print").arg("all-target-specs-json").arg("-Z").arg("unstable-options").run();
let targets_json_str =
String::from_utf8(targets_json.stdout().to_vec()).expect("error not a string");

let j: Value = serde_json::from_str(&targets_json_str).unwrap();
for (target, v) in j.as_object().unwrap() {
let llvm_target = &v["llvm-target"].as_str().unwrap();

if SKIPPED_TARGETS.iter().any(|&to_skip_target| target == to_skip_target) {
continue;
}

// Create a new variable to hold either the mapped target or original llvm_target
let target_to_use = MAPPED_TARGETS
.iter()
.find(|&&(from, _)| from == *llvm_target)
.map(|&(_, to)| to)
.unwrap_or(llvm_target);

// Run Clang's preprocessor for the relevant target, printing default macro definitions.
let clang_output =
clang().args(&["-E", "-dM", "-x", "c", "/dev/null", "-target", &target_to_use]).run();

let defines = String::from_utf8(clang_output.stdout()).expect("Invalid UTF-8");

let minicore_content = rfs::read_to_string(&minicore_path);
let mut rmake_content = format!(
r#"
#![no_std]
#![no_core]
#![feature(link_cfg)]
#![allow(unused)]
#![crate_type = "rlib"]

/* begin minicore content */
{minicore_content}
/* end minicore content */

#[path = "processed_mod.rs"]
mod ffi;
#[path = "tests.rs"]
mod tests;
"#
);

rmake_content.push_str(&format!(
"
const CLANG_C_CHAR_SIZE: usize = {};
const CLANG_C_CHAR_SIGNED: bool = {};
const CLANG_C_SHORT_SIZE: usize = {};
const CLANG_C_INT_SIZE: usize = {};
const CLANG_C_LONG_SIZE: usize = {};
const CLANG_C_LONGLONG_SIZE: usize = {};
const CLANG_C_FLOAT_SIZE: usize = {};
const CLANG_C_DOUBLE_SIZE: usize = {};
const CLANG_C_SIZE_T_SIZE: usize = {};
const CLANG_C_PTRDIFF_T_SIZE: usize = {};
",
parse_size(&defines, "CHAR"),
char_is_signed(&defines),
parse_size(&defines, "SHORT"),
parse_size(&defines, "INT"),
parse_size(&defines, "LONG"),
parse_size(&defines, "LONG_LONG"),
parse_size(&defines, "FLOAT"),
parse_size(&defines, "DOUBLE"),
parse_size(&defines, "SIZE_T"),
parse_size(&defines, "PTRDIFF_T"),
));

// Generate a target-specific rmake file.
// If type misalignments occur,
// generated rmake file name used to identify the failing target.
let file_name = format!("{}_rmake.rs", target.replace("-", "_").replace(".", "_"));

// Attempt to build the test file for the relevant target.
// Tests use constant evaluation, so running is not necessary.
rfs::write(&file_name, rmake_content);
let rustc_output = rustc()
.arg("-Zunstable-options")
.arg("--emit=metadata")
.arg("--target")
.arg(target)
.arg("-o-")
.arg(&file_name)
.run();
rfs::remove_file(&file_name);
if !rustc_output.status().success() {
panic!("Failed for target {}", target);
}
}

// Cleanup
rfs::remove_file("processed_mod.rs");
Comment on lines +131 to +132
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: not necessary, if a run-make test is successful and you only created temporary files under CWD, the whole directory will be cleaned up.

}

// Helper to parse size from clang defines
fn parse_size(defines: &str, type_name: &str) -> usize {
let search_pattern = format!("__SIZEOF_{}__ ", type_name.to_uppercase());
for line in defines.lines() {
if line.contains(&search_pattern) {
if let Some(size_str) = line.split_whitespace().last() {
return size_str.parse().unwrap_or(0);
}
}
}

// Only allow CHAR to default to 1
if type_name.to_uppercase() == "CHAR" {
return 1;
}

panic!("Could not find size definition for type: {}", type_name);
}

fn char_is_signed(defines: &str) -> bool {
!defines.lines().any(|line| line.contains("__CHAR_UNSIGNED__"))
}

/// Parse core/ffi/mod.rs to retrieve only necessary macros and type defines
fn preprocess_core_ffi() {
let mod_path = run_make_support::source_root().join("library/core/src/ffi/mod.rs");
let mut content = rfs::read_to_string(&mod_path);

//remove stability features #![unstable]
let mut re = regex::Regex::new(r"#!?\[(un)?stable[^]]*?\]").unwrap();
content = re.replace_all(&content, "").to_string();

//remove doc features #[doc...]
re = regex::Regex::new(r"#\[doc[^]]*?\]").unwrap();
content = re.replace_all(&content, "").to_string();

//remove lang feature #[lang...]
re = regex::Regex::new(r"#\[lang[^]]*?\]").unwrap();
content = re.replace_all(&content, "").to_string();

//remove non inline modules
re = regex::Regex::new(r".*mod.*;").unwrap();
content = re.replace_all(&content, "").to_string();

//remove use
re = regex::Regex::new(r".*use.*;").unwrap();
content = re.replace_all(&content, "").to_string();

//remove fn fmt {...}
re = regex::Regex::new(r"(?s)fn fmt.*?\{.*?\}").unwrap();
content = re.replace_all(&content, "").to_string();

//rmv impl fmt {...}
re = regex::Regex::new(r"(?s)impl fmt::Debug for.*?\{.*?\}").unwrap();
content = re.replace_all(&content, "").to_string();

let file_name = "processed_mod.rs";

rfs::write(&file_name, content);
}
Loading
Loading