From af08c59e3bcb4e54e1fe24047587ae284b4daa9e Mon Sep 17 00:00:00 2001 From: qwreey Date: Thu, 22 Aug 2024 01:30:21 +0900 Subject: [PATCH] Add lune-std-ffi crate (#243) --- .vscode/settings.json | 4 + Cargo.lock | 54 +++++++ Cargo.toml | 1 + crates/lune-std-ffi/Cargo.toml | 22 +++ crates/lune-std-ffi/readme.md | 129 ++++++++++++++++ crates/lune-std-ffi/src/association.rs | 84 ++++++++++ crates/lune-std-ffi/src/carr.rs | 24 +++ crates/lune-std-ffi/src/cfn.rs | 45 ++++++ crates/lune-std-ffi/src/cstring.rs | 6 + crates/lune-std-ffi/src/cstruct.rs | 127 ++++++++++++++++ crates/lune-std-ffi/src/ctype.rs | 203 +++++++++++++++++++++++++ crates/lune-std-ffi/src/ffibox.rs | 85 +++++++++++ crates/lune-std-ffi/src/ffilib.rs | 57 +++++++ crates/lune-std-ffi/src/ffiraw.rs | 8 + crates/lune-std-ffi/src/ffiref.rs | 59 +++++++ crates/lune-std-ffi/src/lib.rs | 56 +++++++ crates/lune-std-ffi/todo.md | 102 +++++++++++++ crates/lune-std/Cargo.toml | 3 + crates/lune-std/src/library.rs | 7 +- crates/lune/Cargo.toml | 2 + tests/ffi/ptr.luau | 33 ++++ tests/ffi/struct.luau | 2 + types/ffi.luau | 79 ++++++++++ 23 files changed, 1191 insertions(+), 1 deletion(-) create mode 100644 crates/lune-std-ffi/Cargo.toml create mode 100644 crates/lune-std-ffi/readme.md create mode 100644 crates/lune-std-ffi/src/association.rs create mode 100644 crates/lune-std-ffi/src/carr.rs create mode 100644 crates/lune-std-ffi/src/cfn.rs create mode 100644 crates/lune-std-ffi/src/cstring.rs create mode 100644 crates/lune-std-ffi/src/cstruct.rs create mode 100644 crates/lune-std-ffi/src/ctype.rs create mode 100644 crates/lune-std-ffi/src/ffibox.rs create mode 100644 crates/lune-std-ffi/src/ffilib.rs create mode 100644 crates/lune-std-ffi/src/ffiraw.rs create mode 100644 crates/lune-std-ffi/src/ffiref.rs create mode 100644 crates/lune-std-ffi/src/lib.rs create mode 100644 crates/lune-std-ffi/todo.md create mode 100644 tests/ffi/ptr.luau create mode 100644 tests/ffi/struct.luau create mode 100644 types/ffi.luau diff --git a/.vscode/settings.json b/.vscode/settings.json index 95c90cf..3940569 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,5 +23,9 @@ }, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, + "files.associations": { + "*.inc": "c", + "random": "c" } } diff --git a/Cargo.lock b/Cargo.lock index 734320a..4e4b654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,6 +786,29 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "dlopen2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc2c7ed06fd72a8513ded8d0d2f6fd2655a85d6885c48cae8625d80faf28c03" +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.72", +] + [[package]] name = "dunce" version = "1.0.5" @@ -1426,6 +1449,25 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[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" version = "0.8.5" @@ -1557,6 +1599,7 @@ name = "lune-std" version = "0.1.4" dependencies = [ "lune-std-datetime", + "lune-std-ffi", "lune-std-fs", "lune-std-luau", "lune-std-net", @@ -1585,6 +1628,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "lune-std-ffi" +version = "0.1.1" +dependencies = [ + "dlopen2", + "libffi", + "lune-utils", + "mlua", + "mlua-sys", +] + [[package]] name = "lune-std-fs" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 221a0fb..f32f08f 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 0000000..b867401 --- /dev/null +++ b/crates/lune-std-ffi/Cargo.toml @@ -0,0 +1,22 @@ +[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"] } +dlopen2 = "0.6" + +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 0000000..e61256b --- /dev/null +++ b/crates/lune-std-ffi/readme.md @@ -0,0 +1,129 @@ +TODO: rewrite docs + +# Raw + +Data received from external. You can move this data into a box, use it as a ref, or change it directly to a Lua value. +The raw data is not on Lua's heap. + +Raw:toRef() +Convert data into ref. it allocate new lua userdata + +Raw:toBox() +Convert data into box. it allocate new lua userdata + +Raw:intoBox() +Raw:intoRef() + +See type:fromRaw() + +# Box + +`ffi.box(size)` + +Create new userdata with sized by `size` argument. Box is untyped, and have no ABI information. You can write some data into box with `type` + +All operation with box will boundary checked. GC will free heap well. + +일반적으로 포인터를 넘겨주기 위해서 사용됩니다. 박스의 공간은 ref 할 수 있으며. 함수를 수행한 후 루아에서 읽어볼 수 있습니다. + +## :zero() +박스를 0 으로 채워넣습니다. 기본적으로 박스는 초기화될 때 0 으로 채워지기 때문에 박스를 다시 0 으로 초기화하고 싶을 경우에 사용하십시오. + +## :copy(targetbox,size,offset?=0,targetoffset?=0) +박스 안의 값을 다른 박스에 복사합니다. 바운더리가 확인되어지므로 안전합니다. + +## .size +이 박스의 크기입니다. + +## :ref(offset?=0) => ref +이 박스를 참조합니다. 참조가 살아있는 동안 박스는 수거되지 않습니다. 일반적으로 외부의 함수에 포인터를 넘겨주기 위해서 사용됩니다. + +## more stuffs (not planned at this time) + +ref=>buffer conversion, or bit/byte related? + +# Ref (Unsafe) + +바운더리를 처리하지 않는 포인터입니다. 외부에서 받은 포인터, 또는 박스로부터 만들어진 포인터입니다. +ref 는 바운더리를 검사하지 않으므로 안전하지 않습니다. + +## :offset(bytes) + +이 ref 와 상대적인 위치에 있는 ref 를 구합니다. + +## :writefromRef() +다른 ref 안의 값을 읽어와 이 ref 안에 씁니다. 아래와 비슷한 연산을 합니다 +```c +int a = 100,b; +``` + +## :writefromBox() +box 값을 읽어와서 쓰기 + +# Type + +`type` is abstract class that helps encoding data into `box` or decode data from `box` + +## :toBox(luavalue) +Convert lua value to box. box will sized with `type.size` + +## :fromBox(box,offset?=0) +Read data from box, and convert into lua value. +Boundary will checked + +## :intoBox(luavalue,box,offset?=0) +Convert lua value, and write into box +Boundary will checked + +## :fromRef(ref,offset?=0) +포인터가 가르키는 곳의 데이터를 읽어서 루아의 데이터로 변환합니다. + +## :intoRef(luavalue,ref,offset?=0) +포인터가 가르키는 곳에 데이터를 작성합니다. + +## :fromRaw(raw,offset?=0) + + +## :ptr() -> Ptr +Get pointer type + +## :arr(len) -> Arr +Get array type + +## .size + +Byte size of this type. you can initialize box with + +## :cast(box,type) TODO + +# Ptr +Pointer type of some type. + +Ptr is not data converter. It only works for type hint of `struct` or `fn` + +## .inner +Inner type + +## .size +Size of `usize` + +:ptr() +:arr() + +## Arr + +## Void + +`ffi.void` + +Zero sized type. + +## Fn +Prototype type of some function. converts lua function into native function pointer or native function pointer into lua function. + +`ffi.fn({ type }, type) -> fn` + +:toLua( ref ) -> luafunction +:toBox( luafunction ) -> ref + +> TODO: rust, and another ABI support diff --git a/crates/lune-std-ffi/src/association.rs b/crates/lune-std-ffi/src/association.rs new file mode 100644 index 0000000..49f9c8a --- /dev/null +++ b/crates/lune-std-ffi/src/association.rs @@ -0,0 +1,84 @@ +#![allow(clippy::cargo_common_metadata)] + +// 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 registry +// and simulate what mlua does. +// Since mlua does not provide Lua state (private), +// uservalue operations cannot be performed directly, +// so this is the best solution for now. +// If the dependency is deep, the value may be completely destroyed when +// gc is performed multiple times. As an example, there is the following case: +// +// ffi.i32:ptr():ptr() +// box:ref():ref() +// +// Since the outermost pointer holds the definition for the pointer +// type inside it, only the outermost type will be removed on the first gc. +// It doesn't matter much. But if there is a cleaner way, we should choose it +use mlua::prelude::*; + +// Forces 'associated' to persist as long as 'value' is alive. +// 'value' can only hold one value. If you want to keep something else, +// use a table with a different name. +// You can delete the relationship by changing 'associated' to nil +pub fn set_association<'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. +pub fn get_association<'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!(), + } +} + +// Allows reading of registry tables for debugging. +// This helps keep track of data being gc'd. +// However, for security and safety reasons, +// this will not be allowed unless it is a debug build. +#[cfg(debug_assertions)] +pub fn get_table<'lua>(lua: &'lua Lua, regname: &str) -> LuaResult>> { + match lua.named_registry_value::(regname)? { + LuaValue::Nil => Ok(None), + LuaValue::Table(t) => Ok(Some(t)), + _ => panic!(), + } +} diff --git a/crates/lune-std-ffi/src/carr.rs b/crates/lune-std-ffi/src/carr.rs new file mode 100644 index 0000000..9258e89 --- /dev/null +++ b/crates/lune-std-ffi/src/carr.rs @@ -0,0 +1,24 @@ +use libffi::middle::Type; +use mlua::prelude::*; + +// This is a 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. +// However, multidimensional arrays are not impossible to implement +// because they are a series of transcribed one-dimensional arrays. + +// See: https://stackoverflow.com/a/43525176 + +struct CArr { + libffi_type: Type, + length: usize, + size: usize, +} + +impl CArr { + fn new(libffi_type: Type, length: usize) { + Self { libffi_type } + } +} + +impl LuaUserData for CArr {} diff --git a/crates/lune-std-ffi/src/cfn.rs b/crates/lune-std-ffi/src/cfn.rs new file mode 100644 index 0000000..f029cfc --- /dev/null +++ b/crates/lune-std-ffi/src/cfn.rs @@ -0,0 +1,45 @@ +use libffi::middle::{Cif, Type}; +use mlua::prelude::*; + +use crate::ctype::{libffi_type_from_userdata, libffi_types_from_table}; + +// cfn is a type declaration for a function. +// Basically, when calling an external function, this type declaration +// is referred to and type conversion is automatically assisted. + +// However, in order to save on type conversion costs, +// users keep values ​​they will use continuously in a box and use them multiple times. +// Alternatively, if the types are the same,you can save the cost of creating +// a new space by directly passing FfiRaw, +// the result value of another function or the argument value of the callback. + +// Defining cfn simply lists the function's actual argument positions and conversions. +// You must decide how to process the data in Lua. + +// The name cfn is intentional. This is because any *c_void is +// moved to a Lua function or vice versa. + +pub struct CFn { + libffi_cif: Cif, + args: Vec, + ret: Type, +} + +impl CFn { + pub fn new(args: Vec, ret: Type) -> Self { + let libffi_cif = Cif::new(args.clone(), ret.clone()); + Self { + libffi_cif, + args, + ret, + } + } + + pub fn from_lua_table(args: LuaTable, ret: LuaAnyUserData) -> LuaResult { + let args = libffi_types_from_table(&args)?; + let ret = libffi_type_from_userdata(&ret)?; + Ok(Self::new(args, ret)) + } +} + +impl LuaUserData for CFn {} diff --git a/crates/lune-std-ffi/src/cstring.rs b/crates/lune-std-ffi/src/cstring.rs new file mode 100644 index 0000000..cf79b48 --- /dev/null +++ b/crates/lune-std-ffi/src/cstring.rs @@ -0,0 +1,6 @@ +// This is a string type that can be given to an external function. +// To be exact, it converts the Lua string into a c_char array and puts it in the box. +// For this part, initially, i wanted to allow box("lua string"), +// but separated it for clarity. +// This also allows operations such as ffi.string:intoBox(). +// (Write a string to an already existing box) diff --git a/crates/lune-std-ffi/src/cstruct.rs b/crates/lune-std-ffi/src/cstruct.rs new file mode 100644 index 0000000..b5aa47e --- /dev/null +++ b/crates/lune-std-ffi/src/cstruct.rs @@ -0,0 +1,127 @@ +#![allow(clippy::cargo_common_metadata)] + +use mlua::prelude::*; + +use libffi::low::ffi_abi_FFI_DEFAULT_ABI; +use libffi::middle::{Cif, Type}; +use libffi::raw::ffi_get_struct_offsets; +use std::vec::Vec; + +use crate::association::{get_association, set_association}; +use crate::ctype::{libffi_types_from_table, type_name_from_userdata}; + +use super::ctype::CType; + +pub struct CStruct { + libffi_cif: Cif, + libffi_type: Type, + fields: Vec, + offsets: Vec, + size: usize, +} + +const CSTRUCT_INNER: &str = "__cstruct_inner"; + +impl CStruct { + pub fn new(fields: Vec) -> Self { + let libffi_type = Type::structure(fields.clone()); + let libffi_cfi = Cif::new(vec![libffi_type.clone()], Type::void()); + let size = unsafe { (*libffi_type.as_raw_ptr()).size }; + let mut offsets = Vec::::with_capacity(fields.len()); + unsafe { + ffi_get_struct_offsets( + ffi_abi_FFI_DEFAULT_ABI, + libffi_type.as_raw_ptr(), + offsets.as_mut_ptr(), + ); + offsets.set_len(offsets.capacity()); + } + + Self { + libffi_cif: libffi_cfi, + libffi_type, + fields, + offsets, + size, + } + } + + pub fn from_lua_table<'lua>( + lua: &'lua Lua, + table: LuaTable<'lua>, + ) -> LuaResult> { + let fields = libffi_types_from_table(&table)?; + let cstruct = lua.create_userdata(Self::new(fields))?; + table.set_readonly(true); + set_association(lua, CSTRUCT_INNER, cstruct.clone(), table)?; + Ok(cstruct) + } + + // Stringify cstruct for pretty printing something like: + // + pub fn stringify(userdata: &LuaAnyUserData) -> LuaResult { + let field: LuaValue = userdata.get("inner")?; + if field.is_table() { + let table = field + .as_table() + .ok_or(LuaError::external("failed to get inner table."))?; + + // iterate for field + let mut result = String::from(" "); + for i in 0..table.raw_len() { + let child: LuaAnyUserData = table.raw_get(i + 1)?; + result.push_str(format!("{}, ", type_name_from_userdata(&child)?).as_str()); + } + + // size of + result.push_str(format!("size = {} ", userdata.borrow::()?.size).as_str()); + Ok(result) + } else { + Ok(String::from("unnamed")) + } + } + + pub fn get_type(&self) -> Type { + self.libffi_type.clone() + } + + // Get byte offset of nth field + pub fn offset(&self, index: usize) -> LuaResult { + let offset = self + .offsets + .get(index) + .ok_or(LuaError::external("Out of index"))? + .to_owned(); + Ok(offset) + } +} + +impl LuaUserData for CStruct { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("size", |_, this| Ok(this.size)); + + // Simply pass in the locked table used when first creating this object. + // By strongly referencing the table, the types inside do not disappear + // and the user can read the contents as needed. (good recycling!) + fields.add_field_function_get("inner", |lua, this: LuaAnyUserData| { + let table: LuaValue = get_association(lua, CSTRUCT_INNER, this)? + // It shouldn't happen. + .ok_or(LuaError::external("inner field not found"))?; + Ok(table) + }); + } + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("offset", |_, this, index: usize| { + let offset = this.offset(index)?; + Ok(offset) + }); + methods.add_function("ptr", |lua, this: LuaAnyUserData| { + let pointer = CType::pointer(lua, &this)?; + Ok(pointer) + }); + methods.add_meta_function(LuaMetaMethod::ToString, |_, this: LuaAnyUserData| { + let result = CStruct::stringify(&this)?; + Ok(result) + }); + } +} diff --git a/crates/lune-std-ffi/src/ctype.rs b/crates/lune-std-ffi/src/ctype.rs new file mode 100644 index 0000000..fe223c5 --- /dev/null +++ b/crates/lune-std-ffi/src/ctype.rs @@ -0,0 +1,203 @@ +#![allow(clippy::cargo_common_metadata)] + +use std::borrow::Borrow; + +use super::association::{get_association, set_association}; +use super::cstruct::CStruct; +use libffi::middle::{Cif, Type}; +use lune_utils::fmt::{pretty_format_value, ValueFormatConfig}; +use mlua::prelude::*; +// use libffi::raw::{ffi_cif, ffi_ptrarray_to_raw}; + +const POINTER_INNER: &str = "__pointer_inner"; + +pub struct CType { + libffi_cif: Cif, + libffi_type: Type, + size: usize, + name: Option, +} + +// TODO: ARR +// TODO: convert + +impl CType { + pub fn new(libffi_type: Type, name: Option) -> Self { + let libffi_cfi = Cif::new(vec![libffi_type.clone()], Type::void()); + let size = unsafe { (*libffi_type.as_raw_ptr()).size }; + Self { + libffi_cif: libffi_cfi, + libffi_type, + size, + name, + } + } + + pub fn get_type(&self) -> Type { + self.libffi_type.clone() + } + + pub fn pointer<'lua>(lua: &'lua Lua, inner: &LuaAnyUserData) -> LuaResult> { + let value = Self { + libffi_cif: Cif::new(vec![Type::pointer()], Type::void()), + libffi_type: Type::pointer(), + size: size_of::(), + name: Some(format!( + "Ptr<{}({})>", + { + if inner.is::() { + "CStruct" + } else if inner.is::() { + "CType" + } else { + "unnamed" + } + }, + type_name_from_userdata(inner)? + )), + } + .into_lua(lua)?; + + set_association(lua, POINTER_INNER, value.borrow(), inner)?; + + Ok(value) + } + + pub fn stringify(&self) -> String { + match &self.name { + Some(t) => t.to_owned(), + None => String::from("unnamed"), + } + } +} + +impl LuaUserData for CType { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("size", |_, this| Ok(this.size)); + fields.add_field_function_get("inner", |lua, this| { + let inner = get_association(lua, POINTER_INNER, this)?; + match inner { + Some(t) => Ok(t), + None => Ok(LuaNil), + } + }); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function("ptr", |lua, this: LuaAnyUserData| { + let pointer = CType::pointer(lua, &this)?; + Ok(pointer) + }); + + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + let name = this.stringify(); + Ok(name) + }); + } +} + +// export all default c-types +pub fn create_all_types(lua: &Lua) -> LuaResult> { + Ok(vec![ + ( + "u8", + CType::new(Type::u8(), Some(String::from("u8"))).into_lua(lua)?, + ), + ( + "u16", + CType::new(Type::u16(), Some(String::from("u16"))).into_lua(lua)?, + ), + ( + "u32", + CType::new(Type::u32(), Some(String::from("u32"))).into_lua(lua)?, + ), + ( + "u64", + CType::new(Type::u64(), Some(String::from("u64"))).into_lua(lua)?, + ), + ( + "i8", + CType::new(Type::i8(), Some(String::from("i8"))).into_lua(lua)?, + ), + ( + "i16", + CType::new(Type::i16(), Some(String::from("i16"))).into_lua(lua)?, + ), + ( + "i32", + CType::new(Type::i32(), Some(String::from("i32"))).into_lua(lua)?, + ), + ( + "i64", + CType::new(Type::i64(), Some(String::from("i64"))).into_lua(lua)?, + ), + ( + "f32", + CType::new(Type::f32(), Some(String::from("f32"))).into_lua(lua)?, + ), + ( + "f64", + CType::new(Type::f64(), Some(String::from("f64"))).into_lua(lua)?, + ), + ( + "void", + CType::new(Type::void(), Some(String::from("void"))).into_lua(lua)?, + ), + ]) +} + +// get Vec from table(array) of c-types userdata +pub fn libffi_types_from_table(table: &LuaTable) -> LuaResult> { + let len: usize = table.raw_len(); + let mut fields = Vec::with_capacity(len); + + for i in 0..len { + // Test required + let value = table.raw_get(i + 1)?; + match value { + LuaValue::UserData(field_type) => { + fields.push(libffi_type_from_userdata(&field_type)?); + } + _ => { + return Err(LuaError::external(format!( + "Unexpected field. CStruct, CType or CArr is required for element but got {}", + pretty_format_value(&value, &ValueFormatConfig::new()) + ))); + } + } + } + + Ok(fields) +} + +// get libffi_type from any c-types userdata +pub fn libffi_type_from_userdata(userdata: &LuaAnyUserData) -> LuaResult { + if userdata.is::() { + Ok(userdata.borrow::()?.get_type()) + } else if userdata.is::() { + Ok(userdata.borrow::()?.get_type()) + } else { + Err(LuaError::external(format!( + "Unexpected field. CStruct, CType or CArr is required for element 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() + ) + ))) + } +} + +// stringify any c-types userdata (for recursive) +pub fn type_name_from_userdata(userdata: &LuaAnyUserData) -> LuaResult { + if userdata.is::() { + let name = userdata.borrow::()?.stringify(); + Ok(name) + } else if userdata.is::() { + let name = CStruct::stringify(userdata)?; + Ok(name) + } else { + Ok(String::from("unnamed")) + } +} diff --git a/crates/lune-std-ffi/src/ffibox.rs b/crates/lune-std-ffi/src/ffibox.rs new file mode 100644 index 0000000..94f1f99 --- /dev/null +++ b/crates/lune-std-ffi/src/ffibox.rs @@ -0,0 +1,85 @@ +#![allow(clippy::cargo_common_metadata)] + +// It is an untyped, sized memory area that Lua can manage. +// This area is safe within Lua. Operations have their boundaries checked. +// It is basically intended to implement passing a pointed space to the outside. +// It also helps you handle data that Lua cannot handle. +// Depending on the type, operations such as sum, mul, and mod may be implemented. +// There is no need to enclose all data in a box; +// rather, it creates more heap space, so it should be used appropriately +// where necessary. + +use super::association::set_association; +use super::ffiref::FfiRef; +use core::ffi::c_void; +use mlua::prelude::*; +use std::boxed::Box; + +const BOX_REF_INNER: &str = "__box_ref"; + +pub struct FfiBox(Box<[u8]>); + +impl FfiBox { + pub fn new(size: usize) -> Self { + Self(vec![0u8; size].into_boxed_slice()) + } + + pub fn size(&self) -> usize { + self.0.len() + } + + // pub fn copy(&self, target: &mut FfiBox) {} + + pub fn get_ptr(&self) -> *mut c_void { + self.0.as_ptr() as *mut c_void + } + + // bad naming. i have no idea what should i use + pub fn luaref<'lua>( + lua: &'lua Lua, + this: LuaAnyUserData<'lua>, + ) -> LuaResult> { + let target = this.borrow::()?; + + let luaref = lua.create_userdata(FfiRef::new(target.get_ptr()))?; + + set_association(lua, BOX_REF_INNER, luaref.clone(), this.clone())?; + + Ok(luaref) + } + + pub fn zero(&mut self) { + self.0.fill(0u8); + } +} + +impl LuaUserData for FfiBox { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("size", |_, this| Ok(this.size())); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method_mut("zero", |_, this, ()| { + this.zero(); + Ok(()) + }); + methods.add_function("ref", |lua, this: LuaAnyUserData| { + let luaref = FfiBox::luaref(lua, this)?; + Ok(luaref) + }); + methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.size())); + methods.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| { + dbg!(&this.0.len()); + let mut buff = String::from("[ "); + for i in &this.0 { + buff.push_str(i.to_owned().to_string().as_str()); + buff.push_str(", "); + } + buff.pop(); + buff.pop(); + buff.push_str(" ]"); + let luastr = lua.create_string(buff.as_bytes())?; + Ok(luastr) + }); + } +} diff --git a/crates/lune-std-ffi/src/ffilib.rs b/crates/lune-std-ffi/src/ffilib.rs new file mode 100644 index 0000000..9dfbf3b --- /dev/null +++ b/crates/lune-std-ffi/src/ffilib.rs @@ -0,0 +1,57 @@ +use std::ffi::c_void; + +use super::association::set_association; +use dlopen2::symbor::Library; +use mlua::prelude::*; + +use crate::ffiref::FfiRef; + +pub struct FfiLib(Library); + +const SYM_INNER: &str = "__syn_inner"; + +// COMMENT HERE +// For convenience, it would be nice to provide a way to get +// symbols from a table with type and field names specified. +// But right now, we are starting from the lowest level, so we will make it later. + +// I wanted to provide something like cdef, +// but that is beyond the scope of lune's support. +// Higher-level bindings for convenience are much preferable written in Lua. + +impl FfiLib { + pub fn new(libname: String) -> LuaResult { + match Library::open(libname) { + Ok(t) => Ok(Self(t)), + Err(err) => Err(LuaError::external(format!("{err}"))), + } + } + + pub fn get_sym<'lua>( + lua: &'lua Lua, + this: LuaAnyUserData<'lua>, + name: String, + ) -> LuaResult> { + let lib = this.borrow::()?; + let sym = unsafe { + lib.0 + .symbol::<*mut c_void>(name.as_str()) + .map_err(|err| LuaError::external(format!("{err}")))? + }; + + let luasym = lua.create_userdata(FfiRef::new(*sym))?; + + set_association(lua, SYM_INNER, luasym.clone(), this.clone())?; + + Ok(luasym) + } +} + +impl LuaUserData for FfiLib { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function("dlsym", |lua, (this, name): (LuaAnyUserData, String)| { + let luasym = FfiLib::get_sym(lua, this, name)?; + Ok(luasym) + }); + } +} diff --git a/crates/lune-std-ffi/src/ffiraw.rs b/crates/lune-std-ffi/src/ffiraw.rs new file mode 100644 index 0000000..6d64e58 --- /dev/null +++ b/crates/lune-std-ffi/src/ffiraw.rs @@ -0,0 +1,8 @@ +// This is raw data coming from outside. +// Users must convert it to a Lua value, reference, or box to use it. +// The biggest reason for providing this is to allow the user to +// decide whether to move the data to a heap that Lua can manage (box), +// move it directly to Lua's data, or think of it as a pointer. +// This will help you distinguish between safe operations and +// relatively insecure operations, and help ensure that as little +// data copy as possible occurs, while allowing you to do little restrictions. diff --git a/crates/lune-std-ffi/src/ffiref.rs b/crates/lune-std-ffi/src/ffiref.rs new file mode 100644 index 0000000..5b17966 --- /dev/null +++ b/crates/lune-std-ffi/src/ffiref.rs @@ -0,0 +1,59 @@ +use super::association::set_association; +use core::ffi::c_void; +use mlua::prelude::*; +use std::ptr; + +// A referenced space. It is possible to read and write through types. +// This operation is not safe. This may cause a memory error in Lua +// if use it incorrectly. +// If it references an area managed by Lua, +// the box will remain as long as this reference is alive. + +pub struct FfiRef(*mut c_void); + +const REF_INNER: &str = "__ref_inner"; + +impl FfiRef { + pub fn new(target: *mut c_void) -> Self { + Self(target) + } + + // bad naming. i have no idea what should i use + pub fn luaref<'lua>( + lua: &'lua Lua, + this: LuaAnyUserData<'lua>, + ) -> LuaResult> { + let target = this.borrow::()?; + + let luaref = lua.create_userdata(FfiRef::new(ptr::from_ref(&target.0) as *mut c_void))?; + + set_association(lua, REF_INNER, luaref.clone(), this.clone())?; + + Ok(luaref) + } + + pub unsafe fn deref(&self) -> Self { + Self::new(*self.0.cast::<*mut c_void>()) + } + + pub unsafe fn offset(&self, offset: isize) -> Self { + Self::new(self.0.offset(offset)) + } +} + +impl LuaUserData for FfiRef { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("deref", |_, this, ()| { + let ffiref = unsafe { this.deref() }; + Ok(ffiref) + }); + methods.add_method("offset", |_, this, offset: isize| { + let ffiref = unsafe { this.offset(offset) }; + Ok(ffiref) + }); + methods.add_function("ref", |lua, this: LuaAnyUserData| { + let ffiref = FfiRef::luaref(lua, this)?; + Ok(ffiref) + }); + } +} diff --git a/crates/lune-std-ffi/src/lib.rs b/crates/lune-std-ffi/src/lib.rs new file mode 100644 index 0000000..5291616 --- /dev/null +++ b/crates/lune-std-ffi/src/lib.rs @@ -0,0 +1,56 @@ +#![allow(clippy::cargo_common_metadata)] + +use lune_utils::TableBuilder; +use mlua::prelude::*; + +mod association; +mod carr; +mod cfn; +mod cstring; +mod cstruct; +mod ctype; +mod ffibox; +mod ffilib; +mod ffiraw; +mod ffiref; + +use self::association::get_table; +use self::cfn::CFn; +use self::cstruct::CStruct; +use self::ctype::create_all_types; +use self::ffibox::FfiBox; +use self::ffilib::FfiLib; + +/** + Creates the `ffi` standard library module. + + # Errors + + Errors when out of memory. +*/ +pub fn module(lua: &Lua) -> LuaResult { + let ctypes = create_all_types(lua)?; + let result = TableBuilder::new(lua)? + .with_values(ctypes)? + .with_function("box", |_, size: usize| Ok(FfiBox::new(size)))? + // TODO: discuss about function name. matching with io.open is better? + .with_function("dlopen", |_, name: String| { + let lib = FfiLib::new(name)?; + Ok(lib) + })? + .with_function("struct", |lua, types: LuaTable| { + let cstruct = CStruct::from_lua_table(lua, types)?; + Ok(cstruct) + })? + .with_function("fn", |_, (args, ret): (LuaTable, LuaAnyUserData)| { + let cfn = CFn::from_lua_table(args, ret)?; + Ok(cfn) + })?; + + #[cfg(debug_assertions)] + let result = result.with_function("debug_associate", |lua, str: String| { + get_table(lua, str.as_ref()) + })?; + + result.build_readonly() +} diff --git a/crates/lune-std-ffi/todo.md b/crates/lune-std-ffi/todo.md new file mode 100644 index 0000000..307b29a --- /dev/null +++ b/crates/lune-std-ffi/todo.md @@ -0,0 +1,102 @@ + +use libffi::raw::{ffi_cif, ffi_ptrarray_to_raw}; + +// pub fn ffi_get_struct_offsets( +// abi: ffi_abi, +// struct_type: *mut ffi_type, +// offsets: *mut usize, +// ) -> ffi_status; + +- last thing to do +- [ ] Add tests +- [ ] Add docs +- [ ] Typing + +# Raw + +- [ ] Raw:toRef() +- [ ] Raw:toBox() +- [ ] Raw:intoBox() +- [ ] Raw:intoRef() + +# Box + +- [x] ffi.box(size) +- [x] .size +- [x] :zero() +- [?] :ref(offset?=0) => ref + - offset is not impled +- [~] :copy(box,size?=-1,offset?=0) + - working on it + +# Ref (Unsafe) + +- [x] ref:deref() -> ref +- [x] ref:offset(bytes) -> ref +- [x] ref:ref() -> ref + +~~- [ ] ref:fromRef(size,offset?=0) ?? what is this~~ +~~- [ ] ref:fromBox(size,offset?=0) ?? what is this~~ + +# Struct + +- [x] :offset(index) +- [x] :ptr() +- [x] .inner[n] +- [!] .size +- [ ] # +- [x] tostring + +size, offset is strange. maybe related to cif state. + +# Type + +- [ ] :toBox(luavalue) + +Very stupid idea. +from(box|ref|raw, offset) is better idea i think. + +- [ ] :fromBox(box,offset?=0) +- [ ] :intoBox(luavalue,box,offset?=0) +- [ ] :fromRef(ref,offset?=0) +- [ ] :intoRef(luavalue,ref,offset?=0) +- [ ] :fromRaw(raw,offset?=0) + +- [ ] :castBox(box,type) TODO +- [ ] + +- [ ] :sum +- [ ] :mul +- [ ] :sub + +## subtype +- [x] :ptr() -> Ptr +- [~] :arr(len) -> Arr +- [x] .size + + + +# Ptr + +- [x] .inner +- [x] .size +- [x] :ptr() +- [~] :arr() + +## Arr + +## Void + +`ffi.void` + +Zero sized type. + +## Fn +Prototype type of some function. converts lua function into native function pointer or native function pointer into lua function. + +`ffi.fn({ type }, type) -> fn` + +:toLua( ref ) -> luafunction +:toBox( luafunction ) -> ref + +> TODO: rust, and another ABI support diff --git a/crates/lune-std/Cargo.toml b/crates/lune-std/Cargo.toml index 132338f..9121ce3 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.3", 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/library.rs b/crates/lune-std/src/library.rs index 9a301f5..2ac783e 100644 --- a/crates/lune-std/src/library.rs +++ b/crates/lune-std/src/library.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use mlua::prelude::*; /** - A standard library provided by Lune. + A standard library probloxrovided by Lune. */ #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[rustfmt::skip] @@ -18,6 +18,7 @@ pub enum LuneStandardLibrary { #[cfg(feature = "serde")] Serde, #[cfg(feature = "stdio")] Stdio, #[cfg(feature = "roblox")] Roblox, + #[cfg(feature = "ffi")] Ffi, } impl LuneStandardLibrary { @@ -36,6 +37,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 +58,7 @@ 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"), } @@ -82,6 +85,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 +115,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/Cargo.toml b/crates/lune/Cargo.toml index c784669..a95aab8 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/tests/ffi/ptr.luau b/tests/ffi/ptr.luau new file mode 100644 index 0000000..c62eadf --- /dev/null +++ b/tests/ffi/ptr.luau @@ -0,0 +1,33 @@ + +local ffi = require("@lune/ffi") + +-- ptr size test +assert( + ffi.i32:ptr().size == ffi.i64:ptr().size, + "All of Ptr.size must be same.\n".. + "ffi.i32:ptr().size == ffi.i64:ptr().size failed" +) + +-- inner test +local i32ptr = ffi.i32:ptr() +assert( + rawequal(ffi.i32, i32ptr.inner), + "Ptr.inner must be same with their parent\n".. + "raweq ffi.i32 == ffi.i32:ptr().inner failed" +) +assert( + rawequal(i32ptr, i32ptr:ptr().inner), + "Ptr.inner must be same with their parent\n".. + "raweq i32ptr == i32ptr:ptr().inner failed" +) +assert( + rawequal(i32ptr, i32ptr:ptr().inner:ptr().inner:ptr().inner), + "Ptr.inner must be same with their parent\n".. + "raweq i32ptr == i32ptr:ptr().inner:ptr().inner:ptr().inner failed" +) + +-- deep ptr test +local ok,err = pcall(function() + i32ptr:ptr():ptr():ptr():ptr():ptr():ptr():ptr() +end) +assert(ok,`Deep ptr test failed.\n{err}`) diff --git a/tests/ffi/struct.luau b/tests/ffi/struct.luau new file mode 100644 index 0000000..e1c1f8d --- /dev/null +++ b/tests/ffi/struct.luau @@ -0,0 +1,2 @@ + +local ffi = require("@lune/ffi") diff --git a/types/ffi.luau b/types/ffi.luau new file mode 100644 index 0000000..fb45208 --- /dev/null +++ b/types/ffi.luau @@ -0,0 +1,79 @@ +--[=[ + @interface Box + @within FFI + + Box is an untyped, sized memory area that Lua can manage. + This area is safe within Lua. Operations have their boundaries checked. + + You can passing box as raw arguments or as pointer to outside. + It also helps you handle data that Lua cannot handle. or you can reuse box to save cost from convertsion. + Depending on the type, operations such as sum, mul, and mod may be implemented. See Types + + ```lua + ffi.box(size) + ``` + This is a dictionary that will contain the following values: + + * `readOnly` - If the target path is read-only or not +]=] + +export type Box = { + size: number, + ref: (self: Box)->Ref, +} +export type BoxConstructor = (size: number)->Box + +export type Type = {} + +---! FIXME: better typing for PointerSize +export type PointerSize = number -- typeof(5) | typeof(8) + +export type Arr = { + inner: T, + size: number, + ptr: (self: Arr) -> any, +} + +--[=[ + @interface Ptr + @within FFI + +]=] +---! FIXME: due to recursive type limition. hardcoded 6 depth. better idea? +export type Ptr = { + inner: T, + size: PointerSize, + ptr: (self: Ptr)->PtrPtr>, + arr: (self: Ptr, size: number) -> Arr>, +} +export type PtrPtr = { + inner: T, + size: PointerSize, + ptr: (self: PtrPtr)->PtrPtrPtr>, + arr: (self: PtrPtr, size: number) -> Arr>, +} +export type PtrPtrPtr = { + inner: T, + size: PointerSize, + ptr: (self: PtrPtrPtr)->PtrPtrPtrPtr>, + arr: (self: PtrPtrPtr, size: number) -> Arr>, +} +export type PtrPtrPtrPtr = { + inner: T, + size: PointerSize, + ptr: (self: PtrPtrPtrPtr)->PtrPtrPtrPtrPtr>, + arr: (self: PtrPtrPtrPtr, size: number) -> Arr>, +} +export type PtrPtrPtrPtrPtr = { + inner: T, + size: PointerSize, + ptr: (self: PtrPtrPtrPtrPtr)->PtrPtrPtrPtrPtrPtr>, + arr: (self: PtrPtrPtrPtrPtr, size: number) -> Arr>, +} +export type PtrPtrPtrPtrPtrPtr = { + inner: T, + size: PointerSize, + ptr: (self: PtrPtrPtrPtrPtrPtr)->any, -- Yes. At this point. more type is useless. + arr: (self: PtrPtrPtrPtrPtrPtr, size: number) -> Arr>, +} +