Expand description
Julia memory management.
All Julia data (objects) is owned by the garbage collector (GC). The GC is a mark-and-sweep GC, during the mark phase all objects that can be reached from a root are marked as reachable, during the sweep phase all unreachable objects are freed.
Roots are pointers to objects that are present on the GC stack. The GC stack is a linked list of GC frames. A C function that needs to protect one or more objects allocates an appropriately-sized GC frame and pushes it to the GC stack, typically at the beginning of the function call, stores the pointers to objects that are returned by the Julia C API in that frame, and pops the frame from the GC stack before returning from the function.
There are several problems with this approach from a Rust perspective. The API is pretty
unsafe because it depends on manually pushing and popping GC frame to and from the GC stack,
and manually inserting pointers into the frame. More importantly, the GC frame is a
dynamically-sized type that is allocated on the stack with alloca
. This is simply not
supported by Rust, at least not without jumping through a number awkward and limiting hoops.
In order to work around these issues, jlrs doesn’t store the roots in the frame, but uses a
custom object that contains a Vec
of roots (stack). This stack is not part of the public
API, you can only interact with it indirectly by creating a scope first. The sync runtime lets
you create one directly with Julia::scope
, this method takes a closure which takes a
single argument, a GcFrame
. This GcFrame
can access the internal stack and push new
roots to it. The roots that are associated with a GcFrame
are popped from the stack after
the closure returns.
Rather than returning raw pointers to objects, jlrs wraps these pointers in types that
implement the Managed
trait. Methods that return such types typically take an argument
that implements the Target
trait, which ensures the object is rooted. The returned managed
type inherits the lifetime of the target to ensure this data can’t be used after the scope
whose GcFrame
has rooted it ends. Mutable references to GcFrame
implement Target
, when
one is used as a target the returned data remains rooted until the scope ends.
Often you’ll need to create some Julia data that doesn’t need to live as long as the current
scope. A nested scope, with its own GcFrame
, can be created by calling GcFrame::scope
.
In order to return managed data from a child scope it has to be rooted in the GcFrame
of a
parent scope, but this GcFrame
can’t be accessed from the child scope. Instead, you can
create an Output
by reserving a slot on the stack by calling GcFrame::output
. When an
Output
is used as a target, the reserved slot is used to root the data and the returned
managed type inherits the lifetime of the parent scope, allowing it to be returned from the
child scope.
Several other target types exist, they can all be created through methods defined for
GcFrame
. You can find more information about them in the target
module.
Not all targets root the returned data. If you never need to use the data (e.g. because you
call a function that returns nothing
), or you access a global value in a module that is
never mutated while you’re using it, the result doesn’t need to be rooted. A reference to a
rooting target is guaranteed to be a valid non-rooting target. When a non-rooting target is
used, the function doesn’t return an instance of a managed type, but a Ref
to a managed
type to indicate the data has not been rooted.
Modules
- Manage the garbage collector.
- A raw, stack-allocated GC frame.
- Targets for methods that return Julia data.