[Pre-RFC]: Inline assembly


#41

I’m not quite sure what you mean. You don’t need to any constraints for intermediate values that only live in the asm, you can just pick a specific register and use it (and list it in the clobbers, obviously), e.g.,

asm!("
    movl rsi, {ptr}
    movl rdi, rsi
    addl rdi, {len}
    ; loop, incrementing rsi until rsi==rdi
",
    in(reg) ptr=slice.as_ptr(),
    in(reg) len=slice.len(),
    clobber(rsi, rdi),
    ...);

#42

Working with inline assembly was a truly awful experience when I had to spend a couple of weeks wrestling with it about a year ago - from ICEs caused by LLVM assertions that would give unhelpful errors like Assertion `Val && "isa<> used on a null pointer (hint: convert your fn items to fn pointers), to bits of asm being thrown away even when marked as volatile, to (if I’m reading my old notes right) having no way of passing an extern fn into an asm block as a pointer, to the bread and butter of just ICEing if something is a bit wrong with your parameters. I’ve written assembly in C but doing it in Rust is (was?) so much worse that for any project with non-trivial amounts of assembly, I will actually go back to using C.

I don’t see many similar complaints in this thread though, so maybe I’m an outlier? I’m very much in favour of anything that moves things past “perma-unstable hand it all down to LLVM” just because more people might hit these bugs and extra validation can be added into Rust or LLVM can be fixed. I’d even tolerate stabilising as is and just making error reporting better (and add docs with a cheat-sheet of constraints etc).


On the RFC itself: Apologies for the minor derail, and thanks for the RFC, it’s nice to see more movement in this area :slight_smile: I do like the proposal, I like using words for in/out/clobber constraints and I like that we have to do work in Rust-land before passing it down to LLVM. I’d like a bit more detail on what exactly you mean by “additionally mappings for register classes are added as appropriate”? Are you meaning they’ll be added on an as-needed basis, as LLVM says they will? If so, I would like to request “i” right off the bat.


#43

Please do not elide them! Certain common things are completely impossible without argument modifiers, e.g. it’s not possible to write this call in libfringe in any other way (or at least I looked at the LLVM sources and did not discover one).


#44

This is extremely not discoverable, as I had to read LLVM sources to figure it out, but it’s doable with argument modifiers (as I’ve just described).


#45

So I went back and rewrote some of my old assembly to use the proposed syntax. I didn’t really have any of the problems I expected (i.e. @rkruppe was right :slight_smile: ) , so I don’t really have any strong opposition to this RFC anymore…

One minor nit:

It would be nice to be able to look at a piece of inline assembly and trivially tell that there are no inputs/outputs… I guess we could use comments, which is what I did, but syntax would be nicer. The old syntax solved this problem by enforcing a section (separated by “:”) for ins, outs, clobbers, and flags. We could do the same in the new macro:

    let x: u64;
    let y: u64;

    asm_x86!{
        "
        push %rax
        movq $0, {x}
        movq $0, {y}
        "

        in { }

        out {
            x = rax x,
            y = late rcx y, // Rather than lateout; can additionally have `in`
        }

        clobber {
            "rax", "rsp"
        }

        flags { }
};

#46

For a real-world example, here is some assembly code (ARM64) which I am using in my current project:

// Common code for interruptible syscalls
macro_rules! asm_interruptible_syscall {
    () => {
        r#"
            # If a signal interrupts us between 0 and 1, the signal handler
            # will rewind the PC back to 0 so that the interrupt flag check is
            # atomic.
            0:
                ldrb ${0:w}, $2
                cbnz ${0:w}, 2f
            1:
               svc #0
            2:

            # Record the range of instructions which should be atomic.
            .section interrupt_restart_list, "aw"
            .quad 0b
            .quad 1b
            .previous
        "#
    };
}

// There are other versions of this function with different numbers of
// arguments, however they all share the same asm code above.
#[inline]
pub unsafe fn interruptible_syscall3(
    interrupt_flag: &AtomicBool,
    nr: usize,
    arg0: usize,
    arg1: usize,
    arg2: usize,
) -> Interruptible<usize> {
    let result;
    let interrupted: u64;
    asm!(
        asm_interruptible_syscall!()
        : "=&r" (interrupted)
          "={x0}" (result)
        : "*m" (interrupt_flag)
          "{x8}" (nr as u64)
          "{x0}" (arg0 as u64)
          "{x1}" (arg1 as u64)
          "{x2}" (arg2 as u64)
        : "x8", "memory"
        : "volatile"
    );
    if interrupted == 0 {
        Ok(result)
    } else {
        Err(Interrupted)
    }
}

This is what it would look like under the original proposed syntax (I made one minor change, I inverted in the volatile flag and renamed it to pure).

// Common code for interruptible syscalls
macro_rules! asm_interruptible_syscall {
    () => {
        r#"
            # If a signal interrupts us between 0 and 1, the signal handler
            # will rewind the PC back to 0 so that the interrupt flag check is
            # atomic.
            0:
                ldrb {interrupted:w}, {interrupt_flag}
                cbnz {interrupted:w}, 2f
            1:
               svc #0
            2:

            # Record the range of instructions which should be atomic.
            .section interrupt_restart_list, "aw"
            .quad 0b
            .quad 1b
            .previous
        "#
    };
}

// There are other versions of this function with different numbers of
// arguments, however they all share the same asm code above.
#[inline]
pub unsafe fn interruptible_syscall3(
    interrupt_flag: &AtomicBool,
    nr: usize,
    arg0: usize,
    arg1: usize,
    arg2: usize,
) -> Interruptible<usize> {
    let result;
    let interrupted: u64;
    asm!(
        asm_interruptible_syscall!(),
        interrupted = out (reg) interrupted,
        interrupt_flag = in (mem) interrupt_flag, // Does (mem) take an address or an lvalue?
        lateout ("x0") result,
        in ("x8") nr as u64,
        in ("x0") arg0 as u64,
        in ("x1") arg1 as u64,
        in ("x2") arg2 as u64,
        clobber("x8", "memory"),
        // volatile is implied by the absence of the "pure" flag
    );
    if interrupted == 0 {
        Ok(result)
    } else {
        Err(Interrupted)
    }
}

I feel that the new syntax is a lot nicer to use, especially since it supports named parameters and doesn’t require outputs to come before inputs. To make it easier to use in macros, I would suggest make the ordering fully flexible: clobbers and flags can occur at any position and multiple clobber lists are allowed (a union of them is used as the actual clobber list).

Having to explicitly say lateout instead of out is great since it makes you double-check that you aren’t reading any inputs after writing to the output. Making out a safe default will greatly help people who are new to inline asm.


#47

One thing which suddenly strikes me when reading your example is that these two in/out specifiers feel somewhat redundant in the new syntax:

interrupted = out (reg) interrupted,
interrupt_flag = in (mem) interrupt_flag,

If mapping a Rust variable into an inline ASM identifier of the same name is felt to be a frequent use case, perhaps a shorthand which avoids repeating the identifier could be introduced ?


#48

Perhaps we could just use the variable name as the named argument:

interrupted = out (reg),
interrupt_flag = in (mem),

#49

This is actually trickier than it seems since the value attached to a register can an expression (e.g. arg0 as u64) and is not necessarily just an identifier.


#50

It could take a queue from struct construction, where you can write either Foo { bar: some_expr }, or just Foo { bar } if there’s a bar in scope.


#51

At the risk of starting the dreaded bikeshed discussion, I feel like this kind of elision would be easier to introduce in a syntax where the asm binding name comes after the in()/out() specifier, such as the one which @rkruppe hinted at previously:

in(reg) ptr=slice.as_ptr(),
in(reg) len=slice.len(),

In this case, if there is already a “ptr” and a “len” in scope, one would just reword it as:

in(reg) ptr,
in(reg) len,

The main limitation is that this introduces a parsing ambiguity if both expressions (for anonymous arguments) and bindings (for named arguments) are allowed. As far as I can see, such a syntax would only be viable if naming arguments becomes mandatory (which, as @mark-i-m pointed out earlier, can be an unnecessary annoyance for small snippets).


#52

I think that this isn’t a huge issue and that we should just try to stick close to the existing format string syntax.


#53

I generally agree, was just wondering if there was an easy way to sugar out this kind of repetition. But there does not seem to be one, and this minor issue is not worth going for the hard way :slight_smile: