xgx_intern
A high-performance, Hash-based value interner with custom handle types.
Supports any type that implements the Hash trait for internment, and allows custom handle sizes! Perfect for native64<-->wasm32 compatibility.
📖 Overview
Value interning is a technique for deduplicating equal values to save memory and improve performance. An interner stores each unique value only once and provides a lightweight, copyable "handle" (or "symbol") to reference it.
This approach offers two main benefits:
- Memory Efficiency: If you have many duplicate values (e.g., strings in a compiler's AST, repeated keys in a dataset), interning ensures only one copy of each unique value is stored in memory.
- Performance Boost: Comparing two interned values becomes an extremely fast integer comparison (handle vs. handle) instead of a potentially expensive deep comparison (e.g., string vs. string).
xgx_intern provides a flexible and ergonomic implementation of this pattern, suitable for a wide range of applications.
✨ Features
- 🧬 Fully Generic: Works with any type that implements
Eq + Hash. - ⚡ Customizable Hasher: Pluggable hashing algorithm via the
BuildHashertrait. Useahashorfxhashfor a significant speed boost in performance-critical code. - 🏷️ Customizable Handle: Choose the integer size for your handles (
u16,u32,u64, etc.) to perfectly balance memory usage with the expected number of unique items. - 🤲 Ergonomic API: Offers
intern_owned,intern_ref, andintern_cowto handle different ownership scenarios efficiently and avoid unnecessary clones. - 🧠 Smart Pointer & System Types: Efficiently interns
Arc<str>,Rc<str>,Box<str>,PathBuf,OsString, andCStringdirectly from borrowed references (&str,&Path, etc.), enabling zero-allocation lookups for shared strings and system types. - 🔢 Float Support: Includes
HashableF32andHashableF64wrappers to enable reliable interning of floating-point numbers, which don't normally implementEqorHash. - 📋 Order Preserving: Built on
indexmap, the interner preserves the insertion order of unique values. - 📤 Export: Done interning values? Export the whole thing to a
Vec<T>for further simplicity and memory efficiency. - 🔌
no_stdCompatible: Fully supportsno_stdenvironments via thealloccrate. Perfect for embedded systems, kernels, and WASM.
⚠️ WebAssembly Note: When compiling for a
wasm32target, it's critical that you use a handle size ofu32or smaller (u16,u8). Thewasm32architecture has a 32-bit pointer size (usize), so it cannot create handles from larger types likeu64, which would cause an error.
📦 Installation
To add xgx_intern to your project, run:
xgx_intern has just one feature, std, which enables support for native OS types.
no_std Support
To use this crate in a no_std environment, disable the default features (disables the std feature).
Note: In no_std mode, the default RandomState hasher is unavailable. You will likely want to add a no_std compatible hasher like ahash:
🚀 Usage
Example: Interning Strings
This is the most common use case. Here, we intern several strings and observe how duplicates are handled.
use RandomState;
use Interner;
// Create an interner for strings with the default hasher and u32 handles.
let mut interner = new;
// Intern some strings. `intern_ref` clones the data only if it's not already present.
let handle1 = interner.intern_ref.unwrap;
let handle2 = interner.intern_ref.unwrap;
let handle3 = interner.intern_ref.unwrap; // This is a duplicate
// Handles for identical values are guaranteed to be the same.
assert_eq!;
assert_ne!;
// Even though we interned three values, the interner only stores two unique strings.
assert_eq!;
// You can resolve a handle back to the original value for inspection.
assert_eq!;
println!;
// Output: Handle 0 resolved to 'hello'
Example: Interning a Custom Struct
Any type that implements Eq, PartialEq, Hash, and Clone (for intern_ref) can be interned.
use RandomState;
use Interner;
// 1. Define a custom type that can be interned.
// Deriving these traits is usually sufficient.
// 2. Create an interner for your custom type.
let mut interner = new;
// 3. Intern instances of your struct.
let user1 = User ;
let user2 = User ;
let user3 = User ; // A duplicate of user1
let h1 = interner.intern_ref.unwrap;
let h2 = interner.intern_ref.unwrap;
let h3 = interner.intern_ref.unwrap;
// Assert that the duplicate user gets the same handle.
assert_eq!;
assert_ne!;
assert_eq!;
// Resolve the handle to get a reference to the stored user.
let resolved_user = interner.resolve.unwrap;
println!;
// Output: Found user: User { id: 101, username: "alice" }
Example: Zero-Allocation Shared Strings (Arc<str>)
This is a powerful pattern for high-performance applications like web servers or game engines.
Suppose you have a read-heavy system where many threads need to access shared tags or keys (e.g., "content-type", "player_name"). You want to store them as Arc<str> for cheap sharing, but you only have &str references from incoming network packets.
With standard ToOwned, looking up an Arc<str> would usually require allocating a temporary String first. With xgx_intern, the lookup is allocation-free.
use ;
use Interner;
// 1. Configure the interner to store `Arc<str>`.
// This creates a deduplicated, thread-safe string cache.
let mut interner = new;
// 2. Imagine we are parsing a file or network request.
// We only have a borrowed slice, not an owned object.
let raw_input: &str = "application/json";
// 3. Intern the reference.
// - If the value exists: We get the handle immediately. ZERO allocations.
// - If the value is new: We allocate exactly ONE `Arc<str>`.
//
// Without the `FromRef` trait, this would often require allocating a
// temporary `String` just to perform the lookup.
let handle = interner.intern_ref.unwrap;
// 4. Resolve the handle.
// We get back a reference to the `Arc<str>` that lives in the interner.
let tag: & = interner.resolve.unwrap;
assert_eq!;
// 5. Cheaply clone the Arc if you need to pass it to another thread.
let shared_tag = tag.clone;
💡 FromRef Trait Patterns
The FromRef trait is a superpower of this crate. Unlike the standard ToOwned, which rigidly maps a reference to its standard owned form (e.g., &str -> String), FromRef allows you to define lazy transformations.
This allows the interner to act as a Deduplicating Cache or a Memoization Engine, performing expensive work only when absolutely necessary.
1. Basic: Interning Custom Types
If you have a custom type that has a borrowed form (like PathBuf vs Path or your own wrapper types), you can enable intern_ref support by implementing the FromRef trait.
use ;
use ;
;
// 1. Allow creating a PlayerId from a u32 ref (Required for insertion)
// 2. Allow viewing a PlayerId as a u32 (Required for lookup)
2. Advanced: The Deduplicating Parser (Zero-Overhead Parsing)
Scenario: You are processing a stream of raw network packets or log lines. Many messages are identical.
The Problem: With a standard HashMap, you have to parse the bytes into a struct before you can check if you've seen it, wasting CPU on duplicates.
The Solution: xgx_intern looks up the raw bytes first. It triggers the parsing logic (via FromRef) only on a cache miss.
use ;
use Deserialize;
use ;
// 1. Allow looking up MarketData using raw bytes
// 2. Hash based on the raw bytes (identity)
// 3. Define how to PARSE bytes into MarketData.
// This runs ONLY if the packet is new.
3. Advanced: The "Rich Symbol" (Computed Metadata)
Scenario: You are building a compiler or analyzer. You want to intern identifiers, but you also want to know properties about them (e.g., "Is this a keyword?", "What is its hash?").
The Problem: &str -> ToOwned returns String. It cannot return a Symbol struct.
The Solution: Use FromRef to compute metadata during interning.
use ;
use ;
// Transform &str -> Symbol lazily
⚙️ Customization
Using a Faster Hasher
The default RandomState hasher is secure but can be slow. For contexts where DOS resistance is not a concern, a faster non-cryptographic hasher like ahash is an excellent choice.
First, add ahash to your Cargo.toml:
[]
= "0.8"
Then, use its RandomState (which implements BuildHasher) to create the interner:
use RandomState;
use Interner;
// Create an interner that uses the fast `ahash` algorithm.
let mut interner = new;
let handle = interner.intern_owned.unwrap;
println!;
You can see more rust hash benchmarks here: Rust Hash Benchmarks. Please make sure you understand the security and safety characteristics of your use case and your chosen algorithm before using it.
Choosing a Handle Size
The default handle type H is u32, which allows for up to ~4.2 billion unique items. If you know you'll have fewer unique items, you can use a smaller handle type like u16 to save memory.
use RandomState;
use Interner;
// This interner uses u16 handles, limiting it to 65,536 unique items.
// This is perfect for smaller-scale problems and saves memory for each handle.
let mut interner = new;
// The returned handles will now be of type `u16`.
let handle: u16 = interner.intern_ref.unwrap;
assert_eq!;
Conversely, if you need more than u32::MAX items, you can use u64.
⚠️ WebAssembly Note: When compiling for a
wasm32target, it's critical that you use a handle size ofu32or smaller (u16,u8). Thewasm32architecture has a 32-bit pointer size (usize), so it cannot create handles from larger types likeu64, which would cause an error.
⚖️ License
This project is licensed under the (LICENSE-MIT).