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::asm
core::arch::foo64::asm
, wherefoo64
is every architecture supported by theasm
macro.
(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::asm
makes it immediately and syntactically obvious that a given platform is unsupported by dint of a nonexistent symbol, usingarch::asm
on an unsupported platform is still a compiler error. - Inclusion in the prelude: while
arch::asm
is straightforwardly obvious to export from the prelude,arch::foo64::asm
and 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
asm
macro to have slightly different semantics on different architectures, e.g. architecture-specific register classes or clobbering behavior. Whilearch::foo64::asm
is more explicit about this potential difference, it is not necessary for enabling such behavior. - Target-specific stabilization or deprecation: while
arch::foo64::asm
is more straightforward to deprecate/stabilize on a per-target basis,#cfg
can be used to the same effect even forarch::asm
.
The relevant differences between the the two options:
- Importing the symbol: even assuming that
asm
is not added to the prelude,arch::asm
would still be trivial touse
in architecture-dependent code. Conversely, without a prelude addition,arch::foo64::asm
would 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::asm
would have to document all architecture-dependent behavior, which could make it unwieldy.arch::foo64::asm
would isolate architecture-dependent documentation, however, every module would be forced to duplicate all non-architecture-dependentasm
documentation, which seems unfortunate in its own way. - Error reporting in the event of unguarded
asm
: havingarch::asm
, or else havingarch::foo64::asm
but havingasm
in the prelude, increases the chances that someone will write anasm!
invocation that is not guarded by any#cfg
directive 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 supportsasm
in general but was not considered by this specificasm
invocation. Even worse, the code could compile but have unintended behavior. Conversely,arch::foo64::asm
essentially guarantees that an author's code will have to mention their intended platform somewhere, either in ause
or in an expression, and users on different platforms will receive obvious "cannot find valuefoo64::asm
in this scope" errors when attempting to compile it. However, this benefit requires thatasm
never be added to the prelude (and never adding any other way of circumventing the need to mention a platform, e.g. having botharch::asm
andarch::foo64::asm
). - Module proliferation: currently,
asm
supports architectures that do not have a corresponding module understd::arch
. Thearch::foo64::asm
approach 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 thearch
module. - 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::asm
is maximally permissive,arch::foo64::asm
is 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.