Safety
Over the course of this tutorial we've seen a lot of unsafe code, but we've never collected the expectations in a single place.
The main idea behind how safety is modeled in jlrs is: a static view of Julia is safe. We're free to look at whatever's inside, as long as we don't execute any Julia code, either by evaluation or calling functions, or mutate data. Starting the runtime, creating scopes, accessing modules and other globals, accessing the fields of a Value
, this is all perfectly safe behavior. Creating new managed data and accessing it is also safe, the lifetime rules guarantee we can only access it while it's safe to do so.
Mutation is one of the main sources of unsafety, and is considered unsafe in general. There are many types which are mutable, but which must absolutely not be mutated. Take DataType
for example. Yes, it's mutable, but we're very likely to trigger UB if we mutate one. It's also possible to create multiple mutable references to the same data if we're not careful, this is particularly easy to mess up with array data. Whenever we mutate data, we must guarantee that we're allowed to mutate this data, that there are no other active references to this data, and that a write barrier is inserted if necessary. This last point is handled automatically in most cases, the main exception is direct mutable access by tracking a TypedValue
.
Another source of unsafety is calling Julia functions. There is no unsafe keyword in Julia, but there definitely are functions that are unsafe to call. The most obvious example is unsafe_load
, which lets us dereference arbitrary pointers. Its name might include unsafe
, but we can call it just as easily as any other Julia function. Other problems are mostly due to thread-safety, Julia doesn't provide the same guarantees safe Rust does. Nothing prevents a function from spawning or interacting with tasks that access global data, we need to take care that we don't mutably alias this data and properly synchronize between threads.
Evaluating arbitrary code is unsafe for the same reasons as calling Julia functions is. The same holds true for loading a custom system image.
Safe functions that perform some operation that may throw an exception will catch that exception, unchecked functions don't catch exceptions and may skip other important checks as well, rendering them unsafe. Not catching an exception is problematic in functions that are called with ccall
because we have to guarantee we don't jump over pending drops, using custom exception handlers is unsafe for the same reason. When Julia has been embedded, it's possible to write code that triggers an exception when no handler is available, e.g. by calling an unchecked function with invalid arguments in a scope. This is safe because the process is aborted if this happens.