RustCall.jl now supports calling generic Rust functions from Julia. This document explains how to use this feature.
using RustCall
Generic functions in Rust use type parameters (e.g., fn identity<T>(x: T) -> T). RustCall.jl automatically:
- Detects generic functions in
rust""blocks - Monomorphizes them with specific type parameters when called
- Caches the monomorphized instances for reuse
When you define a generic function in a rust"" block, RustCall.jl automatically detects and registers it:
rust"""
#[no_mangle]
pub extern "C" fn identity<T>(x: T) -> T {
x
}
"""
# The function is automatically registered as generic
# When you call it, it's automatically monomorphized
result = @rust identity(Int32(42))::Int32 # => 42
result = @rust identity(Float64(3.14))::Float64 # => 3.14You can also manually register generic functions:
using RustCall
code = """
#[no_mangle]
pub extern "C" fn add<T>(a: T, b: T) -> T {
a + b
}
"""
RustCall.register_generic_function("add", code, [:T])
# Call with different types
result = RustCall.call_generic_function("add", Int32(10), Int32(20)) # => 30When you call a generic function, RustCall.jl infers type parameters from the argument types:
# For function: fn identity<T>(x: T) -> T
# Called with: identity(Int32(42))
# Type parameter T is inferred as Int32The generic function is specialized (monomorphized) with the inferred types:
// Original: fn identity<T>(x: T) -> T { x }
// Specialized: fn identity_i32(x: i32) -> i32 { x }The specialized function is compiled and cached. Subsequent calls with the same type parameters reuse the cached version.
rust"""
#[no_mangle]
pub extern "C" fn first<T, U>(a: T, b: U) -> T {
a
}
"""
# Type parameters are inferred from arguments
result = @rust first(Int32(10), Float64(3.14))::Int32 # => 10You can also explicitly specify type parameters:
# Define the code
code = """
#[no_mangle]
pub extern "C" fn identity<T>(x: T) -> T {
x
}
"""
# Register generic function
RustCall.register_generic_function("identity", code, [:T])
# Explicitly monomorphize
type_params = Dict(:T => Int32)
info = RustCall.monomorphize_function("identity", type_params)
# Call using @rust macro (recommended way)
# Note: After monomorphization, you can call it directly
result = @rust identity(Int32(42))::Int32 # => 42RustCall.jl now supports parsing trait bounds in generic functions. This includes:
- Inline bounds:
fn foo<T: Copy + Clone, U: Debug>(x: T) -> U - Where clauses:
fn foo<T, U>(x: T) -> U where T: Copy, U: Debug - Generic trait bounds:
fn foo<T: Add<Output = T>>(x: T) -> T - Mixed format: Combining inline bounds and where clauses
When registering a generic function, trait bounds are automatically parsed and stored:
using RustCall
# Define a function with trait bounds
code = """
pub fn identity<T: Copy + Clone>(x: T) -> T {
x
}
"""
# Parse the generic function (constraints are automatically extracted)
info = RustCall.parse_generic_function(code, "identity")
println(info.constraints) # Dict(:T => RustCall.TypeConstraints([Copy, Clone]))You can also manually specify constraints when registering a generic function:
using RustCall
code = """
pub fn add<T>(a: T, b: T) -> T {
a + b
}
"""
# Using RustCall.TypeConstraints (recommended)
constraints = Dict(:T => RustCall.TypeConstraints([
RustCall.TraitBound("Copy", String[]),
RustCall.TraitBound("Add", ["Output = T"])
]))
RustCall.register_generic_function("add", code, [:T], constraints)
# Or using the legacy string format (backward compatible)
RustCall.register_generic_function("add_legacy", code, [:T], Dict(:T => "Copy + Add<Output = T>"))You can convert parsed constraints back to Rust syntax:
using RustCall
constraints = Dict(:T => RustCall.TypeConstraints([
RustCall.TraitBound("Copy", String[]),
RustCall.TraitBound("Clone", String[])
]))
rust_str = RustCall.constraints_to_rust_string(constraints)
println(rust_str) # "T: Copy + Clone"While trait bounds are now properly parsed and stored, runtime validation (checking if a Julia type satisfies Rust trait bounds) is not yet implemented. The bounds are stored for:
- Documentation and introspection
- Future code generation improvements
- Error reporting when trait bounds are not satisfied
Type parameter inference is currently simplified:
- One type parameter maps to one argument (for single-parameter functions)
- Multiple type parameters map to multiple arguments in order
More complex inference (e.g., inferring from return type) is not yet supported.
TraitBound(trait_name, type_params)- Represents a single trait bound (e.g.,Copy,Add<Output = T>)TypeConstraints(bounds)- Represents all trait bounds for a type parameterGenericFunctionInfo- Information about a generic Rust function
register_generic_function(func_name, code, type_params, constraints=Dict())- Register a generic functionis_generic_function(func_name)- Check if a function is genericcall_generic_function(func_name, args...)- Call a generic function (auto-monomorphizes)monomorphize_function(func_name, type_params)- Explicitly monomorphize a functionspecialize_generic_code(code, type_params)- Specialize generic code with type parametersinfer_type_parameters(func_name, arg_types)- Infer type parameters from argument types
parse_single_trait(trait_str)- Parse a single trait bound string (e.g.,"Copy","Add<Output = T>")parse_trait_bounds(bounds_str)- Parse multiple trait bounds (e.g.,"Copy + Clone")parse_inline_constraints(type_params_str)- Parse inline type parameters with constraintsparse_where_clause(code)- Parse a where clause from Rust codeparse_generic_function(code, func_name)- Parse a generic function and extract type parameters with constraintsmerge_constraints(c1, c2)- Merge two constraint dictionariesconstraints_to_rust_string(constraints)- Convert constraints back to Rust syntax
GENERIC_FUNCTION_REGISTRY- Maps function names toGenericFunctionInfoMONOMORPHIZED_FUNCTIONS- Maps(function_name, type_params_tuple)toFunctionInfo
rust"""
#[no_mangle]
pub extern "C" fn identity<T>(x: T) -> T {
x
}
"""
# Automatically monomorphized and called
result1 = @rust identity(Int32(42))::Int32 # => 42
result2 = @rust identity(Float64(3.14))::Float64 # => 3.14
println("Int32 result: $result1")
println("Float64 result: $result2")
rust"""
#[no_mangle]
pub extern "C" fn first<T, U>(a: T, b: U) -> T {
a
}
"""
result = @rust first(Int32(10), Float64(20.0))::Int32 # => 10
println("Result: $result")
code = """
#[no_mangle]
pub extern "C" fn multiply<T>(a: T, b: T) -> T {
a * b
}
"""
RustCall.register_generic_function("multiply", code, [:T])
# Call with automatic monomorphization
result = RustCall.call_generic_function("multiply", Int32(5), Int32(6)) # => 30
println("Result: $result")
The specialize_generic_code function:
- Replaces type parameters (
T,U, etc.) with concrete Rust types (i32,f64, etc.) - Removes generic parameter lists (
<T>) - Preserves function structure and attributes
- Check cache for existing monomorphized instance
- If not cached, specialize the code
- Replace function name with specialized name (e.g.,
identity_i32) - Ensure
#[no_mangle]andextern "C"are present - Compile the specialized function
- Load and cache the compiled library
- Return
FunctionInfofor the monomorphized function
Monomorphized functions are cached by:
- Function name
- Type parameters tuple (sorted for consistency)
This ensures that calling the same generic function with the same types reuses the compiled instance.