[ANN] jlrs 0.12: improved compile times, colorful error messages and backtraces, generator tasks, and more!

jlrs is a crate that provides access to most of the Julia C API and can be used to embed Julia in Rust applications. Version 0.12 brings a few new improvements, mostly involving compile time and async capabilities. jlrs 0.12 is only compatible with Julia 1.6.

It has been a while since my last announcement here. Some of the main additions since version 0.5 are the ability to call functions with keyword arguments, inline and bits-union fields can be accessed directly by providing a Rust type with a compatible layout, and data that’s guaranteed to be rooted and data that’s not now have a different type which helps avoid using data that might have been freed by the garbage collector.

As for the additions in this version, let’s start with the compile times. While Rust can call functions written in C, it’s not possible to simply include the header file as you’d do in C or C++. Rather, it’s necessary to provide bindings written in Rust. This is mostly an automated process handled by bindgen. Previously, when jlrs was compiled bindgen always generated the bindings, which took a significant amount of time. Now, pregenerated bindings are included and used automatically. On my machine this leads to compile times that have been reduced by 40 to 75% for a clean debug build depending on the enabled features.

Another small improvement has to do with error messages. Since the previous version, jlrs can convert an exception to its error message and backtrace. It’s now possible to enable ANSI colors for these messages, allowing you to print them with the same colors as the Julia REPL.

The async runtime has received some significant improvements. The async runtime is an optional feature that initializes Julia on a separate thread. Its biggest advantage is that it supports an async task system that can be used to schedule a function call as a Julia Task, while this Task has not completed the async runtime automatically handles other async tasks. Previously only one kind of async task existed, the AsyncTask trait. It had the annoying limitation that all implementations of this trait had to return the same type of data, and it was unnecessarily aware of the channel that’s used to send the result back to the caller. Both these issues are now fixed, different implementations of AsyncTask can use different output types and they’re no longer aware of the return channel.

There are two new kinds of task that the async runtime can handle, blocking tasks and generator tasks. Blocking tasks are relatively simple, rather than implementing a trait you can provide a closure. If the closure is compatible with Julia::scope (i.e. the sync runtime) it’s likely a valid blocking task. Blocking tasks are executed as soon as they’re received, can’t be used to schedule new Tasks, and block the runtime until they’ve completed.

The other new kind of task, the generator task, is (in my opinion) a very fun and powerful new feature. In order to create such a task the GeneratorTask trait must be implemented. While blocking tasks and implementations of AsyncTask run once and return their result, a generator task can be called multiple times and has internal state. This internal state is initialized when the generator task starts running, afterwards a handle is returned which can be used to call it and shared across threads. When a generator task is initialized, a GC-frame is available that can be used to root Julia data. This GC-frame is not dropped until all handles to the task have been dropped, this means the internal state can contain Julia data rooted in that frame.

One final note is that Julia 1.6 might become the new LTS version. If that’s the case, jlrs will keep supporting this version in the future.