pub(crate) mod allocator;
mod page;
pub(crate) use page::LuaPageList;
pub use page::{GcoPageWalk, LuaPage, RawLuaPage};
use core::alloc::Layout;
use core::mem::{offset_of, size_of};
use core::ptr::{self, NonNull};
use crate::debug::DebugRuntime;
use crate::gc::{GcHandle, GcObject, RawGcObject};
use crate::handle::RawHandle;
use crate::handle::sealed::Sealed;
use crate::thread::Thread;
use crate::types::LUA_TNIL;
use crate::{VmError, VmErrorResult};
pub const MAX_SMALL_SIZE: usize = 1024;
pub const MAX_SMALL_SIZE_USED: usize = 1024;
pub const LARGE_PAGE_THRESHOLD: usize = 512;
const EXTERNAL_ALLOCATOR_METADATA_REDUCTION: usize = 24;
pub const PAGE_SMALL_SIZE: usize = 16 * 1024 - EXTERNAL_ALLOCATOR_METADATA_REDUCTION;
pub const PAGE_LARGE_SIZE: usize = 32 * 1024 - EXTERNAL_ALLOCATOR_METADATA_REDUCTION;
pub const BLOCK_HEADER_SIZE: usize =
if core::mem::size_of::<f64>() > core::mem::size_of::<*const ()>() {
core::mem::size_of::<f64>()
} else {
core::mem::size_of::<*const ()>()
};
struct SizeClassConfig {
size_of_class: [i32; crate::memory::LUA_SIZE_CLASSES],
class_for_size: [i8; MAX_SMALL_SIZE + 1],
class_count: usize,
}
impl SizeClassConfig {
const fn new() -> Self {
let mut size_of_class = [0; crate::memory::LUA_SIZE_CLASSES];
let mut class_for_size = [-1; MAX_SMALL_SIZE + 1];
let mut class_count = 0usize;
let mut size = 8i32;
while size < 64 {
size_of_class[class_count] = size;
class_count += 1;
size += 8;
}
size = 64;
while size < 256 {
size_of_class[class_count] = size;
class_count += 1;
size += 16;
}
size = 256;
while size < 512 {
size_of_class[class_count] = size;
class_count += 1;
size += 32;
}
size = 512;
while size <= 1024 {
size_of_class[class_count] = size;
class_count += 1;
size += 64;
}
let mut klass = 0usize;
while klass < class_count {
class_for_size[size_of_class[klass] as usize] = klass as i8;
klass += 1;
}
let mut size = MAX_SMALL_SIZE - 1;
loop {
if class_for_size[size] < 0 {
class_for_size[size] = class_for_size[size + 1];
}
if size == 0 {
break;
}
size -= 1;
}
Self {
size_of_class,
class_for_size,
class_count,
}
}
}
const SIZE_CLASS_CONFIG: SizeClassConfig = SizeClassConfig::new();
pub fn size_class(size: usize) -> i32 {
if size.wrapping_sub(1) < MAX_SMALL_SIZE_USED {
i32::from(SIZE_CLASS_CONFIG.class_for_size[size])
} else {
-1
}
}
pub fn size_of_class(size_class: usize) -> i32 {
debug_assert!(size_class < SIZE_CLASS_CONFIG.class_count);
SIZE_CLASS_CONFIG.size_of_class[size_class]
}
pub const LUA_PAGE_PADDING: usize = if size_of::<*const ()>() == 8 { 8 } else { 12 };
pub const GCO_LINK_OFFSET: usize =
(size_of::<RawGcObject>() + size_of::<*const ()>() - 1) & !(size_of::<*const ()>() - 1);
pub(crate) const LUA_SIZE_CLASSES: usize = 40;
pub(crate) const LUA_MEMORY_CATEGORIES: usize = 256;
const SMALL_BLOCK_ALIGNMENT: usize = BLOCK_HEADER_SIZE;
#[allow(
clippy::missing_safety_doc,
reason = "all methods share the capability-level safety contract"
)]
pub trait MemoryRuntime: Sealed {
unsafe fn new_raw(&self, layout: Layout, memcat: u8) -> VmErrorResult<*mut u8>;
unsafe fn new_gco_(&self, size: usize, memcat: u8) -> VmErrorResult<GcObject>;
unsafe fn free_raw(&self, block: *mut u8, old_layout: Layout, memcat: u8);
unsafe fn new_array<T>(&self, count: usize, memcat: u8) -> VmErrorResult<*mut T> {
unsafe {
let layout = self.array_layout::<T>(count)?;
self.new_raw(layout, memcat).map(|block| block.cast())
}
}
unsafe fn free_array<T>(&self, block: *mut T, count: usize, memcat: u8) {
unsafe {
self.free_raw(
block.cast::<u8>(),
self.array_layout::<T>(count)
.expect("previously allocated array layout remains valid"),
memcat,
);
}
}
unsafe fn free_gco(&self, block: GcObject, old_size: usize, memcat: u8, page: LuaPage);
unsafe fn realloc_raw(
&self,
block: *mut u8,
old_layout: Layout,
new_layout: Layout,
memcat: u8,
) -> VmErrorResult<*mut u8>;
unsafe fn realloc_array<T>(
&self,
block: *mut T,
old_count: usize,
new_count: usize,
memcat: u8,
) -> VmErrorResult<*mut T> {
unsafe {
let old_layout = self
.array_layout::<T>(old_count)
.expect("previously allocated array layout remains valid");
let new_layout = self.array_layout::<T>(new_count)?;
self.realloc_raw(block.cast::<u8>(), old_layout, new_layout, memcat)
.map(|block| block.cast())
}
}
unsafe fn too_big<T>(&self) -> VmErrorResult<T>;
unsafe fn array_layout<T>(&self, count: usize) -> VmErrorResult<Layout> {
match Layout::array::<T>(count) {
Ok(layout) => Ok(layout),
Err(_) => unsafe { self.too_big() },
}
}
unsafe fn visit_gco(
&self,
context: *mut (),
visitor: unsafe fn(*mut (), LuaPage, GcObject) -> bool,
);
}
impl MemoryRuntime for Thread {
unsafe fn new_raw(&self, layout: Layout, memcat: u8) -> VmErrorResult<*mut u8> {
unsafe {
let size = layout.size();
if size == 0 {
return Ok(ptr::null_mut());
}
let size_class_index = if layout.align() <= SMALL_BLOCK_ALIGNMENT {
size_class(size)
} else {
-1
};
let block = if size_class_index >= 0 {
self.new_block(size_class_index).map(NonNull::as_ptr)
} else {
let global = self.global();
global
.allocator()
.as_ref()
.allocate(layout)
.map(NonNull::as_ptr)
};
let block = match block {
Some(block) if !block.is_null() || size == 0 => block,
Some(_) | None => return Err(VmError::Memory),
};
let global_handle = self.global();
let global = global_handle.as_ptr().as_mut().unwrap_unchecked();
global.total_bytes += size;
global.memcat_bytes[memcat as usize] += size;
if let Some(on_allocate) = global.cb.on_allocate {
on_allocate(self, 0, size);
}
Ok(block)
}
}
unsafe fn new_gco_(&self, size: usize, memcat: u8) -> VmErrorResult<GcObject> {
debug_assert!(size >= GCO_LINK_OFFSET + core::mem::size_of::<*const ()>());
unsafe {
let size_class_index = size_class(size);
let block = if size_class_index >= 0 {
self.new_gco_block(size_class_index)
} else {
self.large_gco_page(size).map(|(_, block)| block)
};
let Some(block) = block else {
return Err(VmError::Memory);
};
let global_handle = self.global();
let global = global_handle.as_ptr().as_mut().unwrap_unchecked();
global.total_bytes += size;
global.memcat_bytes[memcat as usize] += size;
if let Some(on_allocate) = global.cb.on_allocate {
on_allocate(self, 0, size);
}
Ok(block)
}
}
unsafe fn free_raw(&self, block: *mut u8, old_layout: Layout, memcat: u8) {
let old_size = old_layout.size();
debug_assert!((old_size == 0) == block.is_null());
unsafe {
if old_size == 0 {
return;
}
let size_class_index = if old_layout.align() <= SMALL_BLOCK_ALIGNMENT {
size_class(old_size)
} else {
-1
};
if size_class_index >= 0 {
if let Some(block) = NonNull::new(block) {
self.free_block(size_class_index, block);
}
} else {
let global = self.global();
global
.allocator()
.as_ref()
.deallocate(NonNull::new_unchecked(block), old_layout);
}
let global_handle = self.global();
let global = global_handle.as_ptr().as_mut().unwrap_unchecked();
global.total_bytes -= old_size;
global.memcat_bytes[memcat as usize] -= old_size;
}
}
unsafe fn free_gco(&self, block: GcObject, old_size: usize, memcat: u8, page: LuaPage) {
unsafe {
let size_class_index = size_class(old_size);
if size_class_index >= 0 {
block.as_ptr().as_mut().unwrap_unchecked().tt = LUA_TNIL as u8;
self.free_gco_block(
size_class_index,
NonNull::new_unchecked(block.as_ptr()),
page,
);
} else {
debug_assert!(page.as_ptr().as_ref().unwrap_unchecked().busy_blocks == 1);
debug_assert!(
page.as_ptr().as_ref().unwrap_unchecked().block_size as usize == old_size
);
debug_assert_eq!(block.as_ptr().cast::<u8>(), page.data_ptr_mut());
let global = self.global();
self.free_page(Some(global.all_gco_page_list()), page);
}
let global_handle = self.global();
let global = global_handle.as_ptr().as_mut().unwrap_unchecked();
global.total_bytes -= old_size;
global.memcat_bytes[memcat as usize] -= old_size;
}
}
unsafe fn realloc_raw(
&self,
block: *mut u8,
old_layout: Layout,
new_layout: Layout,
memcat: u8,
) -> VmErrorResult<*mut u8> {
unsafe {
let old_size = old_layout.size();
let new_size = new_layout.size();
debug_assert!((old_size == 0) == block.is_null());
if old_size == 0 {
return self.new_raw(new_layout, memcat);
}
if new_size == 0 {
self.free_raw(block, old_layout, memcat);
let global = self.global();
if let Some(on_allocate) =
global.as_ptr().as_mut().unwrap_unchecked().cb.on_allocate
{
on_allocate(self, old_size, new_size);
}
return Ok(ptr::null_mut());
}
let new_class = if new_layout.align() <= SMALL_BLOCK_ALIGNMENT {
size_class(new_size)
} else {
-1
};
let old_class = if old_layout.align() <= SMALL_BLOCK_ALIGNMENT {
size_class(old_size)
} else {
-1
};
let result = if new_class >= 0 || old_class >= 0 {
let result = if new_class >= 0 {
self.new_block(new_class).map(NonNull::as_ptr)
} else {
let global = self.global();
global
.allocator()
.as_ref()
.allocate(new_layout)
.map(NonNull::as_ptr)
};
let result = match result {
Some(result) if !result.is_null() || new_size == 0 => result,
Some(_) | None => return Err(VmError::Memory),
};
if old_size > 0 && new_size > 0 {
ptr::copy_nonoverlapping(block, result, old_size.min(new_size));
}
if old_class >= 0 {
if let Some(block) = NonNull::new(block) {
self.free_block(old_class, block);
}
} else {
let global = self.global();
global
.allocator()
.as_ref()
.deallocate(NonNull::new_unchecked(block), old_layout);
}
result
} else {
let global = self.global();
global
.allocator()
.as_ref()
.reallocate(NonNull::new_unchecked(block), old_layout, new_layout)
.map(NonNull::as_ptr)
.ok_or(VmError::Memory)?
};
let global_handle = self.global();
let global = global_handle.as_ptr().as_mut().unwrap_unchecked();
if new_size >= old_size {
global.total_bytes += new_size - old_size;
global.memcat_bytes[memcat as usize] += new_size - old_size;
} else {
global.total_bytes -= old_size - new_size;
global.memcat_bytes[memcat as usize] -= old_size - new_size;
}
if let Some(on_allocate) = global.cb.on_allocate {
on_allocate(self, old_size, new_size);
}
Ok(result)
}
}
unsafe fn too_big<T>(&self) -> VmErrorResult<T> {
unsafe { crate::run_error!(self, "memory allocation error: block too big") }
}
unsafe fn visit_gco(
&self,
context: *mut (),
visitor: unsafe fn(*mut (), LuaPage, GcObject) -> bool,
) {
unsafe {
let global = self.global();
let mut current =
NonNull::new(global.as_ptr().as_ref().unwrap_unchecked().all_gco_pages);
while let Some(page) = current {
let page = LuaPage::from_raw(page);
current = page
.next_page()
.map(|next| NonNull::new_unchecked(next.as_ptr()));
page.visit_page(context, visitor);
}
}
}
}
impl Thread {
pub(crate) unsafe fn new_gco<T: GcHandle>(&self, size: usize, memcat: u8) -> VmErrorResult<T> {
unsafe {
self.new_gco_(size, memcat)
.map(|object| T::from_gc_object(object))
}
}
unsafe fn new_page(
&self,
page_set: Option<LuaPageList>,
page_size: i32,
block_size: i32,
block_count: i32,
) -> Option<LuaPage> {
debug_assert!(
page_size as usize - offset_of!(RawLuaPage, data)
>= block_size as usize * block_count as usize
);
let global = unsafe { self.global() };
let layout =
Layout::from_size_align(page_size as usize, core::mem::align_of::<RawLuaPage>())
.expect("valid page allocation layout");
let raw = unsafe { global.allocator().as_ref().allocate(layout)? };
let page = unsafe { LuaPage::from_raw(raw.cast::<RawLuaPage>()) };
unsafe {
let page_mut = page.as_ptr().as_mut().unwrap_unchecked();
page_mut.prev = ptr::null_mut();
page_mut.next = ptr::null_mut();
page_mut.list_prev = ptr::null_mut();
page_mut.list_next = ptr::null_mut();
page_mut.page_size = page_size;
page_mut.block_size = block_size;
page_mut.free_list = ptr::null_mut();
page_mut.free_next = (block_count - 1) * block_size;
page_mut.busy_blocks = 0;
if let Some(page_set) = page_set {
page_set.link_page(page);
}
}
Some(page)
}
unsafe fn new_class_page(
&self,
free_page_set: LuaPageList,
page_set: Option<LuaPageList>,
size_class_index: u8,
store_metadata: bool,
) -> Option<LuaPage> {
let size_of_class = size_of_class(size_class_index as usize);
let page_size = if size_of_class as usize > LARGE_PAGE_THRESHOLD {
PAGE_LARGE_SIZE
} else {
PAGE_SMALL_SIZE
} as i32;
let block_size = size_of_class
+ if store_metadata {
BLOCK_HEADER_SIZE as i32
} else {
0
};
let block_count = (page_size as usize - offset_of!(RawLuaPage, data)) / block_size as usize;
let page = unsafe { self.new_page(page_set, page_size, block_size, block_count as i32)? };
unsafe {
debug_assert!(free_page_set.get().is_null());
free_page_set.set(page.as_ptr());
}
Some(page)
}
unsafe fn free_page(&self, page_set: Option<LuaPageList>, page: LuaPage) {
let global = unsafe { self.global() };
unsafe {
if let Some(page_set) = page_set {
page_set.unlink_page(page);
}
let layout = Layout::from_size_align(
page.as_ptr().as_ref().unwrap_unchecked().page_size as usize,
core::mem::align_of::<RawLuaPage>(),
)
.expect("valid page allocation layout");
global
.allocator()
.as_ref()
.deallocate(NonNull::new_unchecked(page.as_ptr().cast()), layout);
}
}
unsafe fn free_class_page(
&self,
free_page_set: LuaPageList,
page_set: Option<LuaPageList>,
page: LuaPage,
) {
unsafe {
{
let page_ref = page.as_ptr().as_mut().unwrap_unchecked();
if let Some(mut next) = NonNull::new(page_ref.next) {
next.as_mut().prev = page_ref.prev;
}
if let Some(mut prev) = NonNull::new(page_ref.prev) {
prev.as_mut().next = page_ref.next;
} else if free_page_set.get() == page.as_ptr() {
free_page_set.set(page_ref.next);
}
}
self.free_page(page_set, page);
}
}
unsafe fn new_block(&self, size_class_index: i32) -> Option<NonNull<u8>> {
unsafe {
let global = self.global();
let index = size_class_index as usize;
let free_pages = global.free_page_list(index);
let all_pages = global.all_page_list();
let page = match NonNull::new(free_pages.get()) {
Some(page) => LuaPage::from_raw(page),
None => {
self.new_class_page(free_pages, Some(all_pages), size_class_index as u8, true)?
}
};
debug_assert!(page.as_ptr().as_ref().unwrap_unchecked().prev.is_null());
debug_assert!(
!page
.as_ptr()
.as_ref()
.unwrap_unchecked()
.free_list
.is_null()
|| page.as_ptr().as_ref().unwrap_unchecked().free_next >= 0
);
let block = {
let page_ref = page.as_ptr().as_mut().unwrap_unchecked();
let block = if page_ref.free_next >= 0 {
let block = page.block(page_ref.free_next);
page_ref.free_next -= page_ref.block_size;
page_ref.busy_blocks += 1;
block
} else {
let block = page_ref.free_list;
page_ref.free_list = *LuaPage::metadata_slot(block);
page_ref.busy_blocks += 1;
block
};
*LuaPage::metadata_slot(block) = page.as_ptr().cast::<u8>();
if page_ref.free_list.is_null() && page_ref.free_next < 0 {
free_pages.set(page_ref.next);
if let Some(mut next) = NonNull::new(page_ref.next) {
next.as_mut().prev = ptr::null_mut();
}
page_ref.next = ptr::null_mut();
}
block
};
Some(NonNull::new_unchecked(block.add(BLOCK_HEADER_SIZE)))
}
}
unsafe fn new_gco_block(&self, size_class_index: i32) -> Option<GcObject> {
unsafe {
let global = self.global();
let index = size_class_index as usize;
let free_pages = global.free_gco_page_list(index);
let all_gco_pages = global.all_gco_page_list();
let page = match NonNull::new(free_pages.get()) {
Some(page) => LuaPage::from_raw(page),
None => self.new_class_page(
free_pages,
Some(all_gco_pages),
size_class_index as u8,
false,
)?,
};
debug_assert!(page.as_ptr().as_ref().unwrap_unchecked().prev.is_null());
debug_assert!(
!page
.as_ptr()
.as_ref()
.unwrap_unchecked()
.free_list
.is_null()
|| page.as_ptr().as_ref().unwrap_unchecked().free_next >= 0
);
let block = {
let page_ref = page.as_ptr().as_mut().unwrap_unchecked();
let block = if page_ref.free_next >= 0 {
let block = page.block(page_ref.free_next);
page_ref.free_next -= page_ref.block_size;
page_ref.busy_blocks += 1;
block
} else {
let block = page_ref.free_list;
page_ref.free_list = *LuaPage::free_gco_link_slot(block);
page_ref.busy_blocks += 1;
block
};
if page_ref.free_list.is_null() && page_ref.free_next < 0 {
free_pages.set(page_ref.next);
if let Some(mut next) = NonNull::new(page_ref.next) {
next.as_mut().prev = ptr::null_mut();
}
page_ref.next = ptr::null_mut();
}
block
};
Some(GcObject::from_raw(NonNull::new_unchecked(block.cast())))
}
}
unsafe fn free_block(&self, size_class_index: i32, block: NonNull<u8>) {
unsafe {
let global = self.global();
let index = size_class_index as usize;
let free_pages = global.free_page_list(index);
let all_pages = global.all_page_list();
let raw_block = block.as_ptr().sub(BLOCK_HEADER_SIZE);
let page = LuaPage::from_raw(NonNull::new_unchecked(
(*LuaPage::metadata_slot(raw_block)).cast::<RawLuaPage>(),
));
let should_free_page = {
let page_ref = page.as_ptr().as_mut().unwrap_unchecked();
if page_ref.free_list.is_null() && page_ref.free_next < 0 {
debug_assert!(page_ref.prev.is_null());
debug_assert!(page_ref.next.is_null());
page_ref.next = free_pages.get();
if let Some(mut next) = NonNull::new(page_ref.next) {
next.as_mut().prev = page.as_ptr();
}
free_pages.set(page.as_ptr());
}
*LuaPage::metadata_slot(raw_block) = page_ref.free_list.cast::<u8>();
page_ref.free_list = raw_block;
page_ref.busy_blocks -= 1;
page_ref.busy_blocks == 0
};
if should_free_page {
self.free_class_page(free_pages, Some(all_pages), page);
}
}
}
unsafe fn free_gco_block(
&self,
size_class_index: i32,
block: NonNull<RawGcObject>,
page: LuaPage,
) {
unsafe {
let global = self.global();
let index = size_class_index as usize;
let free_pages = global.free_gco_page_list(index);
let all_gco_pages = global.all_gco_page_list();
let should_free_page = {
let page_ref = page.as_ptr().as_mut().unwrap_unchecked();
if page_ref.free_list.is_null() && page_ref.free_next < 0 {
debug_assert!(page_ref.prev.is_null());
debug_assert!(page_ref.next.is_null());
page_ref.next = free_pages.get();
if let Some(mut next) = NonNull::new(page_ref.next) {
next.as_mut().prev = page.as_ptr();
}
free_pages.set(page.as_ptr());
}
*LuaPage::free_gco_link_slot(block.as_ptr().cast()) =
page_ref.free_list.cast::<u8>();
page_ref.free_list = block.as_ptr().cast();
page_ref.busy_blocks -= 1;
page_ref.busy_blocks == 0
};
if should_free_page {
self.free_class_page(free_pages, Some(all_gco_pages), page);
}
}
}
unsafe fn large_gco_page(&self, size: usize) -> Option<(LuaPage, GcObject)> {
unsafe {
let global = self.global();
let all_gco_pages = global.all_gco_page_list();
let page = self.new_page(
Some(all_gco_pages),
(offset_of!(RawLuaPage, data) + size) as i32,
size as i32,
1,
)?;
let page_ref = page.as_ptr().as_mut().unwrap_unchecked();
let block = page.data_ptr_mut();
page_ref.free_next -= page_ref.block_size;
page_ref.busy_blocks += 1;
Some((
page,
GcObject::from_raw(NonNull::new_unchecked(block.cast::<RawGcObject>())),
))
}
}
}