diff --git a/Cargo.lock b/Cargo.lock index 0feaf573..b32352a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -591,6 +591,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "current_platform" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74858bcfe44b22016cb49337d7b6f04618c58e5dbfdef61b06b8c434324a0bc" + [[package]] name = "dashu" version = "0.4.2" @@ -2689,6 +2695,7 @@ dependencies = [ "crossterm", "crrl", "ctrlc", + "current_platform", "dashu", "derive_more", "dirs-next", diff --git a/Cargo.toml b/Cargo.toml index 67f65bc9..f558953a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ js-sys = "0.3" ouroboros = "0.18" [dev-dependencies] +current_platform = "0.2.0" maplit = "1.0.2" serial_test = "3.1.1" diff --git a/src/ffi.rs b/src/ffi.rs index 55bb525b..36f290ff 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -19,19 +19,23 @@ and for each field: we add to the pointer until we're aligned to the next data t and finally we add the pointer the size of what we've written. */ +use crate::arena::Arena; use crate::atom_table::Atom; +use crate::forms::Number; +use crate::parser::ast::{Fixnum, MightNotFitInFixnum}; -use std::alloc::{self, Layout}; -use std::any::Any; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::error::Error; -use std::ffi::{c_void, CString}; -use std::ptr::addr_of_mut; - -use libffi::low::type_tag::STRUCT; -use libffi::low::{ffi_abi_FFI_DEFAULT_ABI, ffi_cif, ffi_type, prep_cif, types, CodePtr}; +use dashu::Integer; +use libffi::middle::{Arg, Cif, CodePtr, Type}; use libloading::{Library, Symbol}; +use ordered_float::OrderedFloat; +use std::alloc::{self, Layout}; +use std::collections::HashMap; +use std::error::Error; +use std::ffi::{c_char, c_void, CStr, CString}; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::Deref; +use std::ptr::NonNull; pub struct FunctionDefinition { pub name: String, @@ -41,10 +45,120 @@ pub struct FunctionDefinition { #[derive(Debug)] pub struct FunctionImpl { - cif: ffi_cif, - args: Vec<*mut ffi_type>, + cif: Cif, + args: Vec, code_ptr: CodePtr, - return_struct_name: Option, + return_type: FfiType, +} + +impl FunctionImpl { + unsafe fn call_void(&self, args: &[Arg], _: &mut Arena) -> Result { + self.cif.call::<()>(self.code_ptr, args); + Ok(Value::Number(Number::Fixnum(Fixnum::build_with(0)))) + } + + unsafe fn call_int(&self, args: &[Arg], arena: &mut Arena) -> Result + where + Integer: From, + T: Copy + TryInto + MightNotFitInFixnum, + { + let n = self.cif.call::(self.code_ptr, args); + Ok(Value::Number(fixnum!(Number, n, arena))) + } + + unsafe fn call_float(&self, args: &[Arg], _: &mut Arena) -> Result + where + T: Into, + { + let n = self.cif.call::(self.code_ptr, args); + Ok(Value::Number(Number::Float(OrderedFloat(n.into())))) + } + + unsafe fn call_ptr(&self, args: &[Arg], arena: &mut Arena) -> Result { + let ptr = unsafe { self.cif.call::<*mut c_void>(self.code_ptr, args) }; + Ok(Value::Number(fixnum!(Number, ptr as isize, arena))) + } + + unsafe fn call_cstr(&self, args: &[Arg], _: &mut Arena) -> Result { + let ptr = unsafe { + self.cif + .call::>>(self.code_ptr, args) + }; + + if let Some(cstr) = ptr { + Ok(Value::CString( + unsafe { CStr::from_ptr(cstr.as_ptr()) }.to_owned(), + )) + } else { + Ok(Value::Number(Number::Fixnum(Fixnum::build_with(0)))) + } + } + + unsafe fn call_struct( + &self, + return_type_name: Atom, + args: &[Arg], + arena: &mut Arena, + structs_table: &HashMap, + ) -> Result { + let struct_type = structs_table + .get(&*return_type_name.as_str()) + .ok_or(FfiError::StructNotFound)?; + let ffi_type = unsafe { *struct_type.ffi_type.as_raw_ptr() }; + + let layout = Layout::from_size_align(ffi_type.size, ffi_type.alignment.into()) + .map_err(|_| FfiError::LayoutError)?; + + let alloc = FfiStruct::new(layout)?; + + unsafe { + libffi::raw::ffi_call( + self.cif.as_raw_ptr(), + Some(*self.code_ptr.as_safe_fun()), + alloc.ptr.as_ptr(), + args.as_ptr() as *mut *mut c_void, + ) + }; + + let struct_val = struct_type.read( + alloc.ptr.as_ptr(), + &return_type_name.as_str(), + structs_table, + arena, + ); + + drop(alloc); + + struct_val + } + + fn call( + &self, + args: &[Arg], + arena: &mut Arena, + structs_table: &HashMap, + ) -> Result { + let call_fn: unsafe fn(&Self, &[Arg], &mut Arena) -> Result = + match self.return_type { + FfiType::Void => FunctionImpl::call_void, + FfiType::U8 => FunctionImpl::call_int::, + FfiType::I8 | FfiType::Bool => FunctionImpl::call_int::, + FfiType::U16 => FunctionImpl::call_int::, + FfiType::I16 => FunctionImpl::call_int::, + FfiType::U32 => FunctionImpl::call_int::, + FfiType::I32 => FunctionImpl::call_int::, + FfiType::U64 => FunctionImpl::call_int::, + FfiType::I64 => FunctionImpl::call_int::, + FfiType::F32 => FunctionImpl::call_float::, + FfiType::F64 => FunctionImpl::call_float::, + FfiType::Ptr => FunctionImpl::call_ptr, + FfiType::CStr => FunctionImpl::call_cstr, + FfiType::Struct(name) => { + return unsafe { self.call_struct(name, args, arena, structs_table) } + } + }; + unsafe { call_fn(self, args, arena) } + } } #[derive(Debug, Default)] @@ -53,26 +167,375 @@ pub struct ForeignFunctionTable { structs: HashMap, } -#[derive(Clone)] +#[derive(Clone, Debug)] struct StructImpl { - ffi_type: ffi_type, - fields: Vec<*mut ffi_type>, - atom_fields: Vec, + ffi_type: Type, + fields: Vec, } -impl std::fmt::Debug for StructImpl { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("StructImpl") - .field("ffi_type", &&"") - .field("fields", &self.fields) - .field("atom_fields", &self.atom_fields) - .finish() +impl StructImpl { + fn build( + &self, + structs_table: &HashMap, + struct_args: &mut [Value], + ) -> Result { + let args = ArgValue::build_args(struct_args, &self.fields, structs_table)?; + + let ffi_type = unsafe { *self.ffi_type.as_raw_ptr() }; + + let alloc = FfiStruct::new( + Layout::from_size_align(ffi_type.size, ffi_type.alignment.into()) + .map_err(|_| FfiError::LayoutError)?, + )?; + + let Ok(mut current_layout) = Layout::from_size_align(0, 1) else { + return Err(FfiError::LayoutError); + }; + + unsafe fn write_primitive( + ptr: NonNull, + layout: &mut Layout, + val: T, + ) -> Result<(), FfiError> { + let (new_layout, offset) = layout + .extend(Layout::new::()) + .map_err(|_| FfiError::LayoutError)?; + *layout = new_layout; + ptr.byte_offset(offset as isize).cast::().write(val); + Ok(()) + } + + for arg in args { + unsafe { + match arg { + ArgValue::U8(i) => write_primitive(alloc.ptr, &mut current_layout, i)?, + ArgValue::I8(i) => write_primitive(alloc.ptr, &mut current_layout, i)?, + ArgValue::U16(i) => write_primitive(alloc.ptr, &mut current_layout, i)?, + ArgValue::I16(i) => write_primitive(alloc.ptr, &mut current_layout, i)?, + ArgValue::U32(i) => write_primitive(alloc.ptr, &mut current_layout, i)?, + ArgValue::I32(i) => write_primitive(alloc.ptr, &mut current_layout, i)?, + ArgValue::U64(i) => write_primitive(alloc.ptr, &mut current_layout, i)?, + ArgValue::I64(i) => write_primitive(alloc.ptr, &mut current_layout, i)?, + ArgValue::F32(f) => write_primitive(alloc.ptr, &mut current_layout, f)?, + ArgValue::F64(f) => write_primitive(alloc.ptr, &mut current_layout, f)?, + ArgValue::Ptr(p, _) => write_primitive(alloc.ptr, &mut current_layout, p)?, + ArgValue::Struct(arg) => { + let Ok((new_layout, offset)) = current_layout.extend(arg.layout) else { + return Err(FfiError::LayoutError); + }; + + current_layout = new_layout; + + std::ptr::copy( + arg.ptr.as_ptr(), + alloc.ptr.byte_offset(offset as isize).as_ptr(), + arg.layout.size(), + ); + } + } + } + } + + if alloc.layout != current_layout.pad_to_align() { + // sanity check + return Err(FfiError::LayoutError); + } + + Ok(alloc) + } + + fn read( + &self, + ptr: *mut c_void, + struct_name: &str, + struct_table: &HashMap, + arena: &mut Arena, + ) -> Result { + unsafe { + let mut returns = Vec::new(); + + unsafe fn read_primitive( + ptr: *mut c_void, + layout: &mut Layout, + ) -> Result { + let (new_layout, offset) = layout + .extend(Layout::new::()) + .map_err(|_| FfiError::LayoutError)?; + *layout = new_layout; + let n = std::ptr::read::(ptr.byte_offset(offset as isize).cast()); + Ok(n) + } + + unsafe fn read_int( + ptr: *mut c_void, + layout: &mut Layout, + arena: &mut Arena, + ) -> Result + where + T: Copy + TryInto + MightNotFitInFixnum, + Integer: From, + { + let n = read_primitive::(ptr, layout)?; + Ok(Value::Number(fixnum!(Number, n, arena))) + } + + unsafe fn read_float( + ptr: *mut c_void, + layout: &mut Layout, + ) -> Result + where + T: Into, + { + let n = read_primitive::(ptr, layout)?; + Ok(Value::Number(Number::Float(OrderedFloat(n.into())))) + } + + let mut layout = Layout::from_size_align(0, 1).map_err(|_| FfiError::LayoutError)?; + + for field_type in &self.fields { + let val = match field_type { + FfiType::U8 => read_int::(ptr, &mut layout, arena), + FfiType::I8 | FfiType::Bool => read_int::(ptr, &mut layout, arena), + FfiType::U16 => read_int::(ptr, &mut layout, arena), + FfiType::I16 => read_int::(ptr, &mut layout, arena), + FfiType::U32 => read_int::(ptr, &mut layout, arena), + FfiType::I32 => read_int::(ptr, &mut layout, arena), + FfiType::U64 => read_int::(ptr, &mut layout, arena), + FfiType::I64 => read_int::(ptr, &mut layout, arena), + FfiType::Ptr => { + let ptr = read_primitive::<*mut c_void>(ptr, &mut layout)?; + Ok(Value::Number(fixnum!(Number, ptr as isize, arena))) + } + FfiType::CStr => { + let ptr = read_primitive::<*mut c_void>(ptr, &mut layout)?; + Ok(Value::CString(CStr::from_ptr(ptr.cast()).to_owned())) + } + FfiType::F32 => read_float::(ptr, &mut layout), + FfiType::F64 => read_float::(ptr, &mut layout), + FfiType::Struct(substruct) => { + let Some(substruct_type) = struct_table.get(&*substruct.as_str()) else { + return Err(FfiError::StructNotFound); + }; + + let ffi_type = *substruct_type.ffi_type.as_raw_ptr(); + let field_layout = + Layout::from_size_align(ffi_type.size, ffi_type.alignment as usize) + .map_err(|_| FfiError::LayoutError)?; + let (new_layout, offset) = layout + .extend(field_layout) + .map_err(|_| FfiError::LayoutError)?; + layout = new_layout; + let field_ptr = ptr.byte_offset(offset as isize); + let struct_val = substruct_type.read( + field_ptr, + &substruct.as_str(), + struct_table, + arena, + )?; + Ok(struct_val) + } + FfiType::Void => unreachable!("void is not a valid field type"), + }; + returns.push(val?); + } + Ok(Value::Struct(struct_name.to_string(), returns)) + } } } -struct PointerArgs { - pointers: Vec<*mut c_void>, - _memory: Vec>, +struct PointerArgs<'a, 'val> { + memory: Vec, + phantom: PhantomData<&'a mut ArgValue<'val>>, +} + +impl<'args, 'val> PointerArgs<'args, 'val> { + fn new(args: &'args [ArgValue<'val>]) -> Self { + let args = args + .iter() + .map(|arg| match arg { + ArgValue::U8(a) => libffi::middle::arg(a), + ArgValue::I8(a) => libffi::middle::arg(a), + ArgValue::U16(a) => libffi::middle::arg(a), + ArgValue::I16(a) => libffi::middle::arg(a), + ArgValue::U32(a) => libffi::middle::arg(a), + ArgValue::I32(a) => libffi::middle::arg(a), + ArgValue::U64(a) => libffi::middle::arg(a), + ArgValue::I64(a) => libffi::middle::arg(a), + ArgValue::F32(a) => libffi::middle::arg(a), + ArgValue::F64(a) => libffi::middle::arg(a), + ArgValue::Ptr(ptr, _) => Arg::new(ptr), + ArgValue::Struct(s) => unsafe { + std::mem::transmute::<*mut c_void, Arg>(s.ptr.as_ptr()) + }, + }) + .collect(); + + PointerArgs { + memory: args, + phantom: PhantomData, + } + } +} + +impl Deref for PointerArgs<'_, '_> { + type Target = [Arg]; + + fn deref(&self) -> &Self::Target { + &self.memory + } +} + +#[derive(Debug, Clone, Copy)] +enum FfiType { + Void, + Bool, + U8, + I8, + U16, + I16, + U32, + I32, + U64, + I64, + F32, + F64, + Ptr, + CStr, + Struct(Atom), +} + +impl FfiType { + fn from_atom(atom: &Atom) -> Self { + match atom { + atom!("sint64") | atom!("i64") => Self::I64, + atom!("sint32") | atom!("i32") => Self::I32, + atom!("sint16") | atom!("i16") => Self::I16, + atom!("sint8") | atom!("i8") => Self::I8, + atom!("uint64") | atom!("u64") => Self::U64, + atom!("uint32") | atom!("u32") => Self::U32, + atom!("uint16") | atom!("u16") => Self::U16, + atom!("uint8") | atom!("u8") => Self::U8, + atom!("bool") => Self::Bool, + atom!("void") => Self::Void, + atom!("cstr") => Self::CStr, + atom!("ptr") => Self::Ptr, + atom!("f32") => Self::F32, + atom!("f64") => Self::F64, + struct_name => Self::Struct(*struct_name), + } + } + + fn to_type(self, structs_table: &HashMap) -> Result { + Ok(match self { + Self::I64 => libffi::middle::Type::i64(), + Self::I32 => libffi::middle::Type::i32(), + Self::I16 => libffi::middle::Type::i16(), + Self::I8 => libffi::middle::Type::i8(), + Self::U64 => libffi::middle::Type::u64(), + Self::U32 => libffi::middle::Type::u32(), + Self::U16 => libffi::middle::Type::u16(), + Self::U8 => libffi::middle::Type::u8(), + Self::Bool => libffi::middle::Type::i8(), + Self::Void => libffi::middle::Type::void(), + Self::CStr => libffi::middle::Type::pointer(), + Self::Ptr => libffi::middle::Type::pointer(), + Self::F32 => libffi::middle::Type::f32(), + Self::F64 => libffi::middle::Type::f64(), + Self::Struct(struct_name) => structs_table + .get(&*struct_name.as_str()) + .ok_or(FfiError::StructNotFound)? + .ffi_type + .clone(), + }) + } +} + +enum ArgValue<'a> { + U8(u8), + I8(i8), + U16(u16), + I16(i16), + U32(u32), + I32(i32), + U64(u64), + I64(i64), + F32(f32), + F64(f64), + Ptr(*mut c_void, PhantomData<&'a CString>), + Struct(FfiStruct), +} + +impl<'val> ArgValue<'val> { + fn new( + val: &'val mut Value, + arg_type: &FfiType, + structs_table: &HashMap, + ) -> Result { + match arg_type { + FfiType::U8 => Ok(Self::U8(val.as_int()?)), + FfiType::I8 | FfiType::Bool => Ok(Self::I8(val.as_int()?)), + FfiType::U16 => Ok(Self::U16(val.as_int()?)), + FfiType::I16 => Ok(Self::I16(val.as_int()?)), + FfiType::U32 => Ok(Self::U32(val.as_int()?)), + FfiType::I32 => Ok(Self::I32(val.as_int()?)), + FfiType::U64 => Ok(Self::U64(val.as_int()?)), + FfiType::I64 => Ok(Self::I64(val.as_int()?)), + FfiType::F32 => Ok(Self::F32(val.as_float()? as f32)), + FfiType::F64 => Ok(Self::F64(val.as_float()?)), + FfiType::Ptr => Ok(Self::Ptr(val.as_ptr()?, PhantomData)), + FfiType::CStr => Ok(Self::Ptr(val.as_ptr()?, PhantomData)), + FfiType::Struct(atom) => { + let (name, args) = val.as_struct()?; + + if &*atom.as_str() != name { + return Err(FfiError::ValueCast); + } + + let Some(struct_type) = structs_table.get(name) else { + return Err(FfiError::StructNotFound); + }; + + Ok(Self::Struct(struct_type.build(structs_table, args)?)) + } + FfiType::Void => Err(FfiError::InvalidArgumentType), + } + } + + fn build_args( + args: &'val mut [Value], + types: &[FfiType], + structs_table: &HashMap, + ) -> Result, FfiError> { + if types.len() != args.len() { + return Err(FfiError::ArgCountMismatch); + } + + args.iter_mut() + .zip(types) + .map(|(arg, arg_type)| ArgValue::new(arg, arg_type, structs_table)) + .collect::, _>>() + } +} + +struct FfiStruct { + ptr: NonNull, + layout: Layout, +} + +impl FfiStruct { + fn new(layout: Layout) -> Result { + if let Some(ptr) = NonNull::new(unsafe { alloc::alloc(layout) as *mut c_void }) { + Ok(FfiStruct { ptr, layout }) + } else { + Err(FfiError::AllocationFailed) + } + } +} + +impl Drop for FfiStruct { + fn drop(&mut self) { + unsafe { alloc::dealloc(self.ptr.as_ptr().cast(), self.layout) }; + } } impl ForeignFunctionTable { @@ -80,45 +543,35 @@ impl ForeignFunctionTable { self.table.extend(other.table); } - pub fn define_struct(&mut self, name: &str, atom_fields: Vec) { - let mut fields: Vec<_> = atom_fields.iter().map(|x| self.map_type_ffi(x)).collect(); - fields.push(std::ptr::null_mut::()); - let struct_type = ffi_type { - type_: STRUCT, - elements: fields.as_mut_ptr(), - ..Default::default() + pub fn define_struct(&mut self, name: &str, atom_fields: Vec) -> Result<(), FfiError> { + let fields: Vec<_> = atom_fields.iter().map(FfiType::from_atom).collect(); + let struct_type = libffi::middle::Type::structure( + fields + .iter() + .map(|field| field.to_type(&self.structs)) + .collect::, _>>()?, + ); + + unsafe { + // ensure that size and alignment of struct_type are set properly + use libffi::low::{ffi_abi_FFI_DEFAULT_ABI, prep_cif}; + prep_cif( + &mut Default::default(), + ffi_abi_FFI_DEFAULT_ABI, + 1, + struct_type.as_raw_ptr(), + [struct_type.as_raw_ptr()].as_mut_ptr(), + )?; }; + self.structs.insert( name.to_string(), StructImpl { ffi_type: struct_type, fields, - atom_fields, }, ); - } - - fn map_type_ffi(&mut self, source: &Atom) -> *mut ffi_type { - match source { - atom!("sint64") => addr_of_mut!(types::sint64), - atom!("sint32") => addr_of_mut!(types::sint32), - atom!("sint16") => addr_of_mut!(types::sint16), - atom!("sint8") => addr_of_mut!(types::sint8), - atom!("uint64") => addr_of_mut!(types::uint64), - atom!("uint32") => addr_of_mut!(types::uint32), - atom!("uint16") => addr_of_mut!(types::uint16), - atom!("uint8") => addr_of_mut!(types::uint8), - atom!("bool") => addr_of_mut!(types::sint8), - atom!("void") => addr_of_mut!(types::void), - atom!("cstr") => addr_of_mut!(types::pointer), - atom!("ptr") => addr_of_mut!(types::pointer), - atom!("f32") => addr_of_mut!(types::float), - atom!("f64") => addr_of_mut!(types::double), - struct_name => match self.structs.get_mut(&*struct_name.as_str()) { - Some(ref mut struct_type) => &mut struct_type.ffi_type, - None => unreachable!(), - }, - } + Ok(()) } pub(crate) fn load_library( @@ -127,408 +580,135 @@ impl ForeignFunctionTable { functions: &Vec, ) -> Result<(), Box> { let mut ff_table: ForeignFunctionTable = Default::default(); - unsafe { - let library = Library::new(library_name)?; - for function in functions { - let symbol_name: CString = CString::new(function.name.clone())?; - let code_ptr: Symbol<*mut c_void> = - library.get(&symbol_name.into_bytes_with_nul())?; - let mut args: Vec<_> = function.args.iter().map(|x| self.map_type_ffi(x)).collect(); - let mut cif: ffi_cif = Default::default(); - prep_cif( - &mut cif, - ffi_abi_FFI_DEFAULT_ABI, - args.len(), - self.map_type_ffi(&function.return_value), - args.as_mut_ptr(), - ) - .unwrap(); + let library = unsafe { Library::new(library_name) }?; + for function in functions { + let symbol_name: CString = CString::new(function.name.clone())?; + let code_ptr: Symbol<*mut c_void> = + unsafe { library.get(symbol_name.as_bytes_with_nul()) }?; + let args: Vec<_> = function.args.iter().map(FfiType::from_atom).collect(); + let return_type = FfiType::from_atom(&function.return_value); - let return_struct_name = if (*self.map_type_ffi(&function.return_value)).type_ - as u32 - == libffi::raw::FFI_TYPE_STRUCT - { - Some(function.return_value.as_str().to_string()) - } else { - None - }; + let cif = libffi::middle::Cif::new( + args.iter() + .map(|arg| arg.to_type(&self.structs)) + .collect::, _>>()?, + return_type.to_type(&self.structs)?, + ); - ff_table.table.insert( - function.name.clone(), - FunctionImpl { - cif, - args, - code_ptr: CodePtr(code_ptr.into_raw().as_raw_ptr()), - return_struct_name, - }, - ); - } - std::mem::forget(library); + ff_table.table.insert( + function.name.clone(), + FunctionImpl { + cif, + args, + code_ptr: CodePtr(unsafe { code_ptr.into_raw() }.as_raw_ptr()), + return_type, + }, + ); } + std::mem::forget(library); self.merge(ff_table); Ok(()) } - fn build_pointer_args( - args: &mut [Value], - type_args: &[*mut ffi_type], - structs_table: &mut HashMap, - ) -> Result { - let mut pointers = Vec::with_capacity(args.len()); - let mut _memory = Vec::new(); - for i in 0..args.len() { - let field_type = type_args[i]; - unsafe { - macro_rules! push_int { - ($type:ty) => {{ - let n: $type = <$type>::try_from(args[i].as_int()?) - .map_err(|_| FFIError::ValueDontFit)?; - let mut box_value = Box::new(n) as Box; - pointers.push(&mut *box_value as *mut _ as *mut c_void); - _memory.push(box_value); - }}; - } + pub fn exec( + &mut self, + fn_name: &str, + mut args: Vec, + arena: &mut Arena, + ) -> Result { + let fn_impl = self.table.get(fn_name).ok_or(FfiError::FunctionNotFound)?; - match (*field_type).type_ as u32 { - libffi::raw::FFI_TYPE_UINT8 => push_int!(u8), - libffi::raw::FFI_TYPE_SINT8 => push_int!(i8), - libffi::raw::FFI_TYPE_UINT16 => push_int!(u16), - libffi::raw::FFI_TYPE_SINT16 => push_int!(i16), - libffi::raw::FFI_TYPE_UINT32 => push_int!(u32), - libffi::raw::FFI_TYPE_SINT32 => push_int!(i32), - libffi::raw::FFI_TYPE_UINT64 => push_int!(u64), - libffi::raw::FFI_TYPE_SINT64 => push_int!(i64), - libffi::raw::FFI_TYPE_FLOAT => { - let n: f32 = args[i].as_float()? as f32; - let mut box_value = Box::new(n) as Box; - pointers.push(&mut *box_value as *mut _ as *mut c_void); - _memory.push(box_value); - } - libffi::raw::FFI_TYPE_DOUBLE => { - let n: f64 = args[i].as_float()?; - let mut box_value = Box::new(n) as Box; - pointers.push(&mut *box_value as *mut _ as *mut c_void); - _memory.push(box_value); - } - libffi::raw::FFI_TYPE_POINTER => { - let ptr: *mut c_void = args[i].as_ptr()?; - pointers.push(ptr); - } - libffi::raw::FFI_TYPE_STRUCT => { - let (mut ptr, _size, _align) = - Self::build_struct(&mut args[i], structs_table)?; - pointers.push(&mut *ptr as *mut _ as *mut c_void); - _memory.push(ptr); - } - _ => return Err(FFIError::InvalidFFIType), - } - } - } - Ok(PointerArgs { pointers, _memory }) - } + let args = ArgValue::build_args(&mut args, &fn_impl.args, &self.structs)?; - fn build_struct( - arg: &mut Value, - structs_table: &mut HashMap, - ) -> Result<(Box, usize, usize), FFIError> { - match arg { - Value::Struct(ref name, ref mut struct_args) => { - if let Some(ref mut struct_type) = structs_table.clone().get_mut(name) { - let layout = Layout::from_size_align( - struct_type.ffi_type.size, - struct_type.ffi_type.alignment.into(), - ) - .unwrap(); - let align = struct_type.ffi_type.alignment as usize; - let size = struct_type.ffi_type.size; - let ptr = unsafe { alloc::alloc(layout) as *mut c_void }; + let args = PointerArgs::new(&args); - if ptr.is_null() { - panic!("allocation failed") - } - - let mut field_ptr = ptr; - - #[allow(clippy::needless_range_loop)] - for i in 0..(struct_type.fields.len() - 1) { - macro_rules! try_write_int { - ($type:ty) => {{ - field_ptr = field_ptr - .add(field_ptr.align_offset(std::mem::align_of::<$type>())); - let n: $type = <$type>::try_from(struct_args[i].as_int()?) - .map_err(|_| FFIError::ValueDontFit)?; - std::ptr::write(field_ptr as *mut $type, n); - field_ptr = field_ptr.add(std::mem::size_of::<$type>()); - }}; - } - - macro_rules! write { - ($type:ty, $value:expr) => {{ - let data: $type = $value; - std::ptr::write(field_ptr as *mut $type, data); - field_ptr = field_ptr.add(align); - }}; - } - - let field = struct_type.fields[i]; - unsafe { - match (*field).type_ as u32 { - libffi::raw::FFI_TYPE_UINT8 => try_write_int!(u8), - libffi::raw::FFI_TYPE_SINT8 => try_write_int!(i8), - libffi::raw::FFI_TYPE_UINT16 => try_write_int!(u16), - libffi::raw::FFI_TYPE_SINT16 => try_write_int!(i16), - libffi::raw::FFI_TYPE_UINT32 => try_write_int!(u32), - libffi::raw::FFI_TYPE_SINT32 => try_write_int!(i32), - libffi::raw::FFI_TYPE_UINT64 => try_write_int!(u64), - libffi::raw::FFI_TYPE_SINT64 => try_write_int!(i64), - libffi::raw::FFI_TYPE_POINTER => { - write!(*mut c_void, struct_args[i].as_ptr()?) - } - libffi::raw::FFI_TYPE_FLOAT => { - write!(f32, struct_args[i].as_float()? as f32) - } - libffi::raw::FFI_TYPE_DOUBLE => { - write!(f64, struct_args[i].as_float()?) - } - libffi::raw::FFI_TYPE_STRUCT => { - let (struct_ptr, struct_size, struct_align) = - Self::build_struct(&mut struct_args[i], structs_table)?; - field_ptr = field_ptr.add(field_ptr.align_offset(struct_align)); - - std::ptr::copy( - &*struct_ptr as *const _ as *const c_void, - field_ptr, - struct_size, - ); - field_ptr = field_ptr.add(struct_size); - } - _ => { - unreachable!() - } - } - } - } - - #[allow(clippy::from_raw_with_void_ptr)] - Ok((unsafe { Box::from_raw(ptr) }, size, align)) - } else { - Err(FFIError::InvalidStructName) - } - } - _ => Err(FFIError::ValueCast), - } - } - - pub fn exec(&mut self, name: &str, mut args: Vec) -> Result { - let function_impl = self.table.get_mut(name).ok_or(FFIError::FunctionNotFound)?; - let mut pointer_args = - Self::build_pointer_args(&mut args, &function_impl.args, &mut self.structs)?; - - unsafe { - macro_rules! call_and_return { - ($type:ty) => {{ - let mut n: Box = Box::new(0); - libffi::raw::ffi_call( - &mut function_impl.cif, - Some(*function_impl.code_ptr.as_safe_fun()), - &mut *n as *mut _ as *mut c_void, - pointer_args.pointers.as_mut_ptr() as *mut *mut c_void, - ); - Ok(Value::Int(i64::from(*n))) - }}; - } - - match (*function_impl.cif.rtype).type_ as u32 { - libffi::raw::FFI_TYPE_VOID => call_and_return!(i32), - libffi::raw::FFI_TYPE_UINT8 => call_and_return!(u8), - libffi::raw::FFI_TYPE_SINT8 => call_and_return!(i8), - libffi::raw::FFI_TYPE_UINT16 => call_and_return!(u16), - libffi::raw::FFI_TYPE_SINT16 => call_and_return!(i16), - libffi::raw::FFI_TYPE_UINT32 => call_and_return!(u32), - libffi::raw::FFI_TYPE_SINT32 => call_and_return!(i32), - libffi::raw::FFI_TYPE_UINT64 => { - let mut n: Box = Box::new(0); - libffi::raw::ffi_call( - &mut function_impl.cif, - Some(*function_impl.code_ptr.as_safe_fun()), - &mut *n as *mut _ as *mut c_void, - pointer_args.pointers.as_mut_ptr(), - ); - Ok(Value::Int( - i64::try_from(*n).map_err(|_| FFIError::ValueDontFit)?, - )) - } - libffi::raw::FFI_TYPE_SINT64 => call_and_return!(i64), - libffi::raw::FFI_TYPE_POINTER => call_and_return!(*mut c_void), - libffi::raw::FFI_TYPE_FLOAT => { - let mut n: Box = Box::new(0.0); - libffi::raw::ffi_call( - &mut function_impl.cif, - Some(*function_impl.code_ptr.as_safe_fun()), - &mut *n as *mut _ as *mut c_void, - pointer_args.pointers.as_mut_ptr(), - ); - Ok(Value::Float((*n).into())) - } - libffi::raw::FFI_TYPE_DOUBLE => { - let mut n: Box = Box::new(0.0); - libffi::raw::ffi_call( - &mut function_impl.cif, - Some(*function_impl.code_ptr.as_safe_fun()), - &mut *n as *mut _ as *mut c_void, - pointer_args.pointers.as_mut_ptr(), - ); - Ok(Value::Float(*n)) - } - libffi::raw::FFI_TYPE_STRUCT => { - let name = &function_impl - .return_struct_name - .clone() - .ok_or(FFIError::StructNotFound)?; - let struct_type = self.structs.get(name).ok_or(FFIError::StructNotFound)?; - let layout = Layout::from_size_align( - struct_type.ffi_type.size, - struct_type.ffi_type.alignment.into(), - ) - .unwrap(); - let ptr = alloc::alloc(layout) as *mut c_void; - - if ptr.is_null() { - panic!("allocation failed") - } - - libffi::raw::ffi_call( - &mut function_impl.cif, - Some(*function_impl.code_ptr.as_safe_fun()), - &mut *ptr as *mut _, - pointer_args.pointers.as_mut_ptr(), - ); - let struct_val = self.read_struct(ptr, name, struct_type); - #[allow(clippy::from_raw_with_void_ptr)] - drop(Box::from_raw(ptr)); - struct_val - } - _ => unreachable!(), - } - } - } - - fn read_struct( - &self, - ptr: *mut c_void, - name: &str, - struct_type: &StructImpl, - ) -> Result { - unsafe { - let mut returns = Vec::new(); - let mut field_ptr = ptr; - - for i in 0..(struct_type.fields.len() - 1) { - let field = struct_type.fields[i]; - - macro_rules! read_and_push_int { - ($type:ty) => {{ - field_ptr = - field_ptr.add(field_ptr.align_offset(std::mem::align_of::<$type>())); - let n = std::ptr::read(field_ptr as *mut $type); - returns.push(Value::Int(i64::from(n))); - field_ptr = field_ptr.add(std::mem::size_of::<$type>()); - }}; - } - - match (*field).type_ as u32 { - libffi::raw::FFI_TYPE_UINT8 => read_and_push_int!(u8), - libffi::raw::FFI_TYPE_SINT8 => read_and_push_int!(i8), - libffi::raw::FFI_TYPE_UINT16 => read_and_push_int!(u16), - libffi::raw::FFI_TYPE_SINT16 => read_and_push_int!(i16), - libffi::raw::FFI_TYPE_UINT32 => read_and_push_int!(u32), - libffi::raw::FFI_TYPE_SINT32 => read_and_push_int!(i32), - libffi::raw::FFI_TYPE_UINT64 => { - field_ptr = - field_ptr.add(field_ptr.align_offset(std::mem::align_of::())); - let n = std::ptr::read(field_ptr as *mut u64); - returns.push(Value::Int( - i64::try_from(n).map_err(|_| FFIError::ValueDontFit)?, - )); - field_ptr = field_ptr.add(std::mem::size_of::()); - } - libffi::raw::FFI_TYPE_SINT64 => read_and_push_int!(i64), - libffi::raw::FFI_TYPE_POINTER => read_and_push_int!(i64), - libffi::raw::FFI_TYPE_FLOAT => { - field_ptr = - field_ptr.add(field_ptr.align_offset(std::mem::align_of::())); - let n: f32 = std::ptr::read(field_ptr as *mut f32); - returns.push(Value::Float(n.into())); - field_ptr = field_ptr.add(std::mem::size_of::()); - } - libffi::raw::FFI_TYPE_DOUBLE => { - field_ptr = - field_ptr.add(field_ptr.align_offset(std::mem::align_of::())); - let n: f64 = std::ptr::read(field_ptr as *mut f64); - returns.push(Value::Float(n)); - field_ptr = field_ptr.add(std::mem::size_of::()); - } - libffi::raw::FFI_TYPE_STRUCT => { - let substruct = struct_type.atom_fields[i].as_str(); - let struct_type = self - .structs - .get(&*substruct) - .ok_or(FFIError::StructNotFound)?; - field_ptr = field_ptr - .add(field_ptr.align_offset(struct_type.ffi_type.alignment as usize)); - let struct_val = self.read_struct(field_ptr, &substruct, struct_type); - returns.push(struct_val?); - field_ptr = field_ptr.add(struct_type.ffi_type.size); - } - _ => { - unreachable!() - } - } - } - Ok(Value::Struct(name.into(), returns)) - } + fn_impl.call(&args, arena, &self.structs) } } #[derive(Clone, Debug)] pub enum Value { - Int(i64), - Float(f64), + Number(Number), CString(CString), Struct(String, Vec), } impl Value { - fn as_int(&self) -> Result { + fn as_int(&self) -> Result + where + Integer: TryInto, + i64: TryInto, + { match self { - Value::Int(n) => Ok(*n), - _ => Err(FFIError::ValueCast), + Value::Number(Number::Integer(ibig_ptr)) => { + let ibig: &Integer = ibig_ptr; + ibig.clone() + .try_into() + .map_err(|_| FfiError::ValueOutOfRange) + } + Value::Number(Number::Fixnum(fixnum)) => fixnum + .get_num() + .try_into() + .map_err(|_| FfiError::ValueOutOfRange), + _ => Err(FfiError::ValueCast), } } - fn as_float(&self) -> Result { + fn as_float(&self) -> Result { match self { - Value::Float(n) => Ok(*n), - Value::Int(n) => Ok(*n as f64), - _ => Err(FFIError::ValueCast), + &Value::Number(Number::Float(OrderedFloat(f))) => Ok(f), + _ => Err(FfiError::ValueCast), } } - fn as_ptr(&mut self) -> Result<*mut c_void, FFIError> { + fn as_ptr(&mut self) -> Result<*mut c_void, FfiError> { match self { - Value::CString(ref mut cstr) => Ok(&mut *cstr as *mut _ as *mut c_void), - Value::Int(n) => Ok(std::ptr::with_exposed_provenance_mut(*n as usize)), - _ => Err(FFIError::ValueCast), + Value::CString(ref mut cstr) => Ok(cstr.as_ptr().cast_mut().cast()), + Value::Number(Number::Fixnum(fixnum)) => Ok(std::ptr::with_exposed_provenance_mut( + fixnum.get_num() as usize, + )), + _ => Err(FfiError::ValueCast), + } + } + + fn as_struct(&mut self) -> Result<(&str, &mut [Self]), FfiError> { + match self { + Value::Struct(name, values) => Ok((name, values)), + _ => Err(FfiError::ValueCast), } } } #[derive(Debug)] -pub enum FFIError { +pub enum FfiError { ValueCast, - ValueDontFit, - InvalidFFIType, - InvalidStructName, + ValueOutOfRange, + InvalidArgumentType, + InvalidArgument, + InvalidFfiType, + InvalidStruct, FunctionNotFound, StructNotFound, + ArgCountMismatch, + AllocationFailed, + // LayoutError should never occour + LayoutError, + UnsupportedAbi, +} + +impl std::fmt::Display for FfiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} + +impl Error for FfiError {} + +impl From for FfiError { + fn from(value: libffi::low::Error) -> Self { + match value { + libffi::low::Error::Typedef => FfiError::InvalidFfiType, + libffi::low::Error::Abi => FfiError::UnsupportedAbi, + } + } } diff --git a/src/lib/ffi.pl b/src/lib/ffi.pl index cce60ff6..165d5d04 100644 --- a/src/lib/ffi.pl +++ b/src/lib/ffi.pl @@ -11,8 +11,8 @@ The main predicate is `use_foreign_module/2`. It takes a library name (which dep operating system could be a `.so`, `.dylib` or `.dll` file). and a list of functions. Each function is defined by its name, a list of the type of the arguments, and the return argument. -Types available are: `sint8`, `uint8`, `sint16`, `uint16`, `sint32`, `uint32`, `sint64`, -`uint64`, `f32`, `f64`, `cstr`, `void`, `bool`, `ptr` and custom structs, which can be defined +Types available are: `sint8`/`i8`, `uint8`/`u8`, `sint16`/`i16`, `uint16`/`u16`, `sint32`/`i32`, `uint32`/`u32`, `sint64`/`i64`, +`uint64`/`u64`, `f32`, `f64`, `cstr`, `void`, `bool`, `ptr` and custom structs, which can be defined with `foreign_struct/2`. After that, each function on the lists maps to a predicate created in the ffi module which @@ -27,6 +27,12 @@ ffi:FUNCTION_NAME(+InputArg1, ..., +InputArgN, -ReturnArg). % for all return typ ffi:FUNCTION_NAME(+InputArg1, ..., +InputArgN). % for void and bool ``` +## Notes regarding cstr + +- When using `cstr` as an argument type the string will be deallocated once the function returns. +- When using `cstr` as a return type the string will be copied and won't be deallocated. + + ## Example For example, let's see how to define a function from the [raylib](https://www.raylib.com/) library. diff --git a/src/machine/config.rs b/src/machine/config.rs index b9d3f703..d4839d69 100644 --- a/src/machine/config.rs +++ b/src/machine/config.rs @@ -78,7 +78,7 @@ impl OutputStreamConfig { #[derive(Debug)] enum InputStreamConfigInner { - String(String), + String(Cow<'static, str>), Stdin, Channel(Receiver>), } @@ -97,7 +97,7 @@ pub struct InputStreamConfig { impl InputStreamConfig { /// Gets input from string. - pub fn string(s: impl Into) -> Self { + pub fn string(s: impl Into>) -> Self { Self { inner: InputStreamConfigInner::String(s.into()), } @@ -123,7 +123,10 @@ impl InputStreamConfig { fn into_stream(self, arena: &mut Arena, add_history: bool) -> Stream { match self.inner { - InputStreamConfigInner::String(s) => Stream::from_owned_string(s, arena), + InputStreamConfigInner::String(s) => match s { + Cow::Owned(s) => Stream::from_owned_string(s, arena), + Cow::Borrowed(s) => Stream::from_static_string(s, arena), + }, InputStreamConfigInner::Stdin => Stream::stdin(arena, add_history), InputStreamConfigInner::Channel(channel) => Stream::input_channel(channel, arena), } @@ -156,7 +159,7 @@ impl StreamConfig { /// Binds the output and error streams to memory buffers and has an empty input. pub fn in_memory() -> Self { StreamConfig { - user_input: InputStreamConfig::string(""), + user_input: InputStreamConfig::string(String::new()), user_output: OutputStreamConfig::memory(), user_error: OutputStreamConfig::memory(), } diff --git a/src/machine/machine_errors.rs b/src/machine/machine_errors.rs index c6836709..f9b9318d 100644 --- a/src/machine/machine_errors.rs +++ b/src/machine/machine_errors.rs @@ -3,7 +3,7 @@ use crate::atom_table::*; use crate::parser::ast::*; #[cfg(feature = "ffi")] -use crate::ffi::FFIError; +use crate::ffi::FfiError; use crate::forms::*; use crate::functor_macro::*; use crate::machine::heap::*; @@ -613,14 +613,20 @@ impl MachineState { } #[cfg(feature = "ffi")] - pub(super) fn ffi_error(&self, err: FFIError) -> MachineError { + pub(super) fn ffi_error(&self, err: FfiError) -> MachineError { let error_atom = match err { - FFIError::ValueCast => atom!("value_cast"), - FFIError::ValueDontFit => atom!("value_dont_fit"), - FFIError::InvalidFFIType => atom!("invalid_ffi_type"), - FFIError::InvalidStructName => atom!("invalid_struct_name"), - FFIError::FunctionNotFound => atom!("function_not_found"), - FFIError::StructNotFound => atom!("struct_not_found"), + FfiError::ValueCast => atom!("value_cast"), + FfiError::ValueOutOfRange => atom!("value_out_of_range"), + FfiError::InvalidFfiType => atom!("invalid_ffi_type"), + FfiError::InvalidArgumentType => atom!("invalid_argument_type"), + FfiError::InvalidArgument => atom!("invalid_argument"), + FfiError::InvalidStruct => atom!("invalid_struct"), + FfiError::FunctionNotFound => atom!("function_not_found"), + FfiError::StructNotFound => atom!("struct_not_found"), + FfiError::ArgCountMismatch => atom!("mismatched_argument_count"), + FfiError::AllocationFailed => atom!("allocation_failed"), + FfiError::LayoutError => atom!("layout_error"), + FfiError::UnsupportedAbi => atom!("unsupported_abi"), }; let stub = functor!(atom!("ffi_error"), [atom_as_cell(error_atom)]); diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index 3f367a39..08bd4f4e 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -5004,65 +5004,80 @@ impl Machine { #[cfg(feature = "ffi")] #[inline(always)] pub(crate) fn foreign_call(&mut self) -> CallResult { + fn stub_gen() -> Vec { + functor_stub(atom!("foreign_call"), 3) + } + + fn map_arg( + machine_st: &mut MachineState, + source: HeapCellValue, + ) -> Result { + if let Ok(number) = Number::try_from((source, &machine_st.arena.f64_tbl)) { + Ok(Value::Number(number)) + } else if let Some(string) = machine_st.value_to_str_like(source) { + Ok(Value::CString(CString::new(&*string.as_str()).unwrap())) + } else if let Ok(args) = machine_st.try_from_list(source, stub_gen) { + // structs are lists represented as lists + // the head is a string with the struct type name + // the tail are the struct field values + + let mut iter = args.into_iter(); + if let Some(struct_name) = machine_st.value_to_str_like(iter.next().unwrap()) { + Ok(Value::Struct( + struct_name.as_str().to_string(), + iter.map(|x| map_arg(machine_st, x)) + .collect::>()?, + )) + } else { + // empty list is an invalid struct repr + Err(FfiError::InvalidStruct) + } + } else { + Err(FfiError::InvalidArgument) + } + } + let function_name = self.deref_register(1); let args_reg = self.deref_register(2); let return_value = self.deref_register(3); if let Some(function_name) = self.machine_st.value_to_str_like(function_name) { - let stub_gen = || functor_stub(atom!("foreign_call"), 3); - fn map_arg(machine_st: &mut MachineState, source: HeapCellValue) -> crate::ffi::Value { - match Number::try_from((source, &machine_st.arena.f64_tbl)) { - Ok(Number::Fixnum(n)) => Value::Int(n.get_num()), - Ok(Number::Float(n)) => Value::Float(n.into_inner()), - _ => { - let stub_gen = || functor_stub(atom!("foreign_call"), 3); - if let Some(string) = machine_st.value_to_str_like(source) { - Value::CString(CString::new(&*string.as_str()).unwrap()) - } else { - match machine_st.try_from_list(source, stub_gen) { - Ok(args) => { - let mut iter = args.into_iter(); - if let Some(struct_name) = - machine_st.value_to_str_like(iter.next().unwrap()) - { - Value::Struct( - struct_name.as_str().to_string(), - iter.map(|x| map_arg(machine_st, x)).collect(), - ) - } else { - unreachable!() - } - } - _ => { - unreachable!() - } - } - } - } - } - } - match self.machine_st.try_from_list(args_reg, stub_gen) { Ok(args) => { - let args: Vec<_> = args + let args = match args .into_iter() .map(|x| map_arg(&mut self.machine_st, x)) - .collect(); - match self - .foreign_function_table - .exec(&function_name.as_str(), args) + .collect::, _>>() { + Ok(args) => args, + Err(err) => { + let err = self.machine_st.ffi_error(err); + return Err(self.machine_st.error_form(err, stub_gen())); + } + }; + + match self.foreign_function_table.exec( + &function_name.as_str(), + args, + &mut self.machine_st.arena, + ) { Ok(result) => { match result { - Value::Int(n) => self.machine_st.unify_fixnum( - Fixnum::build_with_checked(n).unwrap_or_else(|_| { - todo!("handle integer values that don't fit in fixnum") - }), - return_value, - ), - Value::Float(n) => { - let n = float_alloc!(n, self.machine_st.arena); - self.machine_st.unify_f64(n, return_value) - } + Value::Number(n) => match n { + Number::Float(OrderedFloat(n)) => { + let n = float_alloc!(n, self.machine_st.arena); + self.machine_st.unify_f64(n, return_value) + } + Number::Integer(typed_arena_ptr) => { + self.machine_st.unify_big_int(typed_arena_ptr, return_value) + } + Number::Rational(typed_arena_ptr) => { + self.machine_st + .unify_rational(typed_arena_ptr, return_value); + } + Number::Fixnum(fixnum) => { + self.machine_st.unify_fixnum(fixnum, return_value) + } + }, Value::Struct(name, args) => { let struct_value = resource_error_call_result!( self.machine_st, @@ -5083,10 +5098,8 @@ impl Machine { return Ok(()); } Err(e) => { - let stub = functor_stub(atom!("current_input"), 1); let err = self.machine_st.ffi_error(e); - - return Err(self.machine_st.error_form(err, stub)); + return Err(self.machine_st.error_form(err, stub_gen())); } } } @@ -5102,34 +5115,26 @@ impl Machine { fn build_struct(&mut self, name: &str, mut args: Vec) -> Result { args.insert(0, Value::CString(CString::new(name).unwrap())); - let mut expanded_args = Vec::with_capacity(args.len()); + let cells: Vec<_> = args + .into_iter() + .map(|val| { + Ok(match val { + Value::Number(n) => match n { + Number::Float(OrderedFloat(f)) => { + HeapCellValue::from(float_alloc!(f, self.machine_st.arena)) + } + _ => integer_as_cell!(n), + }, + Value::CString(cstr) => atom_as_cell!(AtomTable::build_with( + &self.machine_st.atom_tbl, + &cstr.into_string().unwrap() + )), + Value::Struct(name, struct_args) => self.build_struct(&name, struct_args)?, + }) + }) + .collect::>()?; - for val in args { - expanded_args.push(match val { - Value::Int(n) => { - if let Ok(fixnum) = Fixnum::build_with_checked(n) { - fixnum_as_cell!(fixnum) - } else { - integer_as_cell!(Number::Integer(arena_alloc!( - Integer::from(n), - &mut self.machine_st.arena - ))) - } - } - Value::Float(n) => HeapCellValue::from(float_alloc!(n, self.machine_st.arena)), - Value::CString(cstr) => atom_as_cell!(AtomTable::build_with( - &self.machine_st.atom_tbl, - &cstr.into_string().unwrap() - )), - Value::Struct(name, struct_args) => self.build_struct(&name, struct_args)?, - }); - } - - sized_iter_to_heap_list( - &mut self.machine_st.heap, - expanded_args.len(), - expanded_args.into_iter(), - ) + sized_iter_to_heap_list(&mut self.machine_st.heap, cells.len(), cells.into_iter()) } #[cfg(feature = "ffi")] @@ -5150,7 +5155,11 @@ impl Machine { Err(e) => return Err(e), }; self.foreign_function_table - .define_struct(&struct_name.as_str(), fields); + .define_struct(&struct_name.as_str(), fields) + .map_err(|err| { + let ffi_error = self.machine_st.ffi_error(err); + self.machine_st.error_form(ffi_error, stub_gen()) + })?; return Ok(()); } self.machine_st.fail = true; diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 18389225..cb48fca4 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -583,10 +583,13 @@ mod private { } } + impl MightNotFitInFixnumSeal for T {} impl MightNotFitInFixnumSeal for i64 {} + impl MightNotFitInFixnumSeal for u64 {} impl MightNotFitInFixnumSeal for &Integer {} impl MightNotFitInFixnumSeal for Integer {} impl MightNotFitInFixnumSeal for usize {} + impl MightNotFitInFixnumSeal for isize {} } #[allow(private_bounds)] diff --git a/tests-pl/ffi_cstr.pl b/tests-pl/ffi_cstr.pl new file mode 100644 index 00000000..4e6fb505 --- /dev/null +++ b/tests-pl/ffi_cstr.pl @@ -0,0 +1,21 @@ +:- use_module(library(os)). +:- use_module(library(ffi)). + +init :- + read(Body), + term_variables(Body, [LIB]), + Body, + use_foreign_module(LIB, [ + 'ffi_cstr_len'([cstr], u64), + 'ffi_example_cstr'([], cstr), + 'ffi_null_cstr'([], cstr) + ]). + +test :- + ffi:'ffi_cstr_len'("Scryer Prolog", Len), + ffi:'ffi_example_cstr'(Str), + ffi:'ffi_null_cstr'(Null), + ffi:'ffi_cstr_len'(0, MaxU64), + write((Len-Str-Null-MaxU64)). + +:- initialization((init,test)). diff --git a/tests-pl/ffi_f64_minus_zero.pl b/tests-pl/ffi_f64_minus_zero.pl new file mode 100644 index 00000000..6ca2b4b3 --- /dev/null +++ b/tests-pl/ffi_f64_minus_zero.pl @@ -0,0 +1,18 @@ +:- use_module(library(os)). +:- use_module(library(ffi)). + +test :- + read(Body), + term_variables(Body, [LIB]), + Body, + use_foreign_module(LIB, ['ffi_f64_minus_zero'([], f64), 'signum'([f64], f64)]), + ffi:'ffi_f64_minus_zero'(N), + A is max(0.0, N), + B is max(N, 0.0), + ffi:'signum'(A, SA), + ffi:'signum'(B, SB), + write((SA, SB)), + -1.0 is SA, % incorrect, based on https://www.swi-prolog.org/pldoc/man?function=max/2 -0.0 is less than 0.0 so A and B should be 0.0 for which signum should be 1 + 1.0 is SB. + +:- initialization(test). diff --git a/tests-pl/ffi_f64_nan.pl b/tests-pl/ffi_f64_nan.pl new file mode 100644 index 00000000..42dcd587 --- /dev/null +++ b/tests-pl/ffi_f64_nan.pl @@ -0,0 +1,12 @@ +:- use_module(library(os)). +:- use_module(library(ffi)). + +test :- + read(Body), + term_variables(Body, [LIB]), + Body, + use_foreign_module(LIB, ['ffi_f64_nan'([], f64)]), + ffi:'ffi_f64_nan'(N), + _ is round(N). + +:- initialization(test). diff --git a/tests-pl/ffi_invalid_type.pl b/tests-pl/ffi_invalid_type.pl new file mode 100644 index 00000000..7565792f --- /dev/null +++ b/tests-pl/ffi_invalid_type.pl @@ -0,0 +1,13 @@ +:- use_module(library(os)). +:- use_module(library(ffi)). + +test :- + read(Body), + term_variables(Body, [LIB]), + Body, + use_foreign_module(LIB, [ + %% should be void instead of c_void + 'ffi_invalid_type'([], c_void) + ]). + +:- initialization(test). diff --git a/tests-pl/ffi_return_values.pl b/tests-pl/ffi_return_values.pl new file mode 100644 index 00000000..ee161259 --- /dev/null +++ b/tests-pl/ffi_return_values.pl @@ -0,0 +1,36 @@ +:- use_module(library(os)). +:- use_module(library(ffi)). + +test :- + read(Body), + term_variables(Body, [LIB]), + Body, + use_foreign_module(LIB, [ + 'ffi_return_values_true'([], bool), + 'ffi_return_values_false'([], bool), + 'ffi_return_values_i8'([], sint8), + 'ffi_return_values_u8'([], uint8), + 'ffi_return_values_i16'([], sint16), + 'ffi_return_values_u16'([], uint16), + 'ffi_return_values_i32'([], sint32), + 'ffi_return_values_u32'([], uint32), + 'ffi_return_values_i64'([], sint64), + 'ffi_return_values_u64'([], uint64), + 'ffi_return_values_f32'([], f32), + 'ffi_return_values_f64'([], f64) + ]), + ffi:'ffi_return_values_true', + (\+ ffi:'ffi_return_values_false'), + ffi:'ffi_return_values_i8'(I8), + ffi:'ffi_return_values_u8'(U8), + ffi:'ffi_return_values_i16'(I16), + ffi:'ffi_return_values_u16'(U16), + ffi:'ffi_return_values_i32'(I32), + ffi:'ffi_return_values_u32'(U32), + ffi:'ffi_return_values_i64'(I64), + ffi:'ffi_return_values_u64'(U64), + ffi:'ffi_return_values_f32'(F32), + ffi:'ffi_return_values_f64'(F64), + write((i8-I8, u8-U8, i16-I16, u16-U16, i32-I32, u32-U32, i64-I64, u64-U64, f32-F32, f64-F64)). + +:- initialization(test). diff --git a/tests-pl/ffi_struct.pl b/tests-pl/ffi_struct.pl new file mode 100644 index 00000000..23ec2af7 --- /dev/null +++ b/tests-pl/ffi_struct.pl @@ -0,0 +1,15 @@ +:- use_module(library(os)). +:- use_module(library(ffi)). + +test :- + read(Body), + term_variables(Body, [LIB]), + Body, + foreign_struct(pg, [uint8, uint16, uint32, uint64, uint8, f32, f64]), + use_foreign_module(LIB, ['construct'([uint8, uint16, uint32, uint64, uint8, f32, f64], pg), 'modify'([pg], pg)]), + ffi:'construct'(8, 12, 46, 40, 127, 1.368, -4.587, PG), + write(("PG"-PG)), nl, + ffi:'modify'(PG, [pg, A, B, C, D, A2, E, F]), + write(("PG2"-[pg, A, B, C, D, A2, E, F])), nl. + +:- initialization(test). diff --git a/tests/scryer/ffi.rs b/tests/scryer/ffi.rs new file mode 100644 index 00000000..c9e0aa1e --- /dev/null +++ b/tests/scryer/ffi.rs @@ -0,0 +1,285 @@ +use std::{ + env::consts::{DLL_PREFIX, DLL_SUFFIX}, + io::Write, + path::{Path, PathBuf}, + process::Stdio, +}; + +use crate::helper::load_module_test_with_input; + +use current_platform::CURRENT_PLATFORM; + +const TMP_DIR: &str = env!("CARGO_TARGET_TMPDIR"); + +// each test is building its own library so that they can easier run in parallel, +// i.e. don't need to wait for a large dynamic library to compile, +// also rusts test infra currently has no functionallity for a setup/befor step +fn build_dynamic_library(name: &str, src: &str) -> PathBuf { + let tmp_dir: &Path = TMP_DIR.as_ref(); + + let mut child = std::process::Command::new("rustc") + .stdin(Stdio::piped()) + .args(["--edition", "2024"]) + .arg(format!("--target={CURRENT_PLATFORM}")) + .arg("--crate-type=dylib") + .arg(format!("--crate-name={name}")) + .arg("--out-dir") + .arg(tmp_dir) + .arg("-") + .spawn() + .unwrap(); + + child + .stdin + .take() + .unwrap() + .write_all(src.as_bytes()) + .unwrap(); + + assert!(child.wait().unwrap().success()); + + tmp_dir.join(format!("{DLL_PREFIX}{name}{DLL_SUFFIX}")) +} + +#[test] +#[cfg_attr(miri, ignore = "ffi")] +fn ffi_f64_nan() { + let dynlib_path = build_dynamic_library( + "ffi_f64_nan", + r##" + #[unsafe(no_mangle)] + extern "C" fn ffi_f64_nan() -> f64 { + f64::NAN + } + "##, + ); + + load_module_test_with_input( + "tests-pl/ffi_f64_nan.pl", + format!("LIB={dynlib_path:?}."), + " error(evaluation_error(undefined),round/1).\n", + ); +} + +#[test] +#[cfg_attr(miri, ignore = "ffi")] +fn ffi_f64_minus_zero() { + let dynlib_path = build_dynamic_library( + "ffi_f64_minus_zero", + r##" + #[unsafe(no_mangle)] + extern "C" fn ffi_f64_minus_zero() -> f64 { + -0.0 + } + + #[unsafe(no_mangle)] + extern "C" fn signum(f: f64) -> f64 { + f.signum() + } + "##, + ); + + // note: ouput is currently wrong correct would be 1.0,1.0 + load_module_test_with_input( + "tests-pl/ffi_f64_minus_zero.pl", + format!("LIB={dynlib_path:?}."), + "-1.0,1.0", + ); +} + +#[test] +#[cfg_attr(miri, ignore = "ffi")] +fn ffi_return_values() { + let dynlib_path = build_dynamic_library( + "ffi_return_values", + r##" + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_true() -> bool { + true + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_false() -> bool { + false + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_i8() -> i8 { + -42 + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_u8() -> u8 { + 73 + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_i16() -> i16 { + -0xBEE + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_u16() -> u16 { + 0xC0DE + } + + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_i32() -> i32 { + -0xBEEFBEE + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_u32() -> u32 { + 0xC0DEB000 + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_i64() -> i64 { + -0xBEEFBEE5C0DEB00 + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_u64() -> u64 { + 0xFEDCBA9876543210 + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_f32() -> f32 { + std::f32::consts::PI + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_return_values_f64() -> f64 { + std::f64::consts::TAU + } + "##, + ); + + let expected = format!( + "i8- {},u8-{},i16- {},u16-{},i32- {},u32-{},i64- {},u64-{},f32-{},f64-{}", + -42, + 73, + -0xBEE, + 0xC0DE, + -0xBEEFBEE, + 0xC0DEB000u32, + -0xBEEFBEE5C0DEB00i64, + 0xFEDCBA9876543210u64, + std::f32::consts::PI as f64, + std::f64::consts::TAU + ); + + load_module_test_with_input( + "tests-pl/ffi_return_values.pl", + format!("LIB={dynlib_path:?}."), + expected.as_str(), + ); +} + +#[test] +#[cfg_attr(miri, ignore = "ffi")] +fn ffi_invalid_type() { + let dynlib_path = build_dynamic_library( + "ffi_invalid_type", + r##" + #[unsafe(no_mangle)] + extern "C" fn ffi_invalid_type() -> () { + } + "##, + ); + + load_module_test_with_input( + "tests-pl/ffi_invalid_type.pl", + format!("LIB={dynlib_path:?}."), + "% Warning: initialization/1 failed for: user:test\n", + ); +} + +#[test] +#[cfg_attr(miri, ignore = "ffi")] +fn ffi_struct() { + let dynlib_path = build_dynamic_library( + "ffi_struct", + r##" + #[repr(C)] + struct PaddingGalore { + a: u8, + b: u16, + c: u32, + d: u64, + a2: u8, + e: f32, + f: f64, + } + + #[unsafe(no_mangle)] + extern "C" fn construct(a: u8, b: u16, c: u32, d: u64, a2: u8, e: f32, f: f64) -> PaddingGalore { + PaddingGalore { + a, + a2, + b, + c, + d, + e, + f, + } + } + + #[unsafe(no_mangle)] + extern "C" fn modify(data: PaddingGalore) -> PaddingGalore { + PaddingGalore { + a: data.a2, + a2: data.a, + b: !data.b, + c: !data.c, + d: !data.d, + e: -data.e, + f: -data.f, + } + } + "##, + ); + + load_module_test_with_input( + "tests-pl/ffi_struct.pl", + format!("LIB={dynlib_path:?}."), + "[P,G]-[pg,8,12,46,40,127,1.3680000305175781,-4.587]\n[P,G,2]-[pg,127,65523,4294967249,18446744073709551575,8,-1.3680000305175781,4.587]\n", + ); +} + +#[test] +#[cfg_attr(miri, ignore = "ffi")] +fn ffi_cstr() { + let dynlib_path = build_dynamic_library( + "ffi_cstr", + r##" + use std::ffi::CStr; + + #[unsafe(no_mangle)] + extern "C" fn ffi_cstr_len(c_str: Option>) -> u64 { + if let Some(c_str) = c_str { + unsafe { CStr::from_ptr(c_str.as_ptr()) }.count_bytes() as u64 + } else { + u64::MAX + } + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_example_cstr() -> *const core::ffi::c_char { + c"Rust Lang".as_ptr() + } + + #[unsafe(no_mangle)] + extern "C" fn ffi_null_cstr() -> *const core::ffi::c_char { + std::ptr::null() + } + "##, + ); + + load_module_test_with_input( + "tests-pl/ffi_cstr.pl", + format!("LIB={dynlib_path:?}."), + format!(r#"13-[R,u,s,t, ,L,a,n,g]-0-{}"#, u64::MAX).as_str(), + ); +} diff --git a/tests/scryer/helper.rs b/tests/scryer/helper.rs index b7c2dd77..910c0258 100644 --- a/tests/scryer/helper.rs +++ b/tests/scryer/helper.rs @@ -1,5 +1,9 @@ use scryer_prolog::MachineBuilder; +use std::borrow::Cow; + +use scryer_prolog::{InputStreamConfig, StreamConfig}; + pub(crate) trait Expectable { #[track_caller] fn assert_eq(self, other: &[u8]); @@ -47,3 +51,16 @@ pub(crate) fn load_module_test_with_tokio_runtime(file: &str, exp expected.assert_eq(wam.test_load_file(file).as_slice()) }); } + +pub(crate) fn load_module_test_with_input( + file: &str, + input: impl Into>, + expected: T, +) { + use scryer_prolog::MachineBuilder; + + let mut wam = MachineBuilder::default() + .with_streams(StreamConfig::in_memory().with_user_input(InputStreamConfig::string(input))) + .build(); + expected.assert_eq(wam.test_load_file(file).as_slice()); +} diff --git a/tests/scryer/issues.rs b/tests/scryer/issues.rs index de5db915..34768866 100644 --- a/tests/scryer/issues.rs +++ b/tests/scryer/issues.rs @@ -56,6 +56,7 @@ fn issue2725_dcg_without_module() { #[cfg(feature = "http")] #[cfg(not(target_arch = "wasm32"))] #[cfg_attr(miri, ignore = "it takes too long to run")] +#[cfg_attr(not(miri), ignore = "flaky due to network requests")] fn http_open_hanging() { load_module_test_with_tokio_runtime( "tests-pl/issue-http_open-hanging.pl", diff --git a/tests/scryer/main.rs b/tests/scryer/main.rs index c46ced24..c4430400 100644 --- a/tests/scryer/main.rs +++ b/tests/scryer/main.rs @@ -2,6 +2,8 @@ mod helper; mod issues; mod src_tests; +mod ffi; + /// To add new cli test copy an existing .toml file in `tests/scryer/cli/issues/` or `tests/scryer/cli/issues/src_tests/`, /// adjust as necessary the `-f` and `--no-add-history` args should be kept but additional args may be added. /// For input on stdin add a .stdin file with the same filename.