rtic-rs / rfcs

11 stars 6 forks source link

move/unique resources #30

Closed perlindgren closed 2 years ago

perlindgren commented 4 years ago

EDIT 2020-04-38


Following up on a discussion with @korken89, and the recent comparison done by @therealprof on various ways to share data to interrupt handlers. RTFM came out on top regarding, robustness/reliability (static guarantees), performance and code-size, but argued against due to added complexity.

The added complexity with RTFM can to some extent be explained by the asymmetric resource access pattern (tasks with highest or unique access to a resource gets a owned pointer the resource, while other tasks need to use the lock API).

One way to approach the problem is to follow the lines of #17, with the addition of explicitly moved resources.

use lm3s6965::Interrupt;
use panic_semihosting as _;

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    struct Resources {
        // An early resource
        #[init(0)]
        shared: u32,

        // A local (move), early resource
        #[task_local]
        #[init(1)]
        l1: u32,

        // An exclusive, early resource
        #[lock_free]
        #[init(1)]
        e1: u32,

        // A local (move), late resource
        #[task_local]
        l2: u32,

        // An exclusive, late resource
        #[lock_free]
        e2: u32,
    }

    #[init]
    fn init(_: init::Context) -> init::LateResources {
        rtfm::pend(Interrupt::UART0);
        rtfm::pend(Interrupt::UART1);
        init::LateResources { e2: 2, l2: 2 }
    }

    // `shared` cannot be accessed from this context
    #[idle(resources =[l1, e2])]
    fn idle(cx: idle::Context) -> ! {
        hprintln!("IDLE:l1 = {}", cx.resources.l1).unwrap();
        hprintln!("IDLE:e2 = {}", cx.resources.e2).unwrap();
        debug::exit(debug::EXIT_SUCCESS);
        loop {}
    }

    // `shared` can be accessed from this context
    #[task(priority = 1, binds = UART0, resources = [shared, l2, e1])]
    fn uart0(cx: uart0::Context) {
        let shared: &mut u32 = cx.resources.shared;
        *shared += 1;
        *cx.resources.e1 += 10;
        hprintln!("UART0: shared = {}", shared).unwrap();
        hprintln!("UART0:l2 = {}", cx.resources.l2).unwrap();
        hprintln!("UART0:e1 = {}", cx.resources.e1).unwrap();
    }

    // l2 should be rejected 
    // notice from a memory safety perspective its still sound
    // but it does not reflect the "scoping" of a task local resource
    #[task(priority = 1, binds = UART1, resources = [shared, l2, e1])]
    fn uart1(cx: uart1::Context) {
        let shared: &mut u32 = cx.resources.shared;
        *shared += 1;

        hprintln!("UART1: shared = {}", shared).unwrap();
        hprintln!("UART1:l2 = {}", cx.resources.l2).unwrap();
        hprintln!("UART1:e1 = {}", cx.resources.e1).unwrap();
    }
    // if priority is changed we should report a better error message
    // currently, we get an error since RTFM detects a potential race
};
UART0: shared = 1
UART0:l2 = 2
UART0:e1 = 11
UART1: shared = 2
UART1:l2 = 2
UART1:e1 = 11
IDLE:l1 = 1
IDLE:e2 = 2

Semantics:

Implementation:

Ergonomics/Usability: Syntax can be discussed. It should however indicate the semantics in an intuitive way. lock_free refers to the scheduling (you get exclusive access). task_local the resource is moved to the task (becoming a task local resource).

Limitations: This will not allow further move of resources at run-time (following the static nature and guarantees by RTFM).

Alternative solutions. RTFM already allows you to express this behaviour implicitly, so doing nothing is an option. However, these new constructs might appeal to the non-RTFM:ers out there. With the current implementation, the asymmetric API can be confusing. This approach would go well with #17, letting the designer having explicit control over task_local and lock_free resources.

Implications. A stepping stone towards a fully symmetric API. We can also get rid of task local variables as static, now they will be declared together with other resources instead, for good/bad, not sure.


korken89 commented 4 years ago

Overall I think you have captured what I was thinking about, great job! This is a feature that will help users express their intent (moving or having a resource).

perlindgren commented 4 years ago

Regarding the static mut for task local.

  fn uart0(c: uart0::Context) {
        static mut KALLE: u32 = 0;
        *KALLE += 1;
        c.resources.p.enqueue(42).unwrap();
    }

Is accepted, while, the below code is rejected.... spot the diff...

#[task(binds = UART0, resources = [p])]
    fn uart0(c: uart0::Context) {
        c.resources.p.enqueue(42).unwrap();
        static mut KALLE: u32 = 0;
        *KALLE += 1;
    }

This is due an implementation detail (not really a bug, just not implemented). A more severe problem is that static mut without unsafe is NOT idiomatic Rust (perhaps one of the objections from @therealprof).

The above proposal would remove this problem, by having idiomatic access to mutable resources, declared inside of the Resources section. The example is kind of lengthy, but it depicts all permutations of use (I think). In practical use it will be as short and to the point as the rest of RTFM.

perlindgren commented 2 years ago

The overarching ideas have been implemented in RTIC 1.0.0. Please re-open if needed.