Skip to main content

luau_vm/memory/
page.rs

1use core::mem::offset_of;
2use core::ptr::{self, NonNull};
3
4use crate::gc::{GcObject, RawGcObject};
5use crate::handle::RawHandle;
6use crate::state::{GlobalState, VmAllocator};
7use crate::types::LUA_TNIL;
8
9use super::{GCO_LINK_OFFSET, LUA_PAGE_PADDING};
10
11/// Mutable traversal state for the allocated objects in one VM page.
12///
13/// Unsafe traversal requires the page to remain live and unmodified, with a
14/// valid block size and end pointer, until the walk completes.
15pub struct GcoPageWalk {
16    pub(super) current: *mut u8,
17    pub(super) end: *mut u8,
18    pub(super) block_size: i32,
19    pub(super) steps: i32,
20}
21#[allow(
22    clippy::missing_safety_doc,
23    reason = "GcoPageWalk's shared traversal contract is documented on GcoPageWalk"
24)]
25impl GcoPageWalk {
26    pub unsafe fn next(&mut self) -> Option<GcObject> {
27        while self.current != self.end {
28            let block = self.current.cast::<RawGcObject>();
29            self.current = unsafe { self.current.add(self.block_size as usize) };
30            self.steps += 1;
31
32            if unsafe { (*block).tt } == LUA_TNIL as u8 {
33                continue;
34            }
35
36            return Some(unsafe { GcObject::from_raw(NonNull::new_unchecked(block)) });
37        }
38
39        None
40    }
41
42    pub fn steps(&self) -> i32 {
43        self.steps
44    }
45}
46
47#[repr(C)]
48pub struct RawLuaPage {
49    pub prev: *mut RawLuaPage,
50    pub next: *mut RawLuaPage,
51    pub list_prev: *mut RawLuaPage,
52    pub list_next: *mut RawLuaPage,
53    pub page_size: i32,
54    pub block_size: i32,
55    pub free_list: *mut u8,
56    pub free_next: i32,
57    pub busy_blocks: i32,
58    pub padding: [u8; LUA_PAGE_PADDING],
59    pub data: [u8; 1],
60}
61
62#[derive(Clone, Copy)]
63#[repr(transparent)]
64pub(crate) struct LuaPageList {
65    head: NonNull<*mut RawLuaPage>,
66}
67
68#[derive(Clone, Copy, PartialEq, Eq)]
69#[repr(transparent)]
70/// Non-owning identity of a VM allocation page.
71///
72/// # Safety model for unsafe methods
73///
74/// The page and its allocation must remain live. Block sizes, offsets, and
75/// traversal metadata must match the page class; page-list or allocator
76/// mutation can invalidate the handle and every pointer derived from it.
77pub struct LuaPage {
78    raw: NonNull<RawLuaPage>,
79}
80
81impl LuaPageList {
82    pub(crate) const unsafe fn from_raw(head: NonNull<*mut RawLuaPage>) -> Self {
83        Self { head }
84    }
85
86    pub(crate) unsafe fn get(&self) -> *mut RawLuaPage {
87        unsafe { *self.head.as_ptr() }
88    }
89
90    pub(crate) unsafe fn set(&self, page: *mut RawLuaPage) {
91        unsafe {
92            *self.head.as_ptr() = page;
93        }
94    }
95
96    pub(crate) unsafe fn link_page(&self, page: LuaPage) {
97        unsafe {
98            let page_ref = page.as_ptr().as_mut().unwrap_unchecked();
99            page_ref.list_next = self.get();
100            if let Some(mut next) = NonNull::new(page_ref.list_next) {
101                next.as_mut().list_prev = page.as_ptr();
102            }
103            self.set(page.as_ptr());
104        }
105    }
106
107    pub(crate) unsafe fn unlink_page(&self, page: LuaPage) {
108        unsafe {
109            let page_ref = page.as_ptr().as_mut().unwrap_unchecked();
110            if let Some(mut list_next) = NonNull::new(page_ref.list_next) {
111                list_next.as_mut().list_prev = page_ref.list_prev;
112            }
113
114            if let Some(mut list_prev) = NonNull::new(page_ref.list_prev) {
115                list_prev.as_mut().list_next = page_ref.list_next;
116            } else if self.get() == page.as_ptr() {
117                self.set(page_ref.list_next);
118            }
119
120            page_ref.list_prev = ptr::null_mut();
121            page_ref.list_next = ptr::null_mut();
122        }
123    }
124}
125
126impl GlobalState {
127    pub(crate) fn allocator(&self) -> NonNull<VmAllocator> {
128        unsafe { self.as_ptr().as_ref().unwrap_unchecked().allocator }
129    }
130
131    pub(crate) unsafe fn free_page_list(&self, size_class_index: usize) -> LuaPageList {
132        unsafe {
133            let slots = (&raw mut (*self.as_ptr()).free_pages).cast::<*mut RawLuaPage>();
134            LuaPageList::from_raw(NonNull::new_unchecked(slots.add(size_class_index)))
135        }
136    }
137
138    pub(crate) unsafe fn free_gco_page_list(&self, size_class_index: usize) -> LuaPageList {
139        unsafe {
140            let slots = (&raw mut (*self.as_ptr()).free_gco_pages).cast::<*mut RawLuaPage>();
141            LuaPageList::from_raw(NonNull::new_unchecked(slots.add(size_class_index)))
142        }
143    }
144
145    pub(crate) unsafe fn all_page_list(&self) -> LuaPageList {
146        unsafe {
147            LuaPageList::from_raw(NonNull::new_unchecked(&raw mut (*self.as_ptr()).all_pages))
148        }
149    }
150
151    pub(crate) unsafe fn all_gco_page_list(&self) -> LuaPageList {
152        unsafe {
153            LuaPageList::from_raw(NonNull::new_unchecked(
154                &raw mut (*self.as_ptr()).all_gco_pages,
155            ))
156        }
157    }
158
159    pub fn all_gco_pages(&self) -> Option<LuaPage> {
160        unsafe { NonNull::new((*self.as_ptr()).all_gco_pages).map(|raw| LuaPage::from_raw(raw)) }
161    }
162
163    pub fn sweep_gco_page(&self) -> Option<LuaPage> {
164        unsafe { NonNull::new((*self.as_ptr()).sweep_gco_page).map(|raw| LuaPage::from_raw(raw)) }
165    }
166
167    pub fn set_sweep_gco_page(&self, page: Option<LuaPage>) {
168        unsafe {
169            (*self.as_ptr()).sweep_gco_page = page.map_or(ptr::null_mut(), |page| page.as_ptr());
170        }
171    }
172}
173
174#[allow(
175    clippy::missing_safety_doc,
176    reason = "LuaPage's shared raw-handle contract is documented on LuaPage"
177)]
178impl LuaPage {
179    pub const unsafe fn from_raw(raw: NonNull<RawLuaPage>) -> Self {
180        Self { raw }
181    }
182
183    pub fn data_ptr(&self) -> *const u8 {
184        unsafe { self.as_ptr().cast::<u8>().add(offset_of!(RawLuaPage, data)) }
185    }
186
187    pub fn data_ptr_mut(&self) -> *mut u8 {
188        unsafe { self.as_ptr().cast::<u8>().add(offset_of!(RawLuaPage, data)) }
189    }
190
191    /// Returns a block address at `offset` bytes from the page data.
192    ///
193    /// # Safety
194    ///
195    /// `offset` must be non-negative, aligned for the page's block layout, and
196    /// identify a complete block within this page's allocation.
197    pub unsafe fn block(&self, offset: i32) -> *mut u8 {
198        unsafe { self.data_ptr_mut().add(offset as usize) }
199    }
200
201    pub fn page_blocks(&self) -> i32 {
202        let raw = unsafe { self.as_ptr().as_ref().unwrap_unchecked() };
203        ((raw.page_size as usize - offset_of!(RawLuaPage, data)) / raw.block_size as usize) as i32
204    }
205
206    /// # Safety
207    ///
208    /// `block` must address a live allocator block whose leading storage is a
209    /// pointer-sized metadata slot.
210    pub unsafe fn metadata_slot(block: *mut u8) -> *mut *mut u8 {
211        block.cast::<*mut u8>()
212    }
213
214    /// # Safety
215    ///
216    /// `block` must address a live GC block large enough to contain the
217    /// collector free-list link at `GCO_LINK_OFFSET`.
218    pub unsafe fn free_gco_link_slot(block: *mut u8) -> *mut *mut u8 {
219        unsafe { block.add(GCO_LINK_OFFSET).cast::<*mut u8>() }
220    }
221
222    pub unsafe fn gco_walk(&self) -> (GcoPageWalk, i32) {
223        let page = unsafe { self.as_ptr().as_ref().unwrap_unchecked() };
224        let block_count =
225            (page.page_size as usize - offset_of!(RawLuaPage, data)) / page.block_size as usize;
226
227        (
228            GcoPageWalk {
229                current: unsafe {
230                    self.data_ptr_mut()
231                        .add((page.free_next + page.block_size) as usize)
232                },
233                end: unsafe {
234                    self.data_ptr_mut()
235                        .add(block_count * page.block_size as usize)
236                },
237                block_size: page.block_size,
238                steps: 0,
239            },
240            page.busy_blocks,
241        )
242    }
243
244    /// `luaM_getnextpage`
245    pub unsafe fn next_page(&self) -> Option<LuaPage> {
246        NonNull::new(unsafe { self.as_ptr().as_ref().unwrap_unchecked().list_next })
247            .map(|page| unsafe { Self::from_raw(page) })
248    }
249
250    /// `luaM_visitpage`
251    pub unsafe fn visit_page(
252        &self,
253        context: *mut (),
254        visitor: unsafe fn(*mut (), LuaPage, GcObject) -> bool,
255    ) {
256        unsafe {
257            let (mut walk, mut busy) = self.gco_walk();
258            while let Some(gco) = walk.next() {
259                let should_delete = visitor(context, *self, gco);
260                if should_delete {
261                    busy -= 1;
262                    if busy == 0 {
263                        break;
264                    }
265                }
266            }
267        }
268    }
269}
270
271impl crate::handle::sealed::Sealed for LuaPage {}
272
273impl RawHandle for LuaPage {
274    type Raw = RawLuaPage;
275
276    fn as_ptr(&self) -> *mut Self::Raw {
277        self.raw.as_ptr()
278    }
279}
280
281impl AsRef<LuaPage> for LuaPage {
282    fn as_ref(&self) -> &LuaPage {
283        self
284    }
285}