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)
unsafe, in this case, is doing nothing to improve safety, and I think it is kind of pointless to require it.
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.