Releases: Taaitaaiger/jlrs
V0.21.1
V0.21.0
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
andTypedValue::as_typed_array
have been added, these methods convert a type value with an array type constructor to the correspondingTyped(Ranked)Array
type.
V0.20.0
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
toBuilder
AsyncRuntimeBuilder
toAsyncBuilder
AsyncRuntime
toExecutor
AsyncJulia
toAsyncHandle
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
andLocalScope
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
Symbol
s 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
This release fixes the bug that prevented packages like RustFFT.jl to precompile on Julia 1.10.
V0.19.1
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
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 HashMap
s 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.
V0.18.0
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 DataType
s and UnionAll
s 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 DataType
s 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 implementCCallReturn
. 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 trackself
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
Merge pull request #87 from Taaitaaiger/dev Beta 5
V0.18.0-beta.4
Merge pull request #86 from Taaitaaiger/dev Print meaningful message when using JlrsCore fails
V0.18.0-beta.3
Bugfix: GC frame was not popped from the GC stack when calling CCall::invoke
on Windows.