Summary
This RFC proposes a new grammar for ffi parameter with Drop
implememted, and two warn-by-default lint #[warn(drop_with_ffi)]
and #[warn(safe_drop_with_ffi)]
, two allow-by-default lint #[allow(unused_drop_ffi_mark)]
and #[allow(missing_ffi_drop_impl)]
Motivation
With FFI interfaces, users may accidently use the wrong type which implement Drop
.
Currently such interface is NOT unsafe
but it might break many assumptions and easily cause a double-free.
Let us consider the following situation:
In R ffi, Rust could allocate R type (called SEXP
since R is a open source version of S), since R has a garbage collector, Rust must ensure the allocated vector is not recycled, thus we must PROTECT
that vector, before return, we must unprotect all the allocated vectors to tell R recycle the unused variables.
Here comes the question, rust encourage us writting a impl Drop for Protected<T>
to do such logic, but there is something different: In ffi interface, Protected<SEXP>
have the exactly same repr as SEXP
, and user may accidently wrote code like
extern "C" fn bad(a:SEXP)->Protected<SEXP>
extern "C" fn even_worse(a:Protected<SEXP>)->SEXP // cancel the protection of `a`, thus `a` might be recycled accidently.
Since it is statisticians rather than programmers who will write the ffi interface, for the base lib which provide SEXP
and Protected<SEXP>
, it is hard to tell statisticians not to use Protected<SEXP>
in ffi interface.
It might be wise to add a #[warn(drop_with_ffi)]
lint, let user know Protected<SEXP>
is a rust type which cannot be send across ffi boundary.
The most surprising thing is that, send a impl Drop
type across ffi is not marked as unsafe
.. I don't think other language (even Rust itself) can deal with such extern call very well. Maybe the better practice is that, use unsafe
block with the parameter that impl Drop.
Detailed Design
This RFC adds a new grammar for both parameter and the return type. Suppose we have
struct Foo {...}
impl Drop for Foo {...}
Then for any extern fn
, these should issue an warning
/// warning: parameter `a` has type `Foo`, which implemented `Drop` trait, might not safe with ffi calls.
/// note: `#[warn(drop_with_ffi)]` on by default
/// note: use `extern fn bad(unsafe a: Foo impl Drop)` instead.
extern fn bad(a:Foo) {...}
/// warning: the returun type `Foo` implemented `Drop`, its `Drop` implementation might not be called, which might cause memory leak and other .
/// note: `#[warn(drop_with_ffi)]` on by default
/// note: use `extern fn bad2() -> unsafe Foo impl Drop` instead.
extern fn bad2()->Foo {...}
If sending such variable with Drop implemented is carefully designed and considered safe, a lint
#[allow(safe_drop_with_ffi)]
could be added
/// warning: parameter `a` has `Drop` implemented, might not safe enough with ffi calls.
/// note: use `extern fn bad(unsafe a: Foo impl Drop)` instead
/// note: If such function is carefully designed, you could add a lint #[allow(safe_drop_with_ffi)]
/// note: `#[warn(safe_drop_with_ffi)]` on by default
extern fn bad(a:Foo impl Drop) {...}
/// warning: the return type has `Drop` implemented, might not safe enough with ffi calls.
/// note: use `extern fn bad() -> unsafe Foo impl Drop` instead
/// note: If such function is carefully designed, you could add a lint #[allow(safe_drop_with_ffi)]
/// note: `#[warn(safe_drop_with_ffi)]` on by default
extern fn bad2()->Foo impl Drop{...}
these should works fine, file no warning by default.
/// normal rust function, should not yield any warning about Foo impl Drop
fn good(a:Foo) { ... }
/// unnccessary Drop, allow by default
fn good2(a:Foo impl Drop) { ... }
/// i32 is not Drop, thus the code is safe, allow by default even if `#[deny(safe_drop_with_ffi)]` is set
extern fn good3(a:i32 impl Drop) { ... }
/// I'm unsafe function, use me wisely:)
unsafe extern fn good4(a:Foo) { ... }
Drawbacks
- This RFC may warn about
extern "Rust" fn bad(a:Foo)
and evenextern "rust-call" fn bad2(a:Foo)
. I have no idea what is the meaning of suchextern
. - for a pass-through function, the following function should be safe enough (since there is no need to call
Drop
function), but the lint would generate warnings.
extern fn pass_through(a:Foo)->Foo {
// deal with a
return a; // thus whether Foo impl Drop is not the concern.
}
Alternatives
- Using a lot of magic things (like macro 2.0) to prevent user writting
Protect<SEXP>
in either the return type or the parameter location. - Writting documents to let user know which type is allowed in ffi interface.
- Adding a
#[deny(no_across_ffi)]
lint and a attribute#[no_across_ffi]
, then we could directly write#[no_across_ffi]struct Protect<T>{...}
Unresolved questions
- Should we add such lint to
extern "Rust" fn
orextern "rust-call" fn
or evenextern "rust-intrinsic" fn
calls? Perhaps anunsafe
is needed - Is there a better grammar dealing with
pass_through
functions?