Technically this does fall under the definition of "unsafe", but I think this case should be an exception to the rule, because it beats the purpose of "unsafe".
The meaning of unsafe
is not simply "this code may cause UB" - it's "this code may cause UB if some invariant is broken, and the compiler is not going to enforce that variant for you". The whole point of unsafe
is to draw as much attention to the invariant as possible, which means the unsafe
functions and types should have a Safety section in its documentation explaining what the invariant is and what may lead to breaking it, and the unsafe
blocks where they are used should have a SAFETY comment that describes how the invariant is maintained.
What are you going to write in the Safety section of rustyfoo::eval
's documentation? "This will result in undefined behavior if the foo code called does undefined behavior"? That's not very helpful.
And what are you going to write in the SAFETY comment where you call rustyfoo::eval
? "The 5000 LoC something_something.foo
script that this eval invokes is not doing anything suspicous"? That's far to broad to be assuring. Especially when that script can be provided by the user or some other third party plugin (which is one of the main reasons to use a scripting language)
The unsafe
, in this case, is doing nothing to improve safety, and I think it is kind of pointless to require it.
EDIT
Actually, now that I think about it, rustyfoo::eval
does not need to be unsafe
even if we are strict with the definition.
The reason FFI functions are unsafe
by default is not that we are snobs who think all other languages are unsafe (we are, but that's not the reason). Maybe the other language is using GC and other mechanisms to get safety at the cost of performance. Maybe that other language is actually Rust and it was using cdylib to produce a library we link against. It could be perfectly safe inside.
It doesn't matter.
The boundary is the thing that's unsafe.
FFI means using the C ABI, and doing anything non-trivial with it is probably going to involve pointers. Rust cannot enforce its invariants when it sends and receives pointers through the C ABI. The other language can't enforce its own invariants either (unless it's C in which case it has no language enforced invariants). And they certainly can't communicate these invariants through the C ABI. Hence - unsafe.
(even without pointers, the C ABI can be unsafe if you define the signature wrong. But that's something that's written in the extern
block and once there the compiler can enforce it, so it's relatively fine)
rustyfoo::eval
is not C ABI. The rustyfoo
crate should have mappings for the primitive foo types, representations for its builtin data structures, and some general wrappers for accessing its user defined types. The boundary between Rust and foo is safe, so Rust should not consider it unsafe
that foo can do UB internally.