Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Interrupt calling conventions #3246

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

phil-opp
Copy link

@phil-opp phil-opp commented Mar 23, 2022

Add compiler support for interrupt calling conventions that are specific to an architecture or target. This way, interrupt handler functions can be written directly in Rust without needing assembly shims.

While there are already some unstable implementations of interrupt calling conventions in rustc, they were only added for experimentation. As these calling conventions showed their worth over the years, the lang team requested an RFC for interrupt calling conventions general, to put them on a path towards stabilization. So this RFC tries to provide the necessary groundwork for such calling conventions.

Rendered

Pre-RFC discussion on internals

@nikomatsakis
Copy link
Contributor

To be sure I understand, this RFC is not proposing any specific interrupt ABI, but simply talking about the idea of interrupt handler ABIs and what their requirements would be?

@phil-opp
Copy link
Author

To be sure I understand, this RFC is not proposing any specific interrupt ABI, but simply talking about the idea of interrupt handler ABIs and what their requirements would be?

Yes, exactly. I added some more context to the PR description. The plan is to use this RFC as a foundation, so that we don't need separate RFCs for each individual interrupt ABI. See @joshtriplett's comment in rust-lang/rust#40180 (comment) for more details.

- the error code is only u64 on 64-bit systems, on 32-bit systems it needs to be u32
- mention the Rust compiler doesn't check these types yet
```
The `error_code` argument is _not_ an optional argument. It is set by the hardware for some interrupt vector, but not for others. The programmer must make sure to always use the correct signature for each interrupt vector, otherwise undefined behavior occurs.

The `StackFrame` type must be a struct that matches the stack frame pushed by the CPU. The `ErrorCode` type must be `u64` on 64-bit targets and `u32` on 32-bit targets. These types are currently _not_ checked by `rustc`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use usize as error code type instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense being explicit here. usize isn't necessarily defined to be the width of a register.

@nikomatsakis
Copy link
Contributor

I'm in favor of this. I guess this is a @rust-lang/lang decision, but I would want @rust-lang/compiler input too!

@nikomatsakis nikomatsakis added the T-lang Relevant to the language team, which will review and decide on the RFC. label Mar 24, 2022
text/0000-interrupt-calling-conventions.md Show resolved Hide resolved

Apart from this limitation, interrupt calling conventions fall under Rust's normal stability guarantees. For this reason, special care must be taken before stabilizing interrupt calling conventions that are implemented outside of `rustc` (e.g. in LLVM).

## Safety
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to say that interrupt routines are implicitly unsafe. Even if the interrupt routine only calls safe Rust functions, it can easily observe intermediate CPU states that are beyond the Rust safety model, or corrupt the state so that undefined behavior occurs after the interrupt routine returns.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify what you mean by "implictly unsafe"? That they should be treated like an unsafe fn so that no unsafe {} blocks are required in it?

I agree this question is something that we should specify in the RFC in more detail. My opinion on this is that we should keep the function body safe, but make all dangerous operations unsafe:

  • Functions with interrupt calling conventions cannot be called from Rust code directly, so there is no way to cause undefined behavior using only safe code.
  • To register a interrupt handler function on the CPU, you typically need some unsafe code (e.g. inline asm) or place it at a specific address using a linker script (which allow all kinds of unsafe operations). In this "unsafe" code, you need to make sure that the referenced function fulfills all necessary safety invariants.
  • We should try to make all operations that corrupt the CPU state unsafe, if possible. For example, the x86_64 crate requires an unsafe block to modify the passed InterruptStackFrame. Ideally, this unsafe block should be required directly by the compiler, for example by requiring raw pointer arguments instead of references for the x86-interrupt calling convention.

I'm happy to discuss this further!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant is that even if the function is not marked as unsafe, the usual rules about Rust memory safety do not apply at all to these functions. There are a lot of restrictions that have to be followed, depending on the rest of the system, and I'm not sure if Rust is actually up for that. (With C or C++, separate compilation with different compiler flags is sometimes used.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like a case by case basis as to how safe each ABI is going to be. The interrupt handler function ABI could (for example) have additional pre and post code inserted by rustc automatically so that the user's source code always runs in a safe environment.

Like we can literally make up these ABIs to do whatever, since normal code is never going to interface with the interrupt handler code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fweimer can you elaborate a bit more about what rules re: Rust memory safety are inapplicable? The function doesn't get access to anything but static data, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see your point. Well then at least the situations I was talking about are fine enough if the compiler is handling it. Though, perhaps fweimer had something else in mind.

Copy link

@fweimer fweimer Oct 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nikomatsakis Some mutex implementations have fast paths for single-threaded operation that do not do anything. I think this would enable an interrupt routine to access data behind a mutex while the interrupted program uses it. (We change pthread_mutex_lock to always perform the mutex update only in EDIT glibc 2.34—starting with that version, single-threaded processes should self-deadlock if the mutex is acquired twice.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once you introduce interrupts you are effectively no longer a single-threaded process as the easiest way to model interrupts in the rust abstract machine is as running on a separate thread which could synchronize with the non-interrupt thread using regular concurrency methods. It just so happens that while the interrupt runs, the non-interrupt thread doesn't make any progress and thus you may deadlock if you wait for it.

Copy link

@chorman0773 chorman0773 Oct 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it can be treated the same as a separate thread precisely because it blocks the non-interrupt code while running and is identified as the same thread.

I've equated it to a signal handler before, which is far more limited in what it can do.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjorn3 The single-thread optimization is an internal C runtime thing that gets disabled if you create another thread using a C runtime facility. The C runtime won't know about the interrupt code and therefore cannot treat the application as multi-threaded.

@mark-i-m
Copy link
Member

mark-i-m commented Apr 1, 2022

I still think I would rather have custom calling conventions in general (i.e., allow me to define my own calling convention and use it). There are a few reasons I think a more general mechanism might be better:

  • I think that could play nicely with naked functions.
  • It would also allow defining architecture-specific conventions in libraries (e.g., as macros) rather than hardcoding them all into the compiler.
  • It would allow people to use custom conventions for things that are not interrupts (e.g., trap and syscall handlers, userspace thread switchers, signal handlers, etc).

@bjorn3
Copy link
Member

bjorn3 commented Apr 1, 2022

While that is a nice idea IMO, neither LLVM, nor GCC, nor Cranelift support that at the moment.

@mark-i-m
Copy link
Member

mark-i-m commented Apr 1, 2022

I don't see why it needs to be. It seems buildable with naked functions and inline assembly. Custom calling conventions would just be sugar with some extra compiler support for checking arguments, generating function prologue/epilogue, etc...

@phil-opp
Copy link
Author

phil-opp commented Apr 1, 2022

It seems buildable with naked functions and inline assembly.

You only need to save the registers that are overwritten by called functions. The register selection happens in LLVM, so it's not possible to know this earlier. So using naked functions, you need to save and restore all registers, which is more expensive.

@mark-i-m
Copy link
Member

mark-i-m commented Apr 1, 2022

@phil-opp I guess my expectation is that anything using a custom calling convention would not be callable from Rust. Moreover, the calling convention would specify who needs to save and restore what registers anyway.

@phil-opp
Copy link
Author

phil-opp commented Apr 1, 2022

Moreover, the calling convention would specify who needs to save and restore what registers anyway.

But how can we implement this without LLVM support?

@mark-i-m
Copy link
Member

mark-i-m commented Apr 1, 2022

I imagine something like this (bikesheddable syntax):

// I haven't fully though through what this would look like...
#[calling_convention(
  name="mark-weird-exception-handler",
  arch="x86_64",
  // custom prologue and epilogue allow setting up the stack or any other state however you want...
  prologue = asm!(...),
  epilogue = asm!(...),
  args=["regs: Regs", "error_code: u64"], // function with this convention must have these arguments
)]

#[no_mangle]
fn extern "mark-weird-exception-handler" handler(regs: Regs, error_code: u64) {
  println!("{regs} {error_code:X}");
  // other code
}

would desugar to something like this:

#[naked]
#[no_mangle]
fn handler() {
  <prologue> // from the calling convention spec above

  asm!("call _handler_inner");

  <epilogue> // from the calling convention spec above
}

#[no_mangle]
fn extern "C" _handler_inner(regs: Regs, error_code: u64) {
   println!("{regs} {error_code:X}");
  // other code
}

The basic idea is that the custom calling convention is tasked with converting a weird calling convention into something the compiler can work with normally (e.g., the C or Rust calling conventions, though I prefer C, since it is specified and gives more control). I think this mostly means fixing up the stack, but it could also include other stateful things, such as enabling/disabling interrupts, fixing up page tables, etc.

The custom calling convention then becomes a simple trampoline mechanism into more normal Rust code.

@phil-opp
Copy link
Author

phil-opp commented Apr 5, 2022

The issue with this approach is that you have to write the assembly for the prologue and epilogue yourself. This includes saving and restoring all registers (on many platforms). To do that in an optimal way, you need to know which registers are actually overwritten by _handler_inner. However, this is not decided until LLVM performs register allocation. So without LLVM support you need to save all registers instead.

One way to work around that problem could be to use LLVM's preserve_all calling convention for your _handler_inner function. This way, LLVM would generate code that automatically restores all registers to their original values before returning, so we would not need to do it manually anymore. The RFC already describes this idea in the "rationale and alternatives" section at the end. I'm happy to explore and discuss this approach further!

@mark-i-m
Copy link
Member

mark-i-m commented Apr 5, 2022

@phil-opp I see. But I'm wondering at the use case -- how important is it to use the minimal set of registers? AFAIK, most kernels save all (general-purpose) registers anyway (e.g., Linux, FreeBSD). If you are planning to do any substantial work during the interrupt/exception handler, you will need to save all of that state anyway -- either because you want to context switch to somewhere else or because you have enough code that the extra registers will be useful.

Are you thinking of an embedded systems use case or something?

@phil-opp
Copy link
Author

phil-opp commented Apr 5, 2022

I don't have any realistic use cases, but you can easily construct an example where the LLVM-generated code is much better:

static TICKS: AtomicUsize = AtomicUsize::new(0);
const THRESHOLD: usize = 10000;

#[no_mangle]
pub extern "x86-interrupt" fn timer_handler(_stack_frame: u8) {
    let prev = TICKS.fetch_add(1, Ordering::Acquire);
    if prev > THRESHOLD {
        // trigger a "context switch" interrupt 
        unsafe { asm!("int 0xf0"); }
    }
}

We want to count the timer ticks with minimal overhead. If a specified threshold value is passed, we trigger a secondary interrupt, which will save the registers and perform a context switch.

When you try this example on godbolt, you see that rustc generates the following assembly:

timer_handler:
        push    rax
        cld
        mov     eax, 1
        lock            xadd    qword ptr [rip + example::TICKS], rax
        cmp     rax, 10000
        jbe     .LBB0_1

        int     240

        pop     rax
        iretq
.LBB0_1:
        pop     rax
        iretq

example::TICKS:
        .zero   8

So we only save and restore the rax register, which leads to a much lower overhead, especially if the timer interrupt fires frequently.

@Lokathor
Copy link
Contributor

Lokathor commented Apr 5, 2022

We certainly should consider "some sort of embedded use case" to be the primary use case with this sort of thing. In which case all register savings are a big win.

@phil-opp
Copy link
Author

phil-opp commented Apr 5, 2022

We certainly should consider "some sort of embedded use case" to be the primary use case with this sort of thing. In which case all register savings are a big win.

Yes, I agree! Does someone have a similar example for some embedded device? Then I'd be happy to extend the motivation section of the RFC based on that.

@Lokathor
Copy link
Contributor

Lokathor commented Apr 5, 2022

On the GBA we're using an assembly interrupt handler which reads a nullable function pointer to an extern "C" fn(u16) (which is presumably written in Rust). If the assembly reads a non-null value then it has to mode switch, align the stack in system mode, save some registers, do the call, and then undo all that. If a custom handler ABI was allowed for use we could just skip over the register saving and stack alignment steps and have LLVM handle any necessary work in each particular extern "gba-irq" fn(u16) implementation.

@chorman0773
Copy link

chorman0773 commented Apr 11, 2022

I posted this on the tracking issue but it makes sense to cross-post to the RFC.

In 32-bit x86 mode, the StackFrame struct can vary dynamically - depending on the call cs.CPL (which may or may not end up in saveCS[1:0]). Specifically, ss:eSP are only pushed by the CPU when calling a lower privileged gate, and only popped when returning to higher privileged code (gate.selector.RPL<CPL, and CPL<cs.RPL respectively - note that it's impossible jump to a higher privileged gate, nor iret to lower privileged code). IDK how or if llvm (and thusly rustc) takes this into account, but that may be a blocker for x86. Note that this does not affect x86_64 - in long mode ss:RSP are always pushed and iret in 64-bit mode (which is almost always the case) always pops ss:RSP.

sethp added a commit to rustbox/rust that referenced this pull request Aug 9, 2023
Similar to prior support added for the mips430, avr, and x86 targets
this change implements the rough equivalent of clang's
[`__attribute__((interrupt))`][clang-attr] for riscv targets, enabling
e.g.

```rust
static mut CNT: usize = 0;

pub extern "riscv-interrupt-m" fn isr_m() {
    unsafe {
        CNT += 1;
    }
}
```

to produce highly effective assembly like:

```asm
pub extern "riscv-interrupt-m" fn isr_m() {
420003a0:       1141                    addi    sp,sp,-16
    unsafe {
        CNT += 1;
420003a2:       c62a                    sw      a0,12(sp)
420003a4:       c42e                    sw      a1,8(sp)
420003a6:       3fc80537                lui     a0,0x3fc80
420003aa:       63c52583                lw      a1,1596(a0) # 3fc8063c <_ZN12esp_riscv_rt3CNT17hcec3e3a214887d53E.0>
420003ae:       0585                    addi    a1,a1,1
420003b0:       62b52e23                sw      a1,1596(a0)
    }
}
420003b4:       4532                    lw      a0,12(sp)
420003b6:       45a2                    lw      a1,8(sp)
420003b8:       0141                    addi    sp,sp,16
420003ba:       30200073                mret
```

(disassembly via `riscv64-unknown-elf-objdump -C -S --disassemble ./esp32c3-hal/target/riscv32imc-unknown-none-elf/release/examples/gpio_interrupt`)

This outcome is superior to hand-coded interrupt routines which, lacking
visibility into any non-assembly body of the interrupt handler, have to
be very conservative and save the [entire CPU state to the stack
frame][full-frame-save]. By instead asking LLVM to only save the
registers that it uses, we defer the decision to the tool with the best
context: it can more accurately account for the cost of spills if it
knows that every additional register used is already at the cost of an
implicit spill.

At the LLVM level, this is apparently [implemented by] marking every
register as "[callee-save]," matching the semantics of an interrupt
handler nicely (it has to leave the CPU state just as it found it after
its `{m|s}ret`).

This approach is not suitable for every interrupt handler, as it makes
no attempt to e.g. save the state in a user-accessible stack frame. For
a full discussion of those challenges and tradeoffs, please refer to
[the interrupt calling conventions RFC][rfc].

Inside rustc, this implementation differs from prior art because LLVM
does not expose the "all-saved" function flavor as a calling convention
directly, instead preferring to use an attribute that allows for
differentiating between "machine-mode" and "superivsor-mode" interrupts.

Finally, some effort has been made to guide those who may not yet be
aware of the differences between machine-mode and supervisor-mode
interrupts as to why no `riscv-interrupt` calling convention is exposed
through rustc, and similarly for why `riscv-interrupt-u` makes no
appearance (as it would complicate future LLVM upgrades).

[clang-attr]: https://clang.llvm.org/docs/AttributeReference.html#interrupt-risc-v
[full-frame-save]: https://github.com/esp-rs/esp-riscv-rt/blob/9281af2ecffe13e40992917316f36920c26acaf3/src/lib.rs#L440-L469
[implemented by]: https://github.com/llvm/llvm-project/blob/b7fb2a3fec7c187d58a6d338ab512d9173bca987/llvm/lib/Target/RISCV/RISCVRegisterInfo.cpp#L61-L67
[callee-save]: https://github.com/llvm/llvm-project/blob/973f1fe7a8591c7af148e573491ab68cc15b6ecf/llvm/lib/Target/RISCV/RISCVCallingConv.td#L30-L37
[rfc]: rust-lang/rfcs#3246
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Aug 9, 2023
…ckh726

feat: `riscv-interrupt-{m,s}` calling conventions

Similar to prior support added for the mips430, avr, and x86 targets this change implements the rough equivalent of clang's [`__attribute__((interrupt))`][clang-attr] for riscv targets, enabling e.g.

```rust
static mut CNT: usize = 0;

pub extern "riscv-interrupt-m" fn isr_m() {
    unsafe {
        CNT += 1;
    }
}
```

to produce highly effective assembly like:

```asm
pub extern "riscv-interrupt-m" fn isr_m() {
420003a0:       1141                    addi    sp,sp,-16
    unsafe {
        CNT += 1;
420003a2:       c62a                    sw      a0,12(sp)
420003a4:       c42e                    sw      a1,8(sp)
420003a6:       3fc80537                lui     a0,0x3fc80
420003aa:       63c52583                lw      a1,1596(a0) # 3fc8063c <_ZN12esp_riscv_rt3CNT17hcec3e3a214887d53E.0>
420003ae:       0585                    addi    a1,a1,1
420003b0:       62b52e23                sw      a1,1596(a0)
    }
}
420003b4:       4532                    lw      a0,12(sp)
420003b6:       45a2                    lw      a1,8(sp)
420003b8:       0141                    addi    sp,sp,16
420003ba:       30200073                mret
```

(disassembly via `riscv64-unknown-elf-objdump -C -S --disassemble ./esp32c3-hal/target/riscv32imc-unknown-none-elf/release/examples/gpio_interrupt`)

This outcome is superior to hand-coded interrupt routines which, lacking visibility into any non-assembly body of the interrupt handler, have to be very conservative and save the [entire CPU state to the stack frame][full-frame-save]. By instead asking LLVM to only save the registers that it uses, we defer the decision to the tool with the best context: it can more accurately account for the cost of spills if it knows that every additional register used is already at the cost of an implicit spill.

At the LLVM level, this is apparently [implemented by] marking every register as "[callee-save]," matching the semantics of an interrupt handler nicely (it has to leave the CPU state just as it found it after its `{m|s}ret`).

This approach is not suitable for every interrupt handler, as it makes no attempt to e.g. save the state in a user-accessible stack frame. For a full discussion of those challenges and tradeoffs, please refer to [the interrupt calling conventions RFC][rfc].

Inside rustc, this implementation differs from prior art because LLVM does not expose the "all-saved" function flavor as a calling convention directly, instead preferring to use an attribute that allows for differentiating between "machine-mode" and "superivsor-mode" interrupts.

Finally, some effort has been made to guide those who may not yet be aware of the differences between machine-mode and supervisor-mode interrupts as to why no `riscv-interrupt` calling convention is exposed through rustc, and similarly for why `riscv-interrupt-u` makes no appearance (as it would complicate future LLVM upgrades).

[clang-attr]: https://clang.llvm.org/docs/AttributeReference.html#interrupt-risc-v
[full-frame-save]: https://github.com/esp-rs/esp-riscv-rt/blob/9281af2ecffe13e40992917316f36920c26acaf3/src/lib.rs#L440-L469
[implemented by]: https://github.com/llvm/llvm-project/blob/b7fb2a3fec7c187d58a6d338ab512d9173bca987/llvm/lib/Target/RISCV/RISCVRegisterInfo.cpp#L61-L67
[callee-save]: https://github.com/llvm/llvm-project/blob/973f1fe7a8591c7af148e573491ab68cc15b6ecf/llvm/lib/Target/RISCV/RISCVCallingConv.td#L30-L37
[rfc]: rust-lang/rfcs#3246
lnicola pushed a commit to lnicola/rust-analyzer that referenced this pull request Aug 21, 2023
Similar to prior support added for the mips430, avr, and x86 targets
this change implements the rough equivalent of clang's
[`__attribute__((interrupt))`][clang-attr] for riscv targets, enabling
e.g.

```rust
static mut CNT: usize = 0;

pub extern "riscv-interrupt-m" fn isr_m() {
    unsafe {
        CNT += 1;
    }
}
```

to produce highly effective assembly like:

```asm
pub extern "riscv-interrupt-m" fn isr_m() {
420003a0:       1141                    addi    sp,sp,-16
    unsafe {
        CNT += 1;
420003a2:       c62a                    sw      a0,12(sp)
420003a4:       c42e                    sw      a1,8(sp)
420003a6:       3fc80537                lui     a0,0x3fc80
420003aa:       63c52583                lw      a1,1596(a0) # 3fc8063c <_ZN12esp_riscv_rt3CNT17hcec3e3a214887d53E.0>
420003ae:       0585                    addi    a1,a1,1
420003b0:       62b52e23                sw      a1,1596(a0)
    }
}
420003b4:       4532                    lw      a0,12(sp)
420003b6:       45a2                    lw      a1,8(sp)
420003b8:       0141                    addi    sp,sp,16
420003ba:       30200073                mret
```

(disassembly via `riscv64-unknown-elf-objdump -C -S --disassemble ./esp32c3-hal/target/riscv32imc-unknown-none-elf/release/examples/gpio_interrupt`)

This outcome is superior to hand-coded interrupt routines which, lacking
visibility into any non-assembly body of the interrupt handler, have to
be very conservative and save the [entire CPU state to the stack
frame][full-frame-save]. By instead asking LLVM to only save the
registers that it uses, we defer the decision to the tool with the best
context: it can more accurately account for the cost of spills if it
knows that every additional register used is already at the cost of an
implicit spill.

At the LLVM level, this is apparently [implemented by] marking every
register as "[callee-save]," matching the semantics of an interrupt
handler nicely (it has to leave the CPU state just as it found it after
its `{m|s}ret`).

This approach is not suitable for every interrupt handler, as it makes
no attempt to e.g. save the state in a user-accessible stack frame. For
a full discussion of those challenges and tradeoffs, please refer to
[the interrupt calling conventions RFC][rfc].

Inside rustc, this implementation differs from prior art because LLVM
does not expose the "all-saved" function flavor as a calling convention
directly, instead preferring to use an attribute that allows for
differentiating between "machine-mode" and "superivsor-mode" interrupts.

Finally, some effort has been made to guide those who may not yet be
aware of the differences between machine-mode and supervisor-mode
interrupts as to why no `riscv-interrupt` calling convention is exposed
through rustc, and similarly for why `riscv-interrupt-u` makes no
appearance (as it would complicate future LLVM upgrades).

[clang-attr]: https://clang.llvm.org/docs/AttributeReference.html#interrupt-risc-v
[full-frame-save]: https://github.com/esp-rs/esp-riscv-rt/blob/9281af2ecffe13e40992917316f36920c26acaf3/src/lib.rs#L440-L469
[implemented by]: https://github.com/llvm/llvm-project/blob/b7fb2a3fec7c187d58a6d338ab512d9173bca987/llvm/lib/Target/RISCV/RISCVRegisterInfo.cpp#L61-L67
[callee-save]: https://github.com/llvm/llvm-project/blob/973f1fe7a8591c7af148e573491ab68cc15b6ecf/llvm/lib/Target/RISCV/RISCVCallingConv.td#L30-L37
[rfc]: rust-lang/rfcs#3246
bjorn3 pushed a commit to rust-lang/rustc_codegen_cranelift that referenced this pull request Sep 1, 2023
Similar to prior support added for the mips430, avr, and x86 targets
this change implements the rough equivalent of clang's
[`__attribute__((interrupt))`][clang-attr] for riscv targets, enabling
e.g.

```rust
static mut CNT: usize = 0;

pub extern "riscv-interrupt-m" fn isr_m() {
    unsafe {
        CNT += 1;
    }
}
```

to produce highly effective assembly like:

```asm
pub extern "riscv-interrupt-m" fn isr_m() {
420003a0:       1141                    addi    sp,sp,-16
    unsafe {
        CNT += 1;
420003a2:       c62a                    sw      a0,12(sp)
420003a4:       c42e                    sw      a1,8(sp)
420003a6:       3fc80537                lui     a0,0x3fc80
420003aa:       63c52583                lw      a1,1596(a0) # 3fc8063c <_ZN12esp_riscv_rt3CNT17hcec3e3a214887d53E.0>
420003ae:       0585                    addi    a1,a1,1
420003b0:       62b52e23                sw      a1,1596(a0)
    }
}
420003b4:       4532                    lw      a0,12(sp)
420003b6:       45a2                    lw      a1,8(sp)
420003b8:       0141                    addi    sp,sp,16
420003ba:       30200073                mret
```

(disassembly via `riscv64-unknown-elf-objdump -C -S --disassemble ./esp32c3-hal/target/riscv32imc-unknown-none-elf/release/examples/gpio_interrupt`)

This outcome is superior to hand-coded interrupt routines which, lacking
visibility into any non-assembly body of the interrupt handler, have to
be very conservative and save the [entire CPU state to the stack
frame][full-frame-save]. By instead asking LLVM to only save the
registers that it uses, we defer the decision to the tool with the best
context: it can more accurately account for the cost of spills if it
knows that every additional register used is already at the cost of an
implicit spill.

At the LLVM level, this is apparently [implemented by] marking every
register as "[callee-save]," matching the semantics of an interrupt
handler nicely (it has to leave the CPU state just as it found it after
its `{m|s}ret`).

This approach is not suitable for every interrupt handler, as it makes
no attempt to e.g. save the state in a user-accessible stack frame. For
a full discussion of those challenges and tradeoffs, please refer to
[the interrupt calling conventions RFC][rfc].

Inside rustc, this implementation differs from prior art because LLVM
does not expose the "all-saved" function flavor as a calling convention
directly, instead preferring to use an attribute that allows for
differentiating between "machine-mode" and "superivsor-mode" interrupts.

Finally, some effort has been made to guide those who may not yet be
aware of the differences between machine-mode and supervisor-mode
interrupts as to why no `riscv-interrupt` calling convention is exposed
through rustc, and similarly for why `riscv-interrupt-u` makes no
appearance (as it would complicate future LLVM upgrades).

[clang-attr]: https://clang.llvm.org/docs/AttributeReference.html#interrupt-risc-v
[full-frame-save]: https://github.com/esp-rs/esp-riscv-rt/blob/9281af2ecffe13e40992917316f36920c26acaf3/src/lib.rs#L440-L469
[implemented by]: https://github.com/llvm/llvm-project/blob/b7fb2a3fec7c187d58a6d338ab512d9173bca987/llvm/lib/Target/RISCV/RISCVRegisterInfo.cpp#L61-L67
[callee-save]: https://github.com/llvm/llvm-project/blob/973f1fe7a8591c7af148e573491ab68cc15b6ecf/llvm/lib/Target/RISCV/RISCVCallingConv.td#L30-L37
[rfc]: rust-lang/rfcs#3246
@nyabinary
Copy link

What's the status of this RFC?

@natevw
Copy link

natevw commented Sep 26, 2023

Should the RISC-V interrupt (rust-lang/rust#111889) be added to the final paragraph of the Motivations section as well?

lnicola pushed a commit to lnicola/rust-analyzer that referenced this pull request Apr 7, 2024
Similar to prior support added for the mips430, avr, and x86 targets
this change implements the rough equivalent of clang's
[`__attribute__((interrupt))`][clang-attr] for riscv targets, enabling
e.g.

```rust
static mut CNT: usize = 0;

pub extern "riscv-interrupt-m" fn isr_m() {
    unsafe {
        CNT += 1;
    }
}
```

to produce highly effective assembly like:

```asm
pub extern "riscv-interrupt-m" fn isr_m() {
420003a0:       1141                    addi    sp,sp,-16
    unsafe {
        CNT += 1;
420003a2:       c62a                    sw      a0,12(sp)
420003a4:       c42e                    sw      a1,8(sp)
420003a6:       3fc80537                lui     a0,0x3fc80
420003aa:       63c52583                lw      a1,1596(a0) # 3fc8063c <_ZN12esp_riscv_rt3CNT17hcec3e3a214887d53E.0>
420003ae:       0585                    addi    a1,a1,1
420003b0:       62b52e23                sw      a1,1596(a0)
    }
}
420003b4:       4532                    lw      a0,12(sp)
420003b6:       45a2                    lw      a1,8(sp)
420003b8:       0141                    addi    sp,sp,16
420003ba:       30200073                mret
```

(disassembly via `riscv64-unknown-elf-objdump -C -S --disassemble ./esp32c3-hal/target/riscv32imc-unknown-none-elf/release/examples/gpio_interrupt`)

This outcome is superior to hand-coded interrupt routines which, lacking
visibility into any non-assembly body of the interrupt handler, have to
be very conservative and save the [entire CPU state to the stack
frame][full-frame-save]. By instead asking LLVM to only save the
registers that it uses, we defer the decision to the tool with the best
context: it can more accurately account for the cost of spills if it
knows that every additional register used is already at the cost of an
implicit spill.

At the LLVM level, this is apparently [implemented by] marking every
register as "[callee-save]," matching the semantics of an interrupt
handler nicely (it has to leave the CPU state just as it found it after
its `{m|s}ret`).

This approach is not suitable for every interrupt handler, as it makes
no attempt to e.g. save the state in a user-accessible stack frame. For
a full discussion of those challenges and tradeoffs, please refer to
[the interrupt calling conventions RFC][rfc].

Inside rustc, this implementation differs from prior art because LLVM
does not expose the "all-saved" function flavor as a calling convention
directly, instead preferring to use an attribute that allows for
differentiating between "machine-mode" and "superivsor-mode" interrupts.

Finally, some effort has been made to guide those who may not yet be
aware of the differences between machine-mode and supervisor-mode
interrupts as to why no `riscv-interrupt` calling convention is exposed
through rustc, and similarly for why `riscv-interrupt-u` makes no
appearance (as it would complicate future LLVM upgrades).

[clang-attr]: https://clang.llvm.org/docs/AttributeReference.html#interrupt-risc-v
[full-frame-save]: https://github.com/esp-rs/esp-riscv-rt/blob/9281af2ecffe13e40992917316f36920c26acaf3/src/lib.rs#L440-L469
[implemented by]: https://github.com/llvm/llvm-project/blob/b7fb2a3fec7c187d58a6d338ab512d9173bca987/llvm/lib/Target/RISCV/RISCVRegisterInfo.cpp#L61-L67
[callee-save]: https://github.com/llvm/llvm-project/blob/973f1fe7a8591c7af148e573491ab68cc15b6ecf/llvm/lib/Target/RISCV/RISCVCallingConv.td#L30-L37
[rfc]: rust-lang/rfcs#3246
lnicola pushed a commit to lnicola/rust-analyzer that referenced this pull request Apr 7, 2024
feat: `riscv-interrupt-{m,s}` calling conventions

Similar to prior support added for the mips430, avr, and x86 targets this change implements the rough equivalent of clang's [`__attribute__((interrupt))`][clang-attr] for riscv targets, enabling e.g.

```rust
static mut CNT: usize = 0;

pub extern "riscv-interrupt-m" fn isr_m() {
    unsafe {
        CNT += 1;
    }
}
```

to produce highly effective assembly like:

```asm
pub extern "riscv-interrupt-m" fn isr_m() {
420003a0:       1141                    addi    sp,sp,-16
    unsafe {
        CNT += 1;
420003a2:       c62a                    sw      a0,12(sp)
420003a4:       c42e                    sw      a1,8(sp)
420003a6:       3fc80537                lui     a0,0x3fc80
420003aa:       63c52583                lw      a1,1596(a0) # 3fc8063c <_ZN12esp_riscv_rt3CNT17hcec3e3a214887d53E.0>
420003ae:       0585                    addi    a1,a1,1
420003b0:       62b52e23                sw      a1,1596(a0)
    }
}
420003b4:       4532                    lw      a0,12(sp)
420003b6:       45a2                    lw      a1,8(sp)
420003b8:       0141                    addi    sp,sp,16
420003ba:       30200073                mret
```

(disassembly via `riscv64-unknown-elf-objdump -C -S --disassemble ./esp32c3-hal/target/riscv32imc-unknown-none-elf/release/examples/gpio_interrupt`)

This outcome is superior to hand-coded interrupt routines which, lacking visibility into any non-assembly body of the interrupt handler, have to be very conservative and save the [entire CPU state to the stack frame][full-frame-save]. By instead asking LLVM to only save the registers that it uses, we defer the decision to the tool with the best context: it can more accurately account for the cost of spills if it knows that every additional register used is already at the cost of an implicit spill.

At the LLVM level, this is apparently [implemented by] marking every register as "[callee-save]," matching the semantics of an interrupt handler nicely (it has to leave the CPU state just as it found it after its `{m|s}ret`).

This approach is not suitable for every interrupt handler, as it makes no attempt to e.g. save the state in a user-accessible stack frame. For a full discussion of those challenges and tradeoffs, please refer to [the interrupt calling conventions RFC][rfc].

Inside rustc, this implementation differs from prior art because LLVM does not expose the "all-saved" function flavor as a calling convention directly, instead preferring to use an attribute that allows for differentiating between "machine-mode" and "superivsor-mode" interrupts.

Finally, some effort has been made to guide those who may not yet be aware of the differences between machine-mode and supervisor-mode interrupts as to why no `riscv-interrupt` calling convention is exposed through rustc, and similarly for why `riscv-interrupt-u` makes no appearance (as it would complicate future LLVM upgrades).

[clang-attr]: https://clang.llvm.org/docs/AttributeReference.html#interrupt-risc-v
[full-frame-save]: https://github.com/esp-rs/esp-riscv-rt/blob/9281af2ecffe13e40992917316f36920c26acaf3/src/lib.rs#L440-L469
[implemented by]: https://github.com/llvm/llvm-project/blob/b7fb2a3fec7c187d58a6d338ab512d9173bca987/llvm/lib/Target/RISCV/RISCVRegisterInfo.cpp#L61-L67
[callee-save]: https://github.com/llvm/llvm-project/blob/973f1fe7a8591c7af148e573491ab68cc15b6ecf/llvm/lib/Target/RISCV/RISCVCallingConv.td#L30-L37
[rfc]: rust-lang/rfcs#3246
RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this pull request Apr 27, 2024
Similar to prior support added for the mips430, avr, and x86 targets
this change implements the rough equivalent of clang's
[`__attribute__((interrupt))`][clang-attr] for riscv targets, enabling
e.g.

```rust
static mut CNT: usize = 0;

pub extern "riscv-interrupt-m" fn isr_m() {
    unsafe {
        CNT += 1;
    }
}
```

to produce highly effective assembly like:

```asm
pub extern "riscv-interrupt-m" fn isr_m() {
420003a0:       1141                    addi    sp,sp,-16
    unsafe {
        CNT += 1;
420003a2:       c62a                    sw      a0,12(sp)
420003a4:       c42e                    sw      a1,8(sp)
420003a6:       3fc80537                lui     a0,0x3fc80
420003aa:       63c52583                lw      a1,1596(a0) # 3fc8063c <_ZN12esp_riscv_rt3CNT17hcec3e3a214887d53E.0>
420003ae:       0585                    addi    a1,a1,1
420003b0:       62b52e23                sw      a1,1596(a0)
    }
}
420003b4:       4532                    lw      a0,12(sp)
420003b6:       45a2                    lw      a1,8(sp)
420003b8:       0141                    addi    sp,sp,16
420003ba:       30200073                mret
```

(disassembly via `riscv64-unknown-elf-objdump -C -S --disassemble ./esp32c3-hal/target/riscv32imc-unknown-none-elf/release/examples/gpio_interrupt`)

This outcome is superior to hand-coded interrupt routines which, lacking
visibility into any non-assembly body of the interrupt handler, have to
be very conservative and save the [entire CPU state to the stack
frame][full-frame-save]. By instead asking LLVM to only save the
registers that it uses, we defer the decision to the tool with the best
context: it can more accurately account for the cost of spills if it
knows that every additional register used is already at the cost of an
implicit spill.

At the LLVM level, this is apparently [implemented by] marking every
register as "[callee-save]," matching the semantics of an interrupt
handler nicely (it has to leave the CPU state just as it found it after
its `{m|s}ret`).

This approach is not suitable for every interrupt handler, as it makes
no attempt to e.g. save the state in a user-accessible stack frame. For
a full discussion of those challenges and tradeoffs, please refer to
[the interrupt calling conventions RFC][rfc].

Inside rustc, this implementation differs from prior art because LLVM
does not expose the "all-saved" function flavor as a calling convention
directly, instead preferring to use an attribute that allows for
differentiating between "machine-mode" and "superivsor-mode" interrupts.

Finally, some effort has been made to guide those who may not yet be
aware of the differences between machine-mode and supervisor-mode
interrupts as to why no `riscv-interrupt` calling convention is exposed
through rustc, and similarly for why `riscv-interrupt-u` makes no
appearance (as it would complicate future LLVM upgrades).

[clang-attr]: https://clang.llvm.org/docs/AttributeReference.html#interrupt-risc-v
[full-frame-save]: https://github.com/esp-rs/esp-riscv-rt/blob/9281af2ecffe13e40992917316f36920c26acaf3/src/lib.rs#L440-L469
[implemented by]: https://github.com/llvm/llvm-project/blob/b7fb2a3fec7c187d58a6d338ab512d9173bca987/llvm/lib/Target/RISCV/RISCVRegisterInfo.cpp#L61-L67
[callee-save]: https://github.com/llvm/llvm-project/blob/973f1fe7a8591c7af148e573491ab68cc15b6ecf/llvm/lib/Target/RISCV/RISCVCallingConv.td#L30-L37
[rfc]: rust-lang/rfcs#3246
RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this pull request Apr 27, 2024
feat: `riscv-interrupt-{m,s}` calling conventions

Similar to prior support added for the mips430, avr, and x86 targets this change implements the rough equivalent of clang's [`__attribute__((interrupt))`][clang-attr] for riscv targets, enabling e.g.

```rust
static mut CNT: usize = 0;

pub extern "riscv-interrupt-m" fn isr_m() {
    unsafe {
        CNT += 1;
    }
}
```

to produce highly effective assembly like:

```asm
pub extern "riscv-interrupt-m" fn isr_m() {
420003a0:       1141                    addi    sp,sp,-16
    unsafe {
        CNT += 1;
420003a2:       c62a                    sw      a0,12(sp)
420003a4:       c42e                    sw      a1,8(sp)
420003a6:       3fc80537                lui     a0,0x3fc80
420003aa:       63c52583                lw      a1,1596(a0) # 3fc8063c <_ZN12esp_riscv_rt3CNT17hcec3e3a214887d53E.0>
420003ae:       0585                    addi    a1,a1,1
420003b0:       62b52e23                sw      a1,1596(a0)
    }
}
420003b4:       4532                    lw      a0,12(sp)
420003b6:       45a2                    lw      a1,8(sp)
420003b8:       0141                    addi    sp,sp,16
420003ba:       30200073                mret
```

(disassembly via `riscv64-unknown-elf-objdump -C -S --disassemble ./esp32c3-hal/target/riscv32imc-unknown-none-elf/release/examples/gpio_interrupt`)

This outcome is superior to hand-coded interrupt routines which, lacking visibility into any non-assembly body of the interrupt handler, have to be very conservative and save the [entire CPU state to the stack frame][full-frame-save]. By instead asking LLVM to only save the registers that it uses, we defer the decision to the tool with the best context: it can more accurately account for the cost of spills if it knows that every additional register used is already at the cost of an implicit spill.

At the LLVM level, this is apparently [implemented by] marking every register as "[callee-save]," matching the semantics of an interrupt handler nicely (it has to leave the CPU state just as it found it after its `{m|s}ret`).

This approach is not suitable for every interrupt handler, as it makes no attempt to e.g. save the state in a user-accessible stack frame. For a full discussion of those challenges and tradeoffs, please refer to [the interrupt calling conventions RFC][rfc].

Inside rustc, this implementation differs from prior art because LLVM does not expose the "all-saved" function flavor as a calling convention directly, instead preferring to use an attribute that allows for differentiating between "machine-mode" and "superivsor-mode" interrupts.

Finally, some effort has been made to guide those who may not yet be aware of the differences between machine-mode and supervisor-mode interrupts as to why no `riscv-interrupt` calling convention is exposed through rustc, and similarly for why `riscv-interrupt-u` makes no appearance (as it would complicate future LLVM upgrades).

[clang-attr]: https://clang.llvm.org/docs/AttributeReference.html#interrupt-risc-v
[full-frame-save]: https://github.com/esp-rs/esp-riscv-rt/blob/9281af2ecffe13e40992917316f36920c26acaf3/src/lib.rs#L440-L469
[implemented by]: https://github.com/llvm/llvm-project/blob/b7fb2a3fec7c187d58a6d338ab512d9173bca987/llvm/lib/Target/RISCV/RISCVRegisterInfo.cpp#L61-L67
[callee-save]: https://github.com/llvm/llvm-project/blob/973f1fe7a8591c7af148e573491ab68cc15b6ecf/llvm/lib/Target/RISCV/RISCVCallingConv.td#L30-L37
[rfc]: rust-lang/rfcs#3246
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants