jlrs 0.2.1

jlrs is a library built around bindings to the Julia C API that enables Julia code to be called from Rust. jlrs can move many kinds of data from Rust to Julia and back, share n-dimensional numerical arrays with Julia directly, call arbitrary functions, and load code from arbitrary Julia source files. jlrs currently only supports Linux.
Documentation

jlrs

Build Status Coverage Status Rust Docs License:MIT

jlrs

The main goal behind jlrs is to provide a simple and safe interface to the Julia C API. Currently this crate has only been tested on Linux, if you try to use it on another OS it will likely fail to generate the bindings to Julia. This crate is currently tested with Julia v1.4.0.

Usage

Add this to your Cargo.toml:

[dependencies]
jlrs = "0.2"

This crate depends on jl-sys which contains the raw bindings to the Julia C API, these are generated by bindgen. The recommended way to install Julia is to download the binaries from the official website, which is distributed in an archive containing a directory called julia-x.y.z. This directory contains several other directories, including a bin directory containing the julia executable.

In order to ensure the julia.h header file can be found, you have to set the JL_PATH environment variable to /path/to/julia-x.y.z. Similarly, in order to load libjulia.so you must add /path/to/julia-x.y.z/lib to the LD_LIBRARY_PATH environment variable. If they can be found at the standard locations, e.g. because you've installed Julia through your package manager, this is not necessary and things should build without setting the JL_PATH environment variable.

Features

A small and incomplete list of features that jlrs supports:

  • Call arbitrary functions from the Julia standard library.
  • Include and call your own Julia code.
  • Convert numbers, strings, n-dimensional arrays and more from Rust to Julia and back.
  • Safely borrow array data from Rust.

Interacting with Julia

The first thing you should do is use the prelude-module with an asterisk, this will bring all the structs and traits you're likely to need in scope. Before you can use Julia it must first be initialized. You do this by calling Julia::init, this method forces you to pick a stack size. You will learn how to choose this value soon. Note that this method can only be called once, if you drop Julia you won't be able to create a new one and have to restart the entire program.

You can call Julia::include to include your own Julia code and either Julia::frame or Julia::dynamic_frame to interact with Julia. If you want to create arrays with more than three dimensions or borrow arrays with more than one, jlrs.jl must be icluded. You can find this file in the root of this crate's github repository. This is necessary because this functionality currently depends on some Julia code defined in that file.

The other two methods, Julia::frame and Julia::dynamic_frame, take a closure that provides you with a StaticFrame and a DynamicFrame respectively. Both types implement the Frame trait. These frames are used to create new values, access Julia modules and their functions, call functions, and copy data back to Rust. Additionally, frames can be nested; you're free to mix static and dynamic frames. The main reason things work this way is that it ensures that all active values are protected from being freed by Julia's garbage collector. Each frame takes at least two slots on the stack whose size was chosen when you initialized Julia, plus an additional one for each value you create and function you call. A StaticFrame forces you to choose the number of slots that will be available, while a DynamicFrame grows dynamically. The slots that were used are reclaimed when the frame goes out of scope.

In order to call a Julia function, you'll need two things: a function to call, and arguments to call it with. You can acquire the function through the module that defines it with Module::function; Module::base and Module::core provide access to Julia's Base and Core module respectively, while everything you include through Julia::include is made available relative to the Main module which you can access by calling Module::main.

Most Julia data is represented by a Value. Basic data types like numbers, booleans, and strings can be created through Value::new and several methods exist to create an n-dimensional array. Julia functions, their arguments and their results are all Values. All Values can be called as functions, whether this will succeed depends on the value actually being a function. You can copy data from Julia to Rust by calling Value::try_unbox.

As a simple example, let's create two values and add them:

use jlrs::prelude::*;
fn main() {
    let mut julia = unsafe { Julia::init(16).unwrap() };
    let x = julia.dynamic_frame(|frame| {
        // Create the two arguments
        let i = Value::new(frame, 2u64)?;
        let j = Value::new(frame, 1u32)?;

        // We can find the addition-function in the base module
        let func = Module::base(frame).function("+")?;

        // Call the function and unbox the result
        let output = func.call2(frame, i, j)?;
        output.try_unbox::<u64>()
    }).unwrap();

    assert_eq(x, 3);
}

You can also do this with a static frame:

use jlrs::prelude::*;
fn main() {
    let mut julia = unsafe { Julia::init(16).unwrap() };
    let x = julia.frame(3, |frame| {
        // Create the two arguments
        let i = Value::new(frame, 2u64)?;
        let j = Value::new(frame, 1u32)?;

        // We can find the addition-function in the base module
        let func = Module::base(frame).function("+")?;

        // Call the function and unbox the result
        let output = func.call2(frame, i, j)?;
        output.try_unbox::<u64>()
    }).unwrap();

    assert_eq(x, 3);
}

For more examples, you can take a look at this crate's integration tests.

Limitations

Calling Julia is entirely single-threaded. You won't be able to use Julia from another thread and while Julia is doing stuff you won't be able to interact with it.