rust-embedded / rust-embedded/cortex-m

RFC: `#[late]` statics

Open
#401 3 comments 1 reaction 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

#[late] static are statics that are initialized at runtime, before the entry
point is called and after RAM initialization happens.

Motivation

Consider the allocator example from the quickstart template:

#[global_allocator]
static ALLOCATOR: CortexMHeap = CortexMHeap::empty();

const HEAP_SIZE: usize = 1024; // in bytes

#[entry]
fn main() -> ! {
    // Initialize the allocator BEFORE you use it
    unsafe { ALLOCATOR.init(cortex_m_rt::heap_start() as usize, HEAP_SIZE) }

    let xs = vec![0, 1, 2];

    // ..
}

Quite a few things can go wrong there: you could forget to initialize the
allocator or you could initialize it too late. In either case you end up with
OOM (abort).

This example can be simplified using a #[late] static:

#[late]
#[global_allocator]
static ALLOCATOR: SyncAllocator = unsafe {
    static mut BYTES: [u8; 1024] = [0; 1024];

    // you are familiar with this transformation
    let bytes: &'static mut [u8] = BYTES;

    // `SyncAllocator` is a newtype over `Mutex<SomeAllocator>`
    SyncAllocator::new(bytes)
}

#[entry]
fn main() -> ! {
    // Growable array allocated on the heap
    let xs = vec![0, 1, 2];

    // ..
}

Design

#[late] statics are not lazy statics. Lazy statics are initialized on
first use, they have runtime cost on each access and memory overhead. #[late]
statics are statics that are initialized before the #[entry] function is
called, accessing them is zero cost and they have no memory overhead.

The above example expands to something like this:

// For details about the `.uninint` section see RFC rust-embedded/cortex-m#398
#[link_section = ".uninit.<random-hash>"]
#[global_allocator]
static ALLOCATOR: Late<SyncAllocator> = {
    #[link_section = ".late.<random-hash>"] // new linker section
    static INITIALIZER: unsafe fn() = initialize;

    unsafe fn initialize() {
        let val = {
            // (proc-macro transformation)
            let BYTES: &'static mut _ = {
                static mut BYTES: [u8; 1024] = [0; 1024];

                &mut BYTES
            };

            // (user code)
            // you are familiar with this transformation
            let bytes: &'static mut [u8] = BYTES;

            // `SyncAllocator` is a newtype over `Mutex<Allocator>`
            SyncAllocator::new(bytes)
        };

        ALLOCATOR.initialize(val);
    }

    Late::uninitialized()
};

The Late newtype will be provided by this crate:

/// IMPLEMENTATION DETAIL. DO NOT USE
#[doc(hidden)]
pub struct Late<T> {
    inner: UnsafeCell<MaybeUninit<T>>,
}

unsafe impl<T> Sync for Late<T> where T: Sync {}

impl<T> Late<T> {
    /// IMPLEMENTATION DETAIL. DO NOT USE
    #[doc(hidden)]
    pub const fn uninitialized() -> Self {
        Late {
            inner: UnsafeCell::new(MaybeUninit::uninitialized()),
        }
    }

    /// IMPLEMENTATION DETAIL. DO NOT USE
    #[doc(hidden)]
    pub unsafe fn initialize(&self, value: T) {
        (*self.inner.get()).set(value)
    }
}

impl<T> Deref for Late<T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { (*self.inner.get()).get_ref() }
    }
}

impl<T> DerefMut for Late<T> {
    fn deref_mut(&mut self) -> &mut T {
        unsafe { (*self.inner.get()).get_mut() }
    }
}

As you can see Deref and DerefMut perform no runtime checks. However,
dereferencing is only valid after Late has been initialize-d.

#[late] statics are initialized before #[entry] / main and after RAM
initialization using r0::run_init_array or its equivalent.

fn Reset() -> ! {
    __pre_init();

    r0::zero_bss(&mut __sbss, &mut __ebss);
    r0::init_data(&mut __sdata, &mut __edata, & __sidata);
    r0::run_init_array(&__slate, &__elate);

    main()
}

#[global_allocator]

To compose with the built-in global_allocator attribute we need to provide
the implementation below in this crate.

unsafe impl<T> GlobalAlloc for Late<T>
where
    T: GlobalAlloc,
{
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        T::alloc(self.deref(), layout)
    }

    unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
        T::dealloc(self.deref(), p, layout)
    }
}

Why unsafe?

Using normal statics within a #[late] static initializer is sound, but using
any #[late] static within the initializer is not because there are no
guarantees about the execution order of #[late] initializers.

Furthermore for global resources like global_allocator trying to use the
resource before it's initialized is also undefined behavior.

#[late]
#[global_allocator]
static ALLOCATOR: SyncAllocator = unsafe {
    static mut BYTES: [u8; 1024] = [0; 1024];

    let xs = vec![0, 1, 2]; // undefined behavior
    ALLOCATOR.alloc(..); // also undefined behavior

    SyncAllocator::new(BYTES)
}

It's not possible to prevent all these issues at compile time so unsafe must
be used. Arguably, we could prevent the first issue by making the static
unnameable (by assigning it a randomly generated name) but this would greatly
reduce the flexibility of #[late] statics.


Thoughts? Are there use cases other than global allocators?

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 with the Design section, the proposed Late<T> implementation, and the Reset entry point that calls r0::run_init_array. Review how the proposed .late and .uninit sections interact with initialization and #[global_allocator]. Done means the RFC’s design is resolved and the required runtime, macro, and linker integration is specified.

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.