use core::alloc::{
GlobalAlloc,
Layout,
};
const PAGE_SIZE: usize = 64 * 1024;
static mut INNER: Option<InnerAlloc> = None;
pub struct BumpAllocator;
unsafe impl GlobalAlloc for BumpAllocator {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if INNER.is_none() {
INNER = Some(InnerAlloc::new());
};
match INNER
.as_mut()
.expect("We just set the value above; qed")
.alloc(layout)
{
Some(start) => start as *mut u8,
None => core::ptr::null_mut(),
}
}
#[inline]
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
self.alloc(layout)
}
#[inline]
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
}
#[cfg_attr(feature = "std", derive(Debug, Copy, Clone))]
struct InnerAlloc {
next: usize,
upper_limit: usize,
}
impl InnerAlloc {
fn new() -> Self {
Self {
next: Self::heap_start(),
upper_limit: Self::heap_end(),
}
}
cfg_if::cfg_if! {
if #[cfg(test)] {
fn heap_start() -> usize {
0
}
fn heap_end() -> usize {
0
}
fn request_pages(&mut self, _pages: usize) -> Option<usize> {
Some(self.upper_limit)
}
} else if #[cfg(feature = "std")] {
fn heap_start() -> usize {
0
}
fn heap_end() -> usize {
0
}
fn request_pages(&mut self, _pages: usize) -> Option<usize> {
unreachable!(
"This branch is only used to keep the compiler happy when building tests, and
should never actually be called outside of a test run."
)
}
} else if #[cfg(target_arch = "wasm32")] {
fn heap_start() -> usize {
extern "C" {
static __heap_base: usize;
}
let heap_start = unsafe { &__heap_base as *const usize as usize };
assert_ne!(heap_start, 0, "Can't find `__heap_base` symbol.");
heap_start
}
fn heap_end() -> usize {
core::arch::wasm32::memory_size(0) * PAGE_SIZE
}
fn request_pages(&mut self, pages: usize) -> Option<usize> {
let prev_page = core::arch::wasm32::memory_grow(0, pages);
if prev_page == usize::MAX {
return None;
}
Some(prev_page * PAGE_SIZE)
}
} else if #[cfg(target_arch = "riscv32")] {
const fn heap_start() -> usize {
0x7000_0000
}
const fn heap_end() -> usize {
0x7000_0400
}
fn request_pages(&mut self, _pages: usize) -> Option<usize> {
None
}
} else {
core::compile_error!("ink! only supports wasm32 and riscv32");
}
}
fn alloc(&mut self, layout: Layout) -> Option<usize> {
let alloc_start = self.next;
let aligned_size = layout.pad_to_align().size();
let alloc_end = alloc_start.checked_add(aligned_size)?;
if alloc_end > self.upper_limit {
let required_pages = required_pages(aligned_size)?;
let page_start = self.request_pages(required_pages)?;
self.upper_limit = required_pages
.checked_mul(PAGE_SIZE)
.and_then(|pages| page_start.checked_add(pages))?;
self.next = page_start.checked_add(aligned_size)?;
Some(page_start)
} else {
self.next = alloc_end;
Some(alloc_start)
}
}
}
#[inline]
fn required_pages(size: usize) -> Option<usize> {
size.checked_add(PAGE_SIZE - 1)
.and_then(|num| num.checked_div(PAGE_SIZE))
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem::size_of;
#[test]
fn can_alloc_no_bytes() {
let mut inner = InnerAlloc::new();
let layout = Layout::new::<()>();
assert_eq!(inner.alloc(layout), Some(0));
let expected_limit =
PAGE_SIZE * required_pages(layout.pad_to_align().size()).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = size_of::<()>();
assert_eq!(inner.next, expected_alloc_start);
}
#[test]
fn can_alloc_a_byte() {
let mut inner = InnerAlloc::new();
let layout = Layout::new::<u8>();
assert_eq!(inner.alloc(layout), Some(0));
let expected_limit =
PAGE_SIZE * required_pages(layout.pad_to_align().size()).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = size_of::<u8>();
assert_eq!(inner.next, expected_alloc_start);
}
#[test]
fn can_alloc_a_foobarbaz() {
let mut inner = InnerAlloc::new();
struct FooBarBaz {
_foo: u32,
_bar: u128,
_baz: (u16, bool),
}
let layout = Layout::new::<FooBarBaz>();
let mut total_size = 0;
let allocations = 3;
for _ in 0..allocations {
assert!(inner.alloc(layout).is_some());
total_size += layout.pad_to_align().size();
}
let expected_limit = PAGE_SIZE * required_pages(total_size).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = allocations * size_of::<FooBarBaz>();
assert_eq!(inner.next, expected_alloc_start);
}
#[test]
fn can_alloc_across_pages() {
let mut inner = InnerAlloc::new();
struct Foo {
_foo: [u8; PAGE_SIZE - 1],
}
let layout = Layout::new::<Foo>();
assert_eq!(inner.alloc(layout), Some(0));
let expected_limit =
PAGE_SIZE * required_pages(layout.pad_to_align().size()).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = size_of::<Foo>();
assert_eq!(inner.next, expected_alloc_start);
let layout = Layout::new::<u16>();
assert_eq!(inner.alloc(layout), Some(PAGE_SIZE));
let expected_limit = 2 * PAGE_SIZE;
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = PAGE_SIZE + size_of::<u16>();
assert_eq!(inner.next, expected_alloc_start);
}
#[test]
fn can_alloc_multiple_pages() {
let mut inner = InnerAlloc::new();
struct Foo {
_foo: [u8; 2 * PAGE_SIZE],
}
let layout = Layout::new::<Foo>();
assert_eq!(inner.alloc(layout), Some(0));
let expected_limit =
PAGE_SIZE * required_pages(layout.pad_to_align().size()).unwrap();
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = size_of::<Foo>();
assert_eq!(inner.next, expected_alloc_start);
let layout = Layout::new::<u8>();
assert_eq!(inner.alloc(layout), Some(2 * PAGE_SIZE));
let expected_limit = 3 * PAGE_SIZE;
assert_eq!(inner.upper_limit, expected_limit);
let expected_alloc_start = 2 * PAGE_SIZE + size_of::<u8>();
assert_eq!(inner.next, expected_alloc_start);
}
}
#[cfg(all(test, feature = "ink-fuzz-tests"))]
mod fuzz_tests {
use super::*;
use quickcheck::{
quickcheck,
TestResult,
};
use std::mem::size_of;
#[quickcheck]
fn should_allocate_arbitrary_sized_bytes(n: usize) -> TestResult {
let mut inner = InnerAlloc::new();
let layout = match Layout::from_size_align(n, size_of::<usize>()) {
Ok(l) => l,
Err(_) => return TestResult::discard(),
};
let size = layout.pad_to_align().size();
assert_eq!(
inner.alloc(layout),
Some(0),
"The given pointer for the allocation doesn't match."
);
let expected_alloc_start = size;
assert_eq!(
inner.next, expected_alloc_start,
"Our next allocation doesn't match where it should start."
);
let expected_limit = PAGE_SIZE * required_pages(size).unwrap();
assert_eq!(
inner.upper_limit, expected_limit,
"The upper bound of our heap doesn't match."
);
TestResult::passed()
}
#[quickcheck]
fn should_allocate_regardless_of_alignment_size(
n: usize,
align: usize,
) -> TestResult {
let aligns = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
let align = aligns[align % aligns.len()];
let mut inner = InnerAlloc::new();
let layout = match Layout::from_size_align(n, align) {
Ok(l) => l,
Err(_) => return TestResult::discard(),
};
let size = layout.pad_to_align().size();
assert_eq!(
inner.alloc(layout),
Some(0),
"The given pointer for the allocation doesn't match."
);
let expected_alloc_start = size;
assert_eq!(
inner.next, expected_alloc_start,
"Our next allocation doesn't match where it should start."
);
let expected_limit = PAGE_SIZE * required_pages(size).unwrap();
assert_eq!(
inner.upper_limit, expected_limit,
"The upper bound of our heap doesn't match."
);
TestResult::passed()
}
#[quickcheck]
fn should_allocate_arbitrary_byte_sequences(sequence: Vec<isize>) -> TestResult {
let mut inner = InnerAlloc::new();
if sequence.is_empty() {
return TestResult::discard()
}
if !sequence.iter().all(|n| n.is_positive()) {
return TestResult::discard()
}
let pages_required = sequence
.iter()
.fold(0, |acc, &x| acc + required_pages(x as usize).unwrap());
let max_pages = required_pages(usize::MAX - PAGE_SIZE + 1).unwrap();
if pages_required > max_pages {
return TestResult::discard()
}
let mut expected_alloc_start = 0;
let mut total_bytes_requested = 0;
let mut total_bytes_fragmented = 0;
for alloc in sequence {
let layout = Layout::from_size_align(alloc as usize, size_of::<usize>());
let layout = match layout {
Ok(l) => l,
Err(_) => return TestResult::discard(),
};
let size = layout.pad_to_align().size();
let current_page_limit = PAGE_SIZE * required_pages(inner.next).unwrap();
let is_too_big_for_current_page = inner.next + size > current_page_limit;
if is_too_big_for_current_page {
let fragmented_in_current_page = current_page_limit - inner.next;
total_bytes_fragmented += fragmented_in_current_page;
expected_alloc_start = inner.upper_limit;
}
assert_eq!(
inner.alloc(layout),
Some(expected_alloc_start),
"The given pointer for the allocation doesn't match."
);
total_bytes_requested += size;
expected_alloc_start = total_bytes_requested + total_bytes_fragmented;
assert_eq!(
inner.next, expected_alloc_start,
"Our next allocation doesn't match where it should start."
);
let pages_required = required_pages(expected_alloc_start).unwrap();
let expected_limit = PAGE_SIZE * pages_required;
assert_eq!(
inner.upper_limit, expected_limit,
"The upper bound of our heap doesn't match."
);
}
TestResult::passed()
}
#[quickcheck]
fn should_not_allocate_arbitrary_byte_sequences_which_eventually_overflow(
sequence: Vec<isize>,
) -> TestResult {
let mut inner = InnerAlloc::new();
if sequence.is_empty() {
return TestResult::discard()
}
if !sequence.iter().all(|n| n.is_positive()) {
return TestResult::discard()
}
let pages_required = sequence
.iter()
.fold(0, |acc, &x| acc + required_pages(x as usize).unwrap());
let max_pages = required_pages(usize::MAX - PAGE_SIZE + 1).unwrap();
if pages_required <= max_pages {
return TestResult::discard()
}
let mut results = vec![];
for alloc in sequence {
let layout = Layout::from_size_align(alloc as usize, size_of::<usize>());
let layout = match layout {
Ok(l) => l,
Err(_) => return TestResult::discard(),
};
results.push(inner.alloc(layout));
}
assert!(
results.iter().any(|r| r.is_none()),
"Expected an allocation to overflow our heap, but this didn't happen."
);
TestResult::passed()
}
}