use core::mem::size_of;
use core::ptr::{self, NonNull};
use luau_common::{ByteSlice, flags};
use crate::call::ThreadStack;
use crate::debug::DebugRuntime;
use crate::function::{FunctionRuntime, RawNativeClosure};
use crate::gc::GcBarrier;
use crate::gc::{GcObject, RawGcObject};
use crate::handle::RawHandle;
use crate::handle::sealed::Sealed;
use crate::memory::{LuaPage, MemoryRuntime};
use crate::metamethod::{TM_N, TmEvent};
use crate::native::{NativeCallContext, NativeCallResult};
use crate::state::ThreadState;
use crate::string::{RawTString, StringRuntime, TString};
use crate::table::{RawLuaTable, TableRuntime};
use crate::thread::Thread;
use crate::types::{LUA_TCLASS, LUA_TOBJECT};
use crate::value::{RAW_TVALUE_NIL, RawTValue, TValue};
use crate::vm::VmOperations;
use crate::{Table, VmErrorResult};
#[repr(C)]
pub struct RawLuauClass {
pub tt: u8,
pub marked: u8,
pub memcat: u8,
pub gc_list: *mut RawGcObject,
pub name: *mut RawTString,
pub static_members: *mut RawTValue,
pub members_to_offset: *mut RawLuaTable,
pub offset_to_member: *mut *mut RawTString,
pub metatable: *mut RawLuaTable,
pub instance_metatable: *mut RawLuaTable,
pub number_of_instance_members: u32,
pub number_of_all_members: u32,
}
#[repr(C)]
pub struct RawLuauObject {
pub tt: u8,
pub marked: u8,
pub memcat: u8,
pub gc_list: *mut RawGcObject,
pub class: *mut RawLuauClass,
pub number_of_members: u32,
pub members: *mut RawTValue,
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Class {
pub(crate) raw: NonNull<RawLuauClass>,
}
#[allow(
clippy::missing_safety_doc,
reason = "all methods share the capability-level safety contract"
)]
pub trait ClassRuntime: Sealed {
unsafe fn new_blank_class(&self, name: TString) -> VmErrorResult<Class>;
unsafe fn add_class_metatable(&self, class: Class) -> VmErrorResult;
unsafe fn new_class(
&self,
name: TString,
members_to_offset: Table,
offset_to_member: *mut TString,
number_of_instance_members: u32,
number_of_static_members: u32,
) -> VmErrorResult<Class>;
unsafe fn inherit_class(&self, child: Class, parent: Class) -> VmErrorResult<Class>;
unsafe fn free_class(&self, class: Class, page: LuaPage);
unsafe fn free_object(&self, object: Object, page: LuaPage);
unsafe fn create_object(&self) -> NativeCallResult;
}
#[allow(
clippy::missing_safety_doc,
reason = "Class's shared raw-handle contract is documented on Class"
)]
impl Class {
pub const unsafe fn from_raw(raw: NonNull<RawLuauClass>) -> Self {
Self { raw }
}
pub unsafe fn name(&self) -> TString {
unsafe {
TString::from_raw(NonNull::new_unchecked(
self.as_ptr().as_ref().unwrap_unchecked().name,
))
}
}
pub unsafe fn members_to_offset(&self) -> Table {
unsafe {
Table::from_raw(NonNull::new_unchecked(
self.as_ptr().as_ref().unwrap_unchecked().members_to_offset,
))
}
}
pub unsafe fn offset_to_member(&self, index: usize) -> TString {
unsafe {
TString::from_raw(NonNull::new_unchecked(
*self
.as_ptr()
.as_ref()
.unwrap_unchecked()
.offset_to_member
.add(index),
))
}
}
pub unsafe fn static_member(&self, index: usize) -> TValue {
unsafe {
TValue::from_raw(NonNull::new_unchecked(
self.as_ptr()
.as_ref()
.unwrap_unchecked()
.static_members
.add(index),
))
}
}
pub fn static_member_count(&self) -> usize {
let class = unsafe { self.as_ptr().as_ref().unwrap_unchecked() };
(class.number_of_all_members - class.number_of_instance_members) as usize
}
pub fn member_count(&self) -> usize {
unsafe {
self.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_all_members as usize
}
}
pub unsafe fn allocation_size(&self) -> usize {
size_of::<RawLuauClass>()
+ self.static_member_count() * size_of::<RawTValue>()
+ self.member_count() * size_of::<*mut RawTString>()
}
pub unsafe fn gc_list(&self) -> Option<GcObject> {
unsafe {
NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().gc_list)
.map(|raw| GcObject::from_raw(raw))
}
}
pub unsafe fn set_gc_list(&self, gc_list: Option<GcObject>) {
unsafe {
self.as_ptr().as_mut().unwrap_unchecked().gc_list =
gc_list.map_or(ptr::null_mut(), |object| object.as_ptr());
}
}
pub unsafe fn metatable(&self) -> Option<Table> {
unsafe {
NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().metatable)
.map(|raw| Table::from_raw(raw))
}
}
pub unsafe fn set_metatable(&self, metatable: Option<Table>) {
unsafe {
self.as_ptr().as_mut().unwrap_unchecked().metatable =
metatable.map_or(core::ptr::null_mut(), |table| table.as_ptr());
}
}
pub unsafe fn instance_metatable(&self) -> Option<Table> {
unsafe {
NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().instance_metatable)
.map(|raw| Table::from_raw(raw))
}
}
pub unsafe fn set_instance_metatable(&self, metatable: Option<Table>) {
unsafe {
self.as_ptr().as_mut().unwrap_unchecked().instance_metatable =
metatable.map_or(core::ptr::null_mut(), |table| table.as_ptr());
}
}
pub unsafe fn member_offset(&self, key: TValue) -> Option<u32> {
let offset = unsafe { self.members_to_offset().get(key) };
if offset.is_nil() {
None
} else {
debug_assert!(offset.is_number());
Some(offset.number_value() as u32)
}
}
pub unsafe fn member_offset_str(&self, key: TString) -> Option<u32> {
let offset = unsafe { self.members_to_offset().get_str(key) };
if offset.is_nil() {
None
} else {
debug_assert!(offset.is_number());
Some(offset.number_value() as u32)
}
}
pub unsafe fn lookup_static_member(&self, key: TValue) -> Option<TValue> {
unsafe {
let offset = self.member_offset(key)?;
if offset
< self
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members
{
None
} else {
Some(
self.static_member(
(offset
- self
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members) as usize,
),
)
}
}
}
pub unsafe fn add_class_member(
&self,
thread: &Thread,
name: TString,
method: TValue,
) -> VmErrorResult {
unsafe {
let Some(offset) = self.member_offset_str(name) else {
return Ok(());
};
debug_assert!(
offset
>= self
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members
&& offset
< self
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_all_members
);
debug_assert!(method.is_function());
let static_member = self.static_member(
(offset
- self
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members) as usize,
);
static_member.set_obj(method);
let class_object: GcObject = (*self).into();
if method.is_collectable() && class_object.is_black() {
let child = method.gc_value();
if child.is_white() {
thread.barrier_forward(class_object, child);
}
}
let global = thread.global();
let mut is_metamethod = name == thread.intern_string(b"__tostring".as_bstr())?;
for index in 0..TM_N {
if is_metamethod {
break;
}
is_metamethod = global.tm_name(index) == name;
}
if is_metamethod {
let instance_metatable = match self.instance_metatable() {
Some(table) => table,
None => {
let table = thread.new_table_internal(0, 1)?;
self.set_instance_metatable(Some(table));
let table_object: GcObject = table.into();
if class_object.is_black() && table_object.is_white() {
thread.barrier_forward(class_object, table_object);
}
table
}
};
let node_cursor = thread.set_str(instance_metatable, name)?;
node_cursor
.node_unchecked()
.value_unchecked()
.set_obj(method);
let table_object: GcObject = instance_metatable.into();
if method.is_collectable() && table_object.is_black() {
let child = method.gc_value();
if child.is_white() {
thread.barrier_forward(table_object, child);
}
}
}
}
Ok(())
}
unsafe fn register_static_member(
&self,
thread: &Thread,
name: TString,
value: TValue,
offset: u32,
static_offset: u32,
) -> VmErrorResult {
unsafe {
let static_member = self.static_member(static_offset as usize);
static_member.set_obj(value);
let class_object: GcObject = (*self).into();
if value.is_collectable() && class_object.is_black() {
let child = value.gc_value();
if child.is_white() {
thread.barrier_forward(class_object, child);
}
}
*self
.as_ptr()
.as_ref()
.unwrap_unchecked()
.offset_to_member
.add(offset as usize) = name.as_ptr();
let members_to_offset = self.members_to_offset();
let node = thread.set_str(members_to_offset, name)?;
node.node_unchecked()
.value_unchecked()
.set_number(offset as f64);
}
Ok(())
}
}
impl crate::handle::sealed::Sealed for Class {}
impl RawHandle for Class {
type Raw = RawLuauClass;
fn as_ptr(&self) -> *mut Self::Raw {
self.raw.as_ptr()
}
}
impl AsRef<Class> for Class {
fn as_ref(&self) -> &Class {
self
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Object {
pub(crate) raw: NonNull<RawLuauObject>,
}
#[derive(Clone, Copy)]
pub enum ObjectMemberLookup {
Cached(TValue),
Resolved { offset: u32, member: TValue },
Missing,
}
#[allow(
clippy::missing_safety_doc,
reason = "Object's shared raw-handle contract is documented on Object"
)]
impl Object {
pub const unsafe fn from_raw(raw: NonNull<RawLuauObject>) -> Self {
Self { raw }
}
pub unsafe fn class(&self) -> Class {
unsafe {
Class::from_raw(NonNull::new_unchecked(
self.as_ptr().as_ref().unwrap_unchecked().class,
))
}
}
pub unsafe fn member(&self, index: usize) -> TValue {
unsafe {
TValue::from_raw(NonNull::new_unchecked(
self.as_ptr().as_ref().unwrap_unchecked().members.add(index),
))
}
}
pub unsafe fn gc_list(&self) -> Option<GcObject> {
unsafe {
NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().gc_list)
.map(|raw| GcObject::from_raw(raw))
}
}
pub unsafe fn set_gc_list(&self, gc_list: Option<GcObject>) {
unsafe {
self.as_ptr().as_mut().unwrap_unchecked().gc_list =
gc_list.map_or(ptr::null_mut(), |object| object.as_ptr());
}
}
pub unsafe fn gc_list_slot(&self) -> *mut *mut RawGcObject {
unsafe { &mut self.as_ptr().as_mut().unwrap_unchecked().gc_list }
}
pub unsafe fn allocation_size(&self) -> usize {
unsafe {
size_of::<RawLuauObject>()
+ self.as_ptr().as_ref().unwrap_unchecked().number_of_members as usize
* size_of::<RawTValue>()
}
}
pub fn offset_in_bounds(&self, offset: u32) -> bool {
let class = unsafe { self.class() };
offset
< unsafe {
class
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_all_members
}
}
pub unsafe fn lookup_member_at_offset(&self, offset: u32) -> TValue {
debug_assert!(self.offset_in_bounds(offset));
unsafe {
let class = self.class();
if offset
< class
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members
{
self.member(offset as usize)
} else {
class.static_member(
(offset
- class
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members) as usize,
)
}
}
}
pub unsafe fn lookup_member(&self, key: TValue) -> Option<TValue> {
unsafe {
self.class()
.member_offset(key)
.map(|offset| self.lookup_member_at_offset(offset))
}
}
pub unsafe fn lookup_member_cached(&self, key: TString, slot: u8) -> ObjectMemberLookup {
unsafe {
let class = self.class();
let slot_offset = u32::from(slot);
if slot_offset
< class
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_all_members
&& class.offset_to_member(slot as usize) == key
{
return ObjectMemberLookup::Cached(self.lookup_member_at_offset(slot_offset));
}
match class.member_offset_str(key) {
Some(offset) => ObjectMemberLookup::Resolved {
offset,
member: self.lookup_member_at_offset(offset),
},
None => ObjectMemberLookup::Missing,
}
}
}
}
impl crate::handle::sealed::Sealed for Object {}
impl RawHandle for Object {
type Raw = RawLuauObject;
fn as_ptr(&self) -> *mut Self::Raw {
self.raw.as_ptr()
}
}
impl AsRef<Object> for Object {
fn as_ref(&self) -> &Object {
self
}
}
fn create_object_callback(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe { thread.create_object() }
}
impl ClassRuntime for Thread {
unsafe fn new_blank_class(&self, name: TString) -> VmErrorResult<Class> {
unsafe {
let class = self.new_gco::<Class>(
core::mem::size_of::<RawLuauClass>(),
self.as_ptr().as_ref().unwrap_unchecked().active_memcat,
)?;
GcObject::from(class).init_header(self, LUA_TCLASS as u8);
let class_ref = class.as_ptr().as_mut().unwrap_unchecked();
class_ref.gc_list = ptr::null_mut();
class_ref.name = name.as_ptr();
class_ref.static_members = ptr::null_mut();
class_ref.members_to_offset = ptr::null_mut();
class_ref.offset_to_member = ptr::null_mut();
class_ref.metatable = ptr::null_mut();
class_ref.instance_metatable = ptr::null_mut();
class_ref.number_of_instance_members = 0;
class_ref.number_of_all_members = 0;
Ok(class)
}
}
unsafe fn add_class_metatable(&self, class: Class) -> VmErrorResult {
unsafe {
let metatable = self.new_table_internal(0, 1)?;
let global_env = self.globals();
let constructor = self.new_native_closure(0, Some(global_env))?;
let debug_name = if flags::LuauManagedDebugNames.get() {
self.intern_string(b"luaR_createobject".as_bstr())?.as_ptr()
} else {
ptr::null_mut()
};
constructor.set_native_data(RawNativeClosure {
function: Some(create_object_callback),
continuation: None,
debug_name_deprecated: (!flags::LuauManagedDebugNames.get())
.then_some("luaR_createobject"),
debug_name,
});
let call_name = self.global().tm_name(TmEvent::Call as usize);
let node_cursor = self.set_str(metatable, call_name)?;
node_cursor
.node_unchecked()
.value_unchecked()
.set_closure_value(constructor);
metatable.as_ptr().as_mut().unwrap_unchecked().readonly = 1;
class.set_metatable(Some(metatable));
Ok(())
}
}
unsafe fn new_class(
&self,
name: TString,
members_to_offset: Table,
offset_to_member: *mut TString,
number_of_instance_members: u32,
number_of_static_members: u32,
) -> VmErrorResult<Class> {
unsafe {
debug_assert_eq!(
self.global()
.as_ptr()
.as_ref()
.unwrap_unchecked()
.gc_threshold,
usize::MAX
);
let class = self.new_blank_class(name)?;
let static_members = self.new_array::<RawTValue>(
number_of_static_members as usize,
class.as_ptr().as_ref().unwrap_unchecked().memcat,
)?;
let class_ref = class.as_ptr().as_mut().unwrap_unchecked();
class_ref.static_members = static_members;
class_ref.members_to_offset = members_to_offset.as_ptr();
class_ref.offset_to_member = offset_to_member.cast();
class_ref.number_of_instance_members = number_of_instance_members;
class_ref.number_of_all_members = number_of_instance_members + number_of_static_members;
for index in 0..number_of_static_members as usize {
class.static_member(index).set_nil();
}
self.add_class_metatable(class)?;
Ok(class)
}
}
unsafe fn inherit_class(&self, child: Class, parent: Class) -> VmErrorResult<Class> {
unsafe {
let child_instance_members = child
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members;
let child_members = child
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_all_members;
let parent_instance_members = parent
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members;
let parent_members = parent
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_all_members;
for index in 0..parent_instance_members {
let name = parent.offset_to_member(index as usize);
if child.member_offset_str(name).is_some() {
let parent_name = parent.name();
let child_name = child.name();
return crate::run_error!(
self,
"Cannot override instance member '%s' of parent class '%s' in child class '%s'",
name.as_bytes(),
parent_name.as_bytes(),
child_name.as_bytes(),
);
}
}
let class = self.new_blank_class(child.name())?;
let mut inherited_static_members = 0u32;
for index in parent_instance_members..parent_members {
let name = parent.offset_to_member(index as usize);
if child.member_offset_str(name).is_none() {
inherited_static_members += 1;
}
}
let member_count = child_members + parent_instance_members + inherited_static_members;
let memcat = class.as_ptr().as_ref().unwrap_unchecked().memcat;
let offset_to_member =
self.new_array::<*mut RawTString>(member_count as usize, memcat)?;
let members_to_offset = self.new_table_internal(0, member_count as i32)?;
{
let class_ref = class.as_ptr().as_mut().unwrap_unchecked();
class_ref.offset_to_member = offset_to_member;
class_ref.members_to_offset = members_to_offset.as_ptr();
class_ref.number_of_all_members = member_count;
}
let mut offset = 0u32;
for index in 0..parent_instance_members {
let name = parent.offset_to_member(index as usize);
*offset_to_member.add(offset as usize) = name.as_ptr();
let value = self.set_str(members_to_offset, name)?;
value
.node_unchecked()
.value_unchecked()
.set_number(offset as f64);
offset += 1;
}
for index in 0..child_instance_members {
let name = child.offset_to_member(index as usize);
*offset_to_member.add(offset as usize) = name.as_ptr();
let value = self.set_str(members_to_offset, name)?;
value
.node_unchecked()
.value_unchecked()
.set_number(offset as f64);
offset += 1;
}
let static_members =
self.new_array::<RawTValue>((member_count - offset) as usize, memcat)?;
{
let class_ref = class.as_ptr().as_mut().unwrap_unchecked();
class_ref.static_members = static_members;
class_ref.number_of_instance_members = offset;
}
let mut static_offset = 0u32;
for index in parent_instance_members..parent_members {
let name = parent.offset_to_member(index as usize);
if child.member_offset_str(name).is_none() {
let value = parent.static_member((index - parent_instance_members) as usize);
class.register_static_member(self, name, value, offset, static_offset)?;
offset += 1;
static_offset += 1;
}
}
for index in child_instance_members..child_members {
let name = child.offset_to_member(index as usize);
let value = child.static_member((index - child_instance_members) as usize);
class.register_static_member(self, name, value, offset, static_offset)?;
offset += 1;
static_offset += 1;
}
debug_assert_eq!(offset, member_count);
self.add_class_metatable(class)?;
if let Some(instance_metatable) = parent.instance_metatable() {
let cloned = self.clone_table_internal(instance_metatable)?;
class.set_instance_metatable(Some(cloned));
}
Ok(class)
}
}
unsafe fn free_class(&self, class: Class, page: LuaPage) {
unsafe {
let class_ref = class.as_ptr().as_ref().unwrap_unchecked();
let memcat = class_ref.memcat;
let static_members = class_ref.static_members;
let static_member_count =
(class_ref.number_of_all_members - class_ref.number_of_instance_members) as usize;
let offset_to_member = class_ref.offset_to_member;
let total_members = class_ref.number_of_all_members as usize;
if !static_members.is_null() {
self.free_array(static_members, static_member_count, memcat);
}
if !offset_to_member.is_null() {
self.free_array(offset_to_member, total_members, memcat);
}
self.free_gco(
class.into(),
core::mem::size_of::<RawLuauClass>(),
memcat,
page,
);
}
}
unsafe fn free_object(&self, object: Object, page: LuaPage) {
unsafe {
let object_ref = object.as_ptr().as_ref().unwrap_unchecked();
let members = object_ref.members;
let number_of_members = object_ref.number_of_members as usize;
let memcat = object_ref.memcat;
self.free_array(members, number_of_members, memcat);
self.free_gco(
object.into(),
core::mem::size_of::<RawLuauObject>(),
memcat,
page,
);
}
}
unsafe fn create_object(&self) -> NativeCallResult {
unsafe {
self.check_type(1, LUA_TCLASS)?;
let class = self.stack_base().value_unchecked().class_value();
let num_args = self.get_top();
let active_memcat = self.as_ptr().as_ref().unwrap_unchecked().active_memcat;
let object =
self.new_gco::<Object>(core::mem::size_of::<RawLuauObject>(), active_memcat)?;
let object_gco: GcObject = object.into();
object_gco.init_header(self, LUA_TOBJECT as u8);
let member_count = class
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members as usize;
let members = self.new_array::<RawTValue>(member_count, active_memcat)?;
let object_ref = object.as_ptr().as_mut().unwrap_unchecked();
object_ref.gc_list = core::ptr::null_mut();
object_ref.class = class.as_ptr();
object_ref.number_of_members = class
.as_ptr()
.as_ref()
.unwrap_unchecked()
.number_of_instance_members;
object_ref.members = members;
for index in 0..member_count {
object.member(index).set_nil();
}
let object_slot = self.stack_top();
object_slot.value_unchecked().set_object_value(object);
self.set_stack_top(object_slot.add(1));
let value_slot = self.stack_top();
value_slot.value_unchecked().set_nil();
self.set_stack_top(value_slot.add(1));
let value_slot_offset = self.save_stack(value_slot);
match num_args {
1 => {}
2 => {
for index in 0..member_count {
let mut key_storage = RAW_TVALUE_NIL;
let key = TValue::from_mut(&mut key_storage);
key.set_string_value(class.offset_to_member(index));
let indexed_value = self.stack_base().add(1).value_unchecked();
let value_slot = self.restore_stack(value_slot_offset);
self.get_table_internal(indexed_value, key, value_slot)?;
let value_slot = self.restore_stack(value_slot_offset);
object.member(index).set_obj(value_slot.value_unchecked());
}
}
_ => {
let class_name = class.name();
return crate::error!(
self,
"wrong number of arguments for constructing a '%s'",
class_name.as_bytes()
)
.map_err(Into::into);
}
}
self.set_stack_top(self.restore_stack(value_slot_offset));
if object_gco.is_black() {
self.barrier_back(object_gco, object.gc_list_slot());
}
Ok(1)
}
}
}