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
Open
211 changes: 211 additions & 0 deletions text/0000-interrupt-calling-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Interrupt Calling Conventions

- Feature Name: `interrupt_calling_conventions`
- Start Date: 2022-02-19
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

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.

# Background

This section gives some introduction to calling conventions in general, summarizes the current support of alternative calling conventions in Rust, and explains why interrupt handlers require special calling conventions.

## Calling Conventions
Calling conventions define how function calls are performed, including:

- how function arguments are passed, e.g. in specific CPU registers, on the stack, as a pointer, etc.
- how the function returns its result
- which registers must be preserved by the function
- setup and clean-up of the stack frame, e.g. whether the caller or callee restores the stack to it's previous state again

Calling conventions are a large part of a function's ABI ([application binary interface](https://en.wikipedia.org/wiki/Application_binary_interface)), so the terms are sometimes used interchangeably.

## Current Support
By default, Rust uses an internal `"Rust"` calling convention, which is not standardized and might change in the future. For interoperating with external code, Rust allows to set the calling convention of a function explicitly through an `extern "calling_conv" fn foo() {}` function qualifier. The calling convention of external functions can be specified through `extern "calling_conv" { fn bar(); }`.

The most common alternative calling convention supported by Rust is `extern "C"`, which can be used to interface with most code written in C. In addition, Rust supports various [other calling conventions](https://doc.rust-lang.org/stable/reference/items/external-blocks.html#abi), which are required in more specific cases. Most alternative calling conventions are only supported on a single architecture, for example the `"aapcs"` ABI that is only supported on ARM systems.

## Interrupt Handlers
While most functions are invoked by other software, there are some cases where the hardware (or its firmware) invokes a function directly. The most common example are [interrupt handler](https://en.wikipedia.org/wiki/Interrupt_handler) functions defined in embedded systems or operating system kernels. These functions are set up to be called directly by the hardware when a specific interrupt fires (e.g. when a network packet arrives). Interrupt handlers are also called _interrupt service routines_ (ISRs).

Depending on the architecture, a special calling convention is required for such interrupt handler functions. For example, interrupt handlers are often required to restore all registers to their previous state before returning because interrupts happen asynchronously while other code is running. Also, they often receive additional state as input and need to follow a special procedure on return (e.g. use the `iretq` instruction on `x86_64`).

# Motivation
[motivation]: #motivation

Since the hardware platform requires a special calling convention for interrupt handlers, we cannot define the handler functions directly in Rust using any of the currently supported calling conventions. Instead, we need to define a wrapper function in raw assembly that acts as a compatibilty layer between the interrupt calling convention and the calling convention of the Rust function. For example, with an `extern "C"` Rust function using the System V AMD64 ABI, the wrapper would need to do the following steps on `x86_64`:

- Backup all registers on the stack that are not preserved by the C calling convention
- This includes all registers except `RBX`, `RSP`, `RBP`, and `R12`–`R15` (these are restored by `extern "C"` functions)
- This also includes floating point and SSE state, which can be huge (unless we are sure that the interrupt handler does not use them)
- Align the stack on a 16-byte boundary (the C calling convention requies)
- Copy the arguments (passed on the stack) into registers (where the C calling convention expects them)
- Call the Rust function
- Clean up the stack, including the alignment bytes and arguments.
- Restore all registers
- Invoke the `iretq` instruction to return from the interrupt

This approach has lots of issues. For one, assembly code is difficult to write and especially difficult to write _correctly_. Errors can easily lead to silent undefined behavior, for example when mixing up two registers when restoring their values. What makes things worse is that the correctness also depends on the compilation settings. For example, there are multiple variants of the C calling convention for `x86_64`, depending on whether the target system is specified as Windows or Unix-compatible.

The other issue of the above approach is its performance overhead. Interrupt handlers are often invoked with a very high frequency and at a high priority, so they should be as efficient as possible. However, custom assembly code cannot be optimized by Rust or LLVM, so no inlining or copy elision happens. Also, the wrapper function needs to save all registers that the Rust function could _possibly_ use, because it does not know which registers are actually written by the function.

To avoid these issues, this RFC proposes to add support for _interrupt calling conventions_ to the Rust language. This makes it possible to define interrupt handlers directly as Rust functions, without requiring any wrapper functions or custom assembly code.

Rust already supports three different interrupt calling conventions as experimental features: `msp430-interrupt`, `x86-interrupt`, and `avr-interrupt`. They are already widely used in embedded and operating system kernel projects, so this feature also seems to be useful in practice.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

In addition to ABIs for interfacing with external code, Rust also supports so-called _interrupt ABIs_ to define interrupt handler functions that can interface directly with the hardware. These ABIs are only needed for bare-metal applications such as embedded systems or operating system kernels. The ABIs are special because they impose requirements on the whole signature of the function, including arguments and return values.

The following interrupt ABIs are currently supported:

- _(unstable)_ `extern "msp430-interrupt"`: Allows to create interrupt handlers MSP430 microcontrollers. Functions must have the signature `unsafe extern "msp430-interrupt" fn()`. To add a function to the interrupt table, use the following snippet:

```rust
#[no_mangle]
#[link_section = "__interrupt_vector_10"]
pub static TIM0_VECTOR: unsafe extern "msp430-interrupt" fn() = tim0;

unsafe extern "msp430-interrupt" fn tim0() {...}
```
- _(unstable)_ `extern "x86-interrupt"`: This calling convention can be used for definining interrupt handlers on 32-bit and 64-bit `x86` targets. Functions must have one of the following two signatures, depending on the interrupt vector:

```rust
extern "x86-interrupt" fn(stack_frame: &ExceptionStackFrame);
extern "x86-interrupt" fn(stack_frame: &ExceptionStackFrame, error_code: u64);
phil-opp marked this conversation as resolved.
Show resolved Hide resolved
```
The `error_code` argument is _not_ an optional argument. It 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.
- _(unstable)_ `extern "avr-interrupt"` and `extern "avr-non-blocking-interrupt"`

_(The above calling conventions are just listed as an example. They are **not** part of this RFC.)_

By using these ABIs, it is possible to implement interrupt handlers directly in Rust, without writing any custom assembly code. This is not only safer and more convenient, it also often results in better performance. The reason for this is that the compiler can employ (cross-function) optimization techniques this way, for example to only backup the CPU registers that are actually used.

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

The exact requirements and properties of the different interrupt calling conventions must be defined and documented before stabilizing them. However, there are some properties and requirements that apply to all interrupt calling conventions.

## Compiler Checks
Interrupt calling conventions have strict requirements that are checked by the Rust compiler:

- They must not be called by Rust code.
- The function signature must satisfy the requirements.
- They are only available on specific targets and might require specific target settings.
- All other requirements imposed by the implementation of the calling convention in LLVM.

If any of these conditions are violated, the compiler throws an error. It should not be possible to cause LLVM errors using interrupt calling conventions.

## Stability
Since interrupt calling conventions are closely tied to a target architecture, they are only as stable as the corresponding target triple, even if the interrupt calling convention is stabilized. If support for a target triple is removed from Rust, removing support for corresponding interrupt calling conventions is _not_ considered a breaking change.

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.

Functions with interrupt calling conventions are considered normal Rust functions. No `unsafe` annotations are required to declare them and thera are no restrictions on their implementation. However, it is not allowed to call such functions from (Rust) code since the custom prelude and epilogue of the functions could lead to memory safety violations. For this reason, the attempt to call a function defined with an interrupt calling convention must result in an hard error that cannot be circumvented through `unsafe` blocks or by allowing some lints.

The only valid way to invoke a function with an interrupt calling convention is to register them as an interrupt handler directly on the hardware, for example by placing their address in an _interrupt descriptor table_ on `x86_64`. There is no way for the compiler to verify that this operation is correct, so special care needs to be taken by the programmer to ensure that no violation of memory safety can occur.

# Drawbacks
[drawbacks]: #drawbacks

Interrupt calling conventions can be quite complex. So even though they are a very isolated feature, they still **add a considerable amount of complexity to the Rust language**. This added complexity could lead to considerable work for alternative Rust compilers/code generators that don't build on top of LLVM. Examples are [`cranelift`](https://github.com/bytecodealliance/wasmtime/tree/main/cranelift), [`gccrs`](https://github.com/Rust-GCC/gccrs), or [`mrustc`](https://github.com/thepowersgang/mrustc).
phil-opp marked this conversation as resolved.
Show resolved Hide resolved

Most interrupt calling conventions are still unstable/undocumented features of LLVM, so we need to be cautious about stabilizing them in Rust. Stabilizing them too early could lead to maintenance problems and **might make LLVM updates more difficult**, e.g. when some barely maintained calling convention is accidentally broken in the latest LLVM release. There is also the danger that LLVM drops support for an interrupt calling convention at some point. If the calling convention is already stabilized in Rust, we would need to find an alternative way to provide that functionality.

The proposed feature is **only needed for applications in a specific niche**, namely embedded programs and operating system kernel.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

As described in the [_Motivation_](#motivation), the main alternative to interrupt calling conventions are wrapper functions written in assembly, e.g. in a naked function. This reduces the maintenance burden for the Rust compiler, but makes interrupt handlers unconvienent to write, more dangerous, and less performant.

## Alternative: Calling Convention that Preserves all Registers

Many of the advantages of compiler-supported interrupt calling conventions come from the automated register handling, i.e. that all registers are restored to their previous state before returning. We might also be able achieve this using a calling convention that preserves all registers, for example LLVM's [`preserve_all`](https://llvm.org/docs/LangRef.html#calling-conventions) calling convention.

Such a calling convention could be platform independent and should be much easier to maintain. It could also be called normally from Rust code and might thus have use cases outside of interrupt handling, e.g. similar to functions annotated as [`#[cold]`](https://doc.rust-lang.org/reference/attributes/codegen.html#the-cold-attribute).

Using such a calling convention, it should be able to create interrupt handler wrappers in assembly with comparable performance. These wrapper function would handle the platform-specific steps of interrupt handling, such as stack alignment, argument preprocessing, and the interrupt epilogue. Since they don't require language support for this, they don't impact the maintainability of the compiler and can evolve independently in libraries. Using proc macros, they could even provide a similar level of usability to users.

While this approach could be considered a good middle ground, full compiler support for interrupt calling convetions is still be the better solution from a usability and performance perspective.

## Alternative: Implementation in `rustc`

Instead of relying on LLVM (or alternative code generators) to implement the interrupt calling conventions, we could also try to implement support for the calling conventions in `rustc` directly. This way, LLVM upgrades would not be affected by this feature and we would be less dependent on LLVM in general. One possible implementation approach for this could be to build upon a calling convention that preserves all registers (see the previous section).

The drawback of this approach is inreased complexity and maintenance cost for `rustc` this way.

## Alternative: Single `interrupt` ABI that depends on the target

Instead of adding multiple target-specific interrupt calling conventions under different names, we could add support for a single cross-platform `interrupt` calling convention. This calling convention would be an alias for the interrupt calling convention of the target system, e.g. `x86-interrupt` when compiling for an `x86` target.

The main advantage of this approach would be that we keep the list of supported ABI variants short, which might make the documentation clearer. However, there are also several drawbacks:

- Some targets have multiple interrupt calling conventions (e.g. avr and avr-non-blocking). This would be difficult to represent with a single `interrupt` calling convention.
- Interrupt handlers on targets require different function signatures. It would be difficult to abstract this cleanly.
- Interrupt handler implementations are often highly target-specific, so that there is not much value in cross-platform handlers. In fact, it could even lead to bugs when an interrupt handler is accidentally reused on a different platform.

# Prior art
[prior-art]: #prior-art

- some interrupt calling conventions are already implemented (see above)
- old RFC
- naked functions
- interrupt attribute in C
<!--
Discuss prior art, both the good and the bad, in relation to this proposal.
A few examples of what this can include are:

- For language, library, cargo, tools, and compiler proposals: Does this feature exist in other programming languages and what experience have their community had?
- For community proposals: Is this done by some other community and what were their experiences with it?
- For other teams: What lessons can we learn from what other communities have done here?
- Papers: Are there any published papers or great posts that discuss this? If you have some relevant papers to refer to, this can serve as a more detailed theoretical background.

This section is intended to encourage you as an author to think about the lessons from other languages, provide readers of your RFC with a fuller picture.
If there is no prior art, that is fine - your ideas are interesting to us whether they are brand new or if it is an adaptation from other languages.

Note that while precedent set by other languages is some motivation, it does not on its own motivate an RFC.
Please also take into consideration that rust sometimes intentionally diverges from common language features.
-->

# Unresolved questions
[unresolved-questions]: #unresolved-questions

- What are the requirements for stabilizing an interrupt calling convention?

<!--
- What parts of the design do you expect to resolve through the RFC process before this gets merged?
- What parts of the design do you expect to resolve through the implementation of this feature before stabilization?
- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC?
-->

# Future possibilities
[future-possibilities]: #future-possibilities

- support for more platforms

<!--
Think about what the natural extension and evolution of your proposal would
be and how it would affect the language and project as a whole in a holistic
way. Try to use this section as a tool to more fully consider all possible
interactions with the project and language in your proposal.
Also consider how this all fits into the roadmap for the project
and of the relevant sub-team.

This is also a good place to "dump ideas", if they are out of scope for the
RFC you are writing but otherwise related.

If you have tried and cannot think of any future possibilities,
you may simply state that you cannot think of anything.

Note that having something written down in the future-possibilities section
is not a reason to accept the current or a future RFC; such notes should be
in the section on motivation or rationale in this or subsequent RFCs.
The section merely provides additional information.
-->