diff --git a/.gitattributes b/.gitattributes index 3cb3e3eb..38d1ab4f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,7 @@ # Ensure all txt files within tests use LF tests/**/*.txt eol=lf + +# Remove test c and typescript from language list +tests/ffi/**/*.c linguist-vendored +tests/ffi/**/*.ts linguist-vendored diff --git a/.gitignore b/.gitignore index 6f7b83e6..b7719c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,15 @@ luneDocs.json luneTypes.d.luau # Files generated by runtime or build scripts + scripts/brick_color.rs scripts/font_enum_map.rs scripts/physical_properties_enum_map.rs + +# Files generated by tests + +/tests/ffi/**/*.so + +# Core dump file + +/core diff --git a/Cargo.lock b/Cargo.lock index 5bc12bed..4ec79331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -787,6 +787,29 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "dlopen2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "dunce" version = "1.0.5" @@ -1422,9 +1445,28 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.160" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" + +[[package]] +name = "libffi" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce826c243048e3d5cec441799724de52e2d42f820468431fc3fceee2341871e2" +dependencies = [ + "libc", + "libffi-sys", +] + +[[package]] +name = "libffi-sys" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36115160c57e8529781b4183c2bb51fdc1f6d6d1ed345591d84be7703befb3c" +dependencies = [ + "cc", +] [[package]] name = "libloading" @@ -1557,6 +1599,7 @@ name = "lune-std" version = "0.1.5" dependencies = [ "lune-std-datetime", + "lune-std-ffi", "lune-std-fs", "lune-std-luau", "lune-std-net", @@ -1585,6 +1628,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "lune-std-ffi" +version = "0.1.1" +dependencies = [ + "dlopen2", + "libc", + "libffi", + "lune-utils", + "mlua", + "mlua-sys", + "num", +] + [[package]] name = "lune-std-fs" version = "0.1.2" @@ -1880,6 +1936,39 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1895,6 +1984,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" diff --git a/Cargo.toml b/Cargo.toml index 221a0fb0..f32f08fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "crates/lune-std-serde", "crates/lune-std-stdio", "crates/lune-std-task", + "crates/lune-std-ffi", "crates/lune-utils", "crates/mlua-luau-scheduler", ] diff --git a/crates/lune-std-ffi/Cargo.toml b/crates/lune-std-ffi/Cargo.toml new file mode 100644 index 00000000..78eebda6 --- /dev/null +++ b/crates/lune-std-ffi/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "lune-std-ffi" +version = "0.1.1" +edition = "2021" +license = "MPL-2.0" +repository = "https://github.com/lune-org/lune" +description = "Lune standard library - FFI" + +[lib] +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mlua = { version = "0.9.9", features = ["luau"] } +mlua-sys = { version = "0.6.2", features = ["luau"] } +num = "0.4.3" +dlopen2 = "0.7.0" +libc = "0.2.162" + +libffi = "3.2.0" + +lune-utils = { version = "0.1.3", path = "../lune-utils" } diff --git a/crates/lune-std-ffi/README.md b/crates/lune-std-ffi/README.md new file mode 100644 index 00000000..a6ad488e --- /dev/null +++ b/crates/lune-std-ffi/README.md @@ -0,0 +1,146 @@ + + +# `lune-std-ffi` + +## Tests & Benchmarks + +See [tests/ffi](../../tests/ffi/README.md) + +## TODO + +- [CString](./src/c/string_info.rs) +- Add buffer as owned data support +- Add math operation for numeric types + > Provide related methods: `CTypeInfo:add(target, from1, from2, ...)` and `:sub` `:mul` `:div` `:mod` `:pow` `:max` `:min` `:gt` `:lt` + > Luau cannot handle f64, i64 or i128, so we should provide math operation for it +- Add bit operation for box/ref + > Luau only supports 32bit bit operations +- Add wchar and wstring support + > For windows API support +- Add varargs support +- Array argument in cfn +- [More box/ref methods](./src/data/helper.rs) + - writeString + - readString + - writeBase64 + - readBase64 + +## Code structure + +### /c + +Define C-ABI type information and provide conversion and casting + +**Structs:** C ABI type informations + +- [**Struct `CArrInfo`:**](./src/c/arr_info.rs) Represents C Array type +- [**Struct `CPtrInfo`:**](./src/c/ptr_info.rs) Represents C Pointer type +- [**Struct `CFnInfo`:**](./src/c/fn_info.rs) Represents C Function signature + > provide `CallableData` and `ClosureData` creator +- [**Struct `CStructInfo`:**](./src/c/struct_info.rs) Represents C Struct type +- [**Struct `CTypeInfo`:**](./src/c/type_info.rs) Represents C type, extended in `/c/types` + +
Mod helper.rs: C ABI type helper + +- **Function `get_conv`, `get_conv_list`:** + get `FfiConvert` from userdata (CStruct, CArr, CPtr, CTypes) +- **Function `get_middle_type`, `get_middle_type_list`:** + get **`libffi::middle::Type`:** from userdata (CFn, CStruct, CArr, CPtr, CTypes) +- **Function `get_size`:** + get size from userdata +- **Function `has_void`:** + check table has void type +- **Function `stringify`:** + stringify any type userdata +- **Function `get_name`:** + get type name from ctype userdata, used for pretty-print +- **Function `is_ctype`:** check userdata is ctype +- **Mod `method_provider`:** provide common userdata method implements + +
+ +#### /c/types + +Export fixed-size source time known types and non-fixed compile time known types +mod.rs implememts type-casting for all CTypes + +
Mod ctype_helper: CTypeInfo helper + +- **Function `get_conv`:** + get `FfiConvert` from ctype userdata, used for struct and array conversion +- **Function `get_middle_type`:** + get **`libffi::middle::Type`:** from ctype userdata +- **Function `get_size`:** + get size from ctype userdata +- **Function `get_name`:** + get type name from ctype userdata, used for pretty-print +- **Function `is_ctype`:** check userdata is ctype + +
+ +--- + +### /data + +**Structs:** Provide memory userdata + +- [**Struct `BoxData`:**](./src/data/box_data/mod.rs) A heap allocated memory with user definable lifetime +- [**Struct `LibData`:**](./src/data/lib_data.rs) A dynamic opened library +- [**Struct `RefData`:**](./src/data/ref_data/mod.rs) A reference that can be used for receiving return data from external function or pass pointer arguments + +**Structs:** Provide function(pointer) userdata + +- [**Struct `CallableData`:**](./src/data/callable_data.rs) A callable function, which can be created from function pointer +- [**Struct `ClosureData`:**](./src/data/closure_data.rs) A closure pointer, which can be created from lua function and can be used for callback + +--- + +### /ffi + +**Traits:** Provide ABI shared common type information trait + +- **Trait `FfiSize`** +- **Trait `FfiSignedness`** + + + +**Structs:** Provide call information + +- **Struct `FfiArg`:** Used for argument boundary checking and callback argument ref flag +- **Struct `FfiResult`:** Used for result boundary checking + +
Trait FfiData: Provide common data handle, including methods below + +- **Method `check_inner_boundary`:** check boundary with offset and size +- **Method `get_inner_pointer`:** returns raw pointer `*mut ()` +- **Method `is_writable`** +- **Method `is_readable`** +- **Method `copy_from`** copy data from another data + +
+ +> Note: `GetFfiData` trait in `data/mod.rs` provides `(LuaValue | LuaAnyUserData).get_data_handle() -> FfiData` method + +**Mods:** Provide common helper functions + +- [**Mod `association.rs`:**](./src/ffi/association.rs) GC utility, used for inner, ret and arg type holding in subtype +- [**Mod `bit_mask.rs`:**](./src/ffi/bit_mask.rs) u8 bitfield helper +- [**Mod `cast.rs`:**](./src/ffi/cast.rs) num cast library wrapper + - **Function `num_cast(from: FfiData, from: FfiData)`:** + Cast number type value inno another number type + + diff --git a/crates/lune-std-ffi/src/c/arr_info.rs b/crates/lune-std-ffi/src/c/arr_info.rs new file mode 100644 index 00000000..1668bd65 --- /dev/null +++ b/crates/lune-std-ffi/src/c/arr_info.rs @@ -0,0 +1,180 @@ +use std::cell::Ref; + +use libffi::middle::Type; +use mlua::prelude::*; + +use super::{association_names::CARR_INNER, helper, method_provider}; +use crate::ffi::{association, libffi_helper::get_ensured_size, FfiConvert, FfiData, FfiSize}; + +// Series of some type. +// It provides the final size and the offset of the index, +// but does not allow multidimensional arrays because of API complexity. +// Multidimensional arrays can be implemented +// because they are a series of transcribed one-dimensional arrays. (flatten) +// We can simply provide array type with struct. +// See: https://stackoverflow.com/a/43525176 +pub struct CArrInfo { + struct_type: Type, + length: usize, + size: usize, + inner_size: usize, + inner_conv: *const dyn FfiConvert, +} + +impl CArrInfo { + pub fn new( + element_type: Type, + length: usize, + inner_conv: *const dyn FfiConvert, + ) -> LuaResult { + let inner_size = get_ensured_size(element_type.as_raw_ptr())?; + let struct_type = Type::structure(vec![element_type.clone(); length]); + + Ok(Self { + struct_type, + length, + size: inner_size * length, + inner_size, + inner_conv, + }) + } + + pub fn from_userdata<'lua>( + lua: &'lua Lua, + type_userdata: &LuaAnyUserData<'lua>, + length: usize, + ) -> LuaResult> { + let fields = helper::get_middle_type(type_userdata)?; + let conv = unsafe { helper::get_conv(type_userdata)? }; + let carr = lua.create_userdata(Self::new(fields, length, conv)?)?; + + association::set(lua, CARR_INNER, &carr, type_userdata)?; + Ok(carr) + } + + pub fn get_length(&self) -> usize { + self.length + } + + pub fn get_middle_type(&self) -> Type { + self.struct_type.clone() + } + + // Stringify for pretty-print + // ex: + pub fn stringify(lua: &Lua, userdata: &LuaAnyUserData) -> LuaResult { + let this = userdata.borrow::()?; + if let Some(LuaValue::UserData(inner_userdata)) = + association::get(lua, CARR_INNER, userdata)? + { + Ok(format!( + " {}, length = {} ", + helper::pretty_format(lua, &inner_userdata)?, + this.length, + )) + } else { + Err(LuaError::external("Failed to retrieve inner type")) + } + } +} + +impl FfiSize for CArrInfo { + fn get_size(&self) -> usize { + self.size + } +} + +impl FfiConvert for CArrInfo { + unsafe fn value_into_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let LuaValue::Table(ref table) = value else { + return Err(LuaError::external("Value is not a table")); + }; + for i in 0..self.length { + let field_offset = (i * self.inner_size) as isize; + let data: LuaValue = table.get(i + 1)?; + + self.inner_conv.as_ref().unwrap().value_into_data( + lua, + field_offset + offset, + data_handle, + data, + )?; + } + Ok(()) + } + + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let table = lua.create_table_with_capacity(self.length, 0)?; + for i in 0..self.length { + let field_offset = (i * self.inner_size) as isize; + table.set( + i + 1, + self.inner_conv.as_ref().unwrap().value_from_data( + lua, + field_offset + offset, + data_handle, + )?, + )?; + } + Ok(LuaValue::Table(table)) + } + + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + dst.get_inner_pointer().byte_offset(dst_offset).copy_from( + src.get_inner_pointer().byte_offset(src_offset), + self.get_size(), + ); + Ok(()) + } +} + +impl LuaUserData for CArrInfo { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("size", |_lua, this| Ok(this.get_size())); + fields.add_field_method_get("length", |_lua, this| Ok(this.get_length())); + fields.add_field_function_get("inner", |lua, this: LuaAnyUserData| { + association::get(lua, CARR_INNER, this)? + .ok_or_else(|| LuaError::external("Failed to retrieve inner field")) + }); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Subtype + method_provider::provide_ptr(methods); + + // ToString + method_provider::provide_to_string(methods); + + // Realize + method_provider::provide_box(methods); + method_provider::provide_read_data(methods); + method_provider::provide_write_data(methods); + method_provider::provide_copy_data(methods); + + methods.add_method("offset", |_lua, this, offset: isize| { + if this.length > (offset as usize) && offset >= 0 { + Ok(this.inner_size * (offset as usize)) + } else { + Err(LuaError::external("Out of index")) + } + }); + } +} diff --git a/crates/lune-std-ffi/src/c/fn_info.rs b/crates/lune-std-ffi/src/c/fn_info.rs new file mode 100644 index 00000000..963d8a0b --- /dev/null +++ b/crates/lune-std-ffi/src/c/fn_info.rs @@ -0,0 +1,218 @@ +use libffi::middle::{Cif, Type}; +use mlua::prelude::*; + +use super::{ + association_names::{ + CALLABLE_CFN, CALLABLE_REF, CFN_ARGS, CFN_RESULT, CLOSURE_CFN, CLOSURE_FUNC, + }, + ctype_helper::is_ctype, + helper, method_provider, CArrInfo, CPtrInfo, CStructInfo, +}; +use crate::{ + data::{CallableData, ClosureData, RefData, RefFlag}, + ffi::{ + association, bit_field::*, libffi_helper::SIZE_OF_POINTER, FfiArg, FfiData, FfiResult, + FfiSignedness, FfiSize, + }, +}; + +// Function pointer type +pub struct CFnInfo { + cif: Cif, + arg_info_list: Vec, + result_info: FfiResult, +} + +impl FfiSignedness for CFnInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiSize for CFnInfo { + fn get_size(&self) -> usize { + SIZE_OF_POINTER + } +} + +const CALLBACK_ARG_REF_FLAG_TYPE: u8 = RefFlag::Readable.value(); +const CALLBACK_ARG_REF_FLAG_PTR: u8 = RefFlag::Dereferenceable.value() | RefFlag::Readable.value(); +const CALLBACK_ARG_REF_FLAG_ARR: u8 = RefFlag::Readable.value() | RefFlag::Offsetable.value(); +const CALLBACK_ARG_REF_FLAG_STRUCT: u8 = RefFlag::Readable.value() | RefFlag::Offsetable.value(); +const CALLBACK_ARG_REF_FLAG_CFN: u8 = RefFlag::Function.value(); + +fn create_arg_info(userdata: &LuaAnyUserData) -> LuaResult { + let callback_ref_flag = if is_ctype(userdata) { + CALLBACK_ARG_REF_FLAG_TYPE + } else if userdata.is::() { + CALLBACK_ARG_REF_FLAG_PTR + } else if userdata.is::() { + CALLBACK_ARG_REF_FLAG_ARR + } else if userdata.is::() { + CALLBACK_ARG_REF_FLAG_STRUCT + } else if userdata.is::() { + CALLBACK_ARG_REF_FLAG_CFN + } else { + return Err(LuaError::external("Unexpected argument type")); + }; + Ok(FfiArg { + size: helper::get_size(userdata)?, + callback_ref_flag, + }) +} + +impl CFnInfo { + pub fn new( + args: Vec, + ret: Type, + arg_info_list: Vec, + result_info: FfiResult, + ) -> LuaResult { + Ok(Self { + cif: Cif::new(args.clone(), ret.clone()), + arg_info_list, + result_info, + }) + } + + pub fn from_table<'lua>( + lua: &'lua Lua, + arg_table: LuaTable, + ret: LuaAnyUserData, + ) -> LuaResult> { + if helper::has_void(&arg_table)? { + return Err(LuaError::external("Arguments can not include void type")); + } + + let args_types = helper::get_middle_type_list(&arg_table)?; + let ret_type = helper::get_middle_type(&ret)?; + + let arg_info_list = helper::create_list(&arg_table, create_arg_info)?; + let result_info = FfiResult { + size: helper::get_size(&ret)?, + }; + + let cfn = + lua.create_userdata(Self::new(args_types, ret_type, arg_info_list, result_info)?)?; + + // Create association to hold argument and result type + association::set(lua, CFN_ARGS, &cfn, arg_table)?; + association::set(lua, CFN_RESULT, &cfn, ret)?; + + Ok(cfn) + } + + // Stringify for pretty-print + // ex: u8 )> + pub fn stringify(lua: &Lua, userdata: &LuaAnyUserData) -> LuaResult { + let mut result = String::from(" ("); + if let (Some(LuaValue::Table(arg_table)), Some(LuaValue::UserData(result_userdata))) = ( + association::get(lua, CFN_ARGS, userdata)?, + association::get(lua, CFN_RESULT, userdata)?, + ) { + let len = arg_table.raw_len(); + for arg_index in 1..=len { + let arg_userdata: LuaAnyUserData = arg_table.raw_get(arg_index)?; + let pretty_formatted = helper::pretty_format(lua, &arg_userdata)?; + result.push_str( + (if len == arg_index { + pretty_formatted + } else { + format!("{pretty_formatted}, ") + }) + .as_str(), + ); + } + result.push_str( + format!(") -> {} ", helper::pretty_format(lua, &result_userdata)?,).as_str(), + ); + Ok(result) + } else { + Err(LuaError::external("Failed to retrieve inner type")) + } + } + + // Create ClosureData with lua function + pub fn create_closure<'lua>( + &self, + lua: &'lua Lua, + this: &LuaAnyUserData, + lua_function: LuaFunction<'lua>, + ) -> LuaResult> { + let closure_data = ClosureData::alloc( + lua, + self.cif.as_raw_ptr(), + self.arg_info_list.clone(), + self.result_info.clone(), + lua.create_registry_value(&lua_function)?, + )?; + + association::set(lua, CLOSURE_CFN, &closure_data, this)?; + association::set(lua, CLOSURE_FUNC, &closure_data, lua_function)?; + + Ok(closure_data) + } + + // Create CallableData from RefData + pub fn create_callable<'lua>( + &self, + lua: &'lua Lua, + this: &LuaAnyUserData, + target_ref: &LuaAnyUserData, + ) -> LuaResult> { + if !target_ref.is::() { + return Err(LuaError::external("Argument 'functionRef' must be RefData")); + } + + let ffi_ref = target_ref.borrow::()?; + if u8_test_not(ffi_ref.flags, RefFlag::Function.value()) { + return Err(LuaError::external( + "Argument 'functionRef' is not a valid function reference", + )); + } + + let callable = lua.create_userdata(unsafe { + CallableData::new( + self.cif.as_raw_ptr(), + self.arg_info_list.clone(), + self.result_info.clone(), + ffi_ref.get_inner_pointer(), + ) + })?; + + association::set(lua, CALLABLE_CFN, &callable, this)?; + association::set(lua, CALLABLE_REF, &callable, target_ref)?; + + Ok(callable) + } + + pub fn get_middle_type() -> Type { + Type::pointer() + } +} + +impl LuaUserData for CFnInfo { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("size", |_lua, _this| Ok(SIZE_OF_POINTER)); + } + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // ToString + method_provider::provide_to_string(methods); + + // Realize + methods.add_function( + "closure", + |lua, (cfn, func): (LuaAnyUserData, LuaFunction)| { + let this = cfn.borrow::()?; + this.create_closure(lua, cfn.as_ref(), func) + }, + ); + methods.add_function( + "callable", + |lua, (cfn, target): (LuaAnyUserData, LuaAnyUserData)| { + let this = cfn.borrow::()?; + this.create_callable(lua, cfn.as_ref(), &target) + }, + ); + } +} diff --git a/crates/lune-std-ffi/src/c/helper.rs b/crates/lune-std-ffi/src/c/helper.rs new file mode 100644 index 00000000..b56ed7f8 --- /dev/null +++ b/crates/lune-std-ffi/src/c/helper.rs @@ -0,0 +1,318 @@ +use libffi::middle::Type; +use lune_utils::fmt::{pretty_format_value, ValueFormatConfig}; +use mlua::prelude::*; + +use super::{ctype_helper, void_info::CVoidInfo, CArrInfo, CFnInfo, CPtrInfo, CStructInfo}; +use crate::{ + data::{BoxData, GetFfiData}, + ffi::{FfiConvert, FfiSize}, +}; + +pub mod method_provider { + use super::*; + + // Implement tostring + pub fn provide_to_string<'lua, Target, M>(methods: &mut M) + where + Target: 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_meta_function(LuaMetaMethod::ToString, |lua, this: LuaAnyUserData| { + stringify(lua, &this) + }); + } + + // Implement ptr method + pub fn provide_ptr<'lua, Target, M>(methods: &mut M) + where + Target: 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_function("ptr", |lua, this: LuaAnyUserData| { + CPtrInfo::from_userdata(lua, &this) + }); + } + + // Implement arr method + pub fn provide_arr<'lua, Target, M>(methods: &mut M) + where + Target: 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_function("arr", |lua, (this, length): (LuaAnyUserData, usize)| { + CArrInfo::from_userdata(lua, &this, length) + }); + } + + // Implement readData method + pub fn provide_read_data<'lua, Target, M>(methods: &mut M) + where + Target: FfiSize + FfiConvert + 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_method( + "readData", + |lua, this, (target, offset): (LuaAnyUserData, Option)| { + let offset = offset.unwrap_or(0); + + let data_handle = &target.get_ffi_data()?; + if !data_handle.check_inner_boundary(offset, this.get_size()) { + return Err(LuaError::external("Out of bounds")); + } + if !data_handle.is_readable() { + return Err(LuaError::external("Unreadable data")); + } + + unsafe { this.value_from_data(lua, offset, data_handle) } + }, + ); + } + + // Implement writeData method + pub fn provide_write_data<'lua, Target, M>(methods: &mut M) + where + Target: FfiSize + FfiConvert + 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_method( + "writeData", + |lua, this, (target, value, offset): (LuaAnyUserData, LuaValue, Option)| { + let offset = offset.unwrap_or(0); + + let data_handle = &target.get_ffi_data()?; + // use or functions + if !data_handle.check_inner_boundary(offset, this.get_size()) { + return Err(LuaError::external("Out of bounds")); + } + if !data_handle.is_writable() { + return Err(LuaError::external("Unwritable data")); + } + + unsafe { this.value_into_data(lua, offset, data_handle, value) } + }, + ); + } + + // Implement copyData method + pub fn provide_copy_data<'lua, Target, M>(methods: &mut M) + where + Target: FfiSize + FfiConvert + 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_method( + "copyData", + |lua, + this, + (dst, src, dst_offset, src_offset): ( + LuaAnyUserData, + LuaAnyUserData, + Option, + Option, + )| { + let dst_offset = dst_offset.unwrap_or(0); + let src_offset = src_offset.unwrap_or(0); + + let dst = &dst.get_ffi_data()?; + // use or functions + if !dst.check_inner_boundary(dst_offset, this.get_size()) { + return Err(LuaError::external("Destination out of bounds")); + } + if !dst.is_writable() { + return Err(LuaError::external("Destination is unwritable")); + } + + let src = &src.get_ffi_data()?; + if !src.check_inner_boundary(dst_offset, this.get_size()) { + return Err(LuaError::external("Source out of bounds")); + } + if !src.is_readable() { + return Err(LuaError::external("Source is unreadable")); + } + + unsafe { this.copy_data(lua, dst_offset, src_offset, dst, src) } + }, + ); + } + + // Implement stringifyData method + pub fn provide_stringify_data<'lua, Target, M>(methods: &mut M) + where + Target: FfiSize + FfiConvert + 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_method( + "stringifyData", + |lua, this, (target, offset): (LuaAnyUserData, Option)| unsafe { + this.stringify_data(lua, offset.unwrap_or(0), &target.get_ffi_data()?) + }, + ); + } + + // Implement box method + pub fn provide_box<'lua, Target, M>(methods: &mut M) + where + Target: FfiSize + FfiConvert + 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_method("box", |lua, this, value: LuaValue| { + let result = lua.create_userdata(BoxData::new(this.get_size()))?; + unsafe { this.value_into_data(lua, 0, &result.get_ffi_data()?, value)? }; + Ok(result) + }); + } +} + +fn get_userdata(value: LuaValue) -> LuaResult { + if let LuaValue::UserData(field_type) = value { + Ok(field_type) + } else { + Err(LuaError::external(format!( + "CStruct, CType, CFn, CVoid or CArr is required but got {}", + pretty_format_value(&value, &ValueFormatConfig::new()) + ))) + } +} + +// Create vec from table with (userdata)->T +pub fn create_list( + table: &LuaTable, + callback: fn(&LuaAnyUserData) -> LuaResult, +) -> LuaResult> { + let len: usize = table.raw_len(); + let mut list = Vec::::with_capacity(len); + + for i in 0..len { + let value: LuaValue = table.raw_get(i + 1)?; + list.push(callback(&get_userdata(value)?)?); + } + + Ok(list) +} + +// Get the dynamic FfiConvert handle from the userData +// This is intended to avoid lookup userdata and lua table every time. (eg: struct) +// The userdata must live longer than the FfiConvert handle +pub unsafe fn get_conv(userdata: &LuaAnyUserData) -> LuaResult<*const dyn FfiConvert> { + if userdata.is::() { + Ok(userdata.to_pointer().cast::() as *const dyn FfiConvert) + } else if userdata.is::() { + Ok(userdata.to_pointer().cast::() as *const dyn FfiConvert) + } else if userdata.is::() { + Ok(userdata.to_pointer().cast::() as *const dyn FfiConvert) + } else { + ctype_helper::get_conv(userdata) + } +} +pub unsafe fn get_conv_list(table: &LuaTable) -> LuaResult> { + create_list(table, |userdata| get_conv(userdata)) +} + +// Get libffi_type from ctype userdata +pub fn get_middle_type(userdata: &LuaAnyUserData) -> LuaResult { + if userdata.is::() { + Ok(userdata.borrow::()?.get_middle_type()) + } else if let Some(middle_type) = ctype_helper::get_middle_type(userdata)? { + Ok(middle_type) + } else if userdata.is::() { + Ok(userdata.borrow::()?.get_middle_type()) + } else if userdata.is::() { + Ok(CPtrInfo::get_middle_type()) + } else if userdata.is::() { + Ok(CVoidInfo::get_middle_type()) + } else if userdata.is::() { + Ok(CFnInfo::get_middle_type()) + } else { + Err(LuaError::external(format!( + "CStruct, CType, CFn, CVoid or CArr is required but got {}", + pretty_format_value( + // Since the data is in the Lua location, + // there is no problem with the clone. + &LuaValue::UserData(userdata.to_owned()), + &ValueFormatConfig::new() + ) + ))) + } +} +pub fn get_middle_type_list(table: &LuaTable) -> LuaResult> { + create_list(table, get_middle_type) +} + +// Get type size from ctype userdata +pub fn get_size(userdata: &LuaAnyUserData) -> LuaResult { + if userdata.is::() { + Ok(userdata.borrow::()?.get_size()) + } else if userdata.is::() { + Ok(userdata.borrow::()?.get_size()) + } else if userdata.is::() { + Ok(userdata.borrow::()?.get_size()) + } else if userdata.is::() { + Ok(userdata.borrow::()?.get_size()) + } else if userdata.is::() { + Ok(userdata.borrow::()?.get_size()) + } else { + ctype_helper::get_size(userdata) + } +} + +// Check lua table has void ctype userdata +pub fn has_void(table: &LuaTable) -> LuaResult { + for i in 0..table.raw_len() { + let value: LuaValue = table.raw_get(i + 1)?; + if get_userdata(value)?.is::() { + return Ok(false); + } + } + Ok(false) +} + +// Stringify any c-type userdata recursively +pub fn stringify(lua: &Lua, userdata: &LuaAnyUserData) -> LuaResult { + if userdata.is::() { + CStructInfo::stringify(lua, userdata) + } else if userdata.is::() { + CArrInfo::stringify(lua, userdata) + } else if userdata.is::() { + CPtrInfo::stringify(lua, userdata) + } else if userdata.is::() { + CFnInfo::stringify(lua, userdata) + } else if userdata.is::() { + CVoidInfo::stringify() + } else if let Some(name) = ctype_helper::get_name(userdata)? { + Ok(String::from(name)) + } else { + Ok(String::from("unknown")) + } +} + +// Get name tag from ctype userdata +pub fn get_tag_name(userdata: &LuaAnyUserData) -> LuaResult { + Ok(if userdata.is::() { + String::from("CStructInfo") + } else if userdata.is::() { + String::from("CArrInfo") + } else if userdata.is::() { + String::from("CPtrInfo") + } else if userdata.is::() { + String::from("CFnInfo") + } else if userdata.is::() { + String::from("CVoidInfo") + } else if ctype_helper::is_ctype(userdata) { + String::from("CTypeInfo") + } else { + String::from("Unknown") + }) +} + +// Emulate 'print' for ctype userdata, but simplified +// Used for print struct field, cfn arguments, etc... +pub fn pretty_format(lua: &Lua, userdata: &LuaAnyUserData) -> LuaResult { + if ctype_helper::is_ctype(userdata) { + stringify(lua, userdata) + } else { + Ok(format!( + "<{}({})>", + get_tag_name(userdata)?, + stringify(lua, userdata)? + )) + } +} diff --git a/crates/lune-std-ffi/src/c/mod.rs b/crates/lune-std-ffi/src/c/mod.rs new file mode 100644 index 00000000..5bd9c1dd --- /dev/null +++ b/crates/lune-std-ffi/src/c/mod.rs @@ -0,0 +1,52 @@ +use lune_utils::TableBuilder; +use mlua::prelude::*; + +mod arr_info; +mod fn_info; +pub mod helper; +mod ptr_info; +mod string_info; +mod struct_info; +mod type_info; +mod types; +mod void_info; + +pub use self::{ + arr_info::CArrInfo, + fn_info::CFnInfo, + helper::method_provider, + ptr_info::CPtrInfo, + string_info::CStringInfo, + struct_info::CStructInfo, + type_info::{CTypeCast, CTypeInfo}, + types::{ctype_helper, export_c_types, export_fixed_types}, + void_info::CVoidInfo, +}; + +// Named registry keys +mod association_names { + pub const CPTR_INNER: &str = "__cptr_inner"; + pub const CARR_INNER: &str = "__carr_inner"; + pub const CSTRUCT_INNER: &str = "__cstruct_inner"; + pub const CFN_RESULT: &str = "__cfn_result"; + pub const CFN_ARGS: &str = "__cfn_args"; + pub const CALLABLE_REF: &str = "__callable_ref"; + pub const CALLABLE_CFN: &str = "__callable_cfn"; + pub const CLOSURE_FUNC: &str = "__closure_func"; + pub const CLOSURE_CFN: &str = "__closure_cfn"; +} + +// Export c namespace +pub fn export_c(lua: &Lua) -> LuaResult { + TableBuilder::new(lua)? + .with_value("void", CVoidInfo::new())? + .with_values(export_c_types(lua)?)? + .with_function("struct", |lua, types: LuaTable| { + CStructInfo::from_table(lua, types) + })? + .with_function("fn", |lua, (args, ret): (LuaTable, LuaAnyUserData)| { + CFnInfo::from_table(lua, args, ret) + })? + .with_value("string", CStringInfo::new())? + .build_readonly() +} diff --git a/crates/lune-std-ffi/src/c/ptr_info.rs b/crates/lune-std-ffi/src/c/ptr_info.rs new file mode 100644 index 00000000..aa774ecd --- /dev/null +++ b/crates/lune-std-ffi/src/c/ptr_info.rs @@ -0,0 +1,192 @@ +use std::cell::Ref; + +use libffi::middle::Type; +use mlua::prelude::*; + +use super::{association_names::CPTR_INNER, ctype_helper, helper, method_provider}; +use crate::{ + data::{GetFfiData, RefData, RefFlag, UNSIZED_BOUNDS}, + ffi::{ + association, libffi_helper::SIZE_OF_POINTER, FfiConvert, FfiData, FfiSignedness, FfiSize, + }, +}; + +const READ_CPTR_REF_FLAGS: u8 = RefFlag::Dereferenceable.value() | RefFlag::Offsetable.value(); +const READ_REF_FLAGS: u8 = + RefFlag::Offsetable.value() | RefFlag::Readable.value() | RefFlag::Writable.value(); + +pub struct CPtrInfo { + inner_is_cptr: bool, +} + +impl FfiSignedness for CPtrInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiSize for CPtrInfo { + fn get_size(&self) -> usize { + SIZE_OF_POINTER + } +} + +impl FfiConvert for CPtrInfo { + // Write address of RefData + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let LuaValue::UserData(value_userdata) = value else { + return Err(LuaError::external(format!( + "Value must be a RefData, BoxData or ClosureData, got {}", + value.type_name() + ))); + }; + *data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::<*mut ()>() = value_userdata.get_ffi_data()?.get_inner_pointer(); + Ok(()) + } + + // Read address, create RefData + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + Ok(LuaValue::UserData( + lua.create_userdata(RefData::new( + *data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::<*mut ()>(), + if self.inner_is_cptr { + READ_CPTR_REF_FLAGS + } else { + READ_REF_FLAGS + }, + UNSIZED_BOUNDS, + ))?, + )) + } + + // Copy Address + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::<*mut ()>() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::<*mut ()>(); + Ok(()) + } +} + +impl CPtrInfo { + // Create pointer type with '.inner' field + // inner can be CArr, CType or CStruct + pub fn from_userdata<'lua>( + lua: &'lua Lua, + inner: &LuaAnyUserData, + ) -> LuaResult> { + let value = lua.create_userdata(Self { + inner_is_cptr: inner.is::(), + })?; + + association::set(lua, CPTR_INNER, &value, inner)?; + + Ok(value) + } + + // Stringify CPtr with inner ctype + pub fn stringify(lua: &Lua, userdata: &LuaAnyUserData) -> LuaResult { + if let LuaValue::UserData(inner_userdata) = userdata.get("inner")? { + let pretty_formatted = helper::pretty_format(lua, &inner_userdata)?; + Ok(if ctype_helper::is_ctype(&inner_userdata) { + pretty_formatted + } else { + format!(" {pretty_formatted} ") + }) + } else { + Err(LuaError::external("Failed to retrieve inner type")) + } + } + + // Return void* + pub fn get_middle_type() -> Type { + Type::pointer() + } +} + +impl LuaUserData for CPtrInfo { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("size", |_lua, _this| Ok(SIZE_OF_POINTER)); + fields.add_field_function_get("inner", |lua, this| { + association::get(lua, CPTR_INNER, this)? + .ok_or_else(|| LuaError::external("Failed to retrieve inner type")) + }); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Subtype + method_provider::provide_ptr(methods); + method_provider::provide_arr(methods); + + // ToString + method_provider::provide_to_string(methods); + + methods.add_method( + "readRef", + |lua, + this, + (target, offset, ref_data): ( + LuaAnyUserData, + Option, + Option, + )| unsafe { + if let Some(ref_userdata) = ref_data { + if !ref_userdata.is::() { + return Err(LuaError::external("")); + } + RefData::update( + lua, + ref_userdata.clone(), + *target + .get_ffi_data()? + .get_inner_pointer() + .byte_offset(offset.unwrap_or(0)) + .cast::<*mut ()>(), + if this.inner_is_cptr { + READ_CPTR_REF_FLAGS + } else { + READ_REF_FLAGS + }, + UNSIZED_BOUNDS, + )?; + Ok(LuaValue::UserData(ref_userdata)) + } else { + this.value_from_data(lua, offset.unwrap_or(0), &target.get_ffi_data()?) + } + }, + ); + methods.add_method( + "writeRef", + |lua, this, (target, value, offset): (LuaAnyUserData, LuaValue, Option)| unsafe { + this.value_into_data(lua, offset.unwrap_or(0), &target.get_ffi_data()?, value) + }, + ); + } +} diff --git a/crates/lune-std-ffi/src/c/string_info.rs b/crates/lune-std-ffi/src/c/string_info.rs new file mode 100644 index 00000000..7fe35684 --- /dev/null +++ b/crates/lune-std-ffi/src/c/string_info.rs @@ -0,0 +1,13 @@ +// TODO: + +use mlua::prelude::*; + +pub struct CStringInfo(); + +impl CStringInfo { + pub fn new() -> Self { + Self() + } +} + +impl LuaUserData for CStringInfo {} diff --git a/crates/lune-std-ffi/src/c/struct_info.rs b/crates/lune-std-ffi/src/c/struct_info.rs new file mode 100644 index 00000000..b175a07e --- /dev/null +++ b/crates/lune-std-ffi/src/c/struct_info.rs @@ -0,0 +1,215 @@ +use std::{cell::Ref, vec::Vec}; + +use libffi::{low, middle::Type, raw::ffi_get_struct_offsets}; +use mlua::prelude::*; + +use super::{association_names::CSTRUCT_INNER, helper, method_provider}; +use crate::ffi::{ + association, libffi_helper::ffi_status_assert, FfiConvert, FfiData, FfiSignedness, FfiSize, +}; + +pub struct CStructInfo { + middle_type: Type, + size: usize, + inner_offset_list: Vec, + inner_conv_list: Vec<*const dyn FfiConvert>, +} + +fn get_field_table<'lua>( + lua: &'lua Lua, + userdata: &LuaAnyUserData<'lua>, +) -> LuaResult> { + let value = association::get(lua, CSTRUCT_INNER, userdata)? + .ok_or_else(|| LuaError::external("Failed to retrieve inner field table: not found"))?; + if let LuaValue::Table(table) = value { + Ok(table) + } else { + Err(LuaError::external( + "Failed to retrieve inner field: not a table", + )) + } +} + +impl CStructInfo { + pub fn new(fields: Vec, inner_conv_list: Vec<*const dyn FfiConvert>) -> LuaResult { + let len = fields.len(); + let mut inner_offset_list = Vec::::with_capacity(len); + let middle_type = Type::structure(fields); + + // Get field offsets with ffi_get_struct_offsets + unsafe { + ffi_status_assert(ffi_get_struct_offsets( + low::ffi_abi_FFI_DEFAULT_ABI, + middle_type.as_raw_ptr(), + inner_offset_list.as_mut_ptr(), + ))?; + inner_offset_list.set_len(len); + } + + // Get tailing padded size of struct (get_ensured_size not required) + let size = unsafe { (*middle_type.as_raw_ptr()).size }; + + Ok(Self { + middle_type, + size, + inner_offset_list, + inner_conv_list, + }) + } + + // Create new CStruct from LuaTable. + // Freeze and hold table + pub fn from_table<'lua>( + lua: &'lua Lua, + table: LuaTable<'lua>, + ) -> LuaResult> { + if helper::has_void(&table)? { + return Err(LuaError::external("Void field in sturct is not allowed")); + } + + let cstruct = lua + .create_userdata(Self::new(helper::get_middle_type_list(&table)?, unsafe { + helper::get_conv_list(&table)? + })?)?; + + // Save field table + table.set_readonly(true); + association::set(lua, CSTRUCT_INNER, &cstruct, table)?; + Ok(cstruct) + } + + // Stringify cstruct for pretty printing + // ex: + pub fn stringify(lua: &Lua, userdata: &LuaAnyUserData) -> LuaResult { + let fields = get_field_table(lua, userdata)?; + let mut stringified = String::from(" "); + + // Children + for i in 0..fields.raw_len() { + let child: LuaAnyUserData = fields.raw_get(i + 1)?; + let pretty_formatted = helper::pretty_format(lua, &child)?; + stringified.push_str(format!("{pretty_formatted}, ").as_str()); + } + + // Size + stringified + .push_str(format!("size = {} ", userdata.borrow::()?.get_size()).as_str()); + Ok(stringified) + } + + // Get byte offset of nth field + pub fn offset(&self, index: usize) -> LuaResult { + Ok(self + .inner_offset_list + .get(index) + .ok_or_else(|| LuaError::external("Out of index"))? + .to_owned()) + } + + pub fn get_middle_type(&self) -> Type { + self.middle_type.clone() + } +} + +impl FfiSize for CStructInfo { + fn get_size(&self) -> usize { + self.size + } +} + +impl FfiSignedness for CStructInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiConvert for CStructInfo { + unsafe fn value_into_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let LuaValue::Table(ref table) = value else { + return Err(LuaError::external("Value is not a table")); + }; + for (index, conv) in self.inner_conv_list.iter().enumerate() { + let field_offset = self.offset(index)? as isize; + let data: LuaValue = table.get(index + 1)?; + conv.as_ref().unwrap().value_into_data( + lua, + field_offset + offset, + data_handle, + data, + )?; + } + Ok(()) + } + + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let table = lua.create_table_with_capacity(self.inner_conv_list.len(), 0)?; + for (i, conv) in self.inner_conv_list.iter().enumerate() { + let field_offset = self.offset(i)? as isize; + table.set( + i + 1, + conv.as_ref() + .unwrap() + .value_from_data(lua, field_offset + offset, data_handle)?, + )?; + } + Ok(LuaValue::Table(table)) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + dst.get_inner_pointer().byte_offset(dst_offset).copy_from( + src.get_inner_pointer().byte_offset(src_offset), + self.get_size(), + ); + Ok(()) + } +} + +impl LuaUserData for CStructInfo { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("size", |_lua, this| Ok(this.get_size())); + } + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Subtype + method_provider::provide_ptr(methods); + method_provider::provide_arr(methods); + + // ToString + method_provider::provide_to_string(methods); + + // Realize + method_provider::provide_box(methods); + method_provider::provide_read_data(methods); + method_provider::provide_write_data(methods); + method_provider::provide_copy_data(methods); + + // Get nth field offset + methods.add_method("offset", |_lua, this, index: usize| this.offset(index)); + // Get nth field type + methods.add_function( + "field", + |lua, (this, field_index): (LuaAnyUserData, usize)| { + let field_table = get_field_table(lua, &this)?; + field_table + .raw_get::<_, Option>(field_index + 1)? + .ok_or_else(|| LuaError::external("Out of index")) + }, + ); + } +} diff --git a/crates/lune-std-ffi/src/c/type_info.rs b/crates/lune-std-ffi/src/c/type_info.rs new file mode 100644 index 00000000..5da8f60d --- /dev/null +++ b/crates/lune-std-ffi/src/c/type_info.rs @@ -0,0 +1,142 @@ +#![allow(clippy::inline_always)] + +use std::{cell::Ref, marker::PhantomData}; + +use libffi::middle::Type; +use lune_utils::fmt::{pretty_format_value, ValueFormatConfig}; +use mlua::prelude::*; + +use super::method_provider; +use crate::{ + data::GetFfiData, + ffi::{libffi_helper::get_ensured_size, FfiConvert, FfiData, FfiSignedness, FfiSize}, +}; + +// Provide type casting +// This trait should be implemented for each types +pub trait CTypeCast { + #[inline(always)] + fn cast( + &self, + from_ctype: &LuaAnyUserData, + into_ctype: &LuaAnyUserData, + _from: &Ref, + _into: &Ref, + _from_offset: isize, + _into_offset: isize, + ) -> LuaResult<()> { + // Error if have no cast implement + Err(Self::cast_failed_with(self, from_ctype, into_ctype)) + } + + fn cast_failed_with( + &self, + from_ctype: &LuaAnyUserData, + into_ctype: &LuaAnyUserData, + ) -> LuaError { + let config = ValueFormatConfig::new(); + LuaError::external(format!( + "Failed to cast {} into {}", + pretty_format_value(&LuaValue::UserData(from_ctype.to_owned()), &config), + pretty_format_value(&LuaValue::UserData(into_ctype.to_owned()), &config), + )) + } +} + +pub struct CTypeInfo { + middle_type: Type, + size: usize, + name: &'static str, + _phantom: PhantomData, +} + +impl FfiSize for CTypeInfo { + fn get_size(&self) -> usize { + self.size + } +} + +impl CTypeInfo +where + T: 'static, + Self: CTypeCast + FfiSignedness + FfiConvert + FfiSize, +{ + pub fn from_middle_type<'lua>( + lua: &'lua Lua, + libffi_type: Type, + name: &'static str, + ) -> LuaResult> { + let size = get_ensured_size(libffi_type.as_raw_ptr())?; + + let ctype = Self { + middle_type: libffi_type, + size, + name, + _phantom: PhantomData, + }; + let userdata = lua.create_userdata(ctype)?; + + Ok(userdata) + } + + pub fn get_name(&self) -> &'static str { + self.name + } + + pub fn get_middle_type(&self) -> Type { + self.middle_type.clone() + } +} + +impl LuaUserData for CTypeInfo +where + T: 'static, + Self: CTypeCast + FfiSignedness + FfiConvert + FfiSize, +{ + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_meta_field(LuaMetaMethod::Type, "CTypeInfo"); + fields.add_field_method_get("size", |_lua, this| Ok(this.get_size())); + fields.add_field_method_get("signedness", |_lua, this| Ok(this.get_signedness())); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Subtype + method_provider::provide_ptr(methods); + method_provider::provide_arr(methods); + + // ToString + method_provider::provide_to_string(methods); + + // Realize + method_provider::provide_box(methods); + method_provider::provide_read_data(methods); + method_provider::provide_write_data(methods); + method_provider::provide_copy_data(methods); + method_provider::provide_stringify_data(methods); + + // Math + // TODO: Math support for numeric types + + methods.add_function( + "cast", + |_lua, + (from_type, into_type, from, into, from_offset, into_offset): ( + LuaAnyUserData, + LuaAnyUserData, + LuaAnyUserData, + LuaAnyUserData, + Option, + Option, + )| { + from_type.borrow::()?.cast( + &from_type, + &into_type, + &from.get_ffi_data()?, + &into.get_ffi_data()?, + from_offset.unwrap_or(0), + into_offset.unwrap_or(0), + ) + }, + ); + } +} diff --git a/crates/lune-std-ffi/src/c/types/f32.rs b/crates/lune-std-ffi/src/c/types/f32.rs new file mode 100644 index 00000000..b13fb4af --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/f32.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + true + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: f32 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/f64.rs b/crates/lune-std-ffi/src/c/types/f64.rs new file mode 100644 index 00000000..1747e39e --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/f64.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + true + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: f64 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/i128.rs b/crates/lune-std-ffi/src/c/types/i128.rs new file mode 100644 index 00000000..e60b1305 --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/i128.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + true + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: i128 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/i16.rs b/crates/lune-std-ffi/src/c/types/i16.rs new file mode 100644 index 00000000..e82920fa --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/i16.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + true + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: i16 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/i32.rs b/crates/lune-std-ffi/src/c/types/i32.rs new file mode 100644 index 00000000..b25515a9 --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/i32.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + true + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: i32 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/i64.rs b/crates/lune-std-ffi/src/c/types/i64.rs new file mode 100644 index 00000000..db4e5673 --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/i64.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + true + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: i64 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/i8.rs b/crates/lune-std-ffi/src/c/types/i8.rs new file mode 100644 index 00000000..4384f747 --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/i8.rs @@ -0,0 +1,82 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + true + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: i8 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::String(t) => t.as_bytes().first().map_or(0, u8::to_owned).as_(), + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer().byte_offset(dst_offset).cast::() = + *src.get_inner_pointer().byte_offset(src_offset).cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/isize.rs b/crates/lune-std-ffi/src/c/types/isize.rs new file mode 100644 index 00000000..bee4db62 --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/isize.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + true + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: isize = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/mod.rs b/crates/lune-std-ffi/src/c/types/mod.rs new file mode 100644 index 00000000..caa2a620 --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/mod.rs @@ -0,0 +1,203 @@ +#![allow(clippy::inline_always)] + +use core::ffi::*; +use std::{any::TypeId, cell::Ref}; + +use libffi::middle::Type; +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use super::{CTypeCast, CTypeInfo}; +use crate::ffi::{num_cast, FfiConvert, FfiData, FfiSize}; + +mod f32; +mod f64; +mod i128; +mod i16; +mod i32; +mod i64; +mod i8; +mod isize; +mod u128; +mod u16; +mod u32; +mod u64; +mod u8; +mod usize; + +// CType userdata export +macro_rules! create_ctypes { + ($lua:ident, $(( $name:expr, $rust_type:ty, $libffi_type:expr ),)* ) => { + Ok(vec![$(( + $name, + CTypeInfo::<$rust_type>::from_middle_type($lua, $libffi_type, $name)?, + ),)*]) + }; +} +pub fn export_c_types(lua: &Lua) -> LuaResult> { + create_ctypes!( + lua, + // Export Compile-time known c-types + ("char", c_char, { + if TypeId::of::() == TypeId::of::() { + Type::c_uchar() + } else { + Type::c_schar() + } + }), + ("uchar", c_uchar, Type::c_uchar()), + ("schar", c_schar, Type::c_schar()), + ("short", c_short, Type::c_short()), + ("ushort", c_ushort, Type::c_ushort()), + ("int", c_int, Type::c_int()), + ("uint", c_uint, Type::c_uint()), + ("long", c_long, Type::c_long()), + ("ulong", c_ulong, Type::c_ulong()), + ("longlong", c_longlong, Type::c_longlong()), + ("ulonglong", c_ulonglong, Type::c_ulonglong()), + ) +} +pub fn export_fixed_types(lua: &Lua) -> LuaResult> { + create_ctypes!( + lua, + // Export Source-time known c-types (fixed) + ("u8", u8, Type::u8()), + ("u16", u16, Type::u16()), + ("u32", u32, Type::u32()), + ("u64", u64, Type::u64()), + ("u128", u128, Type::c_longlong()), + ("i8", i8, Type::i8()), + ("i16", i16, Type::i16()), + ("i32", i32, Type::i32()), + ("i64", i64, Type::i64()), + ("i128", i128, Type::c_ulonglong()), + ("f64", f64, Type::f64()), + ("f32", f32, Type::f32()), + ("usize", usize, Type::usize()), + ("isize", isize, Type::isize()), + ("f32", f32, Type::f32()), + ("f64", f64, Type::f64()), + ) +} + +// Implement type-casting for numeric ctypes +macro_rules! define_cast_num { + ($from_rust_type:ident, $self:ident, $from_ctype:ident, $into_ctype:ident, $from:ident, $into:ident, $fromOffset:ident, $intoOffset:ident, $($into_rust_type:ty)*) => { + $( if $into_ctype.is::>() { + num_cast::<$from_rust_type, $into_rust_type>($from, $into, $fromOffset, $intoOffset) + } else )* { + Err($self.cast_failed_with($from_ctype, $into_ctype)) + } + }; +} +impl CTypeCast for CTypeInfo +where + From: AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive + + AsPrimitive, +{ + fn cast( + &self, + from_info: &LuaAnyUserData, + into_info: &LuaAnyUserData, + from: &Ref, + into: &Ref, + from_offset: isize, + into_offset: isize, + ) -> LuaResult<()> { + define_cast_num!( + From, self, from_info, into_info, from, into, from_offset, into_offset, + f32 f64 i128 i16 i32 i64 i8 isize u128 u16 u32 u64 u8 usize + ) + } +} + +pub mod ctype_helper { + use super::*; + + // To prevent droping NativeConvert, need to ensure userdata keep alive + macro_rules! define_get_conv { + ($userdata:ident, $( $rust_type:ty )*) => { + $( if $userdata.is::>() { + Ok($userdata.to_pointer().cast::>() as *const dyn FfiConvert) + } else )* { + Err(LuaError::external("Unexpected type")) + } + }; + } + #[inline] + pub fn get_conv(userdata: &LuaAnyUserData) -> LuaResult<*const dyn FfiConvert> { + define_get_conv!(userdata, f32 f64 i128 i16 i32 i64 i8 isize u128 u16 u32 u64 u8 usize) + } + + // Get libffi_type of ctype + macro_rules! define_get_middle_type { + ($userdata:ident, $( $rust_type:ty )*) => { + $( if $userdata.is::>() { + Ok(Some($userdata.borrow::>()?.get_middle_type())) + } else )* { + Ok(None) + } + }; + } + #[inline] + pub fn get_middle_type(userdata: &LuaAnyUserData) -> LuaResult> { + define_get_middle_type!(userdata, f32 f64 i128 i16 i32 i64 i8 isize u128 u16 u32 u64 u8 usize) + } + + // Get size of ctype (not including struct, arr, ... only CType<*>) + macro_rules! define_get_size { + ($userdata:ident, $( $rust_type:ty )*) => { + $( if $userdata.is::>() { + Ok($userdata.borrow::>()?.get_size()) + } else )* { + Err(LuaError::external("Unexpected type")) + } + }; + } + #[inline] + pub fn get_size(userdata: &LuaAnyUserData) -> LuaResult { + define_get_size!(userdata, f32 f64 i128 i16 i32 i64 i8 isize u128 u16 u32 u64 u8 usize) + } + + // Get name of ctype + macro_rules! define_get_name { + ($userdata:ident, $( $rust_type:ty )*) => { + $( if $userdata.is::>() { + Ok(Some($userdata.borrow::>()?.get_name())) + } else )* { + Ok(None) + } + }; + } + #[inline] + pub fn get_name(userdata: &LuaAnyUserData) -> LuaResult> { + define_get_name!(userdata, f32 f64 i128 i16 i32 i64 i8 isize u128 u16 u32 u64 u8 usize) + } + + // Check whether userdata is ctype or not + macro_rules! define_is_ctype { + ($userdata:ident, $( $rust_type:ty )*) => { + $( if $userdata.is::>() { + true + } else )* { + false + } + }; + } + #[inline] + pub fn is_ctype(userdata: &LuaAnyUserData) -> bool { + define_is_ctype!(userdata, f32 f64 i128 i16 i32 i64 i8 isize u128 u16 u32 u64 u8 usize) + } +} diff --git a/crates/lune-std-ffi/src/c/types/u128.rs b/crates/lune-std-ffi/src/c/types/u128.rs new file mode 100644 index 00000000..3bb8443b --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/u128.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: u128 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/u16.rs b/crates/lune-std-ffi/src/c/types/u16.rs new file mode 100644 index 00000000..ec1d0721 --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/u16.rs @@ -0,0 +1,91 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiConvert for CTypeInfo { + // Convert luavalue into data, then write into ptr + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: u16 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/u32.rs b/crates/lune-std-ffi/src/c/types/u32.rs new file mode 100644 index 00000000..25942e0b --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/u32.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: u32 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/u64.rs b/crates/lune-std-ffi/src/c/types/u64.rs new file mode 100644 index 00000000..33d9785d --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/u64.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: u64 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/u8.rs b/crates/lune-std-ffi/src/c/types/u8.rs new file mode 100644 index 00000000..8764bfc3 --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/u8.rs @@ -0,0 +1,85 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiConvert for CTypeInfo { + // Convert luavalue into data, then write into ptr + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: u8 = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::String(t) => t.as_bytes().first().map_or(0, u8::to_owned).as_(), + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + + // Read data from ptr, then convert into luavalue + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer().byte_offset(dst_offset).cast::() = + *src.get_inner_pointer().byte_offset(src_offset).cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/types/usize.rs b/crates/lune-std-ffi/src/c/types/usize.rs new file mode 100644 index 00000000..a7902ed1 --- /dev/null +++ b/crates/lune-std-ffi/src/c/types/usize.rs @@ -0,0 +1,90 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use crate::{ + c::type_info::CTypeInfo, + ffi::{FfiConvert, FfiData, FfiSignedness}, +}; + +impl FfiSignedness for CTypeInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiConvert for CTypeInfo { + unsafe fn value_into_data<'lua>( + &self, + _lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()> { + let value: usize = match value { + LuaValue::Integer(t) => t.as_(), + LuaValue::Number(t) => t.as_(), + LuaValue::String(t) => t + .to_string_lossy() + .parse::() + .map_err(LuaError::external)?, + _ => { + return Err(LuaError::external(format!( + "Value must be a Integer, Number or String, got {}", + value.type_name() + ))) + } + }; + unsafe { + *(data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) = value; + } + Ok(()) + } + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult> { + let value = unsafe { + (*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .into_lua(lua)? + }; + Ok(value) + } + unsafe fn copy_data( + &self, + _lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()> { + *dst.get_inner_pointer() + .byte_offset(dst_offset) + .cast::() = *src + .get_inner_pointer() + .byte_offset(src_offset) + .cast::(); + Ok(()) + } + unsafe fn stringify_data( + &self, + _lua: &Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult { + Ok((*data_handle + .get_inner_pointer() + .byte_offset(offset) + .cast::()) + .to_string()) + } +} diff --git a/crates/lune-std-ffi/src/c/void_info.rs b/crates/lune-std-ffi/src/c/void_info.rs new file mode 100644 index 00000000..9fd1968e --- /dev/null +++ b/crates/lune-std-ffi/src/c/void_info.rs @@ -0,0 +1,42 @@ +use libffi::middle::Type; +use mlua::prelude::*; + +use crate::ffi::{FfiSignedness, FfiSize}; + +use super::method_provider; + +pub struct CVoidInfo(); + +impl FfiSignedness for CVoidInfo { + fn get_signedness(&self) -> bool { + false + } +} + +impl FfiSize for CVoidInfo { + fn get_size(&self) -> usize { + 0 + } +} + +impl CVoidInfo { + pub fn new() -> Self { + Self() + } + pub fn get_middle_type() -> Type { + Type::void() + } + pub fn stringify() -> LuaResult { + Ok(String::from("CVoid")) + } +} + +impl LuaUserData for CVoidInfo { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("size", |_lua, _this| Ok(0)); + } + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + method_provider::provide_to_string(methods); + method_provider::provide_ptr(methods); + } +} diff --git a/crates/lune-std-ffi/src/data/box_data/flag.rs b/crates/lune-std-ffi/src/data/box_data/flag.rs new file mode 100644 index 00000000..a983f3ba --- /dev/null +++ b/crates/lune-std-ffi/src/data/box_data/flag.rs @@ -0,0 +1,13 @@ +use crate::ffi::bit_field::*; + +pub enum BoxFlag { + Leaked, +} + +impl BoxFlag { + pub const fn value(&self) -> u8 { + match self { + Self::Leaked => U8_MASK1, + } + } +} diff --git a/crates/lune-std-ffi/src/data/box_data/mod.rs b/crates/lune-std-ffi/src/data/box_data/mod.rs new file mode 100644 index 00000000..1cdf9b4b --- /dev/null +++ b/crates/lune-std-ffi/src/data/box_data/mod.rs @@ -0,0 +1,166 @@ +use std::{ + alloc::{self, Layout}, + boxed::Box, + mem::ManuallyDrop, + ptr, +}; + +use mlua::prelude::*; + +use super::helper::method_provider; +use crate::{ + data::{association_names::REF_INNER, RefBounds, RefData, RefFlag}, + ffi::{association, bit_field::*, FfiData}, +}; + +mod flag; + +pub use self::flag::BoxFlag; + +const FFI_BOX_PRINT_MAX_LENGTH: usize = 1024; + +// Reference which created by lua should not be dereferenceable +const BOX_REF_FLAGS: u8 = + RefFlag::Readable.value() | RefFlag::Writable.value() | RefFlag::Offsetable.value(); + +// Untyped runtime sized memory for luau. +// This operations are safe, have boundaries check. +pub struct BoxData { + flags: u8, + data: ManuallyDrop>, +} + +impl BoxData { + pub fn new(size: usize) -> Self { + let slice = unsafe { + Box::from_raw(ptr::slice_from_raw_parts_mut( + alloc::alloc(Layout::array::(size).unwrap()), + size, + )) + }; + + Self { + flags: 0, + data: ManuallyDrop::new(slice), + } + } + + // Stringify for pretty-print, with hex format content + pub fn stringify(&self) -> String { + if self.size() > FFI_BOX_PRINT_MAX_LENGTH * 2 { + return String::from("length limit exceed"); + } + let mut buff: String = String::with_capacity(self.size() * 2 + 2); + buff.push_str("0x"); + for value in self.data.iter() { + buff.push_str(format!("{:x}", value.to_be()).as_str()); + } + buff + } + + pub fn leak(&mut self) { + self.flags = u8_set(self.flags, BoxFlag::Leaked.value(), true); + } + + // Make FfiRef from box, with boundary check + pub fn luaref<'lua>( + lua: &'lua Lua, + this: LuaAnyUserData<'lua>, + offset: Option, + ) -> LuaResult> { + let target = this.borrow::()?; + let mut bounds = RefBounds::new(0, target.size()); + let mut ptr = unsafe { target.get_inner_pointer() }; + + // Calculate offset + if let Some(t) = offset { + if !bounds.check_offset(t) { + return Err(LuaError::external(format!( + "Offset out of bounds (box.size: {}, got {})", + target.size(), + t + ))); + } + ptr = unsafe { ptr.byte_offset(t) }; + bounds = bounds.offset(t); + } + + let luaref = lua.create_userdata(RefData::new(ptr.cast(), BOX_REF_FLAGS, bounds))?; + + // Make box live longer then ref + association::set(lua, REF_INNER, &luaref, &this)?; + + Ok(luaref) + } + + // Fill with zero + pub fn zero(&mut self) { + self.data.fill(0); + } + + // Get size of box + #[inline] + pub fn size(&self) -> usize { + self.data.len() + } +} + +impl Drop for BoxData { + fn drop(&mut self) { + if u8_test_not(self.flags, BoxFlag::Leaked.value()) { + unsafe { ManuallyDrop::drop(&mut self.data) }; + } + } +} + +impl FfiData for BoxData { + fn check_inner_boundary(&self, offset: isize, size: usize) -> bool { + if offset < 0 { + return false; + } + self.size() - (offset as usize) >= size + } + #[inline] + unsafe fn get_inner_pointer(&self) -> *mut () { + self.data.as_ptr().cast_mut().cast::<()>() + } + fn is_readable(&self) -> bool { + true + } + fn is_writable(&self) -> bool { + true + } +} + +impl LuaUserData for BoxData { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("size", |_lua, this| Ok(this.size())); + } + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + method_provider::provide_copy_from(methods); + method_provider::provide_read_string(methods); + method_provider::provide_write_string(methods); + + // For convenience, :zero returns box itself. + methods.add_function_mut("zero", |_lua, this: LuaAnyUserData| { + this.borrow_mut::()?.zero(); + Ok(this) + }); + methods.add_function_mut( + "leak", + |lua, (this, offset): (LuaAnyUserData, Option)| { + this.borrow_mut::()?.leak(); + BoxData::luaref(lua, this, offset) + }, + ); + methods.add_function( + "ref", + |lua, (this, offset): (LuaAnyUserData, Option)| { + BoxData::luaref(lua, this, offset) + }, + ); + methods.add_meta_method(LuaMetaMethod::ToString, |_lua, this, ()| { + Ok(this.stringify()) + }); + } +} diff --git a/crates/lune-std-ffi/src/data/callable_data.rs b/crates/lune-std-ffi/src/data/callable_data.rs new file mode 100644 index 00000000..1119a2f8 --- /dev/null +++ b/crates/lune-std-ffi/src/data/callable_data.rs @@ -0,0 +1,191 @@ +use core::ffi::c_void; +use std::{ + mem::{self, MaybeUninit}, + ptr, +}; + +use libffi::{ + low::{ffi_cif, CodePtr}, + raw::ffi_call, +}; +use mlua::prelude::*; + +use super::{GetFfiData, RefData}; +use crate::ffi::{FfiArg, FfiData, FfiResult}; + +// A function pointer that luau can call. it stores libffi cif for calling convention. +pub struct CallableData { + cif: *mut ffi_cif, + arg_info_list: Vec, + result_info: FfiResult, + code: CodePtr, +} + +const VOID_RESULT_PTR: *mut () = ptr::null_mut(); +const ZERO_SIZE_ARG_PTR: *mut *mut c_void = ptr::null_mut(); + +// Optimization: +// Use known size array in stack instead of creating new Vec to eliminate heap allocation +macro_rules! create_caller { + ($len:expr) => { + |callable: &CallableData, result: LuaValue, args: LuaMultiValue| unsafe { + // Get `rvalue: *mut c_void` result pointer + let result_pointer = if callable.result_info.size == 0 { + VOID_RESULT_PTR + } else { + result.get_ffi_data()?.get_inner_pointer() + } + .cast::(); + + // Create `avalue: *mut *mut c_void` argument list + let mut arg_list: [MaybeUninit<*mut c_void>; $len] = [MaybeUninit::uninit(); $len]; + for (index, arg) in arg_list.iter_mut().enumerate() { + let arg_value = args + .get(index) + .ok_or_else(|| LuaError::external(format!("Argument {index} required")))? + .as_userdata() + .ok_or_else(|| LuaError::external("Argument must be a RefData"))?; + + if let Ok(arg_ref) = arg_value.borrow::() { + arg.write(arg_ref.get_inner_pointer().cast::()); + } else { + return Err(LuaError::external("Argument must be a RefData")); + } + } + + ffi_call( + callable.cif, + Some(*callable.code.as_safe_fun()), + result_pointer, + // SAFETY: MaybeUninit has the same layout as `T`, and initialized above + mem::transmute::<[MaybeUninit<*mut c_void>; $len], [*mut c_void; $len]>(arg_list) + .as_mut_ptr(), + ); + + Ok(()) + } + }; +} + +// Optimization: +// Call without arguments +unsafe fn zero_size_caller( + callable: &CallableData, + result: LuaValue, + _args: LuaMultiValue, +) -> LuaResult<()> { + let result_pointer = if callable.result_info.size == 0 { + VOID_RESULT_PTR + } else { + result.get_ffi_data()?.get_inner_pointer() + } + .cast::(); + + ffi_call( + callable.cif, + Some(*callable.code.as_safe_fun()), + result_pointer, + ZERO_SIZE_ARG_PTR, + ); + + Ok(()) +} + +// Optimization: sized callers +type Caller = + unsafe fn(callable: &CallableData, result: LuaValue, args: LuaMultiValue) -> LuaResult<()>; +const SIZED_CALLERS: [Caller; 13] = [ + zero_size_caller, + create_caller!(1), + create_caller!(2), + create_caller!(3), + create_caller!(4), + create_caller!(5), + create_caller!(6), + create_caller!(7), + create_caller!(8), + create_caller!(9), + create_caller!(10), + create_caller!(11), + create_caller!(12), +]; + +impl CallableData { + pub unsafe fn new( + cif: *mut ffi_cif, + arg_info_list: Vec, + result_info: FfiResult, + function_pointer: *const (), + ) -> Self { + Self { + cif, + arg_info_list, + result_info, + code: CodePtr::from_ptr(function_pointer.cast::()), + } + } + + // Stringify for pretty-print, with hex format address + pub fn stringify(&self) -> String { + format!("0x{:x}", self.code.as_ptr() as usize) + } + + pub unsafe fn call(&self, result: LuaValue, args: LuaMultiValue) -> LuaResult<()> { + let arg_len = self.arg_info_list.len(); + // Optimization: use sized caller when possible + if arg_len < SIZED_CALLERS.len() { + return SIZED_CALLERS[arg_len](self, result, args); + } + + // Get `rvalue: *mut c_void` result pointer + let result_pointer = if self.result_info.size == 0 { + VOID_RESULT_PTR + } else { + result.get_ffi_data()?.get_inner_pointer() + } + .cast::(); + + // Create `avalue: *mut *mut c_void` argument list + let mut arg_list = Vec::<*mut c_void>::with_capacity(arg_len); + for index in 0..arg_len { + let arg_value = args + .get(index) + .ok_or_else(|| LuaError::external(format!("Argument {index} required")))? + .as_userdata() + .ok_or_else(|| LuaError::external("Argument must be a RefData"))?; + + if let Ok(arg_ref) = arg_value.borrow::() { + arg_list.push(arg_ref.get_inner_pointer().cast::()); + } else { + return Err(LuaError::external("Argument must be a RefData")); + } + } + + // Call libffi::raw::ffi_call + ffi_call( + self.cif, + Some(*self.code.as_safe_fun()), + result_pointer, + arg_list.as_mut_ptr(), + ); + + Ok(()) + } +} + +impl LuaUserData for CallableData { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method( + LuaMetaMethod::Call, + |_lua, this: &CallableData, mut args: LuaMultiValue| { + let result = args.pop_front().ok_or_else(|| { + LuaError::external("First argument 'result' must be a RefData, BoxData or nil") + })?; + unsafe { this.call(result, args) } + }, + ); + methods.add_meta_method(LuaMetaMethod::ToString, |_lua, this, ()| { + Ok(this.stringify()) + }); + } +} diff --git a/crates/lune-std-ffi/src/data/closure_data.rs b/crates/lune-std-ffi/src/data/closure_data.rs new file mode 100644 index 00000000..93d21b49 --- /dev/null +++ b/crates/lune-std-ffi/src/data/closure_data.rs @@ -0,0 +1,161 @@ +use core::ffi::c_void; +use std::{borrow::Borrow, ptr}; + +use libffi::{ + low::{closure_alloc, closure_free, ffi_cif}, + raw::{ffi_closure, ffi_prep_closure_loc}, +}; +use mlua::prelude::*; + +use super::{ + association_names::REF_INNER, + ref_data::{RefBounds, RefData, RefFlag, UNSIZED_BOUNDS}, +}; +use crate::ffi::{ + association, + libffi_helper::{ffi_status_assert, SIZE_OF_POINTER}, + FfiArg, FfiData, FfiResult, +}; + +// A closure that can be created with lua function. +pub struct ClosureData { + lua: *const Lua, + closure: *mut ffi_closure, + code: Box<*mut c_void>, + arg_info_list: Vec, + result_info: FfiResult, + func: LuaRegistryKey, +} + +impl Drop for ClosureData { + fn drop(&mut self) { + unsafe { + closure_free(self.closure); + } + } +} + +const RESULT_REF_FLAGS: u8 = RefFlag::Writable.value() | RefFlag::Offsetable.value(); +const CLOSURE_REF_FLAGS: u8 = RefFlag::Function.value(); + +// Process C -> Lua function call +unsafe extern "C" fn callback( + cif: *mut ffi_cif, + result_pointer: *mut c_void, + arg_pointers: *mut *mut c_void, + closure_data: *mut c_void, +) { + let closure_data = closure_data.cast::().as_ref().unwrap(); + let lua = closure_data.lua.as_ref().unwrap(); + let len = (*cif).nargs as usize; + let mut args = Vec::::with_capacity(len + 1); + + // Push result pointer (ref) + args.push(LuaValue::UserData( + lua.create_userdata(RefData::new( + result_pointer.cast::<()>(), + RESULT_REF_FLAGS, + RefBounds::new(0, closure_data.result_info.size), + )) + .unwrap(), + )); + + // Push arg pointer (ref) + for i in 0..len { + let arg_info = closure_data.arg_info_list.get(i).unwrap(); + args.push(LuaValue::UserData( + lua.create_userdata(RefData::new( + (*arg_pointers.add(i)).cast::<()>(), + arg_info.callback_ref_flag, + RefBounds::new(0, arg_info.size), + )) + .unwrap(), + )); + } + + closure_data + .func + .borrow() + .into_lua(lua) + .unwrap() + .as_function() + .unwrap() + .call::<_, ()>(LuaMultiValue::from_vec(args)) + .unwrap(); +} + +impl ClosureData { + // Allocate new ffi closure with lua function + pub fn alloc( + lua: &Lua, + cif: *mut ffi_cif, + arg_info_list: Vec, + result_info: FfiResult, + func: LuaRegistryKey, + ) -> LuaResult { + let (closure, code) = closure_alloc(); + let code = code.as_mut_ptr(); + + let closure_data = lua.create_userdata(ClosureData { + lua: ptr::from_ref(lua), + closure, + code: Box::new(code), + arg_info_list, + result_info, + func, + })?; + + let closure_data_ptr = ptr::from_ref(&*closure_data.borrow::()?); + + ffi_status_assert(unsafe { + ffi_prep_closure_loc( + closure, + cif, + Some(callback), + closure_data_ptr.cast::().cast_mut(), + code, + ) + })?; + + Ok(closure_data) + } + + // Stringify for pretty-print, with hex format address + pub fn stringify(&self) -> String { + format!("0x{:x}", unsafe { self.get_inner_pointer() } as usize) + } +} + +impl FfiData for ClosureData { + unsafe fn get_inner_pointer(&self) -> *mut () { + ptr::from_ref::<*mut c_void>(&*self.code) + .cast::<()>() + .cast_mut() + } + fn check_inner_boundary(&self, offset: isize, size: usize) -> bool { + (offset as usize) + size <= SIZE_OF_POINTER + } + fn is_readable(&self) -> bool { + false + } + fn is_writable(&self) -> bool { + false + } +} + +impl LuaUserData for ClosureData { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function("ref", |lua, this: LuaAnyUserData| { + let ref_data = lua.create_userdata(RefData::new( + unsafe { this.borrow::()?.get_inner_pointer() }, + CLOSURE_REF_FLAGS, + UNSIZED_BOUNDS, + ))?; + association::set(lua, REF_INNER, &ref_data, &this)?; + Ok(ref_data) + }); + methods.add_meta_method(LuaMetaMethod::ToString, |_lua, this, ()| { + Ok(this.stringify()) + }); + } +} diff --git a/crates/lune-std-ffi/src/data/helper.rs b/crates/lune-std-ffi/src/data/helper.rs new file mode 100644 index 00000000..0b7de25b --- /dev/null +++ b/crates/lune-std-ffi/src/data/helper.rs @@ -0,0 +1,103 @@ +use mlua::prelude::*; + +use super::{FfiData, GetFfiData}; + +pub mod method_provider { + use super::*; + + // Implement copyFrom method + pub fn provide_copy_from<'lua, Target, M>(methods: &mut M) + where + Target: FfiData + 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_function( + "copyFrom", + |_lua, + (this_userdata, src, length, dst_offset, src_offset): ( + LuaAnyUserData, + LuaAnyUserData, + usize, + Option, + Option, + )| unsafe { + let this = this_userdata.borrow::()?; + let dst_offset = dst_offset.unwrap_or(0); + let src_offset = src_offset.unwrap_or(0); + let src = src.get_ffi_data()?; + + if !src.check_inner_boundary(src_offset, length) { + return Err(LuaError::external("Source out of bounds")); + } + if !this.check_inner_boundary(dst_offset, length) { + return Err(LuaError::external("Self out of bounds")); + } + + this.copy_from(&src, length, dst_offset, src_offset); + + Ok(this_userdata.clone()) + }, + ); + } + + // Implement readString method + pub fn provide_read_string<'lua, Target, M>(methods: &mut M) + where + Target: FfiData + 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_method( + "readString", + |lua, this, (length, offset): (usize, Option)| unsafe { + let offset = offset.unwrap_or(0); + + if !this.check_inner_boundary(offset, length) { + return Err(LuaError::external("Source out of bounds")); + } + + lua.create_string(this.read_string(length, offset)) + }, + ); + } + + // Implement writeString method + pub fn provide_write_string<'lua, Target, M>(methods: &mut M) + where + Target: FfiData + 'static, + M: LuaUserDataMethods<'lua, Target>, + { + methods.add_function( + "writeString", + |_lua, + (this_userdata, string, length, dst_offset, src_offset): ( + LuaAnyUserData, + LuaString, + Option, + Option, + Option, + )| unsafe { + let string_len = string.as_bytes().len(); + let dst_offset = dst_offset.unwrap_or(0); + let src_offset = src_offset.unwrap_or(0); + let length = length.unwrap_or_else(|| string_len - src_offset); + let this = this_userdata.borrow::()?; + + // Source string boundary check + if string_len < src_offset + length { + return Err(LuaError::external("Source out of bounds")); + } + + // Self boundary check + if !this.check_inner_boundary(dst_offset, length) { + return Err(LuaError::external("Self out of bounds")); + } + + this.write_string(string, length, dst_offset, src_offset); + Ok(this_userdata.clone()) + }, + ); + } + + // TODO: Bit operation support + // TODO: writeBase64 and readBase64 methods +} diff --git a/crates/lune-std-ffi/src/data/lib_data.rs b/crates/lune-std-ffi/src/data/lib_data.rs new file mode 100644 index 00000000..b8819f9b --- /dev/null +++ b/crates/lune-std-ffi/src/data/lib_data.rs @@ -0,0 +1,68 @@ +use dlopen2::raw::Library; +use mlua::prelude::*; + +use super::{ + association_names::SYM_INNER, + ref_data::{RefData, RefFlag, UNSIZED_BOUNDS}, +}; +use crate::ffi::association; + +const LIB_REF_FLAGS: u8 = RefFlag::Offsetable.value() + | RefFlag::Readable.value() + | RefFlag::Dereferenceable.value() + | RefFlag::Function.value(); + +// Runtime dynamic loaded libraries +pub struct LibData { + name: String, + lib: Library, +} + +impl LibData { + // Open library then return library handle + pub fn new(libname: String) -> LuaResult { + match Library::open(&libname) { + Ok(t) => Ok(Self { + lib: t, + name: libname.clone(), + }), + Err(err) => Err(err.into_lua_err()), + } + } + + // Get named symbol from library + pub fn find_symbol<'lua>( + lua: &'lua Lua, + this: LuaAnyUserData<'lua>, + name: String, + ) -> LuaResult> { + let lib = this.borrow::()?; + let sym = unsafe { + lib.lib + .symbol::<*const ()>(name.as_str()) + .map_err(LuaError::external)? + }; + let ffi_ref = + lua.create_userdata(RefData::new(sym.cast_mut(), LIB_REF_FLAGS, UNSIZED_BOUNDS))?; + + // Library handle should live longer than retrieved symbol + association::set(lua, SYM_INNER, &ffi_ref, &this)?; + + Ok(ffi_ref) + } + + pub fn stringify(&self) -> String { + self.name.clone() + } +} + +impl LuaUserData for LibData { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function("find", |lua, (this, name): (LuaAnyUserData, String)| { + LibData::find_symbol(lua, this, name) + }); + methods.add_meta_method(LuaMetaMethod::ToString, |_lua, this, ()| { + Ok(this.stringify()) + }); + } +} diff --git a/crates/lune-std-ffi/src/data/mod.rs b/crates/lune-std-ffi/src/data/mod.rs new file mode 100644 index 00000000..8294f214 --- /dev/null +++ b/crates/lune-std-ffi/src/data/mod.rs @@ -0,0 +1,68 @@ +use std::cell::Ref; + +use lune_utils::fmt::{pretty_format_value, ValueFormatConfig}; +use mlua::prelude::*; + +mod box_data; +mod callable_data; +mod closure_data; +mod helper; +mod lib_data; +mod ref_data; + +pub use self::{ + box_data::BoxData, + callable_data::CallableData, + closure_data::ClosureData, + lib_data::LibData, + ref_data::{create_nullref, RefBounds, RefData, RefFlag, UNSIZED_BOUNDS}, +}; +use crate::ffi::FfiData; + +// Named registry keys +mod association_names { + pub const REF_INNER: &str = "__ref_inner"; + pub const SYM_INNER: &str = "__syn_inner"; +} + +// Get dynamic FfiData handle from LuaValue and LuaAnyUserData +pub trait GetFfiData { + fn get_ffi_data(&self) -> LuaResult>; + fn is_ffi_data(&self) -> bool; +} +impl GetFfiData for LuaAnyUserData<'_> { + fn get_ffi_data(&self) -> LuaResult> { + if self.is::() { + Ok(self.borrow::()? as Ref) + } else if self.is::() { + Ok(self.borrow::()? as Ref) + } else if self.is::() { + Ok(self.borrow::()? as Ref) + } else { + let config = ValueFormatConfig::new(); + Err(LuaError::external(format!( + "Expected FfiBox, FfiRef or ClosureData. got {}", + pretty_format_value(&LuaValue::UserData(self.to_owned()), &config) + ))) + } + } + fn is_ffi_data(&self) -> bool { + self.is::() | self.is::() | self.is::() + } +} +impl GetFfiData for LuaValue<'_> { + fn get_ffi_data(&self) -> LuaResult> { + self.as_userdata() + .ok_or_else(|| { + let config = ValueFormatConfig::new(); + LuaError::external(format!( + "Expected FfiBox, FfiRef or ClosureData. got {}", + pretty_format_value(self, &config) + )) + })? + .get_ffi_data() + } + fn is_ffi_data(&self) -> bool { + self.as_userdata().map_or(false, GetFfiData::is_ffi_data) + } +} diff --git a/crates/lune-std-ffi/src/data/ref_data/bounds.rs b/crates/lune-std-ffi/src/data/ref_data/bounds.rs new file mode 100644 index 00000000..4bdec8b4 --- /dev/null +++ b/crates/lune-std-ffi/src/data/ref_data/bounds.rs @@ -0,0 +1,99 @@ +// Memory boundaries +pub struct RefBounds { + // How much bytes available above + pub(crate) above: usize, + // How much bytes available below + pub(crate) below: usize, +} + +pub const UNSIZED_BOUNDS: RefBounds = RefBounds { + above: usize::MAX, + below: usize::MAX, +}; + +impl RefBounds { + pub fn new(above: usize, below: usize) -> Self { + Self { above, below } + } + + // Check offset is in boundary + #[inline] + pub fn check_offset(&self, offset: isize) -> bool { + let offset_abs = offset.unsigned_abs(); + match offset.signum() { + -1 => self.above >= offset_abs, + 1 => self.below >= offset_abs, + 0 => true, + _ => unreachable!(), + } + } + + // Check boundary with specific size + // + // -4 ∧ ────── Above = 4 + // -3 │ (Case1) + // -2 │ ┌──── Offset = -2 : offset >= 0 || abs(offset) <= above + // -1 │ │ + // 0 │ │ Size = 4 + // 1 │ │ (Case2) + // 2 │ ∨ ─── End = 2 : end = offset + size; + // 3 │ end <= 0 || end <= below + // 4 ∨ ────── Below = 4 + // + #[inline] + pub fn check_sized(&self, offset: isize, size: usize) -> bool { + // (Case1) offset over above + if offset < 0 && self.above < offset.unsigned_abs() { + return false; + } + + // (Case2) end over below + let end = offset + (size as isize); + end <= 0 || self.below >= end.unsigned_abs() + } + + // Calculate new boundaries with offset + // No boundary checking in here + // + // Above = 3 + // ∧ ───∧─── + // -3│ │ New above = 2 + // -2│ │ + // -1│ <────── Offset = -1 + // 0│ │ + // 1│ │ + // 2│ │ + // 3│ │ New below = 4 + // ∨ ───∨─── + // Below = 3 + // + #[inline] + pub fn offset(&self, offset: isize) -> Self { + let sign = offset.signum(); + let offset_abs = offset.unsigned_abs(); + + Self { + above: match sign { + -1 => self.above - offset_abs, + 1 => self.above + offset_abs, + 0 => self.above, + _ => unreachable!(), + }, + below: match sign { + -1 => self.below + offset_abs, + 1 => self.below - offset_abs, + 0 => self.below, + _ => unreachable!(), + }, + } + } +} + +impl Clone for RefBounds { + fn clone(&self) -> Self { + Self { + above: self.above, + below: self.below, + } + } +} diff --git a/crates/lune-std-ffi/src/data/ref_data/flag.rs b/crates/lune-std-ffi/src/data/ref_data/flag.rs new file mode 100644 index 00000000..a7068adf --- /dev/null +++ b/crates/lune-std-ffi/src/data/ref_data/flag.rs @@ -0,0 +1,23 @@ +use crate::ffi::bit_field::*; + +pub enum RefFlag { + Leaked, + Dereferenceable, + Readable, + Writable, + Offsetable, + Function, +} + +impl RefFlag { + pub const fn value(&self) -> u8 { + match self { + Self::Leaked => U8_MASK1, + Self::Dereferenceable => U8_MASK2, + Self::Writable => U8_MASK3, + Self::Readable => U8_MASK4, + Self::Offsetable => U8_MASK5, + Self::Function => U8_MASK6, + } + } +} diff --git a/crates/lune-std-ffi/src/data/ref_data/mod.rs b/crates/lune-std-ffi/src/data/ref_data/mod.rs new file mode 100644 index 00000000..2f26bdd1 --- /dev/null +++ b/crates/lune-std-ffi/src/data/ref_data/mod.rs @@ -0,0 +1,204 @@ +use std::{mem::ManuallyDrop, ptr}; + +use mlua::prelude::*; + +use super::helper::method_provider; +use crate::{ + data::association_names::REF_INNER, + ffi::{association, bit_field::*, FfiData}, +}; + +mod bounds; +mod flag; + +pub use self::{ + bounds::{RefBounds, UNSIZED_BOUNDS}, + flag::RefFlag, +}; + +// Box:ref():ref() should not be able to modify, Only for external +const REF_REF_FLAGS: u8 = 0; + +const DEREF_REF_FLAG: u8 = RefFlag::Dereferenceable.value() + | RefFlag::Function.value() + | RefFlag::Offsetable.value() + | RefFlag::Readable.value() + | RefFlag::Writable.value(); + +// A referenced memory address box. Possible to read and write through types. +pub struct RefData { + ptr: ManuallyDrop>, + pub(crate) flags: u8, + boundary: RefBounds, +} + +impl RefData { + pub fn new(ptr: *mut (), flags: u8, boundary: RefBounds) -> Self { + Self { + ptr: ManuallyDrop::new(Box::new(ptr)), + flags, + boundary, + } + } + + pub fn update<'lua>( + lua: &'lua Lua, + this: LuaAnyUserData<'lua>, + ptr: *mut (), + flags: u8, + boundary: RefBounds, + ) -> LuaResult<()> { + let mut target = this.borrow_mut::()?; + association::set(lua, REF_INNER, &this, LuaNil)?; + + **target.ptr = ptr; + target.flags = flags; + target.boundary = boundary; + + Ok(()) + } + + // Create reference of this reference + pub fn luaref<'lua>( + lua: &'lua Lua, + this: LuaAnyUserData<'lua>, + ) -> LuaResult> { + let target = this.borrow::()?; + + let luaref = lua.create_userdata(RefData::new( + ptr::from_ref(&**target.ptr) as *mut (), + REF_REF_FLAGS, + RefBounds { + below: 0, + above: size_of::(), + }, + ))?; + + // Make sure new reference live longer then this reference + association::set(lua, REF_INNER, &luaref, &this)?; + + Ok(luaref) + } + + // Dereference this reference + pub unsafe fn dereference(&self) -> LuaResult { + // Check dereferenceable + if !u8_test(self.flags, RefFlag::Dereferenceable.value()) { + return Err(LuaError::external("Reference is not dereferenceable")); + } + + // Check boundary + if !self.boundary.check_sized(0, size_of::()) { + return Err(LuaError::external("Out of bounds")); + } + + Ok(Self::new( + *self.ptr.cast::<*mut ()>(), + DEREF_REF_FLAG, + UNSIZED_BOUNDS, + )) + } + + pub fn is_null(&self) -> bool { + // * ManuallyDrop wrapper + // * Box wrapper + (**self.ptr) as usize == 0 + } + + pub fn leak(&mut self) { + self.flags = u8_set(self.flags, RefFlag::Leaked.value(), true); + } + + // Create new reference with specific offset from this reference + pub unsafe fn offset(&self, offset: isize) -> LuaResult { + // Check offsetable + if u8_test_not(self.flags, RefFlag::Offsetable.value()) { + return Err(LuaError::external("Reference is not offsetable")); + } + + // Check boundary + if !self.boundary.check_offset(offset) { + return Err(LuaError::external(format!( + "Offset out of bounds (high: {}, low: {}, got {})", + self.boundary.above, self.boundary.below, offset + ))); + } + + let boundary = self.boundary.offset(offset); + Ok(Self::new( + self.ptr.byte_offset(offset), + u8_set(self.flags, RefFlag::Leaked.value(), false), + boundary, + )) + } + + // Stringify for pretty-print, with hex format address + pub fn stringify(&self) -> String { + format!("0x{:x}", **self.ptr as usize) + } +} + +impl Drop for RefData { + fn drop(&mut self) { + if u8_test_not(self.flags, RefFlag::Leaked.value()) { + unsafe { ManuallyDrop::drop(&mut self.ptr) }; + } + } +} + +impl FfiData for RefData { + fn check_inner_boundary(&self, offset: isize, size: usize) -> bool { + self.boundary.check_sized(offset, size) + } + #[inline] + unsafe fn get_inner_pointer(&self) -> *mut () { + **self.ptr + } + fn is_readable(&self) -> bool { + u8_test(self.flags, RefFlag::Readable.value()) + } + fn is_writable(&self) -> bool { + u8_test(self.flags, RefFlag::Writable.value()) + } +} + +impl LuaUserData for RefData { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + method_provider::provide_copy_from(methods); + method_provider::provide_read_string(methods); + method_provider::provide_write_string(methods); + + methods.add_method("deref", |_lua, this, ()| unsafe { this.dereference() }); + methods.add_function("offset", |lua, (this, offset): (LuaAnyUserData, isize)| { + let ffiref = unsafe { this.borrow::()?.offset(offset)? }; + let userdata = lua.create_userdata(ffiref)?; + + // If the ref holds a box or reference, make sure the new ref also holds it + if let Some(t) = association::get(lua, REF_INNER, &this)? { + association::set(lua, REF_INNER, &userdata, t)?; + } + + Ok(userdata) + }); + methods.add_function_mut("leak", |lua, this: LuaAnyUserData| { + this.borrow_mut::()?.leak(); + RefData::luaref(lua, this) + }); + methods.add_function("ref", |lua, this: LuaAnyUserData| { + RefData::luaref(lua, this) + }); + methods.add_method("isNull", |_lua, this, ()| Ok(this.is_null())); + methods.add_meta_method(LuaMetaMethod::ToString, |_lua, this, ()| { + Ok(this.stringify()) + }); + } +} + +pub fn create_nullref(lua: &Lua) -> LuaResult { + lua.create_userdata(RefData::new( + ptr::null_mut::<()>().cast(), + 0, + // usize::MAX means that nullptr is can be 'any' pointer type + UNSIZED_BOUNDS, + )) +} diff --git a/crates/lune-std-ffi/src/ffi/association.rs b/crates/lune-std-ffi/src/ffi/association.rs new file mode 100644 index 00000000..f9a68c83 --- /dev/null +++ b/crates/lune-std-ffi/src/ffi/association.rs @@ -0,0 +1,52 @@ +use mlua::prelude::*; + +// This is a small library that helps you set the dependencies of data in Lua. +// In FFI, there is often data that is dependent on other data. +// However, if you use user_value to inform Lua of the dependency, +// a table will be created for each userdata. +// To prevent this, we place a weak reference table in the named registry +// and simulate what mlua does. + +// If the dependency is deep, the value may be completely destroyed when +// gc is performed multiple times. To prevent this situation, FFI should copy +// dependency if possible. + +// You can delete the relationship by changing 'associated' to nil +#[inline] +pub fn set<'lua, T, U>(lua: &'lua Lua, regname: &str, value: T, associated: U) -> LuaResult<()> +where + T: IntoLua<'lua>, + U: IntoLua<'lua>, +{ + let table = match lua.named_registry_value::(regname)? { + LuaValue::Nil => { + let table = lua.create_table()?; + lua.set_named_registry_value(regname, table.clone())?; + let meta = lua.create_table()?; + meta.set("__mode", "k")?; + table.set_metatable(Some(meta)); + table + } + LuaValue::Table(t) => t, + _ => panic!(""), + }; + + table.set(value, associated)?; + + Ok(()) +} + +// Returns the Lua value that 'value' keeps. +// If there is no table in registry, it returns None. +// If there is no value in table, it returns LuaNil. +#[inline] +pub fn get<'lua, T>(lua: &'lua Lua, regname: &str, value: T) -> LuaResult>> +where + T: IntoLua<'lua>, +{ + match lua.named_registry_value::(regname)? { + LuaValue::Nil => Ok(None), + LuaValue::Table(t) => Ok(Some(t.get(value)?)), + _ => panic!(), + } +} diff --git a/crates/lune-std-ffi/src/ffi/bit_field.rs b/crates/lune-std-ffi/src/ffi/bit_field.rs new file mode 100644 index 00000000..175d5882 --- /dev/null +++ b/crates/lune-std-ffi/src/ffi/bit_field.rs @@ -0,0 +1,31 @@ +#![allow(unused)] + +// Simple bit field library for handling data flags + +pub const U8_MASK1: u8 = 1; +pub const U8_MASK2: u8 = 2; +pub const U8_MASK3: u8 = 4; +pub const U8_MASK4: u8 = 8; +pub const U8_MASK5: u8 = 16; +pub const U8_MASK6: u8 = 32; +pub const U8_MASK7: u8 = 64; +pub const U8_MASK8: u8 = 128; + +#[inline] +pub fn u8_test(bits: u8, mask: u8) -> bool { + bits & mask != 0 +} + +#[inline] +pub fn u8_test_not(bits: u8, mask: u8) -> bool { + bits & mask == 0 +} + +#[inline] +pub fn u8_set(bits: u8, mask: u8, val: bool) -> u8 { + if val { + bits | mask + } else { + bits & !mask + } +} diff --git a/crates/lune-std-ffi/src/ffi/cast.rs b/crates/lune-std-ffi/src/ffi/cast.rs new file mode 100644 index 00000000..bbbe5804 --- /dev/null +++ b/crates/lune-std-ffi/src/ffi/cast.rs @@ -0,0 +1,36 @@ +use std::cell::Ref; + +use mlua::prelude::*; +use num::cast::AsPrimitive; + +use super::FfiData; + +// Cast number type to another number type, with num::cast library +#[inline] +pub fn num_cast( + from: &Ref, + into: &Ref, + from_offset: isize, + into_offset: isize, +) -> LuaResult<()> +where + From: AsPrimitive, + Into: 'static + Copy, +{ + let from_ptr = unsafe { + from.get_inner_pointer() + .byte_offset(from_offset) + .cast::() + }; + let into_ptr = unsafe { + into.get_inner_pointer() + .byte_offset(into_offset) + .cast::() + }; + + unsafe { + *into_ptr = (*from_ptr).as_(); + } + + Ok(()) +} diff --git a/crates/lune-std-ffi/src/ffi/libffi_helper.rs b/crates/lune-std-ffi/src/ffi/libffi_helper.rs new file mode 100644 index 00000000..bfb73a51 --- /dev/null +++ b/crates/lune-std-ffi/src/ffi/libffi_helper.rs @@ -0,0 +1,43 @@ +use std::ptr::{self, null_mut}; + +use libffi::{low, raw}; +use mlua::prelude::*; + +pub const SIZE_OF_POINTER: usize = size_of::<*mut ()>(); + +// Get ensured size of ctype (raw::libffi_type) +pub fn get_ensured_size(ffi_type: *mut raw::ffi_type) -> LuaResult { + let mut cif = low::ffi_cif::default(); + let result = unsafe { + raw::ffi_prep_cif( + ptr::from_mut(&mut cif), + raw::ffi_abi_FFI_DEFAULT_ABI, + 0, + ffi_type, + null_mut(), + ) + }; + + ffi_status_assert(result)?; + unsafe { Ok((*ffi_type).size) } +} + +// Converts ffi status into &str for formatting +const FFI_STATUS_NAMES: [&str; 4] = [ + "ffi_status_FFI_OK", + "ffi_status_FFI_BAD_TYPEDEF", + "ffi_status_FFI_BAD_ABI", + "ffi_status_FFI_BAD_ARGTYPE", +]; + +// Check ffi_result is OK +pub fn ffi_status_assert(result: raw::ffi_status) -> LuaResult<()> { + if result == raw::ffi_status_FFI_OK { + Ok(()) + } else { + Err(LuaError::external(format!( + "ffi_status assertion failed. expected result {}, got {}", + FFI_STATUS_NAMES[0], FFI_STATUS_NAMES[result as usize] + ))) + } +} diff --git a/crates/lune-std-ffi/src/ffi/mod.rs b/crates/lune-std-ffi/src/ffi/mod.rs new file mode 100644 index 00000000..36b27d91 --- /dev/null +++ b/crates/lune-std-ffi/src/ffi/mod.rs @@ -0,0 +1,129 @@ +use std::cell::Ref; + +use mlua::prelude::*; + +pub mod association; +pub mod bit_field; +mod cast; +pub mod libffi_helper; + +pub use self::cast::num_cast; + +// Common type information +pub trait FfiSize { + fn get_size(&self) -> usize; +} +pub trait FfiSignedness { + fn get_signedness(&self) -> bool { + false + } +} + +// Provide conversion between luau value and ffi types +pub trait FfiConvert { + // Write LuaValue into FfiData + unsafe fn value_into_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + value: LuaValue<'lua>, + ) -> LuaResult<()>; + + // Read LuaValue from FfiData + unsafe fn value_from_data<'lua>( + &self, + lua: &'lua Lua, + offset: isize, + data_handle: &Ref, + ) -> LuaResult>; + + unsafe fn copy_data( + &self, + lua: &Lua, + dst_offset: isize, + src_offset: isize, + dst: &Ref, + src: &Ref, + ) -> LuaResult<()>; + + unsafe fn stringify_data( + &self, + _lua: &Lua, + _offset: isize, + _data_handle: &Ref, + ) -> LuaResult { + Err(LuaError::external("Stringify method not implemented")) + } +} + +// Provide read, write, boundary check methods for datas +pub trait FfiData { + fn check_inner_boundary(&self, offset: isize, size: usize) -> bool; + unsafe fn get_inner_pointer(&self) -> *mut (); + fn is_writable(&self) -> bool; + fn is_readable(&self) -> bool; + unsafe fn copy_from( + &self, + src: &Ref, + length: usize, + dst_offset: isize, + src_offset: isize, + ) { + self.get_inner_pointer() + .cast::() + .byte_offset(dst_offset) + .copy_from( + src.get_inner_pointer().cast::().byte_offset(src_offset), + length, + ); + } + unsafe fn read_string(&self, length: usize, offset: isize) -> Vec { + let mut string = Vec::::with_capacity(length); + string.as_mut_ptr().copy_from( + self.get_inner_pointer().cast::().byte_offset(offset), + length, + ); + string.set_len(length); + string + } + unsafe fn write_string( + &self, + src: LuaString, + length: usize, + dst_offset: isize, + src_offset: usize, + ) { + self.get_inner_pointer() + .cast::() + .byte_offset(dst_offset) + .copy_from( + src.as_bytes().as_ptr().cast::().byte_add(src_offset), + length, + ); + } +} + +// Function argument informations +pub struct FfiArg { + pub size: usize, + pub callback_ref_flag: u8, +} +impl Clone for FfiArg { + fn clone(&self) -> Self { + Self { + size: self.size, + callback_ref_flag: self.callback_ref_flag, + } + } +} + +// Function result information +pub struct FfiResult { + pub size: usize, +} +impl Clone for FfiResult { + fn clone(&self) -> Self { + Self { size: self.size } + } +} diff --git a/crates/lune-std-ffi/src/lib.rs b/crates/lune-std-ffi/src/lib.rs new file mode 100644 index 00000000..62b2572d --- /dev/null +++ b/crates/lune-std-ffi/src/lib.rs @@ -0,0 +1,39 @@ +#![allow(clippy::cargo_common_metadata)] + +use std::ffi::c_void; + +use libc::free; +use lune_utils::TableBuilder; +use mlua::prelude::*; + +mod c; +mod data; +mod ffi; + +use crate::{ + c::{export_c, export_fixed_types}, + data::{create_nullref, BoxData, GetFfiData, LibData}, +}; + +/** + Creates the `ffi` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { + let result = TableBuilder::new(lua)? + .with_function("nullRef", |lua, ()| create_nullref(lua))? + .with_function("box", |_lua, size: usize| Ok(BoxData::new(size)))? + .with_function("open", |_lua, name: String| LibData::new(name))? + .with_function("isInteger", |_lua, num: LuaValue| Ok(num.is_integer()))? + .with_function("free", |_lua, data: LuaAnyUserData| { + unsafe { free(data.get_ffi_data()?.get_inner_pointer().cast::()) }; + Ok(()) + })? + .with_values(export_fixed_types(lua)?)? + .with_value("c", export_c(lua)?)?; + + result.build_readonly() +} diff --git a/crates/lune-std/Cargo.toml b/crates/lune-std/Cargo.toml index 07762b64..bfe61dab 100644 --- a/crates/lune-std/Cargo.toml +++ b/crates/lune-std/Cargo.toml @@ -24,6 +24,7 @@ default = [ "serde", "stdio", "task", + "ffi", ] datetime = ["dep:lune-std-datetime"] @@ -36,6 +37,7 @@ roblox = ["dep:lune-std-roblox"] serde = ["dep:lune-std-serde"] stdio = ["dep:lune-std-stdio"] task = ["dep:lune-std-task"] +ffi = ["dep:lune-std-ffi"] [dependencies] mlua = { version = "0.9.9", features = ["luau"] } @@ -57,3 +59,4 @@ lune-std-roblox = { optional = true, version = "0.1.4", path = "../lune-std-robl lune-std-serde = { optional = true, version = "0.1.2", path = "../lune-std-serde" } lune-std-stdio = { optional = true, version = "0.1.2", path = "../lune-std-stdio" } lune-std-task = { optional = true, version = "0.1.2", path = "../lune-std-task" } +lune-std-ffi = { optional = true, version = "0.1.1", path = "../lune-std-ffi" } diff --git a/crates/lune-std/src/lib.rs b/crates/lune-std/src/lib.rs index a29bef03..98089127 100644 --- a/crates/lune-std/src/lib.rs +++ b/crates/lune-std/src/lib.rs @@ -6,10 +6,12 @@ mod global; mod globals; mod library; mod luaurc; +mod unsafe_library; pub use self::global::LuneStandardGlobal; pub use self::globals::version::set_global_version; pub use self::library::LuneStandardLibrary; +pub use self::unsafe_library::{get_unsafe_library_enabled, set_unsafe_library_enabled}; /** Injects all standard globals into the given Lua state / VM. diff --git a/crates/lune-std/src/library.rs b/crates/lune-std/src/library.rs index 9a301f57..80e2b5cc 100644 --- a/crates/lune-std/src/library.rs +++ b/crates/lune-std/src/library.rs @@ -2,8 +2,10 @@ use std::str::FromStr; use mlua::prelude::*; +use crate::get_unsafe_library_enabled; + /** - A standard library provided by Lune. + A standard library probloxrovided by Lune. */ #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[rustfmt::skip] @@ -18,6 +20,7 @@ pub enum LuneStandardLibrary { #[cfg(feature = "serde")] Serde, #[cfg(feature = "stdio")] Stdio, #[cfg(feature = "roblox")] Roblox, + #[cfg(feature = "ffi")] Ffi, } impl LuneStandardLibrary { @@ -36,6 +39,7 @@ impl LuneStandardLibrary { #[cfg(feature = "serde")] Self::Serde, #[cfg(feature = "stdio")] Self::Stdio, #[cfg(feature = "roblox")] Self::Roblox, + #[cfg(feature = "ffi")] Self::Ffi, ]; /** @@ -56,6 +60,31 @@ impl LuneStandardLibrary { #[cfg(feature = "serde")] Self::Serde => "serde", #[cfg(feature = "stdio")] Self::Stdio => "stdio", #[cfg(feature = "roblox")] Self::Roblox => "roblox", + #[cfg(feature = "ffi")] Self::Ffi => "ffi", + + _ => unreachable!("no standard library enabled"), + } + } + + /** + Gets whether the library is unsafe. + */ + #[must_use] + #[rustfmt::skip] + #[allow(unreachable_patterns)] + pub fn is_unsafe(&self) -> bool { + match self { + #[cfg(feature = "datetime")] Self::DateTime => false, + #[cfg(feature = "fs")] Self::Fs => false, + #[cfg(feature = "luau")] Self::Luau => false, + #[cfg(feature = "net")] Self::Net => false, + #[cfg(feature = "task")] Self::Task => false, + #[cfg(feature = "process")] Self::Process => false, + #[cfg(feature = "regex")] Self::Regex => false, + #[cfg(feature = "serde")] Self::Serde => false, + #[cfg(feature = "stdio")] Self::Stdio => false, + #[cfg(feature = "roblox")] Self::Roblox => false, + #[cfg(feature = "ffi")] Self::Ffi => true, _ => unreachable!("no standard library enabled"), } @@ -66,11 +95,15 @@ impl LuneStandardLibrary { # Errors - If the library could not be created. + If the library could not be created, or if requiring an unsafe library without enabling the unsafe library. */ #[rustfmt::skip] #[allow(unreachable_patterns)] pub fn module<'lua>(&self, lua: &'lua Lua) -> LuaResult> { + if self.is_unsafe() && !get_unsafe_library_enabled(lua) { + return Err(LuaError::external(format!("Standard library '{}' requires unsafe library enabled", self.name()))); + } + let res: LuaResult = match self { #[cfg(feature = "datetime")] Self::DateTime => lune_std_datetime::module(lua), #[cfg(feature = "fs")] Self::Fs => lune_std_fs::module(lua), @@ -82,6 +115,7 @@ impl LuneStandardLibrary { #[cfg(feature = "serde")] Self::Serde => lune_std_serde::module(lua), #[cfg(feature = "stdio")] Self::Stdio => lune_std_stdio::module(lua), #[cfg(feature = "roblox")] Self::Roblox => lune_std_roblox::module(lua), + #[cfg(feature = "ffi")] Self::Ffi => lune_std_ffi::module(lua), _ => unreachable!("no standard library enabled"), }; @@ -111,6 +145,7 @@ impl FromStr for LuneStandardLibrary { #[cfg(feature = "serde")] "serde" => Self::Serde, #[cfg(feature = "stdio")] "stdio" => Self::Stdio, #[cfg(feature = "roblox")] "roblox" => Self::Roblox, + #[cfg(feature = "ffi")] "ffi" => Self::Ffi, _ => { return Err(format!( diff --git a/crates/lune-std/src/unsafe_library.rs b/crates/lune-std/src/unsafe_library.rs new file mode 100644 index 00000000..31660506 --- /dev/null +++ b/crates/lune-std/src/unsafe_library.rs @@ -0,0 +1,26 @@ +use mlua::prelude::*; + +struct UnsafeLibrary(bool); + +/** + Override unsafe library allowance +*/ +pub fn set_unsafe_library_enabled(lua: &Lua, enabled: bool) { + lua.set_app_data(UnsafeLibrary(enabled)); +} + +/** + Returns whether unsafe libraries are allowed + + # Panics + + Panic if `UnsafeLib` app data doesn't exist. +*/ +#[must_use] +pub fn get_unsafe_library_enabled(lua: &Lua) -> bool { + if let Some(app_data) = lua.app_data_ref::() { + app_data.0 + } else { + false + } +} diff --git a/crates/lune/Cargo.toml b/crates/lune/Cargo.toml index 426782fc..a3df1556 100644 --- a/crates/lune/Cargo.toml +++ b/crates/lune/Cargo.toml @@ -30,6 +30,7 @@ std-roblox = ["dep:lune-std", "lune-std/roblox", "dep:lune-roblox"] std-serde = ["dep:lune-std", "lune-std/serde"] std-stdio = ["dep:lune-std", "lune-std/stdio"] std-task = ["dep:lune-std", "lune-std/task"] +std-ffi = ["dep:lune-std", "lune-std/ffi"] std = [ "std-datetime", @@ -42,6 +43,7 @@ std = [ "std-serde", "std-stdio", "std-task", + "std-ffi", ] cli = ["dep:clap", "dep:include_dir", "dep:rustyline", "dep:zip_next"] diff --git a/crates/lune/src/cli/repl.rs b/crates/lune/src/cli/repl.rs index 78a6bd74..faeff737 100644 --- a/crates/lune/src/cli/repl.rs +++ b/crates/lune/src/cli/repl.rs @@ -17,7 +17,11 @@ enum PromptState { /// Launch an interactive REPL (default) #[derive(Debug, Clone, Default, Parser)] -pub struct ReplCommand {} +pub struct ReplCommand { + /// Allow unsafe libraries + #[clap(long, action)] + r#unsafe: bool, +} impl ReplCommand { pub async fn run(self) -> Result { @@ -38,7 +42,7 @@ impl ReplCommand { let mut prompt_state = PromptState::Regular; let mut source_code = String::new(); - let mut lune_instance = Runtime::new(); + let mut lune_instance = Runtime::new().set_unsafe_library_enabled(self.r#unsafe); loop { let prompt = match prompt_state { diff --git a/crates/lune/src/cli/run.rs b/crates/lune/src/cli/run.rs index 6267ed7d..8f198c48 100644 --- a/crates/lune/src/cli/run.rs +++ b/crates/lune/src/cli/run.rs @@ -14,6 +14,9 @@ use super::utils::files::{discover_script_path_including_lune_dirs, strip_sheban /// Run a script #[derive(Debug, Clone, Parser)] pub struct RunCommand { + /// Allow unsafe libraries + #[clap(long, action)] + r#unsafe: bool, /// Script name or full path to the file to run script_path: String, /// Arguments to pass to the script, stored in process.args @@ -41,7 +44,9 @@ impl RunCommand { }; // Create a new lune runtime with all globals & run the script - let mut rt = Runtime::new().with_args(self.script_args); + let mut rt = Runtime::new() + .with_args(self.script_args) + .set_unsafe_library_enabled(self.r#unsafe); let result = rt .run(&script_display_name, strip_shebang(script_contents)) diff --git a/crates/lune/src/rt/runtime.rs b/crates/lune/src/rt/runtime.rs index 31e5b039..daeb0fd7 100644 --- a/crates/lune/src/rt/runtime.rs +++ b/crates/lune/src/rt/runtime.rs @@ -54,6 +54,7 @@ impl RuntimeInner { feature = "std-serde", feature = "std-stdio", feature = "std-task", + feature = "std-ffi", ))] { lune_std::set_global_version(lua, env!("CARGO_PKG_VERSION")); @@ -76,6 +77,7 @@ impl RuntimeInner { feature = "std-serde", feature = "std-stdio", feature = "std-task", + feature = "std-ffi", ))] { let g_table = lune_std::LuneStandardGlobal::GTable; @@ -130,6 +132,15 @@ impl Runtime { self } + /** + Sets arguments to give in `process.args` for Lune scripts. + */ + #[must_use] + pub fn set_unsafe_library_enabled(self, enabled: bool) -> Self { + lune_std::set_unsafe_library_enabled(self.inner.lua(), enabled); + self + } + /** Runs a Lune script inside of the current runtime. diff --git a/crates/lune/src/tests.rs b/crates/lune/src/tests.rs index d5f56404..0a62219e 100644 --- a/crates/lune/src/tests.rs +++ b/crates/lune/src/tests.rs @@ -31,7 +31,7 @@ macro_rules! create_tests { // The rest of the test logic can continue as normal let full_name = format!("{}/tests/{}.luau", workspace_dir.display(), $value); let script = read_to_string(&full_name).await?; - let mut lune = Runtime::new().with_args( + let mut lune = Runtime::new().set_unsafe_library_enabled(true).with_args( ARGS .clone() .iter() @@ -99,6 +99,34 @@ create_tests! { datetime_to_universal_time: "datetime/toUniversalTime", } +#[cfg(feature = "std-ffi")] +create_tests! { + ffi_external_closure_call_closure: "ffi/external_closure/callClosure", + ffi_external_closure_call_closure_with_pointer: "ffi/external_closure/callClosureWithPointer", + ffi_external_closure_call_hello_world: "ffi/external_closure/callHelloWorld", + ffi_external_math_add_int: "ffi/external_math/addInt", + ffi_external_math_mul_int: "ffi/external_math/mulInt", + ffi_external_pointer_pointer_read: "ffi/external_pointer/pointerRead", + ffi_external_pointer_pointer_write: "ffi/external_pointer/pointerWrite", + ffi_external_print_hello_world: "ffi/external_print/helloWorld", + ffi_external_struct_ab: "ffi/external_struct/ab", + ffi_pretty_print_arr: "ffi/pretty_print/arr", + ffi_pretty_print_box: "ffi/pretty_print/box", + ffi_pretty_print_fn: "ffi/pretty_print/fn", + ffi_pretty_print_ptr: "ffi/pretty_print/ptr", + ffi_pretty_print_struct: "ffi/pretty_print/struct", + ffi_pretty_print_type: "ffi/pretty_print/type", + ffi_types_arr: "ffi/types/arr", + ffi_types_ptr: "ffi/types/ptr", + ffi_types_struct: "ffi/types/struct", + ffi_cast: "ffi/cast", + ffi_free: "ffi/free", + ffi_is_integer: "ffi/isInteger", + ffi_read_boundary: "ffi/readBoundary", + ffi_read_write_string: "ffi/stringReadWrite", + ffi_write_boundary: "ffi/writeBoundary", +} + #[cfg(feature = "std-fs")] create_tests! { fs_files: "fs/files", diff --git a/tests/ffi/README.md b/tests/ffi/README.md new file mode 100644 index 00000000..3298b749 --- /dev/null +++ b/tests/ffi/README.md @@ -0,0 +1,113 @@ + + + +# `tests/ffi` + +## Requirements + +gcc for library compiling (for external-\*) + +## Test Results + +**External tests** + +- [x] [external_closure](./external_closure/init.luau) +- [x] [external_math](./external_math/init.luau) +- [x] [external_pointer](./external_pointer/init.luau) +- [x] [external_print](./external_print/init.luau) +- [x] [external_struct](./external_struct/init.luau) + +**Luau-side** + +- [x] [cast](./cast.luau) +- [x] [free](./free.luau) +- [x] [isInteger](./isInteger.luau) +- [x] [read_boundary](./read_boundary.luau) +- [x] [write_boundary](./write_boundary.luau) + +**Types** + +- [x] [arr](./types/arr.luau) +- [x] [ptr](./types/ptr.luau) +- [x] [struct](./types/struct.luau) + +**Pretty Print** + +- [x] [arr](./pretty_print/arr.luau) +- [ ] [box](./pretty_print/box.luau) Need assertion +- [ ] [ref](./pretty_print/ref.luau) Need assertion +- [ ] [lib](./pretty_print/lib.luau) Need assertion +- [x] [fn](./pretty_print/fn.luau) +- [x] [ptr](./pretty_print/ptr.luau) +- [x] [struct](./pretty_print/struct.luau) +- [x] [type](./pretty_print/type.luau) + +## Benchmark Results + +> Note: LuaJit's os.clock function returns process CPU time (used) which much smaller then Luau's os.clock output. In this benchmark, luau uses 'time.h' instead of os.clock. See [utility/proc_clock](./utility/proc_clock/init.luau) + +

benchmark/external_call

+ +**Target external c function** + +```c +int add(int a, int b) { + return a + b; +} +``` + +bench_scale = 1000000 + +**Lune ffi** + +Command: `cargo run run tests/ffi/benchmark/external_call` +Command: `cargo run --profile=release run tests/ffi/benchmark/external_call` + +- Device1-Linux-PVE + Lune release target: 0.205127 (sec) + Lune dev target: 1.556489 (sec) + + > Commit: ddf0c4c + +- Device2-Windows-11 + Lune release target: 0.1875 (sec) + Lune dev target: ? SEGFUALT (sec) + + > Commit: ddf0c4c + +**C** + +- Device1-Linux-PVE: 0.001949 (sec) + > gcc (GCC) 14.2.1 20240910 + +**LuaJit ffi** + +Command: `luajit tests/ffi/benchmark/external_call/luajit.lua` + +- Device1-Linux-PVE: 0.001682 (sec) + > LuaJIT 2.1.1727870382 + > (flags = JIT ON SSE3 SSE4.1 BMI2 fold cse dce fwd dse narrow loop abc sink fuse) + +**Deno ffi** + +Command: `deno run --unstable-ffi --allow-ffi ./tests/ffi/benchmark/external_call/deno.ts` + +- Device1-Linux-PVE: 0.006384 (sec) + > Deno 1.46.3 (v8 = 12.9.202.5-rusty) + +**Sysinformation** + +- Device1-Linux-PVE + + > CPU: AMD Ryzen 5 7600 (12) @ 5.1 + > MEM: 61898MiB 5600 MT/s + > KERNEL: 6.8.12-2-pve (Proxmox VE 8.2.7 x86_64) + +- Device2-Windows-11 + + > CPU: AMD Ryzen 5 7600 (4) @ 3.800GHz + > MEM: 12250MiB 5600 MT/s + > KERNEL: 10.0.22631 (Windows 11 x86_64) + > HOST: QEMU Standard PC (Q35 + ICH9, 2009) + +
diff --git a/tests/ffi/benchmark/external_call/deno.ts b/tests/ffi/benchmark/external_call/deno.ts new file mode 100644 index 00000000..7332ae18 --- /dev/null +++ b/tests/ffi/benchmark/external_call/deno.ts @@ -0,0 +1,24 @@ +import { libSuffix } from "../../utils/libSuffix.ts"; +import { get_clock, get_offset } from "../../utils/proc_clock/deno.ts"; + +const library_file = "./tests/ffi/benchmark/external_call/lib."+libSuffix; +// @ts-ignore +let library = Deno.dlopen(library_file, { + add: { + parameters: ["i32", "i32"], + result: "i32", + }, +}); + +function bench_add(bench_size: number) { + let add = library.symbols.add; + let value = 0; + const before = get_clock(); + for (let i=0; i + +int add(int a, int b) { + return a + b; +} + +double c_test() { + clock_t before = clock(); + + int a = 0; + for (int i=0; i<1000000; i++) { + a = add(a, 1); + } + + clock_t after = clock(); + + return (double)(after - before) / CLOCKS_PER_SEC; +} diff --git a/tests/ffi/benchmark/external_call/luajit.lua b/tests/ffi/benchmark/external_call/luajit.lua new file mode 100644 index 00000000..b1eb6f43 --- /dev/null +++ b/tests/ffi/benchmark/external_call/luajit.lua @@ -0,0 +1,24 @@ +--!nolint +--!nocheck + +local ffi = require("ffi") +local BENCH_SCALE = 1000000 + +ffi.cdef([[ + int add(int a, int b); +]]) +local lib = ffi.load("./tests/ffi/benchmark/external_call/lib.so") +local add = lib.add +local a = 0 + +local before = os.clock() +for i = 1, BENCH_SCALE do + a = add(a, 1) +end +local after = os.clock() + +print(after - before) +assert( + a == BENCH_SCALE, + string.format("bench_add failed. result expected %d, got %d", BENCH_SCALE, a) +) diff --git a/tests/ffi/cast.luau b/tests/ffi/cast.luau new file mode 100644 index 00000000..9c4d5361 --- /dev/null +++ b/tests/ffi/cast.luau @@ -0,0 +1,13 @@ +local ffi = require("@lune/ffi") + +local floatBox = ffi.f32:box(1.2) +local intBox = ffi.box(ffi.i32.size) + +ffi.f32:cast(ffi.i32, floatBox, intBox) + +local castedInt = ffi.i32:readData(intBox) + +assert( + castedInt == 1 and ffi.isInteger(castedInt), + "castedInt == 1 and ffi.isInteger(castedInt) assersion failed" +) diff --git a/tests/ffi/external_closure/callClosure.luau b/tests/ffi/external_closure/callClosure.luau new file mode 100644 index 00000000..f6bd73c8 --- /dev/null +++ b/tests/ffi/external_closure/callClosure.luau @@ -0,0 +1,13 @@ +local callableWrapper = require("../utils/callableWrapper") +local ffi = require("@lune/ffi") +local lib = require("../utils/compile")("./tests/ffi/external_closure/lib.c") +local c = ffi.c + +-- Create closure +local closure = c.fn({ c.int, c.int }, c.int):closure(function(ret, a, b) + c.int:writeData(ret, c.int:readData(a) + c.int:readData(b)) +end) + +local callClosure = callableWrapper(lib:find("call_closure"), { c.void:ptr() }, c.int) +local result = callClosure(closure:ref()) +assert(result == 72, `callClosure failed. result expected 20000, got {result}`) diff --git a/tests/ffi/external_closure/callClosureWithPointer.luau b/tests/ffi/external_closure/callClosureWithPointer.luau new file mode 100644 index 00000000..24084647 --- /dev/null +++ b/tests/ffi/external_closure/callClosureWithPointer.luau @@ -0,0 +1,15 @@ +local callableWrapper = require("../utils/callableWrapper") +local ffi = require("@lune/ffi") +local lib = require("../utils/compile")("./tests/ffi/external_closure/lib.c") +local c = ffi.c + +-- Create closure +local closureWithPointer = c.fn({ c.int, c.int:ptr() }, c.int) + :closure(function(returnRef, aRef, bRef) + c.int:writeData(returnRef, c.int:readData(aRef) + c.int:readData(bRef:deref())) + end) + +local callClosureWithPointer = + callableWrapper(lib:find("call_closure_with_pointer"), { c.void:ptr() }, c.int) +local result = callClosureWithPointer(closureWithPointer:ref()) +assert(result == 72, `closureWithPointer failed. result expected 20000, got {result}`) diff --git a/tests/ffi/external_closure/callHelloWorld.luau b/tests/ffi/external_closure/callHelloWorld.luau new file mode 100644 index 00000000..50a8b199 --- /dev/null +++ b/tests/ffi/external_closure/callHelloWorld.luau @@ -0,0 +1,11 @@ +local ffi = require("@lune/ffi") +local lib = require("../utils/compile")("./tests/ffi/external_closure/lib.c") +local c = ffi.c + +-- Create closure +local helloWorld = c.fn({}, c.void):closure(function() + print("Hello world in lua closure!") +end) + +local callHelloWorld = c.fn({ c.void:ptr() }, c.void):callable(lib:find("call_hello_world")) +callHelloWorld(nil, helloWorld:ref()) diff --git a/tests/ffi/external_closure/lib.c b/tests/ffi/external_closure/lib.c new file mode 100644 index 00000000..a5e7a775 --- /dev/null +++ b/tests/ffi/external_closure/lib.c @@ -0,0 +1,17 @@ +#include + +typedef int (*lua_closure_t)(int, int); +int call_closure(lua_closure_t lua_closure) { + return lua_closure(12, 24) * 2; +} + +typedef void (*lua_hello_world_t)(); +void call_hello_world(lua_hello_world_t lua_closure) { + lua_closure(); +} + +typedef int (*lua_closure_with_pointer_t)(int, int*); +int call_closure_with_pointer(lua_closure_with_pointer_t lua_closure) { + int b = 24; + return lua_closure(12, &b) * 2; +} diff --git a/tests/ffi/external_math/addInt.luau b/tests/ffi/external_math/addInt.luau new file mode 100644 index 00000000..d63f3132 --- /dev/null +++ b/tests/ffi/external_math/addInt.luau @@ -0,0 +1,9 @@ +local callableWrapper = require("../utils/callableWrapper") +local ffi = require("@lune/ffi") +local lib = require("../utils/compile")("./tests/ffi/external_math/lib.c") +local c = ffi.c + +local addInt = callableWrapper(lib:find("add_int"), { c.int, c.int }, c.int) +local result = addInt(100, 200) + +assert(result == 300, `test_addInt failed. result expected 300, got {result}`) diff --git a/tests/ffi/external_math/lib.c b/tests/ffi/external_math/lib.c new file mode 100644 index 00000000..b5182308 --- /dev/null +++ b/tests/ffi/external_math/lib.c @@ -0,0 +1,7 @@ +int add_int(int a, int b) { + return a + b; +} + +int mul_int(int a, int b) { + return a * b; +} diff --git a/tests/ffi/external_math/mulInt.luau b/tests/ffi/external_math/mulInt.luau new file mode 100644 index 00000000..e14a86bb --- /dev/null +++ b/tests/ffi/external_math/mulInt.luau @@ -0,0 +1,8 @@ +local callableWrapper = require("../utils/callableWrapper") +local ffi = require("@lune/ffi") +local lib = require("../utils/compile")("./tests/ffi/external_math/lib.c") +local c = ffi.c + +local mulInt = callableWrapper(lib:find("mul_int"), { c.int, c.int }, c.int) +local result = mulInt(100, 200) +assert(result == 20000, `test_mulInt failed. result expected 20000, got {result}`) diff --git a/tests/ffi/external_pointer/lib.c b/tests/ffi/external_pointer/lib.c new file mode 100644 index 00000000..7d7440a8 --- /dev/null +++ b/tests/ffi/external_pointer/lib.c @@ -0,0 +1,7 @@ +void pointer_write(int *a) { + *a = 123; +} + +int pointer_read(int *a) { + return *a; +} diff --git a/tests/ffi/external_pointer/pointerRead.luau b/tests/ffi/external_pointer/pointerRead.luau new file mode 100644 index 00000000..e2490a3f --- /dev/null +++ b/tests/ffi/external_pointer/pointerRead.luau @@ -0,0 +1,8 @@ +local callableWrapper = require("../utils/callableWrapper") +local ffi = require("@lune/ffi") +local lib = require("../utils/compile")("./tests/ffi/external_pointer/lib.c") +local c = ffi.c + +local pointerRead = callableWrapper(lib:find("pointer_read"), { c.int:ptr() }, c.int) +local result = pointerRead(c.int:box(123):ref():ref()) +assert(result == 123, `pointerRead failed. result expected 123, got {result}`) diff --git a/tests/ffi/external_pointer/pointerWrite.luau b/tests/ffi/external_pointer/pointerWrite.luau new file mode 100644 index 00000000..72a9b40b --- /dev/null +++ b/tests/ffi/external_pointer/pointerWrite.luau @@ -0,0 +1,9 @@ +local ffi = require("@lune/ffi") +local lib = require("../utils/compile")("./tests/ffi/external_pointer/lib.c") +local c = ffi.c + +local pointerWrite = c.fn({ c.int:ptr() }, c.void):callable(lib:find("pointer_write")) +local aBox = ffi.box(c.int.size) +pointerWrite(nil, aBox:ref():ref()) +local result = c.int:readData(aBox) +assert(result == 123, `pointerWrite failed. result expected 123, got {result}`) diff --git a/tests/ffi/external_print/helloWorld.luau b/tests/ffi/external_print/helloWorld.luau new file mode 100644 index 00000000..665283b1 --- /dev/null +++ b/tests/ffi/external_print/helloWorld.luau @@ -0,0 +1,5 @@ +local ffi = require("@lune/ffi") +local lib = require("../utils/compile")("./tests/ffi/external_print/lib.c") +local c = ffi.c + +c.fn({}, c.void):callable(lib:find("hello_world"))(nil) diff --git a/tests/ffi/external_print/lib.c b/tests/ffi/external_print/lib.c new file mode 100644 index 00000000..d6504921 --- /dev/null +++ b/tests/ffi/external_print/lib.c @@ -0,0 +1,5 @@ +#include + +void hello_world() { + printf("Hello world from external function!"); +} diff --git a/tests/ffi/external_struct/ab.luau b/tests/ffi/external_struct/ab.luau new file mode 100644 index 00000000..114b74c3 --- /dev/null +++ b/tests/ffi/external_struct/ab.luau @@ -0,0 +1,12 @@ +local callableWrapper = require("../utils/callableWrapper") +local ffi = require("@lune/ffi") +local lib = require("../utils/compile")("./tests/ffi/external_struct/lib.c") +local c = ffi.c + +local argStructInfo = c.struct({ c.int, c.int:ptr() }) +local resultStructInfo = c.struct({ c.int, c.int }) + +local ab = callableWrapper(lib:find("ab"), { argStructInfo }, resultStructInfo) +local result = ab({ 100, c.int:box(200):ref() } :: { any }) +assert(result[1] == 300, `ab failed. result expected 300, got {result[1]}`) +assert(result[2] == 20000, `ab failed. result expected 300, got {result[2]}`) diff --git a/tests/ffi/external_struct/lib.c b/tests/ffi/external_struct/lib.c new file mode 100644 index 00000000..f68a0674 --- /dev/null +++ b/tests/ffi/external_struct/lib.c @@ -0,0 +1,14 @@ +typedef struct { + int a; + int* b; +} ArgStruct; + +typedef struct { + int sum; + int mul; +} ResultStruct; + +ResultStruct ab(ArgStruct t) { + ResultStruct result = { t.a+ * t.b, t.a * (*t.b) }; + return result; +} diff --git a/tests/ffi/free.luau b/tests/ffi/free.luau new file mode 100644 index 00000000..5ab91da0 --- /dev/null +++ b/tests/ffi/free.luau @@ -0,0 +1,18 @@ +--!nocheck +--!nolint +local ffi = require("@lune/ffi") + +local box = ffi.box(ffi.i32.size) +local ref = box:leak() + +box = nil + +collectgarbage("collect") +collectgarbage("collect") +collectgarbage("collect") + +ffi.free(ref) + +collectgarbage("collect") +collectgarbage("collect") +collectgarbage("collect") diff --git a/tests/ffi/isInteger.luau b/tests/ffi/isInteger.luau new file mode 100644 index 00000000..a856df0c --- /dev/null +++ b/tests/ffi/isInteger.luau @@ -0,0 +1,7 @@ +local ffi = require("@lune/ffi") + +local int = 0b1 +local float = 0.5 + +assert(ffi.isInteger(int) == true, "ffi.isInteger(int) == true assersion failed") +assert(ffi.isInteger(float) == false, "ffi.isInteger(float) == false assersion failed") diff --git a/tests/ffi/pretty_print/arr.luau b/tests/ffi/pretty_print/arr.luau new file mode 100644 index 00000000..4b78d5e8 --- /dev/null +++ b/tests/ffi/pretty_print/arr.luau @@ -0,0 +1,6 @@ +local ffi = require("@lune/ffi") +local c = ffi.c + +assert(typeof(c.int:arr(5)) :: string == "CArrInfo") +assert(tostring(c.int:arr(5)) == " int, length = 5 ") +assert(tostring(c.int:ptr():arr(5)) == " , length = 5 ") diff --git a/tests/ffi/pretty_print/box.luau b/tests/ffi/pretty_print/box.luau new file mode 100644 index 00000000..e9714343 --- /dev/null +++ b/tests/ffi/pretty_print/box.luau @@ -0,0 +1,4 @@ +local ffi = require("@lune/ffi") +local half_back = 0b_0000_0000_0000_0000_0000_0000_1111_1111 + +print(ffi.i32:box(half_back)) diff --git a/tests/ffi/pretty_print/fn.luau b/tests/ffi/pretty_print/fn.luau new file mode 100644 index 00000000..d20326d7 --- /dev/null +++ b/tests/ffi/pretty_print/fn.luau @@ -0,0 +1,13 @@ +local ffi = require("@lune/ffi") +local c = ffi.c + +assert(typeof(c.fn({ c.int }, c.int)) :: string == "CFnInfo") +assert(tostring(c.fn({ c.int }, c.int)) == " (int) -> int ") +assert(tostring(c.fn({ c.int, ffi.f32 }, c.int)) == " (int, f32) -> int ") +assert(tostring(c.fn({ c.int:ptr() }, c.int)) == " () -> int ") +assert(tostring(c.fn({ c.int }, c.int:ptr())) == " (int) -> ") +assert(tostring(c.fn({ c.int:ptr() }, c.int:ptr())) == " () -> ") +assert( + tostring(c.fn({ c.int:ptr(), c.int:ptr() }, c.int:ptr())) + == " (, ) -> " +) diff --git a/tests/ffi/pretty_print/ptr.luau b/tests/ffi/pretty_print/ptr.luau new file mode 100644 index 00000000..fd3da1d0 --- /dev/null +++ b/tests/ffi/pretty_print/ptr.luau @@ -0,0 +1,6 @@ +local ffi = require("@lune/ffi") +local c = ffi.c + +assert(typeof(c.int:ptr()) :: string == "CPtrInfo") +assert(tostring(c.int:ptr()) == "int") +assert(tostring(c.int:arr(5):ptr()) == " ") diff --git a/tests/ffi/pretty_print/struct.luau b/tests/ffi/pretty_print/struct.luau new file mode 100644 index 00000000..124d4127 --- /dev/null +++ b/tests/ffi/pretty_print/struct.luau @@ -0,0 +1,8 @@ +local ffi = require("@lune/ffi") +local c = ffi.c + +assert(typeof(c.struct({ c.int, c.char })) :: string == "CStructInfo") +assert( + tostring(c.struct({ c.int, c.char:ptr() })) + == ` int, , size = {c.struct({ c.int, c.char:ptr() }).size} ` +) diff --git a/tests/ffi/pretty_print/type.luau b/tests/ffi/pretty_print/type.luau new file mode 100644 index 00000000..af86dcc8 --- /dev/null +++ b/tests/ffi/pretty_print/type.luau @@ -0,0 +1,6 @@ +local ffi = require("@lune/ffi") +local c = ffi.c + +assert(typeof(c.int) :: string == "CTypeInfo") +assert(tostring(c.int) == "int") +assert(tostring(ffi.i32) == "i32") diff --git a/tests/ffi/readBoundary.luau b/tests/ffi/readBoundary.luau new file mode 100644 index 00000000..94c4ff35 --- /dev/null +++ b/tests/ffi/readBoundary.luau @@ -0,0 +1,51 @@ +local ffi = require("@lune/ffi") +local ok + +-- Case1: Success +ok = pcall(function() + local box = ffi.u8:box(1) + ffi.u8:readData(box) +end) +assert(ok, "assersion failed, Case1 should success") + +-- Case2: Fail +ok = pcall(function() + local box = ffi.u8:box(1) + ffi.u16:readData(box) +end) +assert(not ok, "assersion failed, Case2 should fail") + +-- Case3: Success +ok = pcall(function() + local box = ffi.box(ffi.u8.size * 2) + ffi.u16:readData(box) +end) +assert(ok, "assersion failed, Case3 should success") + +-- Case4: Success +ok = pcall(function() + local box = ffi.box(ffi.u8.size * 2) + ffi.u8:readData(box, ffi.u8.size) +end) +assert(ok, "assersion failed, Case4 should success") + +-- Case5: Fail +ok = pcall(function() + local box = ffi.u8:box(1):ref() + ffi.u16:readData(box) +end) +assert(not ok, "assersion failed, Case5 should fail") + +-- Case6: Success +ok = pcall(function() + local box = ffi.box(ffi.u8.size * 2):ref() + ffi.u16:readData(box) +end) +assert(ok, "assersion failed, Case6 should success") + +-- Case7: Fail +ok = pcall(function() + local box = ffi.box(ffi.u8.size * 2):ref(ffi.u16.size) + ffi.u16:readData(box) +end) +assert(not ok, "assersion failed, Case7 should fail") diff --git a/tests/ffi/stringReadWrite.luau b/tests/ffi/stringReadWrite.luau new file mode 100644 index 00000000..43510475 --- /dev/null +++ b/tests/ffi/stringReadWrite.luau @@ -0,0 +1,34 @@ +local ffi = require("@lune/ffi") +local ok + +local str = "hello world" +local strbox = ffi.box(#str):writeString(str) +assert(strbox:readString(#str) == str, "String read write assersion failed") + +-- Case1: Fail +ok = pcall(function() + local box = ffi.box(2) + box:readString(10) +end) +assert(not ok, "assersion failed, Case1 should fail") + +-- Case2: Fail +ok = pcall(function() + local box = ffi.box(2) + box:writeString("hello world") +end) +assert(not ok, "assersion failed, Case2 should fail") + +-- Case3: Fail +ok = pcall(function() + local box = ffi.box(2):ref() + box:readString(10) +end) +assert(not ok, "assersion failed, Case3 should fail") + +-- Case4: Fail +ok = pcall(function() + local box = ffi.box(2):ref() + box:writeString("hello world") +end) +assert(not ok, "assersion failed, Case4 should fail") diff --git a/tests/ffi/types/arr.luau b/tests/ffi/types/arr.luau new file mode 100644 index 00000000..448fff34 --- /dev/null +++ b/tests/ffi/types/arr.luau @@ -0,0 +1,19 @@ +local ffi = require("@lune/ffi") +local ok + +local arr = ffi.i32:arr(4) + +assert(rawequal(arr.length, 4), "length assersion failed, arr.length should be 4") +assert(rawequal(arr.inner, ffi.i32), "inner assersion failed, arr.inner should be ffi.i32") + +-- offset(2) success +ok = pcall(function() + arr:offset(2) +end) +assert(ok, "assersion failed, arr:offset(2) should success") + +-- offset(4) success +ok = pcall(function() + arr:offset(4) +end) +assert(not ok, "assersion failed, arr:offset(4) should fail") diff --git a/tests/ffi/types/ptr.luau b/tests/ffi/types/ptr.luau new file mode 100644 index 00000000..05cdf136 --- /dev/null +++ b/tests/ffi/types/ptr.luau @@ -0,0 +1,31 @@ +local ffi = require("@lune/ffi") + +-- ptr size test +assert( + ffi.i32:ptr().size == ffi.i64:ptr().size, + "size assersion failed, size of pointer should be same with each other (ffi.i32:ptr().size == ffi.i64:ptr().size)" +) + +-- inner test +local i32ptr = ffi.i32:ptr() +assert( + rawequal(ffi.i32, i32ptr.inner), + "inner assersion failed, inner field must be same with their parent" + .. " (ffi.i32 == ffi.i32:ptr().inner)" +) +assert( + rawequal(i32ptr, i32ptr:ptr().inner), + "inner assersion failed, inner field must be same with their parent" + .. " (i32ptr == i32ptr:ptr().inner)" +) +assert( + rawequal(i32ptr, i32ptr:ptr().inner:ptr().inner:ptr().inner), + "inner assersion failed, inner field must be same with their parent" + .. " (i32ptr == i32ptr:ptr().inner:ptr().inner:ptr().inner)" +) + +-- deep ptr test +local ok, err = pcall(function() + i32ptr:ptr():ptr():ptr():ptr():ptr():ptr():ptr() +end) +assert(ok, "deep ptr assersion failed\n" .. (err or "")) diff --git a/tests/ffi/types/struct.luau b/tests/ffi/types/struct.luau new file mode 100644 index 00000000..10edd4a3 --- /dev/null +++ b/tests/ffi/types/struct.luau @@ -0,0 +1,20 @@ +local ffi = require("@lune/ffi") +local ok + +local i32ptr = ffi.i32:ptr() +local struct = ffi.c.struct({ i32ptr, ffi.i32 }) + +assert(rawequal(struct:field(0), i32ptr), "Struct get field failed") +assert(rawequal(struct:field(1), ffi.i32), "Struct get field failed") + +-- offset(2) should fail +ok = pcall(function() + struct:offset(2) +end) +assert(not ok, "assersion failed, struct:offset(2) should fail") + +-- field(2) should fail +ok = pcall(function() + struct:field(2) +end) +assert(not ok, "assersion failed, struct:field(2) should fail") diff --git a/tests/ffi/utils/callableWrapper.luau b/tests/ffi/utils/callableWrapper.luau new file mode 100644 index 00000000..ffa9597c --- /dev/null +++ b/tests/ffi/utils/callableWrapper.luau @@ -0,0 +1,38 @@ +--!nocheck +local ffi = require("@lune/ffi") + +local function callableWrapper( + functionRef: ffi.RefData, + argTypeList: { ffi.CTypes }, + retType: ffi.CTypes +): (...any) -> any + local callable = ffi.c.fn(argTypeList, retType):callable(functionRef) + + return function(...) + local argValues = table.create(#argTypeList + 1) + + local resultBox + if retType ~= ffi.c.void then + resultBox = ffi.box(retType.size) + end + argValues[1] = resultBox + + for index, argType in argTypeList do + local arg = select(index, ...) + if type(arg) == "userdata" then + argValues[index + 1] = arg + else + argValues[index + 1] = argType:box(arg):ref() + end + end + + callable(table.unpack(argValues, 1, #argTypeList + 1)) + + if retType == ffi.c.void then + return nil + end + return retType:readData(resultBox) + end +end + +return callableWrapper diff --git a/tests/ffi/utils/compile.luau b/tests/ffi/utils/compile.luau new file mode 100644 index 00000000..624ddaf0 --- /dev/null +++ b/tests/ffi/utils/compile.luau @@ -0,0 +1,25 @@ +local ffi = require("@lune/ffi") +local process = require("@lune/process") + +local function getLibSuffix(): string + if process.os == "linux" then + return "so" + elseif process.os == "windows" then + return "dll" + elseif process.os == "macos" then + return "dylib" + end + error("Unknown OS") +end + +local function compile(file: string): ffi.LibData + local out = file:gsub("%.c$", "." .. getLibSuffix()) + local gcc = process.exec("gcc", { "-shared", "-o", out, "-fPIC", file }) + if not gcc.ok then + error("Failed to execute gcc command\n" .. gcc.stdout .. gcc.stderr) + end + + return ffi.open(out) +end + +return compile diff --git a/tests/ffi/utils/libSuffix.ts b/tests/ffi/utils/libSuffix.ts new file mode 100644 index 00000000..41dc2818 --- /dev/null +++ b/tests/ffi/utils/libSuffix.ts @@ -0,0 +1,13 @@ +export let libSuffix = ""; +// @ts-ignore +switch (Deno.build.os) { + case "windows": + libSuffix = "dll"; + break; + case "darwin": + libSuffix = "dylib"; + break; + case "linux": + libSuffix = "so"; + break; +} diff --git a/tests/ffi/utils/proc_clock/deno.ts b/tests/ffi/utils/proc_clock/deno.ts new file mode 100644 index 00000000..23ea8d4a --- /dev/null +++ b/tests/ffi/utils/proc_clock/deno.ts @@ -0,0 +1,27 @@ +import { libSuffix } from "../libSuffix.ts"; + +const library_file = "./tests/ffi/utils/proc_clock/lib."+libSuffix; +// @ts-ignore +let library = Deno.dlopen(library_file, { + sizeof_clock: { + parameters: [], + result: "i32", + }, +}); +const sizeof_clock = library.symbols.sizeof_clock(); +const type_clock_t = "u" + (sizeof_clock * 8); +library.close(); +// @ts-ignore +library = Deno.dlopen(library_file, { + get_clock: { + parameters: [], + result: type_clock_t, + }, + get_offset: { + parameters: [type_clock_t, type_clock_t], + result: "f64", + }, +}); + +export const get_clock = library.symbols.get_clock; +export const get_offset = library.symbols.get_offset; diff --git a/tests/ffi/utils/proc_clock/init.luau b/tests/ffi/utils/proc_clock/init.luau new file mode 100644 index 00000000..8877a4c5 --- /dev/null +++ b/tests/ffi/utils/proc_clock/init.luau @@ -0,0 +1,46 @@ +-- FIXME: in windows, we need another library to get process cpu time + +local ffi = require("@lune/ffi") +local process = require("@lune/process") +local isWindows = process.os == "windows" +local c = ffi.c + +local procClock = {} + +local lib = require("../compile")("./tests/ffi/utils/proc_clock/lib.c") + +-- sizeof_clock +local sizeofClock = c.fn({}, c.int):callable(lib:find("sizeof_clock")) +function procClock.sizeofClock(): number + local result = ffi.box(c.int.size) + sizeofClock(result) + return c.int:readData(result) +end +-- get_clock +local clock_t = if isWindows then ffi.f32 else ffi["u" .. (procClock.sizeofClock() * 8)] +assert(clock_t, "clock_t is unknown type") +procClock.getClock = ( + if isWindows + then function(clock: ffi.BoxData | ffi.RefData) + ffi.f32:writeData(clock, os.clock()) + end + else c.fn({}, clock_t):callable(lib:find("get_clock")) +) :: (ffi.BoxData | ffi.RefData) -> () + +-- get_offset +local getOffset: (ffi.BoxData, ffi.RefData, ffi.RefData) -> () = if isWindows + then function(result: ffi.BoxData, before: ffi.RefData, after: ffi.RefData) + ffi.f64:writeData(result, (ffi.f32:readData(after) - ffi.f32:readData(before))) + end + else c.fn({ clock_t, clock_t }, ffi.f64):callable(lib:find("get_offset")) +function procClock.getOffset(before: ffi.BoxData, after: ffi.BoxData): number + local result = ffi.box(ffi.f64.size) + getOffset(result, before:ref(), after:ref()) + return ffi.f64:readData(result) +end + +function procClock.newBox(): (ffi.BoxData, ffi.BoxData) + return ffi.box(clock_t.size), ffi.box(clock_t.size) +end + +return procClock diff --git a/tests/ffi/utils/proc_clock/lib.c b/tests/ffi/utils/proc_clock/lib.c new file mode 100644 index 00000000..bef49b0f --- /dev/null +++ b/tests/ffi/utils/proc_clock/lib.c @@ -0,0 +1,12 @@ +#include +clock_t get_clock() { + return clock(); +} + +int sizeof_clock() { + return sizeof(clock_t); +} + +double get_offset(clock_t before, clock_t after) { + return (double)(after - before) / CLOCKS_PER_SEC; +} diff --git a/tests/ffi/writeBoundary.luau b/tests/ffi/writeBoundary.luau new file mode 100644 index 00000000..afb2b162 --- /dev/null +++ b/tests/ffi/writeBoundary.luau @@ -0,0 +1,45 @@ +local ffi = require("@lune/ffi") +local c = ffi.c +local ok + +-- Case1: Fail +ok = pcall(function() + local box = ffi.box(c.int.size - 1) + c.int:writeData(box, 10) +end) +assert(not ok, "assersion failed, Case1 should fail") + +-- Case2: Success +ok = pcall(function() + local box = ffi.box(c.int.size) + c.int:writeData(box, 10) +end) +assert(ok, "assersion failed, Case2 should success") + +-- Case3: Success +ok = pcall(function() + local box = ffi.box(c.int.size * 2) + c.int:writeData(box, 10, c.int.size) +end) +assert(ok, "assersion failed, Case3 should success") + +-- Case4: Fail +ok = pcall(function() + local box = ffi.box(c.int.size * 2) + c.int:writeData(box, 10, c.int.size * 2) +end) +assert(not ok, "assersion failed, Case4 should fail") + +-- Case5: Success +ok = pcall(function() + local box = ffi.box(c.int.size * 2):ref() + c.int:writeData(box, 10, c.int.size) +end) +assert(ok, "assersion failed, Case5 should success") + +-- Case6: Fail +ok = pcall(function() + local box = ffi.box(c.int.size * 2):ref() + c.int:writeData(box, 10, c.int.size * 2) +end) +assert(not ok, "assersion failed, Case6 should fail") diff --git a/types/ffi.luau b/types/ffi.luau new file mode 100644 index 00000000..6cc8fc1e --- /dev/null +++ b/types/ffi.luau @@ -0,0 +1,1224 @@ +--[=[ + @class FFI + + Built-in library for foreign function interface. + + ### Example usage + lib.c: + ```c + int add(int a, int b) { + return a + b; + } + ``` + init.luau: + ```lua + local ffi = require("@lune/ffi") + + -- Create function signature + local addSignature = ffi.c.fn({ ffi.c.int, ffi.c.int }, ffi.c.int) + + -- Load library + local lib = ffi.open("./lib.so") + + -- Get symbol from library + local addSymbol = lib:find("add") + + -- Create CallableData + local add = addSignature:callable(addSymbol) + + -- Create result box and arguments + local result = ffi.box(ffi.c.int.size) + local a = ffi.c.int:box(1) + local b = ffi.c.int:box(2) + + -- Call external function + add(result, a:ref(), b:ref()) + + -- Get number from result + print(ffi.c.int:readData(result)) + ``` +]=] +local ffi = {} + +--[=[ + @class C + + Namespace for compile time sized c types. +]=] +local c = {} +ffi.c = c + +--#region Data +--[=[ + @class RefData + + A user manageable memory reference. + + It can be GCed, But it doesn't free the referenced memory. +]=] +export type RefData = { + --[=[ + @within RefData + @tag Method + @method deref + + Create a RefData by dereference this reference. + The created reference has no boundaries and has no restrictions. + + This method is unsafe. + + @return A dereferenced `RefData` + ]=] + deref: (self: RefData) -> RefData, + --[=[ + @within RefData + @tag Method + @method offset + + Create a reference with specific offset from this reference. + + The created reference can be GCed and holds same data. + + @param offset Create a reference at the given offset + @return A offseted reference + ]=] + offset: (self: RefData, offset: number) -> RefData, + --[=[ + @within RefData + @tag Method + @method ref + + Create a reference of this reference. + + The created reference keeps the target reference from being garbage collected until created reference itself is collected. + + @return A reference of this reference + ]=] + ref: (self: RefData) -> RefData, + --[=[ + @within RefData + @tag Method + @method leak + + Create a reference of this reference after leaking it. + + GC doesn't manage destruction after this action. You must free it later. + + @return A reference of this reference + ]=] + leak: (self: RefData) -> RefData, + --[=[ + @within RefData + @tag Method + @method isNull + + Check reference is null or not. + + @return Whether reference is null or not + ]=] + isNull: (self: RefData) -> boolean, + --[=[ + @within RefData + @tag Method + @method copyFrom + + Copy content from another data with specific length. + + @param src The source data + @param length The amount of data to copy, in bytes + @param dstOffset The offset in the destination where the content will be pasted + @param srcOffset The offset in the source data from where the content will be copied + @return `RefData` itself for convenience + ]=] + copyFrom: ( + self: RefData, + src: BoxData | RefData, + length: number, + dstOffset: number, + srcOffset: number + ) -> RefData, + --[=[ + @within RefData + @tag Method + @method readString + + Read string from data with specific length without null termination. + + @param length The amount of data to read, in bytes + @param offset Offset to read string from + @return A string + ]=] + readString: (self: RefData, length: number, offset: number?) -> string, + --[=[ + @within RefData + @tag Method + @method writeString + + Write string into data without null termination. + + @param src The source string + @param length The amount of data to write, in bytes + @param dstOffset The offset in the destination where the content will be pasted + @param srcOffset The offset in the source string from where the content will be copied + @return `RefData` itself for convenience + ]=] + writeString: ( + self: RefData, + src: string, + length: number?, + dstOffset: number?, + srcOffset: number? + ) -> RefData, +} + +--[=[ + @class BoxData + + A user manageable heap memory. +]=] +export type BoxData = { + --[=[ + @within BoxData + @tag Field + @field size + + The size of the box. + ]=] + size: number, + + --[=[ + @within BoxData + @tag Method + @method zero + + Fill the box with zero. + + @return `BoxData` itself for convenience + ]=] + zero: (self: BoxData) -> BoxData, + --[=[ + @within BoxData + @tag Method + @method leak + + Create a reference of the box after leaking it. + + GC doesn't manage destruction after this action. You must free it later. + + @param offset Create a reference at the given offset + @return A reference of the box + ]=] + leak: (self: BoxData, offset: number?) -> RefData, + --[=[ + @within BoxData + @tag Method + @method ref + + Create a reference of the box. + + The created reference keeps the box from being garbage collected until the reference itself is collected. + + @param offset Create a reference at the given offset + @return A reference of the box + ]=] + ref: (self: BoxData, offset: number?) -> RefData, + --[=[ + @within BoxData + @tag Method + @method copyFrom + + Copy content from another data with specific length. + + @param src The source data + @param length The amount of data to copy, in bytes + @param dstOffset The offset in the destination where the content will be pasted + @param srcOffset The offset in the source data from where the content will be copied + @return `BoxData` itself for convenience + ]=] + copyFrom: ( + self: BoxData, + src: BoxData | RefData, + length: number, + dstOffset: number, + srcOffset: number + ) -> BoxData, + --[=[ + @within BoxData + @tag Method + @method readString + + Read string from data with specific length without null termination. + + @param length The amount of data to read, in bytes + @param offset Offset to read string from + @return A string + ]=] + readString: (self: BoxData, length: number, offset: number?) -> string, + --[=[ + @within BoxData + @tag Method + @method writeString + + Write string into data without null termination. + + @param src The source string + @param length The amount of data to write, in bytes + @param dstOffset The offset in the destination where the content will be pasted + @param srcOffset The offset in the source string from where the content will be copied + @return `BoxData` itself for convenience + ]=] + writeString: ( + self: BoxData, + src: string, + length: number?, + dstOffset: number?, + srcOffset: number? + ) -> BoxData, +} + +--[=[ + @class LibData + + A dynamic opened library handle. +]=] +export type LibData = { + --[=[ + @within LibData + @tag Method + @method find + + Find a symbol from the dynamic library. + + @param sym The name of the symbol + @return A `Ref` of the found symbol + ]=] + find: (self: LibData, sym: string) -> RefData, +} + +--[=[ + @class CallableData + + A callable external function. + + To call external function, provide memory for save the return value and references for the arguments. + + If return type is `void`, pass `nil`. +]=] +export type CallableData = ( + ret: (RefData | BoxData)?, + ...RefData +) -> () & { + -- apply: (self: Callable, args: Args)->AppliedCallable, +} +-- export type AppliedCallable = ()->() + +--[=[ + @class ClosureData + + A reference that holds lua function. +]=] +export type ClosureData = { + --[=[ + @within ClosureData + @tag Method + @method ref + + Create a reference of the closure. usually can be used for passing function pointer as argument. + + The created reference keeps the closure from being garbage collected until the reference itself is collected. + + @return A reference of the closure + ]=] + ref: (self: ClosureData) -> RefData, +} +--#endregion Data + +--#region C ABI Type Infos +-- NOTE: T is a unique identifier for the `CType` and R is the closest Lua type. +export type CTypeInfo = { + --[=[ + @within CTypeInfo + @tag Field + @field size + + The size of the type in bytes. + ]=] + size: number, + --[=[ + @within CTypeInfo + @tag Field + @field signedness + + The signedness of the type. + ]=] + signedness: boolean, + + -- Subtype + --[=[ + @within CTypeInfo + @tag Method + @Method ptr + + Create a pointer subtype. + + @return A pointer subtype + ]=] + ptr: (self: CTypeInfo) -> CPtrInfo>, + --[=[ + @within CTypeInfo + @tag Method + @Method arr + + Create an array subtype with specific length. + + @param length The length of the array + @return An array subtype + ]=] + arr: (self: CTypeInfo, length: number) -> CArrInfo, R>, + + -- Create/Read/Write/Copy + --[=[ + @within CTypeInfo + @tag Method + @Method box + + Create a box with initial values. + + @param table The array of element values + @return A box + ]=] + box: (self: CTypeInfo, val: R) -> BoxData, + --[=[ + @within CTypeInfo + @tag Method + @method readData + + Read a lua value from reference or box. + + @param target Target to read data from + @param offset Offset to read data from + @return A lua value + ]=] + readData: (self: CTypeInfo, target: RefData | BoxData, offset: number?) -> R, + --[=[ + @within CTypeInfo + @tag Method + @method writeData + + Write a lua value into reference or box. + + @param target Target to write data into + @param value Lua data to write + @param offset Offset to write data into + ]=] + writeData: (self: CTypeInfo, target: RefData | BoxData, value: R, offset: number?) -> (), + --[=[ + @within CTypeInfo + @tag Method + @Method copyData + + Copy values ​​from the source and paste them into the target. + + @param dst Where the content will be pasted + @param src The source data + @param dstOffset The offset in the destination where the content will be pasted + @param srcOffset The offset in the source data from where the content will be copied + ]=] + copyData: ( + self: CTypeInfo, + dst: RefData | BoxData, + src: RefData | BoxData, + dstOffset: number?, + srcOffset: number? + ) -> (), + --[=[ + @within CTypeInfo + @tag Method + @Method stringifyData + + Stringify data. Useful when output numbers, which Luau can't handle. + + @param target The target data + @param offset Offset to stringify data from + ]=] + stringifyData: (self: CTypeInfo, target: RefData | BoxData, offset: number?) -> string, + + -- ETC + -- FIXME: recursive types; 'intoType' should be CTypes + --[=[ + @within CTypeInfo + @tag Method + @Method cast + + Casting data to different type. + + May result in loss of precision. + + @param intoType The target type to convert to + @param fromData Source data to be converted + @param intoData Target to write converted data into + @param fromOffset The offset in the source data + @param intoOffset The offset in the destination + ]=] + cast: ( + self: CTypeInfo, + intoType: any, + fromData: RefData | BoxData, + intoData: RefData | BoxData, + fromOffset: number?, + intoOffset: number? + ) -> (), +} & { ["__phantom"]: T } +type NumCType = CTypeInfo + +export type CPtrInfo = { + --[=[ + @within CPtrInfo + @tag Field + @field size + + The size of a pointer. should be the same for all pointers. + + Equivalent to `ffi.c.usize.size`. + ]=] + size: number, + --[=[ + @within CPtrInfo + @tag Field + @field inner + + The inner type of the pointer. + ]=] + inner: T, + + -- Subtype + -- FIXME: recursive types; result 'any' should be CArrInfo> + --[=[ + @within CPtrInfo + @tag Method + @Method arr + + Create an array subtype with specific length. + + @param length The length of the array + @return An array subtype + ]=] + arr: (self: CPtrInfo, length: number) -> any, + -- FIXME: recursive types; result 'any' should be CPtrInfo> + --[=[ + @within CPtrInfo + @tag Method + @Method ptr + + Create a pointer subtype. + + @return A pointer subtype + ]=] + ptr: (self: CPtrInfo) -> any, + + -- Address + --[=[ + @within CPtrInfo + @tag Method + @Method readRef + + Read address from data, then return RefData. + + Useful when reading pointer fields of structures. + + If the `ref` argument is given, rather than create new RefData, update it. + + @param target Target data to read address from + @param offset Offset to read address from + @param ref RefData to update + @return A lua value + ]=] + readRef: ( + self: CPtrInfo, + target: RefData | BoxData, + offset: number?, + ref: RefData? + ) -> RefData, + --[=[ + @within CPtrInfo + @tag Method + @Method writeRef + + Write address to data. + + Useful when writing pointer fields of structures. + + @param target Target data to write address into + @param ref Memory address to write + @param offset Offset to write address into + ]=] + writeRef: ( + self: CPtrInfo, + target: RefData | BoxData, + ref: RefData | BoxData | ClosureData, + offset: number? + ) -> (), +} + +--[=[ + @class CArrInfo + + A c sized array type information. It can be used for sturct field. + + For function arguments, use CPtr instead. +]=] +export type CArrInfo = { + --[=[ + @within CArrInfo + @tag Field + @field size + + The total size of the array in bytes. + ]=] + size: number, + --[=[ + @within CArrInfo + @tag Field + @field length + + The length of the array. + ]=] + length: number, + --[=[ + @within CArrInfo + @tag Field + @field inner + + The inner element type of the array. + ]=] + inner: T, + + -- Subtype + --[=[ + @within CArrInfo + @tag Method + @Method ptr + + Create a pointer subtype. + + @return A pointer subtype + ]=] + ptr: (self: CArrInfo) -> CPtrInfo>, + + -- Create/Read/Write/Copy + --[=[ + @within CArrInfo + @tag Method + @Method box + + Create a box with initial values. + + @param table The array of field values + @return A box + ]=] + box: (self: CArrInfo, table: { T }) -> BoxData, + --[=[ + @within CArrInfo + @tag Method + @method readData + + Read a lua table from reference or box. + + @param target Target to read data from + @param offset Offset to read data from + @return A table + ]=] + readData: (self: CArrInfo, target: RefData | BoxData, offset: number?) -> { T }, + --[=[ + @within CArrInfo + @tag Method + @method writeData + + Write a lua table into reference or box. + + @param target Target to write data into + @param table Lua data to write + @param offset Offset to write data into + ]=] + writeData: ( + self: CArrInfo, + target: RefData | BoxData, + value: { R }, + target_offset: number? + ) -> (), + --[=[ + @within CArrInfo + @tag Method + @Method copyData + + Copy values ​​from the source and paste them into the target. + + @param dst Where the content will be pasted + @param src The source data + @param dstOffset The offset in the dst where the content will be pasted + @param srcOffset The offset in the source data from where the content will be copied + ]=] + copyData: ( + self: CArrInfo, + dst: RefData | BoxData, + src: RefData | BoxData, + dstOffset: number?, + srcOffset: number? + ) -> (), + + -- ETC + --[=[ + @within CArrInfo + @tag Method + @method offset + + Get the byte offset of the field. + + @param index The element index + @return The byte offset + ]=] + offset: (self: CArrInfo, index: number) -> number, +} + +--[=[ + @class CFnInfo + + A C function pointer type information, with function signature. + + For struct field, array element, or function arguments, use `void:ptr()` instead. +]=] +export type CFnInfo = { + --[=[ + @within CFnInfo + @tag Field + @field size + + The size of a function pointer. + + Equivalent to `ffi.c.usize.size`. + ]=] + size: number, + + -- Function + --[=[ + @within CFnInfo + @tag Method + @method callable + + Create a callable from reference. + + @return A callable + ]=] + callable: (self: CFnInfo, functionRef: RefData) -> CallableData, + --[=[ + @within CFnInfo + @tag Method + @method closure + + Create a closure from lua function. + + To return some data, lua function should write value into ret reference. + + @return A closure + ]=] + closure: (self: CFnInfo, (ret: RefData, ...RefData) -> ()) -> ClosureData, +} + +--[=[ + @class CStructInfo + + A c struct type information. +]=] +export type CStructInfo = { + --[=[ + @within CStructInfo + @tag Field + @field size + + The size of a struct, including padding. + ]=] + size: number, + + -- Subtype + --[=[ + @within CSturctInfo + @tag Method + @method arr + + Create a struct array type. + + @param length The length of the array + @return A struct array type + ]=] + arr: (self: CStructInfo, length: number) -> CArrInfo, + --[=[ + @within CSturctInfo + @tag Method + @method ptr + + Create a struct pointer type. + + @return A struct pointer type + ]=] + ptr: (self: CStructInfo) -> CPtrInfo, + + -- Create/Read/Write/Copy + --[=[ + @within CSturctInfo + @tag Method + @method box + + Create a box with initial value. + + @param table The array of field values + @return A box + ]=] + box: (self: CStructInfo, table: { any }) -> BoxData, + --[=[ + @within CSturctInfo + @tag Method + @method readData + + Read a lua table from reference or box. + + @param target Target to read data from + @param offset Offset to read data from + @return A table + ]=] + readData: (self: CStructInfo, target: RefData | BoxData, offset: number?) -> { any }, + --[=[ + @within CSturctInfo + @tag Method + @method writeData + + Write a lua table into reference or box. + + @param target Target to write data into + @param table Lua data to write + @param offset Offset to write data into + ]=] + writeData: ( + self: CStructInfo, + target: RefData | BoxData, + table: { any }, + offset: number? + ) -> (), + --[=[ + @within CSturctInfo + @tag Method + @method copyData + + Copy values from the source and paste them into the target. + + @param dst Where the content will be pasted + @param src The source data + @param dstOffset The offset in the destination where the content will be pasted + @param srcOffset The offset in the source data from where the content will be copied + ]=] + copyData: ( + self: CStructInfo, + dst: RefData | BoxData, + src: RefData | BoxData, + dstOffset: number?, + srcOffset: number? + ) -> (), + + -- ETC + --[=[ + @within CSturctInfo + @tag Method + @method offset + + Get the field offset. + + @param index The field index + @return The byte offset + ]=] + offset: (self: CStructInfo, index: number) -> number, + --[=[ + @within CSturctInfo + @tag Method + @method field + + Get the field type. + + @param index The field index + @return The field type + ]=] + field: (self: CStructInfo, index: number) -> CTypes, +} + +--[=[ + @class CVoidInfo + + A type that represents c void. can only be used for the function return type. +]=] +export type CVoidInfo = { + --[=[ + @within CVoidInfo + @tag Field + @field size + + The size of the void type. It is always 0. + ]=] + size: number, + + -- Subtype + --[=[ + @within CVoidInfo + @tag Method + @method ptr + + Create a generic pointer type. + + @return Generic pointer type, equivalent to `*void` in C. + ]=] + ptr: (self: CVoidInfo) -> CPtrInfo, +} +c.void = {} :: CVoidInfo +--#endregion C ABI Type Infos + +--#region Fixed size Rust-style types +--[=[ + @prop u8 NumCType + @within FFI + + A 8-bit sized unsigned integer, Equivalent to `uint8_t` in `stdint`. +]=] +ffi.u8 = {} :: u8 +export type u8 = NumCType<"u8"> +--[=[ + @prop u16 NumCType + @within FFI + + A 16-bit sized unsigned integer, Equivalent to `uint16_t` in `stdint`. +]=] +ffi.u16 = {} :: u16 +export type u16 = NumCType<"u16"> +--[=[ + @prop u32 NumCType + @within FFI + + A 32-bit sized unsigned integer, Equivalent to `uint32_t` in `stdint`. +]=] +ffi.u32 = {} :: u32 +export type u32 = NumCType<"u32"> +--[=[ + @prop u64 NumCType + @within FFI + + A 64-bit sized unsigned integer, Equivalent to `uint64_t` in `stdint`. +]=] +ffi.u64 = {} :: u64 +export type u64 = NumCType<"u64"> +--[=[ + @prop u128 NumCType + @within FFI + + A 128-bit sized unsigned integer, Equivalent to `uint128_t` in `stdint`. +]=] +ffi.u128 = {} :: u128 +export type u128 = NumCType<"u128"> +--[=[ + @prop i8 NumCType + @within FFI + + A 8-bit sized signed integer, Equivalent to `int8_t` in `stdint`. +]=] +ffi.i8 = {} :: i8 +export type i8 = NumCType<"i8"> +--[=[ + @prop i16 NumCType + @within FFI + + A 16-bit sized signed integer, Equivalent to `int16_t` in `stdint`. +]=] +ffi.i16 = {} :: i16 +export type i16 = NumCType<"i16"> +--[=[ + @prop i32 NumCType + @within FFI + + A 32-bit sized signed integer, Equivalent to `int32_t` in `stdint`. +]=] +ffi.i32 = {} :: i32 +export type i32 = NumCType<"i32"> +--[=[ + @prop i64 NumCType + @within FFI + + A 64-bit sized signed integer, Equivalent to `int64_t` in `stdint`. +]=] +ffi.i64 = {} :: i64 +export type i64 = NumCType<"i64"> +--[=[ + @prop i128 NumCType + @within FFI + + A 128-bit sized signed integer, Equivalent to `int128_t` in `stdint`. +]=] +ffi.i128 = {} :: i128 +export type i128 = NumCType<"i128"> +--[=[ + @prop f32 NumCType + @within FFI + + A single-precision 32-bit sized floating-point, Almost always equivalent to `float` in C. +]=] +ffi.f32 = {} :: f32 +export type f32 = NumCType<"f32"> +--[=[ + @prop f64 NumCType + @within FFI + + A double-precision 64-bit sized floating-point, Almost always equivalent to `double` in C. +]=] +ffi.f64 = {} :: f64 +export type f64 = NumCType<"f64"> +--[=[ + @prop usize NumCType + @within FFI + + A machine specific pointer sized unsigned integer. +]=] +ffi.usize = {} :: usize +export type usize = NumCType<"usize"> +--[=[ + @prop isize NumCType + @within FFI + + A machine specific pointer sized signed integer. +]=] +ffi.isize = {} :: isize +export type isize = NumCType<"isize"> +--#endregion Fixed size Rust-style types + +--#region Variable size C-style types +--[=[ + @prop char NumCType + @within C + + Compiler defined C `char` type. + + The signedness may differ depending on the compiler and platform. + + You can get signedness by `signedness` field. +]=] +c.char = {} :: char +export type char = NumCType<"char"> +--[=[ + @prop uchar NumCType + @within C + + Compiler defined C `unsigned char` type. + + Mostly equivalent to `u8`. +]=] +c.uchar = {} :: uchar +export type uchar = NumCType<"uchar"> +--[=[ + @prop schar NumCType + @within C + + Compiler defined C `signed char` type. +]=] +c.schar = {} :: schar +export type schar = NumCType<"schar"> +--[=[ + @prop short NumCType + @within C + + Compiler defined C `short` type. +]=] +c.short = {} :: short +export type short = NumCType<"short"> +--[=[ + @prop ushort NumCType + @within C + + Compiler defined C `unsigned short` type. +]=] +c.ushort = {} :: ushort +export type ushort = NumCType<"ushort"> +--[=[ + @prop int NumCType + @within C + + Compiler defined C `int` type. + + The side may differ depending on the compiler and platform. +]=] +c.int = {} :: int +export type int = NumCType<"int"> +--[=[ + @prop uint NumCType + @within C + + Compiler defined C `unsigned int` type. + + The side may differ depending on the compiler and platform. +]=] +c.uint = {} :: uint +export type uint = NumCType<"uint"> +--[=[ + @prop long NumCType + @within C + + Compiler defined C `long` type. + + The side may differ depending on the compiler and platform. +]=] +c.long = {} :: long +export type long = NumCType<"long"> +--[=[ + @prop ulong NumCType + @within C + + Compiler defined C `unsigned long` type. + + The side may differ depending on the compiler and platform. +]=] +c.ulong = {} :: ulong +export type ulong = NumCType<"ulong"> +--[=[ + @prop longlong NumCType + @within C + + Compiler defined C `unsigned longlong` type. +]=] +c.longlong = {} :: longlong +export type longlong = NumCType<"longlong"> +--[=[ + @prop longlong NumCType + @within C + + Compiler defined C `unsigned longlong` type. +]=] +c.ulonglong = {} :: ulonglong +export type ulonglong = NumCType<"ulonglong"> +--#endregion Variable size C-style types + +--[=[ + @class CTypes + + All possible C types. +]=] +export type CTypes = + u8 + | u16 + | u32 + | u64 + | u128 + | i8 + | i16 + | i32 + | i64 + | i128 + | f32 + | f64 + | usize + | isize + | char + | uchar + | schar + | short + | ushort + | int + | uint + | long + | ulong + | longlong + | ulonglong + | CArrInfo + | CPtrInfo + | CFnInfo + | CStructInfo + | CVoidInfo + +--[=[ + @within C + + Create a function signature type information. + + @param args An array of CTypes represents the arguments of the function + @param ret The return type of the function + @return A function signature type information +]=] +function c.fn(args: { CTypes }, ret: CTypes): CFnInfo + return nil :: any +end + +--[=[ + @within C + + Create a struct type information. + + @param fields An array of CTypes represents the fields of the struct + @return A struct type information +]=] +function c.struct(fields: { CTypes }): CStructInfo + return nil :: any +end + +--[=[ + @within FFI + + Create a `Ref` with address 0. + + Can be used for receive a pointer from external function or pass it as an argument. + + @return A zero initialized Ref +]=] +function ffi.nullRef(): RefData + return nil :: any +end + +--[=[ + @within FFI + + Create a `Box` with specific size. + The created box is not filed with zero. + + @param size The size of the new box + @return A allocated box +]=] +function ffi.box(size: number): BoxData + return nil :: any +end + +--[=[ + @within FFI + + Open a dynamic library. + + @param name The name of the target library + @return A dynamic library handle +]=] +function ffi.open(name: string): LibData + return nil :: any +end + +--[=[ + @within FFI + + Return `true` if the second argument is an integer (i32). + + @param val A lua value to check + @return Whether val is an integer or not +]=] +function ffi.isInteger(val: T): boolean + return nil :: any +end + +--[=[ + @within FFI + + Free referenced memory. + + @param data Target memory to free +]=] +function ffi.free(data: RefData | BoxData) + return nil :: any +end + +return ffi