Should we ever stabilize inline assembly?

@RalfJung

Absolutely asm needs to be specified eventually. However, to my knowledge, never before has anyone precisely specified a notion of "correct compilation" for inline assembly embedded in an optimized language. Like, I do literally not know of any industry or academic work that would do this. I appreciate any references anyone has here.

I was thinking about http://www.ccs.neu.edu/home/amal/papers/funtal.pdf, even though it is about typed assembly language, not a normal one.

1 Like

I'm not opposed to intrinsics, but they do not solve my usecases (e.g. writing interrupt handlers) because I want to emit an exact sequence of instructions, and often they are bizarre instructions that don't seem general enough to expose as instrinsics (e.g. 16-bit real-mode instructions).

Not sure if you meant it this way, but I don't consider inline asm to be niche. As a systems programmer, I consider inline asm to be a foundational component of systems language... from my perspective, not having inline asm would be as painful as not having libcore... you can get around it, but it is very off-putting.

15 Likes

I mean that it's niche (in opposition to "general purpose") both in the sense of the number of people who need it as well as the cases where it is needed. I do not share your perspective that inline ASM is a foundational component; if it were, it seems strange that we managed for so long. When optimizations and intrinsics have done their part, the use cases which remain do not seem substantial.

Can you elaborate on this comparison? Surely one is not as frequently needed as the other one?

1 Like

It's not strange at all. We managed to go around 25 releases without explicit SIMD intrinsics available on stable Rust. Yet, depending on who you ask, plenty of folks (myself included) would consider that functionality to be a foundational component of a "systems" programming language. So just because we have been hobbling along doesn't mean we wouldn't immensely benefit from it. There's an element of survivorship bias to it. There were a few times where I talked to people about Rust---before explicit SIMD intrinsics were available---who asked about them and effectively responded with, "oh then I can't use Rust then." And then you don't hear a peep out of them because they aren't represented in conversations like this.

I'm not someone who understands the use cases of inline asm intimately, but what would be strange is if a "systems" programming language gave up on the concept altogether just because it's hard. There is almost certainly some kind of middle ground that can be attained.

37 Likes

This just means that we understand different things by "foundational" -- I don't think SIMD was foundational either. I would reserve that term for well, more fundamental things (like raw pointers). Also, SIMD seems substantially more widely used.

I stand by the idea that it is OK if some use cases would not be supported by Rust. (Well SIMD is, but not everything should be, and it's up for debate what should and what shouldn't.)

I'm open to those. E.g., the proposal due to Oliver seems reasonable to me.

I do firmly believe, that it is not justified, at least in the medium term, to direct the compiler teams resources away from other things, which would, in my view, benefit a great many more users, to focus on the bug-ridden asm! feature.

I fully agree with the first paragraph, that an exact instruction control feels like a useful and sometimes required feature for a system programming language. However, it might be that inline asm in full gcc style might be a mismatching interface to that.

Consider handler code for an interrupt, whose sole job is setting up and dispatching to normal functions with a C call interface. If the asm portion can be encapsulated in a standard C-call interface then the problem is completely different. It's also of different complexity if you remove the ability to output to arbitrary Rust variables, or to read from them. Evidence 1: Redox implements syscalls with inline asm with only a single output. Most of its blocks don't have any input or output. So probably inline asm is more powerful than required.

It is also less powerful. Since it does strictly depend on the compiler for code generation, it will be restrict to common subset and slow evolution. Instead, a good asm interface would focus only on the interface — getting values in and out, and determining assumptions the compiler makes about the block. The internal instructions should not be touched at all since at that point you loose control, and room to know more than the compiler. Such as, executing on faulty hardware? With custom ISA extensions opcodes? etc.

Hopefully that makes some amount of sense.

5 Likes

You’re right: formally specifying assembly is a very hard problem. (Though it has been done to some extent, e.g. by CompCert.)

But the alternative to inline assembly isn’t no assembly; it’s just non-inline assembly.

For kernels, assembly snippets are basically a hard requirement. When, for instance, you don’t have a valid stack pointer and need to set one up, you cannot rely on the compiler’s normal code generation pipeline, which assumes that a stack is always present. In theory you could have some magic intrinsic that expands to the entire sequence of instructions you need to set up the stack. But in practice that doesn’t really work. The instructions you need are highly circumstance-dependent, as well as dependent on how the kernel has designed some of its internal data structures, so there’s no way an intrinsic could be one-size-fits-all. Besides, you couldn’t formally specify such an intrinsic without a lot of the same work that would be needed to specify assembly.

Inline assembly may not be a hard requirement, because an alternative is to use a separate assembler to assemble a standalone assembly file into an object file, which can then be linked into the Rust program. The assembly code can call and be called from Rust code using the C ABI.

But if you want to specify the behavior of the whole program, the situation is not so different from the one with inline assembly. For both inline and external assembly, the assembly code has to follow a set of rules, like: “The input value will be in register X; I need to put the output value in register Y, without disturbing the value of register Z.” It’s just that inline assembly allows the register assignment to be dynamic, whereas the ABI makes a fixed set of choices. But dynamic assignment is a trivial difference compared to the huge task of specifying assembly, which is needed either way.

In truth, this is just a special case of FFI. Since the beginning, Rust has promised that extern “C” functions and #[repr(C)] are compatible with the C ABI, but the C ABI is defined in terms of architecture-level state. If you want to write a complete formal specification of Rust, you cannot avoid talking about that state, and specifying how it interacts with the Rust Abstract Machine state. In practice it may be more useful to write an incomplete specification that doesn’t cover FFI... but then you may as well leave out inline assembly too.

10 Likes

From the post you linked, I'd say register allocation was one of the concerns. There were also:

  • Parsing mnemonics and directives:
    • Avoided by using an external assembler.
  • "elaborate operand description language and constraint system"
    • Not avoided, but unlike Clang, Rust does not need to be compatible with existing C code written for GCC. The full set of constraints supported by GCC is incredibly large, but the vast majority of use cases can be satisfied with literally two constraints:

      1. "put this in any register" (GCC's r)
      2. "put this in a specific register I'm naming" (and we don't need GCC's overcomplicated syntax for that)

      Well, plus a few more for floating point / SIMD registers if you want to support those use cases. But the point is, most of the complexity is completely unneeded in Rust.

  • goto labels
    • We probably won't start with asm goto support anyway, since you can't define goto labels in Rust. It would be nice to have analogous functionality in some form eventually. I don't think it's as complicated to implement as @sunfishcode thinks.
  • Clear rules for what is stable:
    • Most of @sunfishcode's list of rhetorical questions I could literally answer with "yes" or "no". In fact, I'm going to do so in that thread.
5 Likes

I really think "inline assembly is like external C function or FFI" should be specified, not assumed. I already gave an example of asm goto and most seem to be against it, but another good example is SystemTap probe crate already given. It's already-in-the-wild example!

Section directives used by probe crate is NOT like external C function or FFI, so I think it should not be stabilized and probe crate should remain unstable, for now.

Maybe I am confusing multiple people arguing, but I get the impression that "inline assembly is useful, use case X" and "inline assembly is safe/easy to specify/etc because it's just like FFI" are being argued TOGETHER while use case X directly depends on inline assembly being unlike FFI. That shouldn't happen.

6 Likes

I guess this is where we disagree... rust started out as a systems language, and SIMD and bare-metal programming (e.g. kernels) are a core area of systems programming. It would be very disappointing if rust gave up on those use cases, especially since it is already so close...

18 Likes

I think the idea is that inline assembly is a set of features, not an atomic feature. You would be disappointed if Rust gave up on inline assembly, okay. Would you be disappointed if Rust gave up on inline assembly with goto? Inline assembly with section directives? What about, unlike usual implementation of inline assembly, Rust implementation does not allow inline assembly to participate in register allocation? etc.

3 Likes

asm goto does add a bit more complexity, which is another reason not to support it at the start. But I don’t think it’s that bad.

Using pushsection/popsection in an inline asm block gives you no additional semantics you can rely on compared to using them in an external assembly file, so I don’t see a distinction there.

How does one refer to pc (address of nop instruction in case of probe crate) from an external assembly file?

The nop instruction would be in the external assembly file, as part of an assembly function which is (as usual) called by the Rust code. That preserves the semantics needed by probe just fine, albeit with poor performance. The PC would be in a different function, but that doesn't actually matter, because you generally aren't allowed to look at a PC and take action based on what function it appears to be in. In particular, functions need not be contiguous in memory, nor do they need to have corresponding symbols in the object file, so without debug info it may be impossible to tell what function a given PC is in. Backtraces try to guess anyway, so it could affect backtrace output, but those are always a 'best effort' sort of thing.

1 Like

But does it mean that if rustc decides to implement Intel syntax in its asm extension, the alternative compiler will have to implement Intel syntax as well?

By the way, if anybody is interested in discussing this in person, I'll be working on an inline asm RFC during the impl days at Rustfest next week.

12 Likes

I have been thinking about creating a catalogue of asm! snippets used in the real world, so we can see which features actually matter without guessing.

11 Likes

Sounds like a useful project. It might be worth also looking at asm snippets from C code, since the use cases are basically identical.

4 Likes

I don't. The use cases of Rust should be "everything", but in particular, Rust is uniquely suited for systems programming, and I'd like to have a second choice other than C and C-based languages for that.

11 Likes

(Sorry, I had somehow missed the relevant part of the conversation. Post withdrawn. Put me on team josh and burnt sushi, though I think we do all agree that how we solve this need will look different than it does in C/C++/etc.)