support #[target_feature(enable = ...)] on #[naked] functions
Project: rust-lang/rust | Language: Rust | Concepts: api-design, error-handling, testing-strategy, compiler-internals | Difficulty: senior
Link: https://github.com/rust-lang/rust/pull/137720
The problem
Rust's #[naked] functions allow writing functions in pure assembly, but previously, it was not possible to use the #[target_feature(enable = "...")] attribute to enable specific CPU features (such as SIMD instruction sets) for these functions. This meant that writing portable, feature-specific assembly in #[naked] functions was not supported, limiting their usefulness for low-level, cross-platform code.
Why it's non-trivial
Enabling target features for #[naked] functions is tricky because:
- The feature must be enabled only for the duration of the naked function's assembly, not globally.
- Each target/architecture handles feature toggling differently (or not at all), so the implementation must be careful not to leave the compiler in an inconsistent state.
- Some features or targets may not support toggling at all, so the compiler must handle these cases gracefully, possibly by emitting errors or warnings.
- The backend code generation APIs must be extended to pass these extra features through the stack.
The approach
This PR threads extra target features through the codegen stack and implements per-target logic for enabling/disabling features in the generated assembly. It also adds thorough tests for both correct and error cases.
1. Extending the codegen trait and implementations
The codegen trait is extended to accept extra target features as a parameter:
pub trait AsmCodegenMethods<'tcx> {
+ /// Codegen a module-level assembly block.
+ ///
+ /// NOTE: the target features must be the rust target feature names, not backend target
+ /// feature names. This argument is used to forward target features on naked functions.
fn codegen_global_asm(
&mut self,
template: &[InlineAsmTemplatePiece],
operands: &[GlobalAsmOperandRef<'tcx>],
options: InlineAsmOptions,
line_spans: &[Span],
+ extra_rust_target_features: &[String],
);
What this does:
The codegen_global_asm function now receives a slice of extra target features, which will be used to enable features for the duration of the global asm block.
2. Threading the new parameter through all codegen implementations
All codegen backends are updated to accept and forward the new argument, even if they do not use it:
- cx.codegen_global_asm(asm.template, &operands, asm.options, asm.line_spans);
+ cx.codegen_global_asm(asm.template, &operands, asm.options, asm.line_spans, &[]);
What this does:
The default call site now passes an empty slice for extra features, maintaining compatibility for code that doesn't use this new functionality.
3. Passing the function's target features in the naked function codegen path
When generating code for a naked function, the compiler now collects the enabled features and passes them down:
- cx.codegen_global_asm(&template_vec, &operands, options, line_spans);
+ let target_features: Vec<_> =
+ cx.tcx().asm_target_features(instance.def_id()).iter().map(|s| s.to_string()).collect();
+ cx.codegen_global_asm(&template_vec, &operands, options, line_spans, &target_features);
What this does:
For naked functions, the compiler queries the set of enabled target features and ensures they are used during codegen.
4. Translating Rust features to backend features (LLVM example)
For LLVM, the code translates Rust feature names to backend feature flags and combines them with the global features:
- let target_features = self.tcx.global_backend_features(()).join(",");
- let target_cpu = llvm_util::target_cpu(self.tcx.sess);
+ // Globally-enabled features that are already in the backend format.
+ let global_features = self.tcx.global_backend_features(()).iter().map(String::as_str);
+
+ // Features enabled on a particular instance, in the rust format.
+ // These need to be translated to the LLVM format.
+ let function_features: Vec<_> = extra_rust_target_features
+ .iter()
+ .flat_map(|feat| llvm_util::to_llvm_features(self.tcx.sess, feat))
+ .flat_map(|feat| feat.into_iter().map(|f| format!("+{f}")))
+ .collect();
+
+ let function_features = function_features.iter().map(String::as_str);
+ let target_features =
+ global_features.chain(function_features).intersperse(",").collect::<String>();
What this does:
This code ensures that both global and per-function features are enabled in the backend, allowing the naked function to use the requested instructions.
5. Adding comprehensive tests
The PR adds new tests for various targets and features, both for successful usage and for correct error reporting when features are missing:
+// x86_64-LABEL: vpclmulqdq:
+// x86_64: vpclmulqdq
+#[no_mangle]
+#[unsafe(naked)]
+#[cfg(target_arch = "x86_64")]
+#[target_feature(enable = "vpclmulqdq")]
+unsafe extern "C" fn vpclmulqdq() {
+ naked_asm!("vpclmulqdq zmm1, zmm2, zmm3, 4")
+}
And error tests, e.g.:
+//~? ERROR instruction requires: i8mm
What this does:
Tests verify that the feature is enabled only for the naked function and not for subsequent asm blocks, and that errors are reported when trying to use instructions without the required features.
From the review thread
folkertdev: I think this is as far as I'm going to get on my own (well with a bunch of help from taiki too, thanks!). Questions I don't have a good answer to:
- the conversion of the target string to an
Architecture: it seems kind of ad-hoc and easy to miss (new) targets- what do we do with target features that we can't deal with (e.g. the
simd128for wasm): ignore silently, warn, error?- what do we do with targets where we are not sure we can restore to the prior state (e.g. arm8 is likely in this category, some of the other more obscure ones too): try our best? just error out?
folkertdev: Note for my future self: Amanieu and I discussed this at rustweek, and concluded to make this an unstable feature that supports the platforms where the behavior is reliable, and error on other platforms.
Takeaways
- When extending APIs to support new features, thread new parameters through all relevant layers, even if some implementations are no-ops.
- Always translate user-facing attributes (like Rust feature names) to backend-specific representations before code generation.
- For platform-specific features, explicitly handle unsupported or unreliable targets by emitting errors or restricting the feature to an unstable/opt-in basis.
- Add comprehensive tests, including both positive and negative cases, to ensure correct feature scoping and error reporting.