Per-file or per-module compile flags

I am currently writing a little operating system kernel (for now only targetting x86(-64)) as a hobby, and to learn more about that kind of low-level programming. Up to now, I successfully used a "regular osdev" rust setup, with a custom target with all the juicy x86 vectorized ISA extensions disabled. Still, I felt that I still wanted to try to enable these features as an experiment (even though it adds some overhead to context-switches due to a bigger XSAVE region to copy, blah blah blah).
The problem is : I can't use these extensions without enabling them beforehand (by writing the right bits to the appropriate control registers), so I can't just compile my whole kernel with them enabled, with the risk of having the compiler use vectorized instructions where they still were not enabled.

So here is my question : is this possible to compile different files / modules from the same crate with different flags ? Since this is something feasible in C/C++, I think it would be a nice feature to add in the future, if it's not already the case.
Otherwise I suppose I should separate the init code and make it another crate of my project ?

I was also hoping that since #[target_feature(enable = ...)] exists, the opposite #[target_feature(disable = ...)] would also exist, but it's apparently not a thing for now.

EDIT : To clarify a bit, my initial goal was to compile initialization code (i.e. post bootloader code) without vectorized instructions enabled, and the rest of the kernel with them enabled

rust's analogy to C++ tranlation unit is a crate, not a module or file.

you can use multiple crates for you kernel just fine.

3 Likes

Alright, I see. It still feels a bit weird to me, but I suppose I'll just have to live with it.
Thanks for your answer !

For pretty much any compiled language, you can't have cyclic dependencies between separate translation units without introducing some extra level of indirection so that there is a valid sequence to compile them in, and the extra indirection usually makes more work for the developer, complicates the build process, slows down the build, or all three at once :slight_smile:

C solved this by requiring you to provide forward declarations for everything defined in another translation unit even when there isn't a cycle, and always compiling each translation unit totally independently of anything else.

This got more expensive and complex in C++ due to template expansion: a forward declaration of a template isn't sufficient, so you must either explicitly expand the template for each needed combination of parameters somewhere that does have access to the full definition, or just include the full definition into every translation unit that needs it via header files and then rely on the linker to remove duplicate expansions.

Rust just uses entire crates as translation units, so modules in a crate can freely have cyclic references, but separate crates cannot.

C++20 modules solve this in more or less the same way as Rust: each module is a single translation unit (even if it's composed of multiple files, called "module partitions" to distinguish them from non-module source files that are still standalone translation units) and cyclic references between modules aren't allowed. Cyclic references within a module are allowed, and no forward declarations are needed.

1 Like

Yeah, okay, I clearly see the goal and advantages of having crate <==> TU.

Though for now it also has the drawback of not allowing for more fine-grained control over the target features used to compile two different related source files, or even two different functions in the same source file. At least not for disabling them.

Also, about the solution of splitting the code into two crates :

  • how do I specify one JSON target spec for one crate, and another one for the second crate ?
  • if the above is not possible, then how do I disable some target features for e.g. the initialization code ?
  • otherwise maybe it is possible to compile both the crates separately, and then tell cargo to link the initialization code (as a library) to the rest of the kernel ?
  • but then, how ? Especially since both the crates would depend on each other

Thanks a lot for your detailed answer !

You can specify different targets for different crates in .cargo/config.toml but this only gets applied when you run the build inside that specific crate's directory. When using a workspace and running cargo commands at the top level of the workspace it will only use the .cargo/config.toml at the top level and apply it to all crates in the workspace.

I've dealt with this in embedded projects where I want to be able to build some parts of the workspace for the host machine for testing/simulation/etc but I've not had any case where I've needed to build crates for different targets and then link them together, so I'm not sure if there is a well lit path to do that - it doesn't seem like a common use case.

Crates cannot have circular dependencies between them under any circumstances. You would need to break your project up into a set of crates that form a DAG - any shared code used by both the initialization and regular code would have to go into a third crate that they both depend on.