Currently I'm working on a crate writting FFI dlls, and found the following questions:
- some FFI (called
write
for brief) interface is not thread-safe, - other FFI (called
read
for brief) interface could be called concurrencely, butwrite
cannot be called while anyread
is not finished. - the FFI calls Rust with single thread only, no need to consider whether 2 write could be called concurrencely.
#[no_mangle]
extern fn exported(data:i32)->i32 {
{
let readed=read(data); // fine
let readed_2=read(data+1); // multiple data is readed, fine
// write(readed); // should not do that, since the `readed` item could be modified while write is executing.
}
{
write(..Default::default()); // fine
write(..Default::default()); // fine
let readed = read(data); // fine
// write(readed); // cannot write here since readed is not dropped.
}
0
}
#[no_mangle]
extern fn another_exported(data:i32, verbose:bool)->i32{
if (verbose) { println!("data is {data}") }
// let readed = read(data); // cannot be executed here, since `exported` uses write, which could directly modify the `readed` variable.
let result = exported(data);
let readed = read(data); // fine, since no more `write` is called.
result
}
It might be a good idea introducing a new pseudo variable, borrow_checker
.
struct BorrowChecker;
impl BorrowChecker {
pub fn read<'a>(&'a self, data:i32)->Readed<'a> {...}
pub fn write(&mut self, data:*const c_void) {...}
}
#[no_mangle]
extern fn exported(borrow_checker:&mut BorrowChecker, data:i32)->i32 {
{
let readed=borrow_checker.read(data); // fine
let readed_2=borrow_checker.read(data+1); // multiple data is readed, fine
// unsafe { borrow_checker.write(&readed as *const c_void) } // cannot borrow `borrow_checker` as mutable.
}
{
write(ptr::null()); // fine
write(ptr::null()); // fine
let readed = borrow_checker.read(data); // fine
// borrow_checker.write(&readed as *const c_void); // cannot borrow `borrow_checker` as mutable.
}
0
}
#[no_mangle]
extern fn another_exported(borrow_checker:&mut BorrowChecker, data:i32, verbose:bool)->i32{
if (verbose) { println!("data is {data}") }
// let readed = borrow_checker.read(data); // cannot be executed here, since `exported` should borrow borrow_checker exclusively later.
let result = exported(borrow_checker, data);
let readed = borrow_checker.read(data); // fine, since no more `write` is called.
result
}
The modification above suits all the restrictions, except one thing: The FFI knows nothing about &mut BorrowChecker
, and they could not send borrow_checker
directly to Rust, thus the exported functions are not callable outside Rust.
Here comes my request: adding a #[borrow_checker]
flag to some ZST struct
#[borrow_checker]
pub struct BorrowChecker;
The attribute borrow_checker
means that:
- Construct a BorrowChecker needs an
unsafe
block - if
T
is marked asborrow_checker
, all ofT
&mut T
and&T
in function parameters do not modify the ffi signatures, calling such function with omitT
,&mut T
or&T
requires an unsafe block, which means that:
extern fn foo(bc:&mut BorrowChecker){}
// could be called with:
fn main(){
// 1: FFI-compatitable call
unsafe {foo()} // call foo with omitted parameters requires an unsafe block
// 2: Rusty call
let mut bc = unsafe {BorrowChecker}; // construct BorrowChecker is unsafe.
foo(&mut bc); // call foo with `&mut BorrowChecker` is safe.
}
Since the foo
could be called without &mut BorrowChecker
, it could be called in the FFI side, and since foo
has a &mut BorrowChecker
parameter, it could use Rust's borrow checker to check whether the thread-safety rule is violated.
Usage:
#[borrow_checker]
pub struct BorrowChecker;
extern "C" {
// since the borrow of BorrowChecker could be omitted, the ffi interface does not contains `&BorrowChecker` or `&mut BorrowChecker`
fn read(_:&BorrowChecker, data:i32); // It is actually `fn read(data:i32)`
fn write(_:&mut BorrowChecker, data:*const c_void);// It is actually `fn write(data:*const c_void)`
}
#[no_mangle]
// FFI side should call `foo(data)` directly, the `bc` is a marker and thus need not be sent.
extern fn foo(bc:&mut BorrowChecker, data:i32)->i32{...}
Any suggestions?