#![allow(clippy::missing_safety_doc)]
use crate::block::BlockDevice;
use crate::callback_device::CallbackDevice;
use crate::error::Error;
use std::cell::RefCell;
use std::ffi::{c_char, c_int, c_void, CString};
use std::io;
use std::panic::AssertUnwindSafe;
use std::ptr;
use std::slice;
use std::sync::Arc;
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FsCoreErrorCode {
Ok = 0,
Io = 1,
ShortRead = 2,
ReadOnly = 3,
OutOfBounds = 4,
Custom = 5,
NullArg = 6,
Panic = 7,
BadString = 8,
}
impl FsCoreErrorCode {
fn from_error(e: &Error) -> Self {
match e {
Error::Io(_) => FsCoreErrorCode::Io,
Error::ShortRead { .. } => FsCoreErrorCode::ShortRead,
Error::ReadOnly => FsCoreErrorCode::ReadOnly,
Error::OutOfBounds { .. } => FsCoreErrorCode::OutOfBounds,
Error::Custom(_) => FsCoreErrorCode::Custom,
}
}
}
thread_local! {
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}
pub fn set_last_error(message: impl Into<String>) {
let s = message.into();
let cs = CString::new(s.replace('\0', "?")).expect("contains no NUL after replace");
LAST_ERROR.with(|slot| {
*slot.borrow_mut() = Some(cs);
});
}
fn clear_last_error() {
LAST_ERROR.with(|slot| {
*slot.borrow_mut() = None;
});
}
#[unsafe(no_mangle)]
pub extern "C" fn fs_core_last_error_message() -> *const c_char {
LAST_ERROR.with(|slot| {
slot.borrow()
.as_ref()
.map(|cs| cs.as_ptr())
.unwrap_or(ptr::null())
})
}
pub fn ffi_guard<F>(body: F) -> FsCoreErrorCode
where
F: FnOnce() -> Result<(), Error>,
{
clear_last_error();
match std::panic::catch_unwind(AssertUnwindSafe(body)) {
Ok(Ok(())) => FsCoreErrorCode::Ok,
Ok(Err(e)) => {
let code = FsCoreErrorCode::from_error(&e);
set_last_error(e.to_string());
code
}
Err(panic) => {
set_last_error(panic_message(&panic));
FsCoreErrorCode::Panic
}
}
}
fn panic_message(panic: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = panic.downcast_ref::<&'static str>() {
return (*s).to_string();
}
if let Some(s) = panic.downcast_ref::<String>() {
return s.clone();
}
"panic in FFI".to_string()
}
pub struct FsCoreDevice {
inner: Arc<dyn BlockDevice>,
}
impl FsCoreDevice {
pub fn into_handle(inner: Arc<dyn BlockDevice>) -> *mut FsCoreDevice {
Box::into_raw(Box::new(FsCoreDevice { inner }))
}
pub fn inner(&self) -> &Arc<dyn BlockDevice> {
&self.inner
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_close(handle: *mut FsCoreDevice) {
if handle.is_null() {
return;
}
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
drop(Box::from_raw(handle));
}));
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_size_bytes(handle: *const FsCoreDevice) -> u64 {
if handle.is_null() {
return 0;
}
std::panic::catch_unwind(AssertUnwindSafe(|| unsafe { (*handle).inner.size_bytes() }))
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_is_writable(handle: *const FsCoreDevice) -> bool {
if handle.is_null() {
return false;
}
std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
(*handle).inner.is_writable()
}))
.unwrap_or(false)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_read_at(
handle: *const FsCoreDevice,
offset: u64,
buf: *mut u8,
len: usize,
) -> FsCoreErrorCode {
if handle.is_null() || (buf.is_null() && len > 0) {
return FsCoreErrorCode::NullArg;
}
ffi_guard(|| {
let slice_buf = unsafe { slice::from_raw_parts_mut(buf, len) };
unsafe { (*handle).inner.read_at(offset, slice_buf) }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_write_at(
handle: *const FsCoreDevice,
offset: u64,
buf: *const u8,
len: usize,
) -> FsCoreErrorCode {
if handle.is_null() || (buf.is_null() && len > 0) {
return FsCoreErrorCode::NullArg;
}
ffi_guard(|| {
let slice_buf = unsafe { slice::from_raw_parts(buf, len) };
unsafe { (*handle).inner.write_at(offset, slice_buf) }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_flush(handle: *const FsCoreDevice) -> FsCoreErrorCode {
if handle.is_null() {
return FsCoreErrorCode::NullArg;
}
ffi_guard(|| unsafe { (*handle).inner.flush() })
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_file_open(
path: *const c_char,
writable: bool,
) -> *mut FsCoreDevice {
if path.is_null() {
set_last_error("path is null");
return ptr::null_mut();
}
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
let cstr = unsafe { std::ffi::CStr::from_ptr(path) };
let s = match cstr.to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("path is not valid UTF-8");
return ptr::null_mut();
}
};
let dev = if writable {
crate::file_device::FileDevice::open_rw(s)
} else {
crate::file_device::FileDevice::open(s)
};
match dev {
Ok(d) => FsCoreDevice::into_handle(Arc::new(d)),
Err(e) => {
set_last_error(e.to_string());
ptr::null_mut()
}
}
}));
match res {
Ok(p) => p,
Err(panic) => {
set_last_error(panic_message(&panic));
ptr::null_mut()
}
}
}
pub type FsCoreReadCb =
Option<unsafe extern "C" fn(ctx: *mut c_void, offset: u64, buf: *mut u8, len: usize) -> c_int>;
pub type FsCoreWriteCb = Option<
unsafe extern "C" fn(ctx: *mut c_void, offset: u64, buf: *const u8, len: usize) -> c_int,
>;
pub type FsCoreFlushCb = Option<unsafe extern "C" fn(ctx: *mut c_void) -> c_int>;
#[repr(C)]
pub struct FsCoreCallbackCfg {
pub read: FsCoreReadCb,
pub write: FsCoreWriteCb,
pub flush: FsCoreFlushCb,
pub ctx: *mut c_void,
pub size: u64,
}
fn cb_io_err(rc: c_int, op: &str) -> io::Error {
io::Error::other(format!("callback {op} returned {rc}"))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_from_callbacks(
cfg: *const FsCoreCallbackCfg,
) -> *mut FsCoreDevice {
if cfg.is_null() {
set_last_error("cfg is null");
return ptr::null_mut();
}
let res = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
let cfg = &*cfg;
let read_fn = match cfg.read {
Some(f) => f,
None => {
set_last_error("cfg.read is null");
return ptr::null_mut();
}
};
let write_fn = cfg.write;
let flush_fn = cfg.flush;
let ctx_addr = cfg.ctx as usize;
let size = cfg.size;
let read_cb: crate::callback_device::ReadCb = Box::new(move |off, buf| {
let ctx = ctx_addr as *mut c_void;
let rc = read_fn(ctx, off, buf.as_mut_ptr(), buf.len());
if rc == 0 {
Ok(())
} else {
Err(cb_io_err(rc, "read"))
}
});
let write_cb: Option<crate::callback_device::WriteCb> = write_fn.map(|f| {
Box::new(move |off, buf: &[u8]| {
let ctx = ctx_addr as *mut c_void;
let rc = f(ctx, off, buf.as_ptr(), buf.len());
if rc == 0 {
Ok(())
} else {
Err(cb_io_err(rc, "write"))
}
}) as crate::callback_device::WriteCb
});
let flush_cb: Option<crate::callback_device::FlushCb> = flush_fn.map(|f| {
Box::new(move || {
let ctx = ctx_addr as *mut c_void;
let rc = f(ctx);
if rc == 0 {
Ok(())
} else {
Err(cb_io_err(rc, "flush"))
}
}) as crate::callback_device::FlushCb
});
let dev = CallbackDevice {
size,
read: read_cb,
write: write_cb,
flush: flush_cb,
};
FsCoreDevice::into_handle(Arc::new(dev))
}));
match res {
Ok(p) => p,
Err(panic) => {
set_last_error(panic_message(&panic));
ptr::null_mut()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_slice_ro(
parent: *const FsCoreDevice,
start: u64,
length: u64,
) -> *mut FsCoreDevice {
if parent.is_null() {
set_last_error("parent is null");
return ptr::null_mut();
}
let res = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
let parent_arc = (*parent).inner().clone();
let parent_read: Arc<dyn crate::block::BlockRead> = parent_arc;
let slice = crate::slice::OwnedSlice::new(parent_read, start, length);
FsCoreDevice::into_handle(Arc::new(slice))
}));
match res {
Ok(p) => p,
Err(panic) => {
set_last_error(panic_message(&panic));
ptr::null_mut()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fs_core_device_slice_rw(
parent: *const FsCoreDevice,
start: u64,
length: u64,
) -> *mut FsCoreDevice {
if parent.is_null() {
set_last_error("parent is null");
return ptr::null_mut();
}
let res = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
let parent_arc = (*parent).inner().clone();
let slice = crate::slice::OwnedRwSlice::new(parent_arc, start, length);
FsCoreDevice::into_handle(Arc::new(slice))
}));
match res {
Ok(p) => p,
Err(panic) => {
set_last_error(panic_message(&panic));
ptr::null_mut()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
fn tmp_image(bytes: &[u8]) -> String {
use std::sync::atomic::{AtomicU32, Ordering};
static C: AtomicU32 = AtomicU32::new(0);
let n = C.fetch_add(1, Ordering::Relaxed);
let p = std::env::temp_dir()
.join(format!("fs_core_ffi_{}_{n}.img", std::process::id()))
.to_string_lossy()
.into_owned();
File::create(&p).unwrap().write_all(bytes).unwrap();
p
}
#[test]
fn open_read_close_round_trip() {
let path = tmp_image(b"hello, fs-core ffi");
let cpath = CString::new(path.as_str()).unwrap();
let h = unsafe { fs_core_file_open(cpath.as_ptr(), false) };
assert!(!h.is_null(), "open failed");
unsafe {
assert_eq!(fs_core_device_size_bytes(h), 18);
assert!(!fs_core_device_is_writable(h));
let mut buf = [0u8; 5];
let rc = fs_core_device_read_at(h, 0, buf.as_mut_ptr(), buf.len());
assert_eq!(rc, FsCoreErrorCode::Ok);
assert_eq!(&buf, b"hello");
let rc = fs_core_device_write_at(h, 0, b"x".as_ptr(), 1);
assert_eq!(rc, FsCoreErrorCode::ReadOnly);
fs_core_device_close(h);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn null_args_return_null_arg() {
let mut buf = [0u8; 4];
let rc = unsafe { fs_core_device_read_at(ptr::null(), 0, buf.as_mut_ptr(), buf.len()) };
assert_eq!(rc, FsCoreErrorCode::NullArg);
let rc = unsafe { fs_core_device_flush(ptr::null()) };
assert_eq!(rc, FsCoreErrorCode::NullArg);
}
#[test]
fn last_error_populated_on_open_failure() {
let cpath = CString::new("/path/that/does/not/exist/we/hope").unwrap();
let h = unsafe { fs_core_file_open(cpath.as_ptr(), false) };
assert!(h.is_null());
let msg = fs_core_last_error_message();
assert!(!msg.is_null());
let s = unsafe { std::ffi::CStr::from_ptr(msg).to_string_lossy().into_owned() };
assert!(!s.is_empty(), "expected an error message");
}
use std::sync::{Arc as StdArc, Mutex as StdMutex};
struct CbState {
data: Vec<u8>,
flushed: u32,
}
unsafe extern "C" fn t_read(ctx: *mut c_void, offset: u64, buf: *mut u8, len: usize) -> c_int {
let st = unsafe { &mut *(ctx as *mut CbState) };
let off = offset as usize;
if off + len > st.data.len() {
return 5; }
unsafe {
std::ptr::copy_nonoverlapping(st.data.as_ptr().add(off), buf, len);
}
0
}
unsafe extern "C" fn t_write(
ctx: *mut c_void,
offset: u64,
buf: *const u8,
len: usize,
) -> c_int {
let st = unsafe { &mut *(ctx as *mut CbState) };
let off = offset as usize;
if off + len > st.data.len() {
return 5;
}
unsafe {
std::ptr::copy_nonoverlapping(buf, st.data.as_mut_ptr().add(off), len);
}
0
}
unsafe extern "C" fn t_flush(ctx: *mut c_void) -> c_int {
let st = unsafe { &mut *(ctx as *mut CbState) };
st.flushed += 1;
0
}
#[test]
fn callback_device_round_trip_rw() {
let mut st = Box::new(CbState {
data: vec![0u8; 32],
flushed: 0,
});
for (i, b) in st.data.iter_mut().enumerate() {
*b = i as u8;
}
let ctx = &mut *st as *mut CbState as *mut c_void;
let cfg = FsCoreCallbackCfg {
read: Some(t_read),
write: Some(t_write),
flush: Some(t_flush),
ctx,
size: 32,
};
let h = unsafe { fs_core_device_from_callbacks(&cfg) };
assert!(!h.is_null(), "device_from_callbacks returned NULL");
unsafe {
assert_eq!(fs_core_device_size_bytes(h), 32);
assert!(fs_core_device_is_writable(h));
let mut buf = [0u8; 4];
let rc = fs_core_device_read_at(h, 4, buf.as_mut_ptr(), buf.len());
assert_eq!(rc, FsCoreErrorCode::Ok);
assert_eq!(buf, [4, 5, 6, 7]);
let payload = [0xDE, 0xAD, 0xBE, 0xEF];
let rc = fs_core_device_write_at(h, 8, payload.as_ptr(), payload.len());
assert_eq!(rc, FsCoreErrorCode::Ok);
let rc = fs_core_device_flush(h);
assert_eq!(rc, FsCoreErrorCode::Ok);
let mut readback = [0u8; 4];
let rc = fs_core_device_read_at(h, 8, readback.as_mut_ptr(), readback.len());
assert_eq!(rc, FsCoreErrorCode::Ok);
assert_eq!(readback, payload);
fs_core_device_close(h);
}
assert_eq!(st.flushed, 1);
assert_eq!(&st.data[8..12], &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn callback_device_readonly_when_write_null() {
let mut st = Box::new(CbState {
data: vec![0xAAu8; 16],
flushed: 0,
});
let ctx = &mut *st as *mut CbState as *mut c_void;
let cfg = FsCoreCallbackCfg {
read: Some(t_read),
write: None,
flush: None,
ctx,
size: 16,
};
let h = unsafe { fs_core_device_from_callbacks(&cfg) };
assert!(!h.is_null());
unsafe {
assert!(!fs_core_device_is_writable(h));
let rc = fs_core_device_write_at(h, 0, [1u8].as_ptr(), 1);
assert_eq!(rc, FsCoreErrorCode::ReadOnly);
assert_eq!(fs_core_device_flush(h), FsCoreErrorCode::Ok);
fs_core_device_close(h);
}
let _ = StdArc::new(StdMutex::new(0u8));
}
#[test]
fn callback_device_null_cfg_returns_null() {
let h = unsafe { fs_core_device_from_callbacks(ptr::null()) };
assert!(h.is_null());
let msg = fs_core_last_error_message();
assert!(!msg.is_null());
}
}