jlrs is a crate for the Rust programming language that provides access to most of the Julia C API, it can be used to embed Julia in Rust applications and to use functionality from the Julia C API when writing ccall
able functions in Rust. Version 0.17 is compatible with Julia 1.8 by default, 1.6 if the lts
feature is enabled, and Julia 1.9 if the beta
feature is enabled. The minimum supported version of Rust is 1.65.
Memory management
The biggest change in this version is a redesign of how jlrs ensures the Julia GC is aware of data that is currently used in Rust. This is now handled by a custom resizable stack, which has several advantages over the old system. For example, methods can no longer fail due to running out of stack space; methods like Value::new
and Call::call
that could only fail due to this limitation are now infallible and no longer return a Result
. The async runtime can allocate a stack for each task which gets rid of the Mode
trait. And, it’s now possible for a function called through ccall
to throw an exception with CCall::throw_exception
.
The Scope
and PartialScope
traits, which were previously used to ensure that newly allocated Julia data would remain valid by rooting that data, have been replaced with the Target
trait. Unlike the old traits, when a target is used it depends on the type of the target whether or not the returned data is rooted. Thanks to this property, all methods that take a target can return either rooted or unrooted data. As a result, all methods that returned unrooted data, e.g. Call::call_unrooted
, have been removed because this can now be achieved by calling Call::call
with a non-rooting target. Methods that used to take an argument that implemented Scope
now take an ExtendedTarget
instead, which isn’t restricted to using an Output
for the final result but can use any target.
The Global
struct has been renamed to Unrooted
because it’s a target that leaves the result unrooted. Any immutable reference to a target is a valid non-rooting target, you should rarely need to use an Unrooted
compared to the more prominent role Global
used to play. Methods and closures that previously took both a Global
and a GcFrame
now only take a GcFrame
to account for this.
Layout changes
Because all methods that return Julia data can return rooted or unrooted data depending on the used target, I’ve decided to get rid of a major difference between Ref<T: Wrapper>
and T: Wrapper
: both types now contain a non-null pointer internally. Methods that previously returned a Ref
now take a target, if the data might be undefined the return type is wrapped in an Option
.
The ValidLayout
trait has been split into a ValidLayout
and ValidField
trait. ValidLayout
can be implemented by all types that provide a valid layout for some type of Julia data, while ValidField
can only be implemented by types that provide a valid layout when the type is used as a field type.
Due to these changes all bindings previously generated with JlrsReflect.jl are now invalid. A new version of that package has been released, generated bindings will derive ValidField
in addition to ValidLayout
whenever applicable.
Improved borrow checking
Array
s and Value
s can be tracked, while they’re tracked it’s safe to access their contents. Tracking data no longer requires a GcFrame
, rather it’s tracked using a global ledger inspired by neon. Thanks to this change, multiple arrays can be mutably borrowed at the same time and ccall
ed functions that need to access array data no longer need to create a scope first.
Foreign types
You can now create custom foreign types by implementing the ForeignType
trait. Types that implement this trait can be moved from Rust to Julia, making Julia’s GC responsible for freeing this data. This can be useful if you want to embed a Rust crate into a Julia package, but is also used internally to implement the previously mentioned dynamic stack.
Kind-of multithread async runtime
Blocking tasks can now be posted to Julia rather than being called from the runtime thread, which allows multiple blocking tasks to run in parallel. When a blocking task is posted a new thread is spawned in Julia which calls the blocking task through ccall
.
A custom channel is now used to send tasks to the async runtime. The reason for this change is that the async runtime can make use of multiple threads when the beta
feature is enabled.
Julia 1.9 – actually multithreaded async runtime
The most exciting feature added to Julia 1.9 IMO is thread adoption, which allows multiple foreign threads to call into Julia. This lets jlrs create additional worker threads for the async runtime. The number of worker threads can be set with the runtime builder, when a task is sent to the runtime it can be handled by either the runtime thread or any of the worker threads.
Julia 1.9 can also make use of two thread pools, :default
and :interactive
. The number of threads allocated to this pool can be set with the runtime builder. The methods of the CallAsync
trait all target one of these thread pools.
No more import libraries
Import libraries don’t have to be generated anymore when using jlrs with the MSVC toolchain. Rust used to require these import libraries, but with the recently stabilized support for linking against a raw dylib this is no longer necessary.
Example
use jlrs::prelude::*;
fn main() {
// Initializing Julia is unsafe because it can race with another crate
// that does the same.
let mut julia = unsafe { RuntimeBuilder::new().start().unwrap() };
let mut frame = StackFrame::new();
let mut julia = julia.instance(&mut frame);
// Create a scope to call into Julia
julia
.scope(|mut frame| {
// Create a new two-dimensional Julia array from a `Vec`.
let data = vec![1u64, 2u64, 3u64, 4u64];
let arr = Array::from_vec(frame.as_extended_target(), data, (2, 2))?
.into_jlrs_result()?;
// Sum all the elements in the array. Calling a Julia function is unsafe
// because it can do arbitrary things. Summing the elements of this array is
// is safe because we've just created it.
let sum_fn = Module::base(&frame).function(&mut frame, "sum")?;
let result = unsafe { sum_fn.call1(&mut frame, arr.as_value()) }
.into_jlrs_result()?
.unbox::<u64>()?;
assert_eq!(result, 10);
Ok(())
})
.unwrap();
}