Skip to content

Releases: Taaitaaiger/jlrs

V0.21.1

10 Nov 20:54
f783f7c
Compare
Choose a tag to compare
v0.21.1

Bump version (#155)

V0.21.0

11 Jul 18:01
6841a62
Compare
Choose a tag to compare

v0.21

  • Support generating bindings for Julia enums with integer base types in combination with JlrsCore.Reflect and the Enum derive macro.

  • Fix several bugs related to how type parameters are handled in code generated by julia_module!.

  • TypedValue::as_typed_ranked_array and TypedValue::as_typed_array have been added, these methods convert a type value with an array type constructor to the corresponding Typed(Ranked)Array type.

V0.20.0

22 Jun 19:17
057ae08
Compare
Choose a tag to compare

Version 0.20 is a big release that introduces a few new features. This version supports Julia 1.6 up to and including 1.11, the MSRV is 1.77.

Low-level changes

  • jl-sys redesign

Historically jlrs exposed many implementation details of the Julia C API and reimplemented others in Rust. Some of these reimplementations existed in jl-sys, others in jlrs. This version changes this completely: jl-sys hides as many implementation details from Rust as possible. For example, most pointers to Julia data are exposed as opaque pointers to Rust; if a specific field must be accessed, a function written in C is used to do so.

  • LTO

The major drawback of this previous change is that it introduces a lot of overhead: what used to be a simple pointer dereference is now a function call. On Linux this lost performance can be restored by enabling the lto feature and setting several flags to compile with support for cross-language LTO. See the docs for the prerequisites and relevant flags.

Runtime module changes

Several types and traits have been renamed:

  • RuntimeBuilder to Builder
  • AsyncRuntimeBuilder to AsyncBuilder
  • AsyncRuntime to Executor
  • AsyncJulia to AsyncHandle

The sync runtime, Julia, has been deprecated in favor of the local runtime, LocalHandle. Types like LocalHandle and AsyncHandle should be considered handles to the Julia runtime, which shuts down when all handles have been dropped. More on these handles later.

Builder

Added features:

  • Multithreaded runtime

When the multi-rt feature is enabled, Julia can be initialized in a way that allows directly calling into Julia from multiple threads. More info about this feature can be found in the section about the multithreaded runtime. Both builder types provide a start_mt and spawn_mt method.

  • Thread count

The number of (interactive) threads available to Julia can be set by Builder, not just the AsyncBuilder.

  • Starting and spawing runtimes

Some runtimes can either be started or spawned. If it's started, Julia is initialized on the current thread and a new thread is spawned and provided with a handle to the runtime. If it's spawned, Julia is initialized on a separate thread and a handle is provided directly.

Changed features:

  • Safety

Initializing Julia is now considered safe. Using a custom system image is unsafe.

Dropped features:

  • Worker threads

Support for worker threads has been dropped, Async(Runtime)Builder::n_worker_threads has been removed.

Local runtime

Added features:

  • Local runtime

The main difference between the sync and local runtimes is that the local runtime is started without a Stack. As such, the local runtime's LocalHandle can only create statically-sized local scopes, and is unable to create dynamically-sized scopes.

  • Using

LocalHandle::using/Julia::using can be called to evaluate a using statement.

Deprecated features:

  • Sync runtime

The sync runtime has been deprecated in favor of the local runtime.

Async runtime

Added features

  • Using

AsyncHandle::using can be called to evaluate a using statement.

  • Closing

An AsyncHandle can be closed by calling AsyncHandle::close, this shuts down the runtime or pool. More on pools later. You can check if an AsyncHandle has been closed by calling AsyncHandle::is_closed.

Changed features:

  • Task return channel

Async and persistent tasks always use a tokio oneshot channel to return their result. Methods like AsyncHandle::task no longer take the sending half of a return channel, rather the receiving end of the oneshot channel is returned after the task has been successfully dispatched.

Dropped features:

  • async-std

The only implementation of Executor provided by jlrs is Tokio. If you must use async-std, you must implement the Executor trait yourself.

  • Worker threads

Support for using the async runtime with worker threads has been dropped.

  • Resizing the backing channel

The capacity of the channel used to communicate with the async runtime can no longer be adjusted.

  • Posting blocking tasks

Support for posting blocking tasks has been dropped.

Multithreaded runtime

Added features

  • Multithreaded runtime

With the multithreaded runtime you can call into Julia from arbitrary threads as long as you have an MtHandle. This handle can be cloned and sent to other threads. The Julia runtime shuts down when all handles have been dropped.

  • Thread pools

The multithreaded and async runtimes can be used together to create thread pools that can call into Julia. This pool behaves just like an async runtime, and is initeracted with using an AsyncHandle. Workers can be added or removed with AsyncHandle::try_add_worker and AsyncHandle::try_remove_worker.

Handles

Added features

  • Weak handle

A WeakHandle is similar to a LocalHandle and is a safe alternative for the Unrooted target.

  • Delegated handle

A DelegatedHandle is also similar to a LocalHandle and is only available in delegated tasks. More on delegated tasks later.

  • WithStack

The local and multithreaded runtimes don't allocate a Stack, the WithStack trait can be used to provide these handles with a Stack to enable using dynamically-sized scopes.

Changed featues

  • CCall

CCall is now considered a handle instead of existing at top-level.

Memory module changes

Scope

Added features

  • Unsized local scopes

Unsized local scopes take their size as an argument at runtime rather than as a const generic.

Changed features

  • Scope and LocalScope traits

Methods like GcFrame::scope and GcFrame::local_scope have been moved to the Scope and LocalScope traits respectively. These traits have a type parameter, the type of the data returned by the scope.

  • Arbitrary return type

Scopes are no longer required to return a JlrsResult, but can return data of any type. If the compiler is unable to infer the return type, annotate the closure or use the Returning::returning/LocalReturning::local_returning methods.

Data module changes

Layout

Added features

  • Complex numbers

When the complex feature is enabled, jlrs maps num's Complex type to Julia's Complex type.

Dropped features

  • SSA values

The SSAValue type has been removed.

Managed

Added features

  • Static symbols

Symbols can now be cached by defining a static symbol with the define_static_symbol or define_static_binary_symbol macros.

Changed features

  • Array redesign: ArrayBase

The array module has been changed quite significantly. There is now a single base type for arrays, ArrayBase. Previously independent types like Array and TypedArray are now aliases for the ArrayBase type.

  • Array redesign: ArrayBase parameters

ArrayBase has two parameters, a type parameter T and a const isize N. T is either Unknown to indicate the element type is unknown, or some type that implements ConstructType. In the latter case the type serves as the type constuctor of the element type. Previously T could refer to the layout of the elements in the array, this is no longer the case. The const isize N is the rank of the array if N >= 0, otherwise it is -1 to indicate the rank of the array is unknown.

  • Array redesign: Array constructors

Array constructors are implemented for ArrayBase, rather than the old separate types having separate constructor methods. Which methods are available depends on wheter or not the element type is statically known; ArrayBase::new is only available if T: ConstructType. If T is Unknown, ArrayBase::new_for is available which takes the element type as an argument.

  • Array redesign: Array accessors

Array accessors have had the opposite treatment compared to arrays: the single accessor type and its many aliases have been disentangled into separate types. There's an accessor for each kind of array layout, distinction between mutable and immutable accessors is also made at the type level.

  • Expr promotion

The Expr type has been promoted from an internal to a managed type.

Dropped features

  • Internal types

jlrs used to expose many internal types if the internal-types feature was enabled. These types and that feature have been removed. YAGNI.

  • Many field accessors

Many accessors for specific fields of managed types have been removed, only accessors for useful fields have been retained. These fields are mostly implementation details of Julia and prone to change, while offering little to no value to external users.

  • Array manipulation methods

Several array manipulation methods have been completely removed, e.g. Array::reshape. These functions are no longer available in the C API in Julia 1.11.

Types

Added features

  • Fast type and array keys

Types that implement ConstructType and have type parameters have to use a relatively slow caching mechanism, while types that don't have type parameters can use a much faster mechanism. The define_fast_key and define_fast_array_key macros let you define zero-sized types for specific cases that can use the fast caching mechanism.

V0.19.2

11 Nov 14:48
a630ab5
Compare
Choose a tag to compare

This release fixes the bug that prevented packages like RustFFT.jl to precompile on Julia 1.10.

V0.19.1

07 Oct 18:06
b0f6691
Compare
Choose a tag to compare

This release provides updated bindings and fixes a bug on Windows when Julia 1.10 is used by adding jl_small_typeof to the bindings.

V0.19.0

30 Jul 17:10
a28acae
Compare
Choose a tag to compare

Version 0.19 puts a major focus on performance, which has led to the introduction of several new features. This release is also compatible with the recent first beta of Julia 1.10, except on Windows.

Fast TLS

Julia's fast TLS is enabled when a runtime feature is selected. Libraries that are loaded by Julia, e.g. rustfft-jl, must not enable any runtime features. In order for this to function correctly applications that embed Julia must be compiled with the following flag: -Clink-args=-Wl,-export-dynamic.

Local scopes

Local scopes with the local targets LocalGcFrame, LocalOutput, and LocalReusableSlot have been added. Local scopes are very similar to the dynamic scopes and targets that have existed for a while with a few key differences: a LocalGcFrame has a constant size and is allocated on the stack. Local scopes can be created using arbitrary targets, thanks to this, functions that used to take an ExtendedTarget now take a Target and create a local scope.

Overall, dynamically-sized scopes and constantly-sized local scopes have similar performance characteristics, with one major exception: creating the "first" dynamic scope and resizing its backing storage is expensive. For embedders this shouldn't be a major problem because this happens when Julia is initialized, but for exporters it can be a major issue if an exported function creates a dynamic scope. The cost associated with creating an initial dynamic scope must be paid every time the function is called. For this reason it's strongly recommended that exporters limit themselves to using local scopes.

Caches

Symbols and types are cached after they've been constructed. A global in a module can also be cached by calling Module::typed_global_cached but you have to ensure that the cached global is never replaced by other data in Julia. These caches are typically implemented as HashMaps protected with an RwLock, the hash functions that are used have been selected with care based on benchmarks. If a type has no type parameters it's cached in a separate, local static variable rather than the global cache to avoid the locking-and-looking overhead.

GC-safe synchronization primitives

While the caches mentioned above are great for performance there is a pretty big issue: what happens if a number of threads are waiting for access to a cache and the thread that holds the lock starts waiting for garbage to be collected? The answer is, unsurprisingly, that the whole process grinds to a halt because we've reached a deadlock; the cache won't be unlocked until garbage has been collected, and garbage can't be collected while those threads are waiting for the cache to be unlocked. To prevent this deadlocj from happening, wrappers around RwLock, Mutex and FairMutex from parking-lot, and the sync version of OnceCell from once-cell have been added which allow for garbage to be collected while they block. The latter is used rather than OnceLock from the standard library to avoid having to increase the minimum supported version of Rust, which is still 1.65.

Catching exceptions

Exceptions can be caught with catch_exceptions which takes two arguments: a closure which is called in a try-block, and another closure that is called on exceptions with the caught one.

This is probably the single most dangerous function you can use in jlrs today due to the way how exceptions work in Julia: setjmp and longjmp. When an exception is thrown longjmp is called and control flow jumps to the last place setjmp has been called, if there is any cleanup code associated with a Rust function that is jumped over it won't be called. To what degree this is sound is not completely clear to me, but from what I can tell it should be fine if the stack frames of all Rust functions that are jumped over are Plain Old Frames (POFs), which can be trivially deallocated because they have no cleanup code associated with them. A stack frame is a POF if there are no pending drops, so you must ensure that there are no pending drops before calling a function that might throw.

If you do depend on some data that implements Drop and use it across a function that may throw, you can attach a parachute if that data has no lifetimes and is thread-safe by calling AttachParachute::attach_parachute. This moves ownership of the data to Julia and lets the GC clean it up.

The advantage of catch_exceptions over functions that catch exceptions is that if you need to call multiple functions that may throw, you only create one try-catch block rather than one for every function you call and creating a try-catch block is quite expensive. It's fine to create local scopes in the fallible closure and jumping out of them, they don't depend on Drop, jumping out of dynamic scopes is unsound.

Export improvements

Exported methods and functions can now be annotated with #[gc_safe] and #[untracked_self]. The first can be used by long-running functions that don't need to call into Julia to allow the GC to collect garbage while that function is being called. The second one only affects methods that take self in some way, when it's used the self parameter is not tracked before it's accessed. While tracking is useful to enforce that Rust's borrowing rules are respected, it is quite expensive and depends on Drop so it's nice to have the option to skip it.

Exported types with type parameters can be created by implementing ParametricVariant and ParametricBase. You can export the implementations for each type you care about by iterating over an array types, this also applies to exported methods and functions:

julia_module! {
    for T in [f32, f64] {
        for U in [T, i32] {
            struct HasParams<T, U>;
            in HasParams<T, U> fn get(&self) -> T as has_params_get;            
        }

        fn has_generic(t: T) -> T;
    }
}

It's also possible to throw an exception from an exported function. This used to require creating a RustResult to return the exception to Julia before throwing it to avoid jumping over Rust code. This is no longer necessary, an exported function can return either JlrsResult<T> or Result<T, ValueRet>, if an error is returned it's converted to a JlrsCore.JlrsError or thrown directly respectively. It's guaranteed this exception only jumps over POFs, RustResult has been deprecated.

IsBits and HasLayout derive traits

When bindings are generated with JlrsCore.Reflect two new traits are derived when applicable: IsBits and HasLayout. The first indicates that the type is an isbits type if all type parameters that affect its layout as isbits types. The second connects implementations of ConstructType to ValidLayout, it's only derived for types that have separate layout and type constructor types.

The main purpose of IsBits is that it can be used with Value::new_bits, this is a more generic variation of Value::new that can be used with types that implement IsBits rather than just those which implement IntoJulia which can't have any type parameters.

Docs.rs
Crate
GitHub

V0.18.0

13 May 17:21
ec936fd
Compare
Choose a tag to compare

Version 0.18 brings a new major feature to jlrs: the ability to export types and functions written in Rust to Julia. Like CxxWrap.jl, these exported items can be made available in Julia with very little code. Libraries that use this feature can be distributed as a JLL package.

Other changes mostly serve to facilitate this new feature.

Improved version and platform support

Previously jlrs only supported the stable and LTS versions of Julia, this restriction has been relaxed and jlrs now supports Julia 1.6 up to and including 1.9. A default version is no longer assumed, you must always enable a version feature to indicate what version of Julia you're targeting.

The most important improvement regarding platform support is that macOS has finally been added to the list of supported platforms. Both embedding and binding are actively tested, but only with CI. Some other platforms, including FreeBSD, might work, but this hasn't been tested. The only platform that fails to build with BinaryBuilder.jl, which is used to build the previously mentioned JLL packages, is 32-bit Windows.

Disentangled wrappers

A lot of things were called wrappers, even though some of those things didn't even really wrap something. The wrappers concept has been ditched completely; it's about Julia data. This data has a type, a layout, and is managed by Julia's GC. Modules have been renamed and reorganized to better reflect that.

Async tasks and worker threads

If Julia 1.9 is used, jlrs can start the async runtime with additional worker threads. Async tasks must now set their thread affinity with the associated Affinity type. Accepted types are DispatchAny, DispatchMain, and DispatchWorker.

JlrsCore.jl package

In order to function correctly, jlrs depends on some code written in Julia. This code used to be included as a string an evaluated at runtime, but is now distributed as a separate package: JlrsCore.jl. This package is automatically installed if it's not yet available if you embed Julia in a Rust application, but you can opt out of this behavior with the RuntimeBuilder.

The code of the JlrsReflect.jl package has been moved to this package, and is now available as the JlrsCore.Reflect module.

The JlrsCore.Wrap module provides two macros, @wrapmodule and @initjlrs, to make items exported by a library written in Rust available in Julia.

Type constructors

It's now possible to construct a Julia type object from a Rust type using the ConstructType trait. This trait allows for arbitrarily complex type objects to be constructed as long as they can be expressed as a type that implements this trait in Rust.

Type constructors have been added for all abstract types from the Base and Core modules. The bindings generated by JlrsCore.Reflect implement this trait when no type parameters are elided, otherwise another binding is generated that doesn't elide any type parameters and implements this trait.

Type constructors for all abstract DataTypes and UnionAlls from the Base and Core modules have been added to the types module, and can be generated with JlrsCore.Reflect for other abstract types.

Opaque types

There are now two ways that a Rust type can be exposed to Julia: either as an opaque or a foreign type. The main difference is that an opaque type can't contain references to Julia data, while a foreign type can and requires implementing a custom mark function. To make a type exposable as an opaque type, all you need to do is implement the OpaqueType marker trait.

jlrs-macros crate

The jlrs-derive crate has been replaced with jlrs-macros. In addition to the derive macros, it provides a julia_version and julia_module macros.

The julia_version macro can be used to conditionally compile code depending on the targeted Julia version. For example, the following function is only available is Julia 1.7 or 1.8 is targeted:

    #[julia_version(since = "1.7", until = "1.8")]
    fn example() { }

The julia_module macro generates an initialization function that is called by the code generated by the @wrapmodule macro from JlrsCore.Wrap. This initialization function creates DataTypes for all exported opaque and foreign types, exposes global and constant data, and returns enough information to generate functions that call the exported Rust functions.

This macro has a many more useful features:

  • Exported items can be documented with the #[doc] attribute, this documentation is available in the Julia REPL and can be included in documentation generated with Documenter.jl.

  • Exported items can be renamed, and the new name can end in an exclamation mark to indicate that it modifies its arguments. Existing functions in other modules can be extended by renaming them to their fully qualified name.

  • All arguments of an exported function must implement CCallArg and its return type must implement CCallReturn. These traits are used to create fully-typed function signatures and ccall invocations. Multiple functions can be exported with the same name as long as their argument types are different.

  • Methods of opaque and foreign types can be exported directly, the extern "C" function necessary to call this from Julia is generated automatically. These generated functions track self to ensure no aliasing rules are broken.

  • You can write functions that dispatch their work to a thread pool, the generated Julia code uses an AsyncCondition to wait for the dispatched work to complete.

This macro can be used as follows:

    julia_module! {
        // An initialization function named `example_init_fn` is generated
        become example_init_fn;
        
        // This struct must implement `OpaqueType` or `ForeignType`
        #[doc = "    MyExportedType"]
        #[doc = ""]
        #[doc = "A type implemented in Rust"]
        struct MyExportedType;
        
        // Export a constructor for `MyExportedType`
        in MyExportedType fn new(value: i32) -> TypedValueRet<MyExportedType> as MyExportedType;
        
        // Export a method that takes a (mutable) reference to `self`, such a method must return a
        // `RustResultRet` because tracking `self` can fail. This result is automatically
        // unwrapped on success, while an exception is thrown on failure.
        in MyExportedType fn increment(&mut self) -> RustResultRet<Nothing> as increment!;
        
        // When `self` is taken by value, `self` is tracked and cloned. 
        in MyExportedType fn get_cloned(self) -> RustResultRet<i32>;
    }

The crate must be compiled as a shared system library by setting the lib type to cdylib in the crate's Cargo.toml file. After compiling it, it can be loaded in Julia like this:

    module Example
    using JlrsCore.Wrap
    
    @wrapmodule("./path/to/libexample", :example_init_fn)
    
    function __init__()
        @initjlrs
    end
    end

V0.18.0-beta.5

10 May 21:24
256d9d9
Compare
Choose a tag to compare
Merge pull request #87 from Taaitaaiger/dev

Beta 5

V0.18.0-beta.4

09 May 21:19
ac703d6
Compare
Choose a tag to compare
Merge pull request #86 from Taaitaaiger/dev

Print meaningful message when using JlrsCore fails

V0.18.0-beta.3

06 May 10:57
b8b3357
Compare
Choose a tag to compare

Bugfix: GC frame was not popped from the GC stack when calling CCall::invoke on Windows.