[ANN] jlrs 0.14: MSVC support, runtime builder, improved return types, nested field access

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 ccallable functions in Rust. Version 0.14 introduces a few new features and many improvements. By default, this new version is compatible with Julia 1.7. Support for Julia 1.6 can be enabled by enabling the lts feature, support for Julia 1.8.0-rc1 by enabling the rc1 feature.

  • MSVC support

It’s now possible to use jlrs with the MSVC toolchain. However, since Julia doesn’t provide lib files and Rust doesn’t support linking with dll files directly as far as I’m aware, you must generate these files yourself. The def files required for this can be found in the repository, instructions are available in the readme and docs.

  • Builders and runtimes

When Julia is embedded in a Rust application it must be initialized before it can be used. To do so a RuntimeBuilder must be used. This builder lets you configure several options like a custom system image, the number of threads Julia can use, and the backing runtime and channel for an async runtime. Both the tokio-rt and async-std-rt feature can now be enabled simultaneously, you can implement your own backing runtime by implementing the AsyncRuntime trait.

  • Scopes

Methods that call the Julia C API can only be called from a scope. Every scope has its own GC frame which is used to root Julia data, ensuring it can be used until that scope ends. Previous versions of jlrs provided specific methods to create a scope that returns Julia data rooted in a parent scope. The major drawback of that approach was that every method that returned Julia data had to return it as a Value. These custom methods have been removed. A method that returns rooted Julia data can be called with an Output reserved in some GC frame or a mutable reference to the current GC frame, the lifetime of the returned data depends on which was used. This lifetime ensures the data can be returned from the scope as long as it has been rooted in an older frame.

As a result of this change, many methods now return Julia data of the correct type. For example Array::new returns an Array and JuliaString::new a JuliaString.

  • FieldAccessor

The new FieldAccessor allows accessing arbitrarily deeply nested data in a Value directly, without boxing intermediate levels or needing to know their layout. The FieldAccessor can handle arbitrary field types, including arrays and bits unions and supports accessing atomic fields.

  • Example
use jlrs::prelude::*;

fn example() {
    // Initializing Julia is unsafe because it can race with another crate
    // that does the same.
    let mut julia = unsafe { RuntimeBuilder::new().start().unwrap() };

    // Create a base scope
    julia
        .scope(|global, mut frame| {
            let mut data = vec![1u64, 2u64, 3u64, 4u64];
            let output = frame.output()?;

            // Create a nested scope and create a new array backed by data borrowed
            // from Rust. The `Output` must be converted to an `OutputScope` because
            // `Array::from_slice` needs to allocate temporary data.
            // This only serves to illustrate that this data can be returned from a
            // nested scope.
            let arr = frame.scope(|mut frame| {
                let output_scope = output.into_scope(&mut frame);
                Array::from_slice(output_scope, &mut 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.
            let sum_fn = Module::base(global).function(&mut frame, "sum")?;
            let result = unsafe { sum_fn.call1(&mut frame, arr.as_value()) }?
                .into_jlrs_result()?
                .unbox::<u64>()?;

            assert_eq!(result, 10);


            // Create a new tuple: ((1, 2),)
            let tuple = Value::new(&mut frame, Tuple1(Tuple2(1u64, 2u64)))?;
                
            // Access the second field of the inner tuple:
            let elem = tuple
                .field_accessor(&frame)
                .field(0)?
                .field(1)?
                .access::<u64>()?;

            assert_eq!(elem, 2);

            Ok(())
    })
    .unwrap();
}

Docs.rs
Crate
Github

13 Likes