rust-embedded / rust-embedded/cortex-m

[RFC] Change the cortex-m-rt attribute syntax

Open
#407 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
1k
Forks
202
Avg merge
6d 2h
Merged PRs (30d)
2

Description

Summary

Change the syntax of interrupt and exception handlers provided by cortex-m-rt to be more obvious,
more internally consistent, and closer to RTFM's:

  • Resource Syntax: Resources are declared as function arguments of type &mut T, with the initial value provided via an #[init(<expr>)] attribute.
  • Handler Names: Handler macros are now invoked with the exception name as an argument (eg. #[exception(SVCall)]), instead of using the handler function name as the interrupt/exception to handle.
  • Handler Arguments:
    • When declaring a DefaultHandler, the IRQ number can be requested by attaching #[irqn] to an i16 function argument. Currently the handler signature is required to take exactly one i16 argument.
    • Exception handlers other than DefaultHandler can access the exception frame by attaching #[exception_frame] to an argument of type &cortex_m_rt::ExceptionFrame.

Motivation

Resource Syntax

The biggest motivation for changing the syntax used by cortex-m-rt is to improve the way handler resources work.

Currently, handlers defined via cortex-m-rt's #[interrupt], #[exception] and #[entry] macros
can declare resources owned by them by declaring a list of static mut items as their first
statements. The procedural macros then transform them to function arguments, and their types are
changed from T to &mut T. This comes with a few drawbacks:

  • It silently modifies Rust language semantics, which is surprising and can make communication about the syntax more difficult.
  • It does not work consistently: Resources must be the first N statements inside the handler. Other static mut items in the function are just regular static muts. While this is potentially fixable, it still leaves static mut meaning something very different in handler functions than in any other function.
  • Its implementation requires modifying the function signature, making the function not callable from other Rust code as expected.

Here is an example that shows the shortcomings of the current syntax:

#[exception]
fn SVCall() {
    static mut RESOURCE: u8 = 123;

    // Type of `RESOURCE` is `&mut u8` here

    ();

    static mut RESOURCE2: u8 = 42;

    // Type of `RESOURCE2` is `u8` (it's a normal `static mut` item, so any access is `unsafe`)
}

// `SVCall` cannot be called by normal Rust code, as it was transformed (and renamed) by the macro:
fn somewhere_else() {
    SVCall();
}
// error[E0425]: cannot find function, tuple struct or tuple variant `SVCall` in this scope
//   --> examples/minimal.rs:16:5
//    |
// 16 |     SVCall();
//    |     ^^^^^^ not found in this scope

This RFC proposes a new way of declaring resources below, which should address all of these issues.

Handler Names

Another minor issue with the current implementation is that the name of the handler function determines the exception or interrupt that is handled:

#[interrupt]
fn USART1() {
    // ...
}

This forces the user to write an unidiomatic function name containing upper-case letters, and which
might not describe the purpose of the handler very clearly.

For example, an MCU might have 3 USARTs with dedicated interrupts, each used to handle a different external peripheral. In such an application it would be clearer to call those handlers gps_rx, modem_rx, and user_rx instead of USART0, USART1 and USART2.

Handler Arguments

Finally, two exception handlers, DefaultHandler and HardFault, are special in that they take an
argument of type i16 and &ExceptionFrame, respectively. The &ExceptionFrame in particular is
provided by an assembly shim that cannot be disabled.

This RFC proposes a way to add the &ExceptionFrame (actually, &mut ExceptionFrame) to any
exception handler, and makes it optional for HardFault. The i16 exception number passed to
DefaultHandler is also made optional.

How handlers are expanded now

Note how the code for declaring an SVCall exception handler is currently transformed by the
#[exception] attribute:

#[exception]
fn SVCall() {
    static mut RESOURCE: u8 = 123;

    ();

    static mut RESOURCE2: u8 = 42;
}

This code becomes:

#[export_name = "SVCall"]
pub unsafe extern "C" fn __cortex_m_rt_SVCall_trampoline() {
   __cortex_m_rt_SVCall({
       static mut RESOURCE: u8 = 123;
       &mut RESOURCE
   })
}

fn __cortex_m_rt_SVCall(RESOURCE: &mut u8) {
    ();

    static mut RESOURCE2: u8 = 42;
}

The proposed syntax will embrace this transformation instead of hiding it.

Detailed design

To start off, this is how the above code would be written with the syntax proposed by this RFC:

#[exception(SVCall)]
fn svc_handler(
    #[init(123)]
    resource: &mut u8,
) {
    ();

    static mut RESOURCE2: u8 = 42;
}

It expands to the following code:

#[export_name = "SVCall"]
pub unsafe extern "C" fn __cortex_m_rt_SVCall_trampoline() {
    svc_handler({
        static mut RESOURCE: u8 = 123;
        &mut RESOURCE
    })
}

fn svc_handler(
    resource: &mut u8,
) {
    ();

    static mut RESOURCE2: u8 = 42;
}

Notably, the user-defined function stays as-is and is not transformed at all. The type of the
resource doesn't get silently changed, and the name of the function can be freely chosen.

In addition to #[init], resource arguments also accept the following built-in Rust attributes:
#[export_name], #[link_section], #[no_mangle], #[used]. When these are encountered, they
will be applied to the generated static item in the trampoline, allowing fine-grained control
over the allocated memory.

Like now, resources declared on the #[entry] handler can be given 'static lifetime, while other
handlers are restricted to elided lifetimes to ensure soundness. This check now has to be done in
the macro implementation by inspecting the parameter type. Named lifetime parameters are not
permitted in the parameter type. So a parameter of type &'a mut T would be rejected, while
&mut Type<'static> would be allowed.

Exception Frame access

Currently, the HardFault handler is required to take an immutable &ExceptionFrame. The proposed syntax makes that optional and opt-in.

To obtain an &ExceptionFrame with the new syntax, the #[exception_frame] attribute has to be
placed on the handler argument.

Another possible improvement to the &ExceptionFrame API is described in issue #234. While it is
also a breaking change, it is orthogonal to the syntax changes proposed in this RFC.

A more complicated example

#[exception(DefaultHandler)]
fn default(
    #[init(false)]
    flag: &mut bool,

    #[irqn]
    irq: i16,
) {
    *flag = true;
    hprintln!("Unhandled IRQ #{}", irq).ok();
}

#[exception(HardFault)]
fn fault(
    #[exception_frame]
    frame: &ExceptionFrame,

    #[init(0)]
    fault_count: &mut u32,
) {
    *fault_count += 1;
    hprintln!("HardFault: {:?}", frame).ok();
}

This will get expanded to:

#[export_name = "DefaultHandler"]
pub unsafe extern "C" fn __cortex_m_rt_DefaultHandler_trampoline() {
    // ... fetch IRQN like it does now ...
    let irqn = ...;

    default({
        static mut FLAG: bool = false;
        &mut FLAG
    }, {
        irqn
    })
}

fn default(
    flag: &mut bool,
    irq: i16,
) {
    *flag = true;
    hprintln!("Unhandled IRQ #{}", irq).ok();
}

#[export_name = "HardFault"]
pub unsafe extern "C" fn __cortex_m_rt_HardFault_trampoline(ef: &ExceptionFrame) {
    // The `ef` argument is provided by the ASM trampoline, like before.

    svc_handler({
        ef
    }, {
        static mut FAULT_COUNT: u32 = 0;
        &mut FAULT_COUNT
    })
}

fn fault(
    frame: &ExceptionFrame,
    fault_count: &mut u32,
) {
    *fault_count += 1;
    hprintln!("HardFault: {:?}", frame).ok();
}

How We Teach This

Most of the API documentation in cortex-m-rt will have to be rewritten to reflect these changes.

The Book also makes use of cortex-m-rt, mostly in the Exceptions and Interrupts chapters.
These will likewise be updated to reflect the changes.

The cortex-m-quickstart repository will be
updated to the new version of cortex-m-rt once this change has been released. Other repositories
and HALs will follow.

With the proposed changes, cortex-m-rt should generally become easier to teach, since it moves
things closer to RTFM and normal Rust.

Drawbacks

This is a breaking change. The proposed syntax is completely incompatible to the current syntax. Any users of cortex-m-rt will have to migrate.

Alternatives

  • Do nothing and keep the old syntax.
  • Only do a subset of the changes proposed here (eg. leave out the #[exception(<name>)] change and continue using the function name to determine the exception).
  • Adopt an entirely different syntax

Unresolved questions

As is tradition with syntactic changes, a large amount of bikeshedding is expected.

The syntax of the #[exception] attribute could be adjusted to match RTFM's more closely:
#[exception(binds = ExceptionName)] instead of #[exception(ExceptionName)]. This syntax would also be more extensible: New arguments beside binds could be added easily without changing the rest of the syntax.

We could completely remove the support for the irqn argument for DefaultHandler. The number of the currently handled IRQ can be fetched at any time by using SCB::vect_active() (though this API could use some improvement).

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the RFC's Detailed design and the existing #[entry], #[interrupt], and #[exception] entry points. Review the Exceptions and Interrupts chapters in the Book; done means the syntax decision is settled, handlers and resources work as specified, and the documented examples are updated.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
embedded-iot
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.