Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt, clippy, rust-src
components: rustfmt, clippy, rust-src, miri

- name: Install libdbus
run: sudo apt-get install -y libdbus-1-dev
Expand All @@ -52,6 +52,18 @@ jobs:
- name: Build
run: cargo build --no-default-features --features ${{matrix.features}}

- name: Test
if: matrix.features == 'std'
run: cargo test --features ${{matrix.features}}

- name: Miri Setup
if: matrix.features == 'std'
run: cargo miri setup

- name: Miri Test
if: matrix.features == 'std'
run: cargo miri test --features ${{matrix.features}}

- name: Examples
if: matrix.features == 'os'
run: cargo build --release --examples --features ${{matrix.features}},nix,log,examples
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ rustcrypto = ["rs-matter/rustcrypto"]
os = ["backtrace", "rs-matter/os", "rustcrypto", "embassy-time/std"]
backtrace = ["std", "rs-matter/backtrace"]
async-io-mini = ["std", "edge-nal-std/async-io-mini"]
std = ["alloc", "rs-matter/std", "edge-nal-std"]
std = ["alloc", "rs-matter/std", "edge-nal-std", "critical-section/std"]
alloc = ["embedded-svc/alloc"]
examples = ["log", "os", "nix", "embassy-time-queue-utils/generic-queue-64", "zeroconf"]

Expand Down Expand Up @@ -191,6 +191,7 @@ bitflags = "2"
nix = { version = "0.27", features = ["net"], optional = true }

[dev-dependencies]
critical-section = "1.0"
static_cell = "2.1"
futures-lite = "1"
async-compat = "0.2"
Expand Down
123 changes: 100 additions & 23 deletions src/bump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
//! `rustc` not being very intelligent w.r.t. stack usage in async functions.

use core::marker::PhantomData;
use core::mem::MaybeUninit;
use core::mem::{self, MaybeUninit};
use core::pin::Pin;
use core::ptr::NonNull;
use core::slice;

use embassy_sync::blocking_mutex::raw::RawMutex;
use rs_matter::utils::cell::RefCell;
Expand Down Expand Up @@ -102,35 +103,38 @@ impl<const N: usize, M: RawMutex> Bump<N, M> {
T: Sized,
{
self.inner.lock(|inner| {
let mut inner = inner.borrow_mut();

let size = core::mem::size_of_val(&object);
// SAFETY:
// The idea is to have a large chunk of memory allocated on the stack,
// with this function one can reserve a chunk of that memory for an object
// of type T.
//
// To reserve the memory, it will move the offset forward by the size required
// for T, and return a **mutable** reference to it.
//
// Given that it returns a mutable reference to it, there cannot be any other
// references to that memory location. This is ensured by the offset.

let size = mem::size_of_val(&object);

let mut inner = inner.borrow_mut();
let offset = inner.offset;
let memory = unsafe { inner.memory.assume_init_mut() };

info!(
"BUMP[{}]: {}b (U:{}b/F:{}b)",
location,
size,
offset,
memory.len() - offset
inner.memory.len() - offset
);

let remaining = &mut memory[offset..];
let remaining_len = remaining.len();

let (t_buf, r_buf) = align_min::<T>(remaining, 1);

// Safety: We just allocated the memory and it's properly aligned
// SAFETY: The lifetime of the returned reference is bound to &self -> it will not outlive the data it is borrowing.
let ptr = unsafe {
let ptr = t_buf.as_ptr() as *mut T;
ptr.write(object);
let t_buf = inner.allocate_for::<T>(1);

NonNull::new_unchecked(ptr)
};
t_buf[0].write(object);

inner.offset += remaining_len - r_buf.len();
NonNull::new_unchecked(t_buf[0].as_mut_ptr())
};

BumpBox {
ptr,
Expand All @@ -143,7 +147,7 @@ impl<const N: usize, M: RawMutex> Bump<N, M> {
/// A box-like container that uses bump allocation
pub struct BumpBox<'a, T> {
ptr: NonNull<T>,
_allocator: core::marker::PhantomData<&'a ()>,
_allocator: PhantomData<&'a ()>,
}

impl<T> BumpBox<'_, T> {
Expand Down Expand Up @@ -182,14 +186,14 @@ impl<T> Drop for BumpBox<'_, T> {
}

struct Inner<const N: usize> {
memory: MaybeUninit<[u8; N]>,
memory: [MaybeUninit<u8>; N],
offset: usize,
}

impl<const N: usize> Inner<N> {
const fn new() -> Self {
Self {
memory: MaybeUninit::uninit(),
memory: [const { MaybeUninit::uninit() }; N],
Copy link
Collaborator

Choose a reason for hiding this comment

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

Have you confirmed that [const { MaybeUninit::uninit() }; N], would absolutely, positively NOT allocate on-stack and them move to the final destination?

To my understanding, only MaybeUninit::uninit() is guaranteed to have this property, regardless of the compiler optimization settings.

Copy link
Author

Choose a reason for hiding this comment

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

I tried it out, and it worked fine, but based on my research, it seems like this is not always the case (seems to depend on compiler optimizations). Given that we are working with 1.77, the const expression is sadly unavailable.

offset: 0,
}
}
Expand All @@ -200,10 +204,47 @@ impl<const N: usize> Inner<N> {
offset: 0,
})
}

/// Allocate space for `count` objects of type `T`
///
/// # Panics
///
/// If there is not enough memory left in the bump allocator to
/// allocate the requested objects.
///
/// # Safety
///
/// This function returns a mutable reference to the allocated memory
/// that lives independently of the lifetime of `self`.
/// This could result in undefined behavior where the reference outlives
/// the bump allocator itself.
///
/// The caller must ensure that the returned reference does not outlive
/// the bump allocator.
unsafe fn allocate_for<'s, 'b, T>(&'s mut self, count: usize) -> &'b mut [MaybeUninit<T>] {
// We can only use the memory from the current offset onwards, because
// the previous memory might be in use by previously allocated objects.
let remaining = &mut self.memory[self.offset..];
let remaining_len = remaining.len();
// The t_buf will be where the caller can place their objects,
// and r_buf should be the remaining unused memory.
let (t_buf, r_buf) = align_min::<T>(remaining, count);
self.offset += remaining_len - r_buf.len();

// This creates an unbounded lifetime, see the safety section of this function.
//
// It is necessary, because technically only one mutable reference can exist
// to self.memory, but because it is an array, the mutable reference to self.memory
// can be split into multiple mutable references to its parts.
slice::from_raw_parts_mut(t_buf.as_mut_ptr(), t_buf.len())
}
}

fn align_min<T>(buf: &mut [u8], count: usize) -> (&mut [MaybeUninit<T>], &mut [u8]) {
if count == 0 || core::mem::size_of::<T>() == 0 {
fn align_min<T>(
buf: &mut [MaybeUninit<u8>],
count: usize,
) -> (&mut [MaybeUninit<T>], &mut [MaybeUninit<u8>]) {
if count == 0 || mem::size_of::<T>() == 0 {
return (&mut [], buf);
}

Expand All @@ -215,7 +256,7 @@ fn align_min<T>(buf: &mut [u8], count: usize) -> (&mut [MaybeUninit<T>], &mut [u
// Shrink `t_buf` to the number of requested items (count)
let t_buf = &mut t_buf[..count];
let t_leading_buf0_len = t_leading_buf0.len();
let t_buf_size = core::mem::size_of_val(t_buf);
let t_buf_size = mem::size_of_val(t_buf);

let (buf0, remaining_buf) = buf.split_at_mut(t_leading_buf0_len + t_buf_size);

Expand All @@ -226,3 +267,39 @@ fn align_min<T>(buf: &mut [u8], count: usize) -> (&mut [MaybeUninit<T>], &mut [u

(t_buf, remaining_buf)
}

#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;

use alloc::vec::Vec;
use rs_matter::utils::sync::blocking::raw::StdRawMutex;

const BUMP_SIZE: usize = 1024;
const DEFAULT_VALUE: u32 = 0xDEADBEEF;

#[test]
fn test_one_concurrent_borrow() {
static BUMP: Bump<BUMP_SIZE, StdRawMutex> = Bump::new();

for _ in 0..(BUMP_SIZE / mem::size_of_val(&DEFAULT_VALUE)) {
let b1 = BUMP.alloc(DEFAULT_VALUE, "test1");

assert_eq!(*b1, DEFAULT_VALUE);
}
}

#[test]
fn test_multiple_concurrent_borrow() {
static BUMP: Bump<BUMP_SIZE, StdRawMutex> = Bump::new();

let mut all_boxes = Vec::new();
for i in 0..(BUMP_SIZE / mem::size_of::<usize>()) {
all_boxes.push(alloc!(BUMP, i));
}

for (i, b) in all_boxes.into_iter().enumerate() {
assert_eq!(*b, i);
}
}
}
Loading