We have a CPI that uses ~4000 bytes of heap, and it executes a few times in a loop, so for a few executes the program will fail with memory allocation failed, out of memory
.
The solution has been to move the heap pointer manually with a custom global allocator, like so:
let heap_start = unsafe { A.pos() };
ctx.accounts.big_bad_cpi(
args
)?;
unsafe { A.move_cursor(heap_start); }
This works well. Is there a better way to tell the Solana runtime to release the heap allocations made by the CPI? It seems like, since a CPI doesn't do anything to the program's variables, it should reset the heap pointer automatically to where it started when the CPI returns control.
Here is the bump allocator, based on Solana's reference implementation:
pub struct BumpAllocator {
pub start: usize,
pub len: usize,
}
impl BumpAllocator {
const RESERVED_MEM: usize = 1 * size_of::<*mut u8>();
/// Return heap position as of this call
pub unsafe fn pos(&self) -> usize {
let pos_ptr = self.start as *mut usize;
*pos_ptr
}
/// Reset heap start cursor to position.
/// ### This is very unsafe, use wisely
pub unsafe fn move_cursor(&self, pos: usize) {
let pos_ptr = self.start as *mut usize;
*pos_ptr = pos;
}
}
unsafe impl std::alloc::GlobalAlloc for BumpAllocator {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let pos_ptr = self.start as *mut usize;
let mut pos = *pos_ptr;
if pos == 0 {
// First time, set starting position
pos = self.start + self.len;
}
pos = pos.saturating_sub(layout.size());
pos &= !(layout.align().wrapping_sub(1));
if pos < self.start + BumpAllocator::RESERVED_MEM {
return null_mut();
}
*pos_ptr = pos;
pos as *mut u8
}
#[inline]
unsafe fn dealloc(&self, _: *mut u8, _: Layout) {
// no dellaoc in Solana runtime :*(
}
}
#[global_allocator]
static A: BumpAllocator = BumpAllocator {
start: HEAP_START_ADDRESS as usize,
len: HEAP_LENGTH,
};