Skip to content

ctest: Add translation of Rust types. #4501

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

mbyx
Copy link
Contributor

@mbyx mbyx commented Jun 17, 2025

Description

The ability to translate various Rust types that are translatable into equivalent C types. Additionally some basic unit tests have been added.

Sources

Checklist

  • Relevant tests in libc-test/semver have been updated
  • No placeholder or unstable values like *LAST or *MAX are
    included (see #3131)
  • Tested locally (cd libc-test && cargo test --target mytarget);
    especially relevant for platforms that may not be checked in CI

@rustbot rustbot added ctest Issues relating to the ctest crate S-waiting-on-review labels Jun 17, 2025
@mbyx
Copy link
Contributor Author

mbyx commented Jun 17, 2025

There's quite a lot of unreachable or unimplemented because the original ctest2 did the same. If that should be changed to minimize it then please tell. This is mostly a direct port of the original code over to syn, there may be some bugs.

@mbyx mbyx force-pushed the ctest-c-translation branch from fc9641c to 1d766de Compare June 17, 2025 17:30
@mbyx
Copy link
Contributor Author

mbyx commented Jun 19, 2025

Tests seem to fail at a parse int error, so rustc --version is giving a different result there for some reason. EDIT: I forgot nightly is a thing. That's embarassing.

@mbyx mbyx force-pushed the ctest-c-translation branch from 43a06e3 to 3c411d1 Compare June 19, 2025 15:17
@mbyx
Copy link
Contributor Author

mbyx commented Jun 19, 2025

The error on msvc goes away if I replace const bool ON = true with #define ON true in the hierarchy.h file, but I don't know why it doesn't allow it.
freebsd-15 is spurious but freebsd-13 seems to also error on hierarchy, and most likely something in generate_files is causing it.

@mbyx
Copy link
Contributor Author

mbyx commented Jun 20, 2025

From what I understand powerpc64-unknown-linux-gnu is being cross compiled on a normal linux distro, causing a mismatch in the library file and the environment it is being built in.

@mbyx mbyx force-pushed the ctest-c-translation branch from 6d9706f to 030ce77 Compare June 23, 2025 05:44
@mbyx mbyx force-pushed the ctest-c-translation branch from 030ce77 to 4241857 Compare June 23, 2025 05:54
Comment on lines 12 to 13
.or_else(|_| env::var("CC"))
.or_else(|_| env::var(format!("CC_{target_key}")))
Copy link
Contributor

Choose a reason for hiding this comment

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

For linker, the env is LD. CC is just the compiler (don't you need that too though?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doesn't the compiler also act as the linker if given the correct arguments? I didn't think of it that much but passing whatever is set for those three variables as the linker worked (and in the CI it's the compiler not the linker).

Copy link
Contributor

Choose a reason for hiding this comment

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

Often they are but not necessarily, that's the difference between the CC and LD variables. The CARGO_*_LINKER env doesn't actually need to build anything (it's only used for rustc's final link) so somebody could pass ld rather than gcc. At least, that should work but the flags do need to line up.

So you should just reexport all variables starting with CC for the cc crate (probably just iterate the env and export everything starting with CC) and then use the CARGO_*_LINKER var to the rustc invocation with -Clinker=....

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In every Dockerfile, one of those three environment variables is set for the linker and it's always referencing the compiler, which is why it is like that.
image

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, in our cases we use the compiler for both. But we don't want it to blow up if this isn't the case of course, so we shouldn't assume that they will always be the same.

That snip actually has a good point though, the AR_* variables also need to be forwarded.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would they need to be? The ENV vars are set before it runs and they are not unset by cc. TARGET and HOST are only set for the duration of build.rs so they have to be forwarded. The linker and runner are forwarded there for convenience.

From it's docs:
"In addition to the above optional environment variables, cc-rs has some functions with hard requirements on some variables supplied by cargo’s build-script driver that it has the TARGET, OUT_DIR, OPT_LEVEL, and HOST variables."

So those 4 are the only ones that really have to be forwarded, linker and runner are just for convenience. In one of the previous CI runs for the powerpc64 architecture, I remember the CC_target variable was set in the debug output of cc despite ctest not setting it because it was already set by ENV.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough, you're right that we don't actually need to manually do all of these. Anything CARGO_* does need to be extracted because we need to pass it to rustc (for linking) or the runner (for qemu) since we're not going via Cargo. But the rest we can just let cc figure out.

}

/// Configures the host.
pub fn host(&mut self, host: &str) -> &mut Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't in the original ctest; why is it needed here? cc should be able to figure out the host.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before ctest only worked inside of build.rs which set the HOST env var for us, but ctest-next forwards those from the build.rs instead so it has to be manually set for cc inside generate. I thought that it would be better to also have the option to manually set it.

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like this isn't used for tests or anything currently, so let's just remove it for now. Easy to add back once/if we need it.

Comment on lines 50 to 52
gen.header("simple.h")
.generate(crate_path, "simple_out")
.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

.header should be an included header and not an output, right? Where is simple.h coming from?

Copy link
Contributor

Choose a reason for hiding this comment

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

To clarify: we should be building a simple.c file (using the template) and asserting it has the same contents as tests/input/simple.c. Or, if CTEST_BLESS=1 is present, update the file instead. That way we see what the generated output looks like.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But isn't that different from the old ctest? Here it already has a manually written header file for t1.c, it uses t1.rs to generate t1gen.c and t1gen.rs, t1gen.c includes the header file t1.h and uses it to define test functions we call in t1gen.rs via ffi. Then finally it would build bin/t1.rs that includes the original t1.rs as well as the generated one, to run the tests.
So the generated C file just holds tests to be ran and uses the header file to do it.
Screenshot 2025-06-26 101358

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, you are correct. In that case we should have both t1.h which is handwritten, and t1.c which is the autogenerated file that gets updated if CTEST_BLESS is set.

Copy link
Contributor

Choose a reason for hiding this comment

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

^ I forgot we also build a Rust file; that should get blessed as well. I guess if the test file is basic.rs, name the output file basic.out.rs. Maybe also basic.out.c rather than basic.c for some consistency?

Copy link
Contributor

Choose a reason for hiding this comment

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

We need to ensure that we also reject fat pointer types like &str and &[T]

Comment on lines 50 to 52
gen.header("simple.h")
.generate(crate_path, "simple_out")
.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

To clarify: we should be building a simple.c file (using the template) and asserting it has the same contents as tests/input/simple.c. Or, if CTEST_BLESS=1 is present, update the file instead. That way we see what the generated output looks like.

@tgross35
Copy link
Contributor

Have you been able to run any cross build+test locally by the way? Thinking about things a bit more, I'm not sure things will work with the current setup (building in integration tests). The biggest problem is that on cross compiled targets it will be trying to run host tools from within the runner: e.g. when you're running an integration test in qemu-system-arm, it's going to try to launch rustc and cc which don't exist, or somehow try to use the host's which are for the wrong target. That probably explains the loongarch failure.

Unfortunately I think this might mean we have to use the original ctest's method of using a separate test crate, running ctest in build.rs. You're welcome to try to get things working, but I expect the cross-arch targets might not be possible without quite a bit of hacking.

For this PR: we need to have the "bless" tests that show what the generated .c and .rs files look like (#4501 (comment)) but you don't need to worry about actually compiling the tests here. (do check that the generated files build+run without errors locally, though).

@mbyx
Copy link
Contributor Author

mbyx commented Jun 26, 2025

I was able to get powerpc64 to cross compile from x86_64 linux (such that only the CARGO_target_LINKER and CARGO_target_RUNNER variables had to be set), and of course non cross compiled does work. I wanted to try to cross compile loongarch locally but had problems installing the correct linker and such. I'll keep trying on the side.

Running the test doesn't invoke rustc or cc though, so qemu isn't really a problem. The only problems that can occur are during the building step when we need to make sure cargo test has access to the correct linker and runner (via the CARGO_target_LINKER and CARGO_target_RUNNER variables, these are set for all CI jobs, so this is a non issue), that cc has access to the correct linker (via CC and CC_target, this is where most problems occur), and that rustc has access to the correct linker (in theory this should just require passing CARGO_target_LINKER to it, and so far I haven't seen any problems here).

It should be possible, but I do agree it would be somewhat hacky, making sure that specific environment variables are set and forwarded.

@tgross35
Copy link
Contributor

Running the test doesn't invoke rustc or cc though

Doesn't it? The #[test] functions call .generate(..), which definitely seems to call cc.

@mbyx
Copy link
Contributor Author

mbyx commented Jun 26, 2025

Oh wait, you're right. It wasn't a problem when I tried cross compiling powerpc64, it probably does use the host provided cc and rustc, but so long as we specify the correct linker and such with the environment variables it won't fail.

@mbyx mbyx force-pushed the ctest-c-translation branch from 7a675b9 to 721e13e Compare June 27, 2025 12:40
@mbyx mbyx requested a review from tgross35 June 27, 2025 12:43
@mbyx
Copy link
Contributor Author

mbyx commented Jun 27, 2025

I'm not very familiar with how Rust syntax should be translated to C, so the unit tests were made according to my understanding, if the translation is wrong I'll change the parser. Similarly I made a bunch of changes to templates/test.rs that I have no idea if they're correct (they do work for the test case I added in simple.rs).

}

/// Configures the host.
pub fn host(&mut self, host: &str) -> &mut Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like this isn't used for tests or anything currently, so let's just remove it for now. Easy to add back once/if we need it.

/// Generate the Rust and C testing files.
///
/// Returns the path to t generated file.
pub fn generate_files(
Copy link
Contributor

Choose a reason for hiding this comment

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

For each generated file, you should do a regex to replace \n{3,} with \n\n to get rid of all the extra whitespace that the templates seem to insert.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, it looks like Askama has #[template(whitespace = "suppress")] or #[template(whitespace = "minimize")]. Might be worth playing with before the regex thing.

@mbyx mbyx requested a review from tgross35 June 28, 2025 10:42
Copy link
Contributor

@tgross35 tgross35 left a comment

Choose a reason for hiding this comment

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

Few remaining small comments, then I think we can get this merged!

Also please rebase + clean up history.

Comment on lines +1 to +2
[build]
rustflags = ["--cfg", "procmacro2_semver_exempt"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, this is kind of crummy isn't it. .cargo/config.toml is kind of unreliable since it doesn't set config for anything that uses this as a dependency, I was thinking this was a Cargo feature.

So for now, could you just comment out the .file() .start() etc parts of the message with a FIXME(ctest):...? These just stabilized so we should be able to use them somewhat soon dtolnay/proc-macro2#503.

.collect::<Vec<_>>(),
["malloc"]
translator.translate_type(&ty),
Ok("uint8_t (*const *) [5]".to_string())
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we can just figure this out once we have better testing with C. For now, just comment out the test with a FIXME

Comment on lines +35 to +41
let new_content = fs::read_to_string(&new_file)?
.replace("\r\n", "\n")
.replace("\r", "");
let old_content = fs::read_to_string(&old_file)?
.replace("\r\n", "\n")
.replace("\r", "");

Copy link
Contributor

Choose a reason for hiding this comment

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

You should be able to remove the first replace for each file, since the second replace does the same thing.

.replace("\r", "");

let equal = new_content == old_content;
if env::var("LIBCBLESS").is_ok() && !equal {
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
if env::var("LIBCBLESS").is_ok() && !equal {
if env::var("LIBC_BLESS").is_ok() && !equal {

///
/// If the contents do not match and LIBCBLESS is set, overwrite the
/// test file with the content of the generated file.
fn bless_equal(new_file: impl AsRef<Path>, old_file: impl AsRef<Path>) -> Result<bool> {
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to have a Result here I think, you can just do the assertions in this function. Use https://docs.rs/pretty_assertions/latest/pretty_assertions/ so there is a nice diff.

Comment on lines +10 to +17
use super::*;

use std::mem;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::fmt::{Debug, LowerHex};
use std::ptr;
use std::slice;
use std::ffi::CStr;
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
use super::*;
use std::mem;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::fmt::{Debug, LowerHex};
use std::ptr;
use std::slice;
use std::ffi::CStr;
use std::ffi::CStr;
use std::fmt::{Debug, LowerHex};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::{mem, ptr, slice};
use super::*;

Making this match rustfmt

Comment on lines +98 to +102
if FAILED.load( std::sync::atomic::Ordering::Relaxed) {
panic!("some tests failed");
} else {
println!("PASSED {} tests", NTESTS.load( std::sync::atomic::Ordering::Relaxed));
}
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
if FAILED.load( std::sync::atomic::Ordering::Relaxed) {
panic!("some tests failed");
} else {
println!("PASSED {} tests", NTESTS.load( std::sync::atomic::Ordering::Relaxed));
}
if FAILED.load(std::sync::atomic::Ordering::Relaxed) {
panic!("some tests failed");
} else {
println!(
"PASSED {} tests",
NTESTS.load(std::sync::atomic::Ordering::Relaxed)
);
}

Same thing, just making rustfmt happy with the output files

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ctest Issues relating to the ctest crate S-waiting-on-review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants