[Pre-RFC #2]: Inline assembly

Yep. It's a question of whether you're certain what the code in your call stack is doing. Normally, all code is allowed to assume that stack variables won't be trashed before their destructors are run. In theory, even the core crate could randomly decide to start taking advantage of that assumption. Any function that takes a callback, like Option::map, could be changed to start maintaining a global linked list of all currently running instances of that function... for some reason:

fn map(f: impl FnOnce(whatever) -> whatever) -> whatever {
    let mut link: LinkedListEntry;
    add_to_global_linked_list(&mut link);
    f(); // call user's callback
    remove_from_global_linked_list(&mut link);
}

Is that likely to happen? No. But I don't think there's currently any guarantee that it won't happen.

1 Like

Sounds like the semantics will have to be architecture-specific then, which doesn't seem surprising.

A symbol outside the current crate but visible to the current crate isn't necessarily outside the current shared library.

Also, it should be possible to pass in and call through pointers to functions in the GOT, with well-defined semantics.

We should avoid implicit tying completely. If we want tying we should have proper syntax for it.

[emphasis mine]

I assume this is a copy paste error?

I think requiring tying to an explicit out _ would be more readable.

Tying is more economic and readable than forcing the use of a temporary.

I'm not sure what the syntax should be, but I think they should be specified next to each other without a comma between. Maybe some thing like in(reg) expr => out place.

I think just sym _ should work for this.

1 Like

If you pass in the function pointer with in(reg), sure. But if you want to reference it by name, the code sequence required does depend on the linking style / platform.

For what it's worth, I'd rather force a temporary. Inline assembly is complicated enough as it is; if you can omit a feature with no performance loss and not too much ergonomic loss, I'd rather omit it. YMMV.

3 Likes

Can we have a #[cfg] feature to detect the presence of llvm_asm!?

2 Likes

How about stabilizing llvm_asm!("<arch>", <current_syntax>) as a function that returns true if the arch is correct and the backend supports this particular syntax (as specified by LLVM) and false otherwise? (as well as check_llvm_asm! that only checks but does nothing)

This requires almost no effort to implement in rustc, and allows Cranelift to implement any subset of the LLVM assembly syntax, returning false for anything else and letting the generic fallback code (that is already needed to support unknown architectures) handle it.

2 Likes

People will probably forget to check the return value, likely causing wrong results or UB on unknown archs instead of the current compile error.

#[must_use] equivalent solves that

  1. that is just a lint. people will ignore it I think
  2. returning a bool will be confusing for people who havent read the docs.

What about something like:

match_asm! {
    x86 | x86_64 => "cli";
    arm | aarch64 => "cpsid i";
    _ => {
        // arbitrary rust code
        panic!("cant disable interrupts");
    }
}

Honestly I feel that this sort of additional functionality can be left to crates.io. I would prefer focusing on the core asm functionality in this RFC.

2 Likes

For matching on target arch I agree. We already have cfg_if and cfg(target_arch). For the support of inline asm, it can't be part of a crate. It has to be part of rustc.

How about extending asm!() to include the architecture that the block is for? If the compiler is asked to compile for the wrong architecture, it will then be able to detect that there is a problem. Similar to what @bill_myers said earlier:

asm!(x86_64, "foo", ...);

Alternatively, the first argument could be some kind of struct that is defined at compile time. e.g.

#[derive(Debug, Copy, Clone)]
enum Architecture {
    x86_64,
    ARM,
    RISCV,
    WASM_1_0,
    // etc., etc., etc
}

#[derive(Debug)]
struct AsmParameters {
    architecture: Architecture,
    formal_equivalent: &'static str,
}

impl AsmParameters {
    const fn new() -> AsmParameters {
        AsmParameters {
            architecture: Architecture::x86_64,
            formal_equivalent: "",
        }
    }

    const fn architecture(&self, architecture: Architecture) -> AsmParameters {
        AsmParameters {
            architecture,
            formal_equivalent: self.formal_equivalent,
        }
    }

    const fn formal_equivalent(&self, formal_equivalent: &'static str) -> AsmParameters {
        AsmParameters {
            architecture: self.architecture,
            formal_equivalent: formal_equivalent,
        }
    }
}

fn main() {
    const ASM_PARAMETERS: AsmParameters = AsmParameters::new()
        .architecture(Architecture::ARM)
        .formal_equivalent("Something and SMT solver can ingest.");

    unsafe {
        asm!(ASM_PARAMETERS, "Everything in the RFC so far.")
    }
}

The advantage of this method is that we can add in new parameters in the future as needed. This will handle many of the problems that were brought up in the earlier conversation about how to stabilize asm!()

2 Likes

This is... kind of a rabbit hole. Do you distinguish things like SSE? AVX? NEON? The litany of RISC-V extensions? If such a feature exists, and I write RISC-V code that has mul; mulh, it would be nice to require the M extension. in addition to just RISC-V.

(There's also some muttering about thumb to be made here but I'm not an ARM person.)

5 Likes

Personally, I'm in favor of somehow specifying the expected architecture/assembler pair in the asm! invocation. That would explicitly handle Intel vs AT&T vs whatever assembly language as well.

This is a very minor concern, though. I just want the default to be uniformly "system assembler" or "specified assembler". (Plus, spitting asm to an assembler it isn't targeting is just asking for arcane issues.)

2 Likes

Having to manually specify the architecture or similar in an asm directive makes every such directive more verbose, and I don't think it provides commensurate benefit.

3 Likes

I was about to post a similar concern. When writing assembly code for RISC-V I would like to write the efficient variant for systems where the M extension is present, and a less-efficient but performant variant for systems where that extension is absent. The syntax for this needs to identify the base architecture, as well as indicate the presence of absence of specific variants while omitting all the irrelevant variants. RISC-V is the poster-child for this problem, as it has many mostly-disjoint standard, proposed standard, and user-defined variants, as well as the group variant G (which is short for I+M+A+F+D) that probably should not be permitted in the list.)

1 Like

Not as much a rabbit hole as you'd think though; as @Amanieu stated in the RFC:

which suggests that there will be a limited set of architectures supported at any one time. As for variants, etc., you could have something like a set of nested enums, or something more esoteric.

That said, I do see your point about how this can lead to a combinatorial explosion. I don't really see a good way around it while at the same time giving the compiler some extra information so that it can do bounds checking on assembly.

In addition, this makes it possible to decide on what new parameters we want to add to the asm!() macro in the future. Even if you don't like my Architecture example, what about some kind of rust equivalent key, or something that an SMT solver can ingest? That will make it possible for the compiler to check the borrow rules even on assembly that it doesn't understand. This would take some work, and I'm not suggesting that it be done now, but it could be added later, if it looks like it's needed.

Right, but the concern here is that ISAs can vary wildly in shape and size (some examples from this thread, Itanium's hilarious same-core ambi-endianess, NaT values, and RISC-V's O(infinity) extension combinations). If we choose to specify "ISA checking" of assembly, we need to be extremely careful that we do not accidentally exclude useful predicates for non-mainstream ISAs.

I think this RFC should include some variant of "here is a complete list of the boxes you need to tick to implement support for a new ISA". This should involve, at a minimum, mapping register constraint classes, but I suspect there are a lot more constraints too, like "implementation defined behavior" definitions (e.g., I think further up someone muttered something about how Itanium's NaT values might result in problems with "freezing").

1 Like

Or just use #[cfg(target_feature = "...")] to select different asm implementations depending on the target features.

I don't see a very strong need for additional feature/architecture checking in the asm macro when this can already be done with existing Rust features.

Similarly with AT&T syntax: unless someone can come up with a strong case for supporting it, I would prefer restricting asm! to only supporting Intel syntax.

3 Likes