Skip to content

Namespace the asm! macro #84019

Closed
Closed
@bstrie

Description

@bstrie

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:

  1. 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
  2. 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:

  1. core::arch::asm
  2. core::arch::foo64::asm, where foo64 is every architecture supported by the asm 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:

  1. 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, using arch::asm on an unsupported platform is still a compiler error.
  2. 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.
  3. 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. While arch::foo64::asm is more explicit about this potential difference, it is not necessary for enabling such behavior.
  4. 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 for arch::asm.

The relevant differences between the the two options:

  1. Importing the symbol: even assuming that asm is not added to the prelude, arch::asm would still be trivial to use 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 of arch::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!(...);
}
  1. 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-dependent asm documentation, which seems unfortunate in its own way.
  2. Error reporting in the event of unguarded asm: having arch::asm, or else having arch::foo64::asm but having asm in the prelude, increases the chances that someone will write an asm! 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 supports asm in general but was not considered by this specific asm 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 a use or in an expression, and users on different platforms will receive obvious "cannot find value foo64::asm in this scope" errors when attempting to compile it. However, this benefit requires that asm never be added to the prelude (and never adding any other way of circumventing the need to mention a platform, e.g. having both arch::asm and arch::foo64::asm).
  3. Module proliferation: currently, asm supports architectures that do not have a corresponding module under std::arch. The arch::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 the asm! macro for the benefit of alternative backends. However, in either scenario alternative backends would still need to provide a patch to support new targets in asm!, 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 under arch::, although to some degree that is the point of the arch module.
  4. 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: where arch::asm is maximally permissive, arch::foo64::asm is maximally strict. With arch::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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-inline-assemblyArea: Inline assembly (`asm!(…)`)F-asm`#![feature(asm)]` (not `llvm_asm`)T-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions