Back Original

LightClone - Compile-Time Safety for Cheap Clones in Rust

DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)

I've been writing High Level Rust with immutable data, functional pipelines, and liberal cloning. This approach has helped me get acquainted with the language using patterns I enjoy while sidestepping a lot of the issues around ownership, lifetimes, and async. The approach works, but it has a subtle gotcha - cheap and expensive clones look identical in Rust. You can accidentally add a String field to a struct you're cloning in a hot loop and tank your performance with zero compiler warnings.

I built LightClone to fix this. It's a derive macro that enforces cheap clones at compile time - if any field is expensive to clone, your code won't compile.

The Problem with Clones in Rust

In Rust, both cheap and expensive clones use the same .clone() method:

cheap_arc.clone()         // ~10ns refcount bump
expensive_string.clone()  // ~500ns heap allocation + copy

The fix for expensive clones is to use Arc<str> instead of String, imbl::Vector instead of Vec, and so on. This gets you 7-150x speedups on clone-heavy code.

But nothing stops you from accidentally introducing an expensive field during refactoring and there's no way to tell if a clone is expensive or cheap from the clone site. You change one field from Arc<str> to String and your performance regresses silently.

How LightClone Works

LightClone is a trait that only allows cheap-to-clone types:

How LightClone works

The derive macro enforces all fields implement LightClone:

// This won't compile - String is expensive
#[derive(Clone, LightClone)]
struct Person {
    id: i64,
    name: String,  // ERROR: String doesn't implement LightClone
}

// This compiles - all fields are cheap
#[derive(Clone, LightClone)]
struct Person {
    id: i64,
    name: Arc<str>,  // OK: Arc is cheap
}

Call .light_clone() or .lc() instead of .clone() when you want to assert cheapness:

let p2 = person.light_clone();  // Guaranteed cheap

Usage

Add the dependency with feature flags for the crates you use:

[dependencies]
light_clone = { version = "0.4", features = ["imbl", "uuid", "chrono"] }

Then derive and use:

use light_clone::LightClone;
use std::sync::Arc;

#[derive(Clone, LightClone)]
struct Config {
    name: Arc<str>,
    max_connections: u32,
    timeout_ms: u64,
    tags: imbl::Vector<Arc<str>>,
}

let config = Config { /* ... */ };
let clone = config.light_clone();  // Compile-time guarantee this is cheap

LightClone also provides LightStr as an ergonomic alias for Arc<str>:

use light_clone::{LightStr, IntoLightStr};

let name: LightStr = "Alice".into_light_str();
let clone = name.light_clone();  // O(1) - just bumps refcount

Available feature flags:

Prior Art: Dupe

I'll be honest: I didn't know Dupe existed when I started building LightClone. Facebook's Gazebo library has a Dupe trait that solves the same problem - marking types that are cheap to clone. It's mature with 1M+ downloads.

LightClone uses the same approach as Dupe: a marker trait that requires Clone, a derive macro that validates fields at compile time, and a method (.dupe() vs .lc() / light_clone()) that delegates to .clone(). The trait provides compile-time safety while the actual cloning uses the standard Clone implementation.

The meaningful difference is library support. Dupe only covers standard library types. If you're using imbl, rpds, bytes, smol_str, uuid, or chrono - there's no Dupe support.

Crate Dupe LightClone
Arc<T>, Rc<T> Yes Yes
im, imbl (persistent collections) No Yes
rpds (persistent collections) No Yes
bytes No Yes
smol_str No Yes
uuid, chrono, time No Yes
rust_decimal, ordered-float No Yes

If you're using standard library types only, Dupe is battle-tested and a solid choice. If you need ecosystem support for immutable data structures - which you do for High Level Rust patterns - LightClone fills that gap.

There's also implicit-clone from the Yew project, but it's focused on UI framework patterns rather than general-purpose use.

The Tradeoff

LightClone adds friction. You have to convert types at boundaries (String -> Arc<str>) and your team needs to understand the pattern.

But this trades runtime performance bugs for compile-time errors. That's a trade I'm happy to make - it aligns with Rust's philosophy of paying upfront rather than later and keeps me from accidentally making perf mistakes I didn't intend.

And for anything actually performance critical, you'll likely want to use mutations instead of heavy cloning anyway.

Next

LightClone turns invisible performance footguns into compiler errors. If you're writing functional Rust and want confidence that your clones stay cheap, give it a try.

I'm still learning Rust, so feedback is welcome. Star it on GitHub if you want to support development, and open issues or PRs if you hit problems.

If you liked this post you might also like: