varmap_proc_macro 0.1.1

Procedural macros for the varmap library.
Documentation
  • Coverage
  • 100%
    4 out of 4 items documented0 out of 3 items with examples
  • Size
  • Source code size: 27.77 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 293.02 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 5s Average build duration of successful builds.
  • all releases: 4s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • gdt050579/varmap
    2 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • gdt050579

varmap

A Rust library for heterogeneous, typed key–value maps where each key holds exactly one value of a known type at a time. Values are stored in a compact 16-byte tagged representation; strings, byte slices, and large integers use an internal arena so reads return borrowed references (&str, &[u8]) without per-lookup allocation.

Three map types share the same value encoding and getter API, but differ in how keys are represented:

Map Key type Best when
VarMap Precomputed Key (64-bit hash) Keys are known at compile time (var! macro)
StrVarMap &str (FNV-1a hash at runtime) Keys come from user input or config at runtime
EnumVarMap Enum variant (#[derive(EnumVarMap)]) Fixed, closed set of keys known at compile time

Usage model

VarMap is aimed at write once, read many workloads: you populate keys (configuration, request context, parsed attributes) and then read them repeatedly with cheap typed getters and borrowed &str / &[u8] results.

The internal arena is append-only. Calling set on an existing key updates the map entry, but any previous arena allocation for that key is not reclaimed. Each overwrite of an arena-backed value (strings and byte slices longer than 14 bytes, i128 / u128, Ipv6Addr, custom types, etc.) adds new arena space while the old payload remains until the whole map is reset. Scalars and short strings (≤14 bytes) live entirely in the fixed 16-byte cell, so overwriting those does not grow the arena.

Pattern Suitability
Set each key once, then many reads Ideal
Occasional updates to a few keys Fine
Frequent updates to the same keys with large / arena-backed values Poor — arena memory grows; prefer clear() and repopulate, or another map
Hot loop re-assigning every key every tick Poor — use clear() between logical snapshots, or avoid varmap

Use clear() when you need a fresh binding of all keys (for example between requests or benchmark iterations). That resets the arena and removes current entries while preserving already allocated capacity for future writes.

Installation

Add to your Cargo.toml:

[dependencies]
varmap = "0.1"

The proc-macros (var!, #[derive(EnumVarMap)], #[derive(VarMapValue)]) are re-exported from the varmap crate.

Supported value types

Built-in types (via set / get / typed getters):

  • Integers: i8i64, u8u64, i128, u128
  • Floats: f32, f64
  • bool, char
  • &str, &[u8] (stored in the arena; returned as borrows)
  • IpAddr, Ipv4Addr, Ipv6Addr

Strings and byte slices up to 14 bytes are stored inline in the map; larger payloads are arena-allocated.

Custom Copy types with alignment 1–16 can use #[derive(VarMapValue)] (see Custom types).


StrVarMap

Use when variable names are ordinary strings at runtime (configuration keys, script variables, etc.). Keys are hashed with FNV-1a on each set / get.

use varmap::StrVarMap;

let mut map = StrVarMap::new();

map.set("port", 8080u16);
map.set("enabled", true);
map.set("host", "api.example.com");

assert_eq!(map.get_u16("port"), Some(8080));
assert_eq!(map.get_bool("enabled"), Some(true));
assert_eq!(map.get_str("host"), Some("api.example.com"));
assert_eq!(map.contains("port"), true);

// Generic get when the type is inferred or specified explicitly
let port: u16 = map.get("port").unwrap();

Typed getters

All three maps provide the same convenience methods:

get_bool, get_u8, get_u16, get_u32, get_u64, get_i8, get_i16, get_i32, get_i64, get_f32, get_f64, get_str, get_bytes, get_char, get_ip, get_ipv4, get_ipv6

Or use the generic API:

map.get::<u32>("user.age");
map.get::<&str>("user.name");

In-place updates

All three maps (VarMap, StrVarMap, and EnumVarMap) support in-place mutation through update. This is useful for counters and custom Copy values that implement VarMapValue::update (the derive macro generates this automatically).

use varmap::{StrVarMap, VarMapValue};

#[derive(VarMapValue, Copy, Clone, Eq, PartialEq, Debug)]
struct Stats {
    hits: u32,
    misses: u32,
}

let mut map = StrVarMap::new();
map.set("count", 10u32);
map.set("stats", Stats { hits: 1, misses: 0 });

assert!(map.update::<u32>("count", |n| *n += 5));
assert!(map.update::<Stats>("stats", |s| s.hits += 1));

assert_eq!(map.get_u32("count"), Some(15));
assert_eq!(map.get::<Stats>("stats").map(|s| s.hits), Some(2));

update returns false if the key is missing, the stored type does not match, or the type cannot be updated in place.

Other operations

map.clear();   // drop all keys and reset the arena

VarMap

Use when key names are fixed at compile time. The var! macro expands to a Key holding the FNV-1a hash of the string literal, so lookups skip string hashing and comparison.

use varmap::{VarMap, Key, var};

let mut map = VarMap::new();

// Compile-time keys (recommended)
map.set(var!("user.age"), 32u32);
map.set(var!("user.name"), "Alice");

assert_eq!(map.get_u32(var!("user.age")), Some(32));
assert_eq!(map.get_str(var!("user.name")), Some("Alice"));

assert!(map.update::<u32>(var!("user.age"), |age| *age += 1));
assert_eq!(map.get_u32(var!("user.age")), Some(33));

// Or supply a precomputed hash yourself
map.set(Key::new(0x0123_4567_89AB_CDEF), 1u8);

StrVarMap is a thin wrapper around VarMap that hashes &str keys at runtime; VarMap + var! is the faster option when names are static.

Limits

VarMap (and therefore StrVarMap) supports at most 65 536 distinct keys (index stored in the low 16 bits of the internal hash entry).


EnumVarMap

Use when the key set is a closed enum known at compile time. Each variant maps to a fixed slot (one per enum discriminant), giving O(1) access with no hashing or binary search.

Derive EnumVarMap on your enum (requires #[repr(u16)], Copy, and at most 65 536 variants):

use varmap::EnumVarMap;

#[derive(EnumVarMap, Copy, Clone, Debug)]
#[repr(u16)]
enum ConfigKey {
    Port,
    Enabled,
    Host,
}

let mut map = EnumVarMap::<ConfigKey>::new();

map.set(ConfigKey::Port, 8080u16);
map.set(ConfigKey::Enabled, true);
map.set(ConfigKey::Host, "api.example.com");

assert_eq!(map.get_u16(ConfigKey::Port), Some(8080));
assert_eq!(map.get_bool(ConfigKey::Enabled), Some(true));
assert_eq!(map.get_str(ConfigKey::Host), Some("api.example.com"));
assert_eq!(map.contains(ConfigKey::Port), true);

EnumVarMap owns its own arena (unlike StrVarMap, which delegates to an inner VarMap). Memory for every enum variant slot is reserved up front, including variants that have not been set yet.


Custom types

Types that are Copy, have alignment between 1 and 16 bytes, and contain only plain data can derive VarMapValue:

use varmap::{StrVarMap, VarMapValue};

#[derive(VarMapValue, Copy, Clone, Eq, PartialEq, Debug)]
struct Point {
    x: i32,
    y: i32,
}

let mut map = StrVarMap::new();
let p = Point { x: 1, y: 2 };
map.set("origin", p);

let back: &Point = map.get("origin").unwrap();
assert_eq!(*back, p);

#[derive(VarMapValue)] also generates in-place update support, so custom values can be mutated with map.update::<T>(...) on all three map types.

Generics, unions, and non-Copy types are not supported by the derive macro.


Choosing a map

                    compile-time keys?
                           │
              ┌────────────┴────────────┐
             yes                        no
              │                          │
        fixed enum?              StrVarMap
              │
     ┌────────┴────────┐
    yes               no
     │                 │
EnumVarMap      VarMap + var!
  • EnumVarMap — fastest reads/writes when the key set is an enum; pays fixed storage for every variant slot.
  • VarMap + var! — same value layer as StrVarMap, but no per-operation string hashing.
  • StrVarMap — simplest API when keys are dynamic strings.

Performance

Benchmarks live in the banches crate. They reflect the intended read-heavy pattern: each run performs 10 000 outer iterations over 256 string keys. Update tests call clear() at the start of every iteration, then write all keys once (so arena growth from repeated overwrites is not measured). Read tests populate the map once, then only read in the timed loop — matching write-once, read-many usage.

Results below are averaged over repeated runs; columns are time, allocation during init, and allocation during the timed loop (via a tracking allocator).

Implementation Scenario Time Init alloc Run alloc
EnumVarMap Update – large strings 23 ms 4 112 B 16 384 B
VarMap Update – large strings 74 ms 0 B 28 672 B
StrVarMap Update – large strings 90 ms 0 B 29 696 B
HashMap Update – large strings 175 ms 0 B 30 641 B
BTreeMap Update – large strings 258 ms 0 B 26 985 B
EnumVarMap Update – small strings 31 ms 4 112 B 0 B
VarMap Update – small strings 75 ms 0 B 12 288 B
StrVarMap Update – small strings 92 ms 0 B 12 288 B
HashMap Update – small strings 174 ms 0 B 22 026 B
BTreeMap Update – small strings 255 ms 0 B 18 370 B
EnumVarMap Read – large strings 16 ms 20 496 B 0 B
VarMap Read – large strings 59 ms 28 672 B 0 B
HashMap Read – large strings 58 ms 30 641 B 0 B
StrVarMap Read – large strings 85 ms 28 672 B 0 B
BTreeMap Read – large strings 93 ms 26 985 B 0 B
EnumVarMap Read – small strings 10 ms 4 112 B 0 B
VarMap Read – small strings 35 ms 12 288 B 0 B
HashMap Read – small strings 52 ms 22 026 B 0 B
StrVarMap Read – small strings 54 ms 12 288 B 0 B
BTreeMap Read – small strings 90 ms 18 370 B 0 B

Large values are long descriptive strings (~50+ bytes); small values are short tokens (a few bytes), many of which use inline storage (≤14 bytes) instead of the arena.

Takeaways

  1. Read benchmarks are the sweet spot (populate once, read many): EnumVarMap leads by a wide margin (e.g. 10 ms vs 35–54 ms for small strings on read).
  2. EnumVarMap is fastest overall: direct indexing avoids hash tables and, for VarMap/StrVarMap, sorted hash-vector lookup.
  3. VarMap beats StrVarMap by roughly 15–20% because var! removes runtime FNV-1a over key names.
  4. All three varmap types beat HashMap and BTreeMap on the measured workloads. Update numbers assume clear() per iteration; sustained in-place overwrites without clear() would allocate more because of the append-only arena.
  5. EnumVarMap trades memory for speed: ~4 KiB upfront per 256-key enum (empty slots), but zero run-time allocation on small-string read/update tests once initialized.
  6. Inline small strings (≤14 bytes) avoid arena use: EnumVarMap reports 0 bytes allocated during small-string update loops.

Reproduce locally from the workspace root:

cargo run -p banches -- RUN 10000 10

Use cargo run -p banches -- LIST to see individual benchmark names and filter with an optional fourth argument (e.g. enumvarmap).


License

MIT — see repository and crate metadata for details.