use crate::errnoPolicy::globalReadErrno;
use crate::ffi::types::primitive::{Arg, FfiArg, FfiPrimitive};
use crate::ffi::types::primitive::Callback;
use crate::ffi::types::primitive::DynamicList;
use crate::ffi::types::primitive::Primitive;
use crate::ffi::types::{Type, Value};
use crate::ffi::callback::sendable::Sendable;
use serde::Serialize;
use std::sync::atomic::Ordering;
use std::sync::atomic::AtomicU64;
use std::cell::RefCell;
use std::path::PathBuf;
use crate::pathResolver::{PathResolver, resolveGlobal};
use std::cell::UnsafeCell;
use crate::ffi::allocatedMemory::AllocatedMemory;
use crate::ffi::errors::FFIError;
use crate::ffi::library::{sendRawRequest, nextLibraryId, registerLibrary, Library};
use crate::zygote::{ClonedZygote, FFIRequest, ZygoteGuard};
struct HeavyStack
{
pathResolver: Option<PathResolver>,
readErrno: Option<bool>
}
#[doc(hidden)]
pub struct ScopeGuard
{
inner: UnsafeCell<Option<HeavyStack>>
}
impl ScopeGuard
{
#[doc(hidden)]
#[inline(always)]
pub const fn new() -> Self
{
Self {
inner: UnsafeCell::new(None)
}
}
}
thread_local!{
static ScopeStack: RefCell<Vec<*const ScopeGuard>> = const { RefCell::new(Vec::new()) };
}
pub(super) fn currentScopeReadErrno() -> Option<bool>
{
ScopeStack.with(|stack| {
let guardPtr: *const ScopeGuard = *stack.borrow().last()?;
let slot: &Option<HeavyStack> = unsafe{ &*(*guardPtr).inner.get() };
slot.as_ref()?.readErrno
})
}
pub struct Scope<'g>
{
guard: &'g ScopeGuard,
}
impl<'g> Scope<'g>
{
#[doc(hidden)]
#[inline(always)]
pub fn new(guard: &'g ScopeGuard) -> Self
{
ScopeStack.with(|s| s.borrow_mut().push(guard as *const ScopeGuard));
Self { guard }
}
pub fn addSearchPath(&self, path: impl Into<PathBuf>) -> ()
{
let slot: &mut Option<HeavyStack> = unsafe{ &mut *self.guard.inner.get() };
slot.get_or_insert_with(|| HeavyStack{ pathResolver: None, readErrno: None })
.pathResolver.get_or_insert_with(PathResolver::default)
.addPath(path);
}
pub fn setReadErrno(&self, enabled: bool) -> ()
{
let slot: &mut Option<HeavyStack> = unsafe{ &mut *self.guard.inner.get() };
slot.get_or_insert_with(|| HeavyStack{ pathResolver: None, readErrno: None })
.readErrno = Some(enabled);
}
pub fn load(&self, libraryPath: &str) -> Result<Library<'g>, FFIError>
{
let slot: &Option<HeavyStack> = unsafe{ &*self.guard.inner.get() };
let resolved: String = slot.as_ref()
.and_then(|s| s.pathResolver.as_ref())
.and_then(|r| r.resolve(libraryPath))
.or_else(|| resolveGlobal(libraryPath))
.unwrap_or_else(|| libraryPath.to_string());
let libraryId: usize = nextLibraryId();
registerLibrary(libraryId, &resolved);
Ok(Library::new(libraryId, resolved))
}
pub fn alloc(&self, length: usize) -> Result<AllocatedMemory<'g>, FFIError>
{
let stack: &mut Option<HeavyStack> = unsafe{ &mut *self.guard.inner.get() };
if stack.is_none() {
*stack = Some(HeavyStack{
pathResolver: None,
readErrno: None
});
}
match sendRawRequest(FFIRequest::Alloc { length })? {
Value::Pointer(address) => Ok(AllocatedMemory::new(address, length)),
_ => Err(FFIError::Other("Alloc did not return a pointer".to_string())),
}
}
pub fn allocStruct(&self, fields: &[Type]) -> Result<AllocatedMemory<'g>, FFIError>
{
let stack: &mut Option<HeavyStack> = unsafe{ &mut *self.guard.inner.get() };
if stack.is_none() {
*stack = Some(HeavyStack{
pathResolver: None,
readErrno: None
});
}
match sendRawRequest(FFIRequest::AllocDynamicStruct { fields: fields.to_vec() })? {
Value::Struct(parts) if parts.len() == 2 => match (&parts[0], &parts[1]) {
(Value::Pointer(address), Value::Usize(size)) => Ok(AllocatedMemory::new(*address, *size)),
_ => Err(FFIError::Other("AllocDynamicStruct returned an unexpected shape".to_string())),
},
_ => Err(FFIError::Other("AllocDynamicStruct did not return a pointer+size pair".to_string())),
}
}
pub fn allocAligned(&self, length: usize, alignment: usize) -> Result<AllocatedMemory<'g>, FFIError>
{
let stack: &mut Option<HeavyStack> = unsafe{ &mut *self.guard.inner.get() };
if stack.is_none() {
*stack = Some(HeavyStack{
pathResolver: None,
readErrno: None
});
}
match sendRawRequest(FFIRequest::AllocAligned { length, alignment })? {
Value::Pointer(address) => Ok(AllocatedMemory::new(address, length)),
_ => Err(FFIError::Other("AllocAligned did not return a pointer".to_string())),
}
}
#[inline]
pub fn free(pointer: impl Into<usize>) -> Result<(), FFIError>
{
sendRawRequest(FFIRequest::Free {
pointer: pointer.into()
})?;
Ok(())
}
#[inline]
pub fn readMemory(pointer: impl Into<usize>, length: usize) -> Result<Vec<u8>, FFIError>
{
let value: Value = sendRawRequest(FFIRequest::ReadMemory {
pointer: pointer.into(),
length,
})?;
value.try_into()
}
pub fn writeMemory(pointer: impl Into<usize>, value: impl FfiArg) -> Result<(), FFIError>
{
sendRawRequest(FFIRequest::WriteMemory {
pointer: pointer.into(),
value: value.intoFfiValue().0,
})?;
Ok(())
}
pub fn readDynamicStruct(
pointer: impl Into<usize>,
fields: &[Type],
) -> Result<DynamicList, FFIError>
{
match sendRawRequest(FFIRequest::ReadDynamicStruct {
pointer: pointer.into(),
fields: fields.to_vec(),
})? {
Value::Struct(values) => Ok(DynamicList::fromValues(values)),
other => Err(FFIError::Other(format!(
"ReadDynamicStruct: expected Value::Struct, got {:?}",
other
))),
}
}
pub fn writeDynamicStruct(
pointer: impl Into<usize>,
fields: &[Type],
values: Vec<Arg>
) -> Result<(), FFIError>
{
let values: Vec<Value> = values.into_iter().map(|a: Arg| a.0).collect();
sendRawRequest(FFIRequest::WriteDynamicStruct {
pointer: pointer.into(), fields: fields.to_vec(), values
})?;
Ok(())
}
pub fn callPointer<T: FfiPrimitive>(
&self,
pointer: impl Into<usize>,
args: Vec<Arg>
) -> Result<T, FFIError>
{
self.callPointerImpl(pointer, args, None)
}
#[inline]
pub fn callvPointer(
&self,
pointer: impl Into<usize>,
args: Vec<Arg>
) -> Result<(), FFIError>
{
self.callPointer::<()>(pointer, args)
}
#[inline]
pub fn callPointerErrno<T: FfiPrimitive>(
&self,
pointer: impl Into<usize>,
args: Vec<Arg>
) -> Result<T, FFIError>
{
self.callPointerImpl(pointer, args, Some(true))
}
fn callPointerImpl<T: FfiPrimitive>(
&self,
pointer: impl Into<usize>,
args: Vec<Arg>,
readErrno: Option<bool>
) -> Result<T, FFIError>
{
let readErrno: bool = readErrno.unwrap_or_else(|| currentScopeReadErrno().unwrap_or_else(globalReadErrno));
let args: Vec<Value> = args.into_iter().map(|a: Arg| a.0).collect();
let raw: Value = sendRawRequest(FFIRequest::CallPointer {
pointer: pointer.into(),
args,
resultType: T::TypeTag,
readErrno
})?;
T::fromFfiValue(Arg(raw))
}
#[inline]
pub fn lastErrno() -> Option<i32>
{
crate::ffi::library::lastErrno()
}
pub fn callback<State: Serialize + Send, Output: Primitive>(
&self,
f: Sendable<State, Output>
) -> Callback
{
static nextID: AtomicU64 = AtomicU64::new(1);
let id: u64 = nextID.fetch_add(1, Ordering::SeqCst);
sendRawRequest(FFIRequest::RegisterCallback {
id,
bytes: f.encode().expect("encode callback"),
argTypes: f.argTypes,
returnType: f.returnType
}).expect("register callback failed");
Callback(id)
}
}
impl<'g> Drop for Scope<'g>
{
fn drop(&mut self) -> () { ScopeStack.with(|s| { s.borrow_mut().pop(); }); }
}
pub struct FFIScope
{
_zygote: ZygoteGuard,
guard: ScopeGuard,
}
impl FFIScope
{
pub fn enter() -> Result<Self, FFIError>
{
let zygote: ClonedZygote = ClonedZygote::getMeClone()
.map_err(|e| FFIError::Other(format!("failed to acquire zygote clone: {}", e)))?;
let _zygote: ZygoteGuard = ZygoteGuard::enter(zygote);
let guard: ScopeGuard = ScopeGuard::new();
Ok(Self { _zygote, guard })
}
pub fn scope(&self) -> Scope<'_>
{
Scope::new(&self.guard)
}
}
#[macro_export]
macro_rules! callPointer
{
($scope:expr, $pointer:expr $(, $args:expr)* $(,)?) => {
$scope.callPointer($pointer, vec![$($crate::ffi::types::primitive::Arg::from($args)),*])
};
}
#[macro_export]
macro_rules! callvPointer
{
($scope:expr, $pointer:expr $(, $args:expr)* $(,)?) => {
$scope.callvPointer($pointer, vec![$($crate::ffi::types::primitive::Arg::from($args)),*])
};
}
#[cfg(test)]
mod tests
{
use crate::ffi;
use crate::ffi::allocatedMemory::AllocatedMemory;
use crate::ffi::types::Type;
use crate::ffi::types::primitive::{Pointer, Arg, DynamicList};
use crate::ffi::errors::FFIError;
use crate::ffi::library::Library;
use crate::ffi::scope::Scope;
use crate::ffi::scope::FFIScope;
#[test]
fn free() -> ()
{
ffi!(|scope| {
let libc: Library = scope.load("libc.so.6")?;
let ptr: Pointer = libc.call("malloc").arg::<usize>(16).result()?;
Scope::free(ptr)?;
Ok(())
}).expect("Scope::free failed");
}
#[test]
fn readMemory() -> ()
{
let bytes: Vec<u8> = ffi!(|scope| {
let libc: Library = scope.load("libc.so.6")?;
let ptr: Pointer = libc.call("malloc").arg::<usize>(8).result()?;
libc.call("memset")
.arg(ptr)
.arg::<i32>(0xAB)
.arg::<usize>(8)
.void()?;
let readBytes: Vec<u8> = Scope::readMemory(ptr, 8)?;
Scope::free(ptr)?;
Ok(readBytes)
}).expect("Scope::readMemory failed");
assert_eq!(bytes, vec![0xABu8; 8]);
}
#[test]
fn writeMemory() -> ()
{
let len: usize = ffi!(|scope| {
let libc: Library = scope.load("libc.so.6")?;
let ptr: Pointer = libc.call("malloc").arg::<usize>(32).result()?;
Scope::writeMemory(ptr, c"hello")?;
let result: usize = libc.call("strlen").arg(ptr).result()?;
Scope::free(ptr)?;
Ok(result)
}).expect("Scope::writeMemory failed");
assert!(matches!(len, 5));
}
#[test]
fn allocStructResolvesLayoutSize() -> ()
{
let length: usize = ffi!(|scope| {
let mem: AllocatedMemory = scope.allocStruct(&[Type::I32, Type::Pointer])?;
Ok(mem.length())
}).expect("Scope::allocStruct failed");
assert_eq!(length, 16);
}
#[test]
fn writeThenReadDynamicStruct() -> ()
{
let shape: Vec<Type> = vec![Type::I32, Type::I64];
let (a, b): (i32, i64) = ffi!(|scope| {
let mem: AllocatedMemory = scope.allocStruct(&shape)?;
Scope::writeDynamicStruct(mem.address(), &shape, vec![
Arg::from(7i32),
Arg::from(9_000_000_000i64),
])?;
let fields: DynamicList = Scope::readDynamicStruct(mem.address(), &shape)?;
Ok((fields.get(0)?, fields.get(1)?))
}).expect("writeDynamicStruct/readDynamicStruct roundtrip failed");
assert_eq!((a, b), (7, 9_000_000_000));
}
#[test]
fn scopeDefaultEnablesErrno() -> ()
{
let errno: Option<i32> = ffi!(|scope| {
scope.setReadErrno(true);
let libc: Library = scope.load("libc.so.6")?;
let fd: i32 =
libc.call("open")
.arg(c"/no/such/chillffi/scope/path")
.arg::<i32>(0 )
.result()?; assert_eq!(fd, -1);
Ok(Scope::lastErrno())
}).expect("scope errno default test failed");
assert_eq!(errno, Some(libc::ENOENT));
}
#[test]
fn allocAligned() -> ()
{
ffi!(|scope| {
let mem16: AllocatedMemory = scope.allocAligned(64, 16)?;
assert_eq!(mem16.address() % 16, 0, "16-byte alignment not met");
let mem32: AllocatedMemory = scope.allocAligned(64, 32)?;
assert_eq!(mem32.address() % 32, 0, "32-byte alignment not met");
let mem64: AllocatedMemory = scope.allocAligned(64, 64)?;
assert_eq!(mem64.address() % 64, 0, "64-byte alignment not met");
Ok(())
}).expect("allocAligned failed");
}
#[test]
fn scopeRetention() -> ()
{
let (r1, r2): (f64, i32) = (|| -> Result<_, FFIError>
{
let ffiScope: FFIScope = FFIScope::enter()?;
let scope: Scope<'_> = ffiScope.scope();
let libm: Library = scope.load("libm.so.6")?;
let r1: f64 = libm.call("sqrt").arg::<f64>(9.0).result()?;
let libc: Library = scope.load("libc.so.6")?;
let _abs: i32 = libc.call("abs").arg::<i32>(-7).result()?;
let mem: AllocatedMemory = scope.alloc(32)?;
Scope::writeMemory(mem.address(), c"retained")?;
let r2: usize = libc.call("strlen").arg(mem.address()).result()?;
Ok((r1, r2 as i32))
})().expect("FFIScope flow failed");
assert!((r1 - 3.0).abs() < f64::EPSILON, "sqrt(9) != 3, got {}", r1);
assert_eq!(r2, 8, "strlen(\"retained\") != 8, got {}", r2);
}
}