-
Notifications
You must be signed in to change notification settings - Fork 13.9k
Description
Click here for the final summary report.
Original issue preserved below:
As of today, most macros in Rust, despite being declared by the stdlib, are not properly namespaced in the sense that every other type, function, and trait in the stdlib is namespaced. Instead, historically, macros "live" in the crate root due to the technical limitations of Rust 1.0, which did not support namespacing for macros. This is distinct from items such as Option or Vec which are merely available as though they live in the crate root (via the prelude), but are actually defined at std::option::Option and std::vec::Vec. This has two primary downsides:
- The stdlib API index is polluted by a wall of random, unrelated macros, many of which are relatively unimportant or irrelevant: https://doc.rust-lang.org/std/index.html#macros
- Defining an item in the crate root is tantamount to exporting that item from the prelude, which means that adding a macro there totally bypasses the discussion of whether the macro should be exported from the prelude.
In Rust 1.51, ptr::addr_of became the first stable macro to not be defined in the crate root. The machinery exists to namespace macros, and there seems to be at least loose consensus that this is worth using for new macros in the stdlib.
One of the last remaining open questions for the stabilization of the asm macro is where it should live. Given the above, and the history of prior discussion on this topic, there are two options for consideration:
core::arch::asmcore::arch::foo64::asm, wherefoo64is every architecture supported by theasmmacro.
(For conciseness I will only be referring to asm in this document, but this decision also applies to any and all related macros, such as global_asm.)
First, the non-advantages of either option:
- Compile-time rejection of unsupported platforms: while
arch::foo64::asmmakes it immediately and syntactically obvious that a given platform is unsupported by dint of a nonexistent symbol, usingarch::asmon an unsupported platform is still a compiler error. - Inclusion in the prelude: while
arch::asmis straightforwardly obvious to export from the prelude,arch::foo64::asmand friends could still possibly be exported from the prelude via#cfg. - Architecture-dependent behavior: even aside from the literal assembly code itself, it is possible for the
asmmacro to have slightly different semantics on different architectures, e.g. architecture-specific register classes or clobbering behavior. Whilearch::foo64::asmis more explicit about this potential difference, it is not necessary for enabling such behavior. - Target-specific stabilization or deprecation: while
arch::foo64::asmis more straightforward to deprecate/stabilize on a per-target basis,#cfgcan be used to the same effect even forarch::asm.
The relevant differences between the the two options:
- Importing the symbol: even assuming that
asmis not added to the prelude,arch::asmwould still be trivial tousein architecture-dependent code. Conversely, without a prelude addition,arch::foo64::asmwould have to fall back on a few patterns when writing architecture-dependent code, some of which are more verbose than others. Below, Pattern add an IL type checker #4 is the one that comes closest to the ergonomics ofarch::asm, but it may be non-obvious, and it involves using a glob-import, which some may find distasteful (or even be linting against):
// Pattern #1 ----------------------------------------
// fully-qualified names
#[cfg(foo64)]
fn qux() {
std::arch::foo64::asm!(...);
}
#[cfg(bar64)]
fn qux() {
std::arch::bar64::asm!(...);
}
// Pattern #2 ----------------------------------------
// doubled #cfg attributes
#[cfg(foo64)]
use std::arch::foo64::asm;
#[cfg(bar64)]
use std::arch::bar64::asm;
#[cfg(foo64)]
fn qux() {
asm!(...);
}
#[cfg(bar64)]
fn qux() {
asm!(...);
}
// Pattern #3 ----------------------------------------
// context-local use
#[cfg(foo64)]
fn qux() {
use std::arch::foo64::asm;
asm!(...);
}
#[cfg(bar64)]
fn qux() {
use std::arch::bar64::asm;
asm!(...);
}
// Pattern #4 ----------------------------------------
// glob import
use std::arch::*;
#[cfg(foo64)]
fn qux() {
foo64::asm!(...);
}
#[cfg(bar64)]
fn qux() {
bar64::asm!(...);
}- Documentation: the documentation of
arch::asmwould have to document all architecture-dependent behavior, which could make it unwieldy.arch::foo64::asmwould isolate architecture-dependent documentation, however, every module would be forced to duplicate all non-architecture-dependentasmdocumentation, which seems unfortunate in its own way. - Error reporting in the event of unguarded
asm: havingarch::asm, or else havingarch::foo64::asmbut havingasmin the prelude, increases the chances that someone will write anasm!invocation that is not guarded by any#cfgdirective or any other mention of the original author's platform. Such an unguarded invocation would probably give unhelpful errors if the code is ever compiled for a platform that supportsasmin general but was not considered by this specificasminvocation. Even worse, the code could compile but have unintended behavior. Conversely,arch::foo64::asmessentially guarantees that an author's code will have to mention their intended platform somewhere, either in auseor in an expression, and users on different platforms will receive obvious "cannot find valuefoo64::asmin this scope" errors when attempting to compile it. However, this benefit requires thatasmnever be added to the prelude (and never adding any other way of circumventing the need to mention a platform, e.g. having botharch::asmandarch::foo64::asm). - Module proliferation: currently,
asmsupports architectures that do not have a corresponding module understd::arch. Thearch::foo64::asmapproach would require a module for all supported architectures going forward. This includes adding modules for targets that may not ever be supported by rustc itself (e.g. SPIR-V), but are supported by theasm!macro for the benefit of alternative backends. However, in either scenario alternative backends would still need to provide a patch to support new targets inasm!, and adding a new module for any unstably-supported target doesn't seem like a particularly onerous part of that process. However, given enough time (and enough alternative backends, e.g. a GCC one) this could lead to quite a few submodules underarch::, although to some degree that is the point of thearchmodule. - Platform-agnostic assembly: it is possible for a single
asm!invocation to properly support multiple targets. This is the flipside of point 3 stated above: wherearch::asmis maximally permissive,arch::foo64::asmis maximally strict. Witharch::foo64::asm, writing platform-agnostic assembly would require a pattern like the following:
#[cfg(foo64)]
use std::arch::foo64 as fooish;
#[cfg(foo32)]
use std::arch::foo32 as fooish;
fn qux() {
fooish::asm!(...);
}TL;DR: the benefits of either approach are fairly small. arch::asm is easier to use, but only if asm is not added to the prelude. arch::foo64::asm could lead to better error messages in some cases, but likewise only if asm is not added to the prelude. If the decision is made to add asm to the prelude, then there is essentially no advantage to either option.