attach global target features to module-level assembly
Project: rust-lang/rust | Language: Rust | Concepts: api-design, build-systems, error-handling | Difficulty: senior
Link: https://github.com/rust-lang/rust/pull/160594
The problem
When using module-level assembly (global_asm!), the global target features and CPU information were not being forwarded to LLVM. This meant that, especially during Link Time Optimization (LTO) or when merging modules, LLVM could lose track of which features or CPU were enabled. As a result, LLVM might reject valid assembly code that uses instructions gated on certain features, even though those features were globally enabled for the build.
Why it's non-trivial
A naive fix might simply attach the assembly to the module without considering the global target features or CPU. However, during LTO or cross-module merging, LLVM needs explicit information about which features and CPU are enabled to correctly validate and assemble the code. Failing to forward this information can lead to subtle, hard-to-debug errors, especially on targets with feature-gated instructions.
The approach
1. Forward target features and CPU to LLVM when attaching module-level assembly
Previously, the code just appended the assembly:
- llvm::append_module_inline_asm(self.llmod, template_str.as_bytes());
Now, it collects the global backend features and target CPU, and passes them to LLVM:
+ let target_features = self.tcx.global_backend_features(()).join(",");
+ let target_cpu = llvm_util::target_cpu(self.tcx.sess);
+
+ llvm::append_module_inline_asm(
+ self.llmod,
+ template_str.as_bytes(),
+ &target_features,
+ target_cpu,
+ );
This change ensures that the module-level assembly is tagged with the correct features and CPU, so LLVM can validate it appropriately.
2. Update the Rust-LLVM FFI and wrapper to accept the new parameters
The Rust FFI definition for appending module-level assembly was updated from:
- pub(crate) fn LLVMAppendModuleInlineAsm(
- M: &Module,
- Asm: *const c_uchar, // See "PTR_LEN_STR".
- Len: size_t,
- );
to:
+ pub(crate) fn LLVMRustAppendModuleInlineAsm(
+ M: &Module,
+ Asm: *const c_uchar, // See "PTR_LEN_STR".
+ AsmLen: size_t,
+ TargetFeatures: *const c_uchar, // See "PTR_LEN_STR".
+ TargetFeaturesLen: size_t,
+ TargetCpu: *const c_uchar, // See "PTR_LEN_STR".
+ TargetCpuLen: size_t,
+ );
This allows passing both the features and CPU string to the C++ wrapper.
3. Update the safe Rust wrapper
The Rust-side wrapper was updated to match the new FFI:
-pub(crate) fn append_module_inline_asm<'ll>(llmod: &'ll Module, asm: &[u8]) {
- unsafe {
- LLVMAppendModuleInlineAsm(llmod, asm.as_ptr(), asm.len());
- }
-}
+pub(crate) fn append_module_inline_asm<'ll>(
+ llmod: &'ll Module,
+ asm: &[u8],
+ target_features: &str,
+ target_cpu: &str,
+) {
+ unsafe {
+ LLVMRustAppendModuleInlineAsm(
+ llmod,
+ asm.as_ptr(),
+ asm.len(),
+ target_features.as_ptr(),
+ target_features.len(),
+ target_cpu.as_ptr(),
+ target_cpu.len(),
+ );
+ }
+}
This ensures all call sites now provide the required feature and CPU information.
4. Update the C++ wrapper to use the new LLVM API (when available)
The C++ wrapper was extended to support the new fields:
+extern "C" void LLVMRustAppendModuleInlineAsm(
+ LLVMModuleRef M, const char *Asm, size_t AsmLen, const char *TargetFeatures,
+ size_t TargetFeaturesLen, const char *TargetCPU, size_t TargetCPULen) {
+#if LLVM_VERSION_GE(23, 0)
+ Module::GlobalAsmProperties Props;
+ Props.TargetFeatures = std::string(TargetFeatures, TargetFeaturesLen);
+ Props.TargetCPU = std::string(TargetCPU, TargetCPULen);
+ unwrap(M)->appendModuleInlineAsm(
+ Module::GlobalAsmFragment(std::string(Asm, AsmLen), Props));
+#else
+ unwrap(M)->appendModuleInlineAsm(StringRef(Asm, AsmLen));
+#endif
+}
This uses the new LLVM 23+ API to attach the properties, and falls back to the old method for older LLVM versions.
5. Update call sites that embed bitcode sections
When embedding bitcode via inline assembly, the new signature is used, but with empty feature/cpu strings since they're not relevant here:
- llvm::append_module_inline_asm(llmod, &asm);
+ llvm::append_module_inline_asm(llmod, &asm, "", "");
6. Add regression tests
A new test (tests/ui/asm/global-target-feature.rs) was added to ensure that the correct features are preserved for module-level assembly on various architectures and optimization settings.
From the review thread
folkertdev: Fair. So what this does is attach the target features and cpu information to piece of assembly. Previously the target information would sometimes get lost, especially when doing LTO and potentially merging modules with different target features. LLVM would error when assembly used instructions that are gated on a particular target feature, even though the feature is in fact globally enabled.
My practical question for you is: am I forwarding the right target features here? and might a particular target cpu that we forward imply target features that we didn't intend to enable?
RalfJung: > any nasty side-effects we might be overlooking here?
I don't have the slightest clue what it means to forward target features to assembly.^^
Takeaways
- When passing data between build stages or tools (e.g., from a compiler to a backend like LLVM), always explicitly forward all configuration and environment details that affect code generation, such as target features and CPU.
- APIs and FFI boundaries should be version-aware: when extending an interface, provide fallback behavior for older versions to maintain compatibility.
- Tests for platform-specific or feature-dependent code should cover multiple architectures and optimization settings to catch subtle integration bugs.