[][src]Crate jlrs

The main goal behind jlrs is to provide a simple and safe interface to the Julia C API. Currently this crate is only tested on Linux and Windows in combination with Julia 1.4.2, if you try to use it on another OS or with an earlier version of Julia it will likely fail to generate the bindings or crash when these bindings are used.

Features

An incomplete list of features that are currently supported by jlrs:

  • Access arbitrary Julia modules and their contents.
  • Call arbitrary Julia functions.
  • Include and use your own Julia code.
  • Create values that Julia can use, and convert them back to Rust, from Rust.
  • Access the type information and fields of values and check their properties.
  • Support for mapping isbits tuples and structs to Rust structs.
  • Create and use n-dimensional arrays.

Generating the bindings

This crate depends on jl-sys which contains the raw bindings to the Julia C API, these are generated by bindgen. You can find the requirements for using bindgen in their User Guide.

Linux

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, either /usr/include/julia/julia.h must exist, or you have to set the JULIA_DIR environment variable to /path/to/julia-x.y.z. The environment variable can be used to override the default. Similarly, in order to load libjulia.so you must add /path/to/julia-x.y.z/lib to the LD_LIBRARY_PATH environment variable.

Windows

The recommended way to install Julia is to download the installer from the official website, which will install Julia in a folder called Julia-x.y.z. This folder contains several other folders, including a bin folder containing the julia.exe executable. You must set the JULIA_DIR environment variable to the Julia-x.y.z folder and add Julia-x.y.z\bin to the PATH environment variable. For example, if Julia is installed at D:\Julia-x.y.z, JULIA_DIR must be set to D:\Julia-x.y.z and D:\Julia-x.y.z\bin must be added to PATH.

Additionally, MinGW must be installed through Cygwin. To install this and all potentially required dependencies, follow steps 1-4 of the instructions for compiling Julia on Windows using Cygwin and MinGW. You must set the CYGWIN_DIR environment variable to the installation folder of Cygwin; this folder contains some icons, Cygwin.bat and folders with names like usr and bin. For example, if Cygwin is installed at D:\cygwin64, CYGWIN_DIR must be set to D:\cygwin64.

Julia is compatible with the GNU toolchain on Windows. If you use rustup, you can set the toolchain for a project that depends on jl-sys by calling the command rustup override set stable-gnu in the project root folder.

Using this crate

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 into scope. Before you can use Julia it must first be initialized. You do this by calling Julia::init. 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. If you want to use a custom system image, you must call Julia::init_with_image instead of Julia::init.

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 have improved support for backtraces jlrs.jl must be included. You can find this file in the root of this crate's github repository. This is necessary because this functionality 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 Global, and either a StaticFrame or DynamicFrame respectively. Global is a token that lets you access Julia modules their contents, and other global values, while the frames are used to deal with local Julia data.

Local data must be handled properly: Julia is a programming language with a garbage collector that is unaware of any references to data outside of Julia. In order to make it aware of this usage a stack must be maintained. You choose this stack's size when calling Julia::init. The elements of this stack are called stack frames; they contain a pointer to the previous frame, the number of protected values, and that number of pointers to values. The two frame types offered by jlrs take care of all the technical details, a DynamicFrame will grow to the required size while a StaticFrame has a definite number of slots. These frames can be nested (ie stacked) arbitrarily.

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.

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. Each value will be protected by a frame, and the two share a lifetime in order to enforce that a value can only be used as long as its protecting frame hasn't been dropped. Julia functions, their arguments and their results are all Values too. 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::cast.

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

let mut julia = unsafe { Julia::init(16).unwrap() };
julia.dynamic_frame(|global, 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(global).function("+")?;

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

You can also do this with a static frame:

let mut julia = unsafe { Julia::init(16).unwrap() };
// Three slots; two for the inputs and one for the output.
julia.frame(3, |global, frame| {
    // Create the two arguments, each value requires one slot
    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(global).function("+")?;

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

This is only a small example, other things can be done with Value as well: their fields can be accessed if the Value is some tuple or struct. They can contain more complex data; if a function returns an array or a module it will still be returned as a Value. There complex types are compatible with Value::cast. Additionally, you can create Outputs in a frame in order to protect a value from with a specific frame; this value will share that frame's lifetime.

Custom types

Two traits can be used to make your own structs work in combination with Value::new and Value::cast, JuliaTuple and JuliaStruct. The first can be used in combination with tuple structs in Rust, it will map to a tuple in Julia whose field types match the field types in Rust. The second can be used in combination with structs with named fields in Rust and must be explicitly mapped to a struct in Julia.

Lifetimes

While reading the documentation for this crate, you will see that a lot of lifetimes are used. Most of these lifetimes have a specific meaning:

  • 'base is the lifetime of a frame created through Julia::frame or Julia::dynamic_frame. This lifetime prevents you from using global Julia data outside of a frame.

  • 'frame is the lifetime of an arbitrary frame; in the base frame it will be the same as 'base. This lifetime prevents you from using Julia data after the frame that protects it from garbage collection goes out of scope.

  • 'data or 'borrow is the lifetime of data that is borrowed. This lifetime prevents you from mutably aliasing data and trying to use it after the borrowed data is dropped.

  • 'output is the lifetime of the frame that created the output. This lifetime ensures that when Julia data is protected by an older frame this data can be used until that frame goes out of scope.

Limitations

Calling Julia is entirely single-threaded. You won't be able to use Julia from another thread than the thread that has been used to initialize Julia, and while Julia is doing stuff you won't be able to interact with it.

Modules

error

Everything related to errors.

frame

Frames ensure Julia's garbage collector is properly managed.

global

Access token for global Julia data.

prelude

Reexports structs and traits you're likely to need.

traits

All traits used by this crate.

value

Julia values and functions.

Structs

Julia

This struct can be created only once during the lifetime of your program. You must create it with Julia::init or Julia::init_with_image before you can do anything related to Julia. While this struct exists, Julia is active; dropping it causes the shutdown code to be called.