| name | julia-to-rust |
| description | Use when migrating Julia codebases to Rust — covers JIT to AOT compilation, multiple dispatch to traits/enums, Array to ndarray, DataFrames to polars, Flux to burn/candle, and incremental replacement strategy. Includes canonical code patterns, common mistakes, and reference implementations. |
| updated | 2026-07-30T00:00:00.000Z |
Julia to Rust Migration
Architecture Mapping
Julia is a JIT-compiled dynamic language with multiple dispatch as its core paradigm, designed for high-performance numerical computing. Rust is an AOT-compiled systems language with a trait system, zero-cost abstractions, and explicit control over memory.
| Julia Concept | Rust Equivalent |
|---|
| Julia runtime + LLVM JIT | rustc + LLVM (AOT compilation) |
| Pkg (Project.toml / Manifest.toml) | Cargo.toml + Cargo.lock |
| Julia REPL | evcxr (Rust REPL) / cargo script |
| Pluto / Jupyter notebooks | evcxr (Jupyter kernel) / mdBook |
| Revise.jl (hot reloading) | Not available — requires recompilation; use cargo watch |
@time / @benchmark | criterion / divan / std::time::Instant |
@code_llvm / @code_native | cargo rustc -- --emit=llvm-ir / cargo asm |
@profview | samply / perf / flamegraph |
| PackageCompiler.jl | cargo build --release (always AOT) |
| CUDA.jl / AMDGPU.jl | cudarc / wgpu / rustacuda |
@spawn / @spawnat (Distributed) | rayon / mpi crate / tokio::spawn |
Julia's main advantage is the interactive, exploratory loop aided by JIT. When migrating to Rust, pair the compiled binary with evcxr for exploration or keep Julia for prototyping while Rust handles production deployment.
Type System Mapping
Julia is dynamically typed with optional type annotations for dispatch. Rust is statically typed with generics and trait bounds.
| Julia Type | Rust Type | Notes |
|---|
Int64 / Int32 | i64 / i32 | Choose based on domain |
Float64 / Float32 | f64 / f32 | Julia default is Float64 |
Bool | bool | Direct mapping |
String | String / &str | |
Symbol | &'static str (interned) / custom Symbol type | Use string_cache crate for interning |
Nothing | () (unit type) / Option<T> | |
Missing | Option<T> | Julia's sentinel for missing data |
Vector{T} / Array{T,1} | Vec<T> / ndarray::Array1<T> | |
Matrix{T} / Array{T,2} | ndarray::Array2<T> / nalgebra::DMatrix<T> | |
Array{T,N} | ndarray::Array<T, D> | ndarray uses const generics for dimension |
Dict{K,V} | HashMap<K,V> / BTreeMap<K,V> | |
Set{T} | HashSet<T> / BTreeSet<T> | |
Tuple | (A, B, C) native tuple | Direct mapping |
NamedTuple | (field: Type, ...) / struct | Named fields require a struct |
Parametric Types to Generics
struct MyContainer<T> {
data: Vec<T>,
}
impl<T> MyContainer<T> {
fn new() -> Self {
Self { data: Vec::new() }
}
fn push(&mut self, item: T) {
self.data.push(item);
}
}
use std::fmt::Display;
use std::ops::Add;
struct Calculator<T: Add<Output = T> + Display> {
value: T,
}
Memory & Ownership Model
Julia uses a tracing garbage collector with a generational design. Rust uses ownership with no GC.
| Julia Memory Pattern | Rust Translation |
|---|
| GC-managed heap objects | Ownership — values dropped at scope exit |
| Immutable objects (struct, Tuple) | Stack-allocated or heap via Box |
| Mutable objects (arrays, Dict) | Owned values with &mut borrowing |
| Circular references | Weak<T> |
finalizer(f, obj) | Drop trait |
unsafe_wrap(Array, pointer) | unsafe { ... } with raw pointer |
pointer_from_objref | &raw const / &raw mut (raw references) |
| Pooled arrays / memory arenas | bumpalo / typed-arena crate |
deepcopy() | .clone() / serde::Serialize round-trip |
@view (array views) | ArrayView / ArrayViewMut (ndarray slices) |
copyto!(dest, src) | dest.copy_from(&src) (ndarray) / dest.clone_from(&src) |
Array Views and Ownership
use ndarray::{Array1, s};
fn use_views() {
let v = Array1::from_vec(vec![1, 2, 3, 4, 5]);
let view = v.slice(s![1..4]);
let mut v_mut = v.clone();
{
let mut view_mut = v_mut.slice_mut(s![1..4]);
view_mut.fill(99);
}
}
Concurrency / Async Translation
Julia has coroutines (Tasks), multi-threading, and distributed computing. Rust provides async/await, OS threads, and rayon.
| Julia Concurrency | Rust Equivalent |
|---|
@async / @sync | Tokio tasks — tokio::spawn(async { ... }) |
@spawn (threads) | `rayon::spawn( |
@threads for | rayon::par_iter() |
@distributed for | mpi crate / rayon on cluster node |
Channel{T} | tokio::sync::mpsc / std::sync::mpsc |
put!(ch, val) / take!(ch) | tx.send(val).await / rx.recv().await |
@fetchfrom / @spawnat | MPI send / recv |
Future (Julia) | tokio::task::JoinHandle |
fetch(f::Future) | handle.await.unwrap() |
Threads.@threads (loop parallel) | rayon::par_iter().for_each() |
Threads.@spawn | std::thread::spawn |
Base.Threads.atomic_add! | AtomicU64::fetch_add |
ReentrantLock | std::sync::Mutex (non-reentrant by design) |
Task Migration Example
use tokio::sync::mpsc;
async fn producer(tx: mpsc::Sender<i32>) {
for i in 1..=10 {
tx.send(i * i).await.unwrap();
}
}
async fn consumer(mut rx: mpsc::Receiver<i32>) {
while let Some(result) = rx.recv().await {
println!("{result}");
}
}
#[tokio::main]
async fn main() {
let (tx, rx) = mpsc::channel(32);
let h1 = tokio::spawn(producer(tx));
let h2 = tokio::spawn(consumer(rx));
let _ = tokio::join!(h1, h2);
}
Build System & Dependencies
| Julia Tool | Rust Equivalent |
|---|
Pkg.add("Package") | cargo add <crate> |
using Package / import Package | use crate_name; |
Project.toml / Manifest.toml | Cargo.toml / Cargo.lock |
] activate . | Working directory auto-activated |
Pkg.test("Package") | cargo test |
Pkg.update() | cargo update |
Pkg.instantiate() | cargo fetch (automatic on build) |
JULIA_DEPOT_PATH | CARGO_HOME |
JULIA_LOAD_PATH | Module path under src/ |
BinaryBuilder.jl (cross-compile) | cross crate / cargo-zigbuild |
PackageCompiler.jl (AOT) | cargo build --release (always AOT) |
Test.jl / @test / @testset | #[cfg(test)] mod tests / cargo test |
Documenter.jl | cargo doc |
Aqua.jl (quality) | cargo clippy / cargo audit |
Cargo.toml for a Julia-to-Rust Scientific Project
[package]
name = "my-scientific-app"
version = "0.1.0"
edition = "2021"
[dependencies]
ndarray = { version = "0.16", features = ["rayon", "blas"] }
nalgebra = "0.33"
ndarray-stats = "0.6"
linregress = "0.5"
polars = { version = "0.45", features = ["lazy", "csv", "parquet"] }
rayon = "1.10"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
plotters = { version = "0.3", features = ["evcxr", "line_series"] }
burn = { version = "0.16", features = ["wgpu", "train"] }
num-traits = "0.2"
tracing = "0.1"
thiserror = "2"
clap = { version = "4", features = ["derive"] }
[profile.release]
opt-level = 3
=
=
Standard Library & Ecosystem Mapping
| Julia Library | Rust Equivalent |
|---|
Base.map(f, arr) | arr.mapv(f) (ndarray) / iter.map(f) |
Base.filter(f, arr) | iter.filter(f) |
Base.reduce(f, arr) | iter.fold(init, f) |
Base.broadcast(f, arr) | arr.mapv(f) (ndarray) / iter.map(f) (element-wise) |
Base.cat(arrs...; dims=2) | ndarray::stack(Axis(1), &[a, b]) |
LinearAlgebra | nalgebra / ndarray-linalg / faer |
LinearAlgebra.eigen(A) | nalgebra::DMatrix::symmetric_eigen() |
LinearAlgebra.svd(A) | nalgebra::SVD::new(A) |
LinearAlgebra.qr(A) | nalgebra::QR::new(A) |
LinearAlgebra.cholesky(A) | nalgebra::Cholesky::new(A) |
Statistics.mean(x) | ndarray_stats::QuantileExt / manual |
Statistics.std(x) | ndarray_stats::MaybeQuantileExt / manual |
Distributions.jl | rand_distr crate |
Random.rand(n) | rand::random::<f64>() / rand::thread_rng() |
DifferentialEquations.jl | diffsol / ode_solvers crate |
Optim.jl | argmin / nlopt crate |
JuMP.jl (optimization modeling) | crate |
Broadcasting Translation
use ndarray::Array1;
fn broadcast_op(x: &Array1<f64>, y: &Array1<f64>) -> Array1<f64> {
x.mapv(|v| v.sin()) + &y.mapv(|v| v.cos()) * 2.0
}
fn broadcast_fused(x: &Array1<f64>, y: &Array1<f64>) -> Array1<f64> {
ndarray::azip!((x in x, y in y) x.sin() + y.cos() * 2.0)
}
Canonical Patterns
Pattern 1: Multiple Dispatch to Trait / Enum
trait Process {
fn process(&self) -> Self;
}
impl Process for i32 {
fn process(&self) -> Self { self * 2 }
}
impl Process for String {
fn process(&self) -> Self { self.to_uppercase() }
}
#[derive(Debug)]
enum Value {
Int(i32),
Float(f64),
Str(String),
Vec(Vec<f64>),
}
impl Value {
fn (&) <Value, & > {
{
Value::(x) => (Value::(x * )),
Value::(x) => (Value::((x * ).() / )),
Value::(s) => (Value::(s.())),
_ => (),
}
}
}
Pattern 2: Macro to Declarative/Proc Macro
macro_rules! twice {
($expr:expr) => {
$expr;
$expr;
};
}
Pattern 3: Lazy Evaluation with Iterators
fn pipeline() -> i32 {
(1..=10)
.map(|x| x * x)
.sum()
}
fn complex_pipeline(data: &[f64]) -> Option<f64> {
data.iter()
.filter(|&&x| x > 0.0)
.map(|&x| x.sqrt() * 2.0)
.reduce(|acc, x| acc + x)
}
Pattern 4: Type-Stable Struct with Validation
#[derive(Debug, Clone)]
pub struct Config {
host: String,
port: u16,
}
impl Config {
pub fn new(host: impl Into<String>, port: u16) -> Result<Self, &'static str> {
if port == 0 {
return Err("端口不能为 0");
}
Ok(Self { host: host.into(), port })
}
}
Pattern 5: Differential Equation Solver
use ode_solvers::{Dopri5, OdeSolverState, System};
struct ExponentialDecay { pub lambda: f64 }
impl System<f64, OdeSolverState<f64>> for ExponentialDecay {
fn system(&self, _t: f64, y: &[f64], dy: &mut [f64]) {
dy[0] = -self.lambda * y[0];
}
}
fn solve_ode() {
let system = ExponentialDecay { lambda: 0.5 };
let mut solver = Dopri5::new(
system,
0.0,
10.0,
0.01,
vec![1.0],
1e-6,
1e-8,
);
solver.integrate().();
}
Pattern 6: DataFrames.jl to Polars
use polars::prelude::*;
fn process_dataframe(path: &str) -> PolarsResult<DataFrame> {
let df = CsvReadOptions::default()
.try_into_reader_with_file_path(Some(path.into()))?
.finish()?;
df.lazy()
.filter(col("value").gt(10.0))
.with_column((col("value") * 2.0).alias("doubled"))
.group_by(&[col("category")])
.agg(&[col("doubled").mean()])
.collect()
}
FFI & Incremental Migration
| Strategy | Tool | When to Use |
|---|
| Call Rust from Julia | jlrs crate / ccall with Rust cdylib | Replace hot Julia functions |
| Call Julia from Rust | jlrs crate (embed Julia runtime) | Keep complex Julia ecosystem packages |
| Shared memory via Arrow | arrow crate in both | Zero-copy data exchange |
| IPC via REST/gRPC | axum/tonic | Micro-service architecture |
| Standalone binary | Pure Rust | Batch processing pipelines |
| Python bridge | PyO3 (Julia calls Python → calls Rust) | Legacy integration |
jlrs: Rust Function Callable from Julia
use jlrs::prelude::*;
fn fast_sum(arr: ArrayView1<f64>) -> f64 {
arr.iter().sum()
}
Migration Order for Julia Projects
- Profile with
@profview to identify bottlenecks. JIT overhead is typically in first-call latency and type instability.
- Rewrite type-unstable Julia functions as type-stable Rust functions callable via jlrs or ccall.
- Replace numerical kernels (matrix ops, ODE solvers) with Rust equivalents, using ndarray/nalgebra.
- Port data pipeline logic (CSV/JSON processing) from DataFrames.jl to polars.
- Extract ML training loops from Flux.jl to burn/candle for deployment.
- Replace the CLI/entry point with a Rust binary; keep Julia only for interactive notebooks.
Common Mistakes
Mistake 1: Expecting JIT-Like Start-up Speed
Mistake 2: Trying to Reproduce Multiple Dispatch Exactly
Mistake 3: Over-allocating in Loops (Julia's GC Pattern)
fn compute_bad(n: usize) -> Vec<f64> {
let mut result = Vec::new();
for i in 0..n {
let temp = vec![i as f64; 100];
result.push(temp.iter().sum());
}
result
}
fn compute_good(n: usize) -> Vec<f64> {
let mut result = Vec::with_capacity(n);
let mut buffer = vec![0f64; 100];
for i in 0..n {
buffer.fill(i as f64);
result.push(buffer.().());
}
result
}
Mistake 4: Treating Rust Arrays as 1-Indexed
fn get_element_bad(matrix: &ndarray::Array2<f64>, i: usize, j: usize) -> f64 {
matrix[(i, j)]
}
fn get_element_good(matrix: &ndarray::Array2<f64>, i: usize, j: usize) -> Option<&f64> {
if i == 0 || j == 0 { return None; }
matrix.get((i - 1, j - 1))
}
Mistake 5: Unnecessary Mutable Patterns
fn add_noise_bad(data: &mut [f64]) {
for i in 0..data.len() {
data[i] += rand::random::<f64>() * 0.01;
}
}
fn add_noise_good(data: &[f64]) -> Vec<f64> {
use rand::Rng;
let mut rng = rand::thread_rng();
data.iter()
.map(|&v| v + rng.gen::<f64>() * 0.01)
.collect()
}
Mistake 6: Ignoring rustc Auto-Vectorization
fn dot_product(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
Reference Implementations
| Project | Description | Migration Pattern |
|---|
| jlrs | Julia-Rust interop toolkit | Bidirectional FFI; embed Julia in Rust |
| polars | DataFrame library | DataFrames.jl alternative written in Rust |
| nalgebra | Linear algebra library | General purpose; similar to Julia's LinearAlgebra |
| faer | High-performance linear algebra | BLAS/LAPACK in pure Rust; similar scope to Julia arrays |
| burn | Deep learning framework | Flux.jl alternative; multi-backend (wgpu, CUDA, ROCm) |
| candle | Minimalist ML framework | Lightweight Flux.jl alternative by HuggingFace |
| ode_solvers | ODE solvers in Rust | DifferentialEquations.jl subset |
| symbolica | Computer algebra system | Symbolics.jl analogue |
| evcxr | Rust REPL for Jupyter | Interactive exploration, similar to Julia REPL/Pluto |
Cross-Reference
- python-to-rust — For migrating numpy/scipy workflows similar to Julia's numerical stack
- r-to-rust — For migrating statistical modeling and data frame operations
- go-to-rust — For migrating concurrent service code that Julia calls via HTTP/IPC
- c-to-rust — For migrating the C/Fortran libraries that Julia wraps via ccall
- cpp-to-rust — For migrating C++ scientific libraries used as Julia dependencies