1 unstable release
Uses new Rust 2024
| 0.1.0 | Oct 25, 2025 |
|---|
#316 in Compression
195KB
4.5K
SLoC
wavefst
Modern Rust reader and writer for the Fast Signal Trace (FST) waveform format.
wavefst streams FST data without copying, stays compatible with the original C libfst
implementation, and layers ergonomic APIs on top of the low-level block layout. Readers, writers,
benchmarks, and fuzz targets all live in the same crate so you can inspect existing traces, build
new ones, or validate interoperability from a single dependency.
Highlights
- Full block coverage – header, geometry, hierarchy, blackout, and every value-change variant
(
FST_BL_VCDATA*) are decoded and encoded symmetrically. - Zero-copy iteration – value changes stream straight from decoded buffers with alias handling, initial frames, and timestamps resolved for you.
- Pluggable compression – raw, zlib, LZ4, and FastLZ logic is reusable between reader and writer pipelines, each gated behind a feature flag.
- Async, SIMD, serde – optional helpers wrap the synchronous APIs for async I/O, fast ASCII→bit packing, and serialisable hierarchy/value-change snapshots.
- Tooling ready – Criterion benches, libFuzzer harnesses, and integration tests are included to keep regressions in check.
Installation
cargo add wavefst
The default feature set enables gzip/zlib (gzip), LZ4 (lz4), memory mapping (mmap), and the
SSE2 packed-bit fast path (simd). Disable them with --no-default-features and opt back into the
ones you need.
Quick Start
Reading a trace
use wavefst::{ReaderBuilder, SignalValue};
fn dump_changes(path: &str) -> wavefst::Result<()> {
let file = std::fs::File::open(path)?;
let mut reader = ReaderBuilder::new(file).build()?;
while let Some(mut block) = reader.next_value_changes()? {
while let Some(event) = block.next() {
let event = event?;
println!("t={} handle={} value={:?}", event.timestamp, event.handle, event.value);
}
}
Ok(())
}
Writing a trace
use std::borrow::Cow;
use wavefst::{
ChainCompression, FstWriter, GeomEntry, Header, ScopeType, SignalValue, TimeCompression,
VarDir, VarType,
};
fn build_example(path: &str) -> wavefst::Result<()> {
let file = std::fs::File::create(path)?;
let mut writer = FstWriter::builder(file)
.chain_compression(ChainCompression::Lz4)
.time_compression(TimeCompression::Zlib)
.build()?;
writer.begin_scope(ScopeType::VcdModule, "tb", None)?;
let bit = writer.add_variable(VarType::VcdWire, VarDir::Implicit, "bit", GeomEntry::Fixed(1))?;
let vec = writer.add_variable(VarType::VcdWire, VarDir::Implicit, "vec", GeomEntry::Fixed(8))?;
writer.end_scope()?;
let mut header = Header::default();
header.version = "wavefst-demo".into();
header.vc_section_count = 1;
header.end_time = 20;
writer.write_header(header)?;
writer.emit_change(0, bit, SignalValue::Bit('0'))?;
writer.emit_change(10, bit, SignalValue::Bit('1'))?;
writer.emit_change(12, vec, SignalValue::Vector(Cow::Borrowed("10101010")))?;
writer.finish()?;
Ok(())
}
Feature Flags
| Feature | Default | Description |
|---|---|---|
gzip |
✅ | Enable zlib/deflate support (hierarchy and VC blocks, optional z-wrapper). |
lz4 |
✅ | Support LZ4-compressed hierarchy blocks and value-change chains. |
fastlz |
⛔️ | Add FastLZ decompression/compression for value-change chains. |
parallel |
⛔️ | Use Rayon to decode chain payloads in parallel while keeping results sorted. |
serde |
⛔️ | Provide serialisable hierarchy and value-change snapshots (serde_support). |
mmap |
✅ | Expose the memory-mapped reader backend (io::MemoryMap). |
async |
⛔️ | Include buffered async wrappers (async_support) built on tokio. |
simd |
✅ | Use SSE2 to accelerate ASCII vector packing (falls back to scalar elsewhere). |
Disable defaults with --no-default-features and enable the subset you need, for example:
cargo add wavefst --no-default-features --features "gzip parallel"
Performance
Criterion benches (release profile, Apple M-series, macOS 25.0.0)
| Benchmark | Raw | Zlib | LZ4 |
|---|---|---|---|
reader_next_value_changes |
32.3 µs | 41.7 µs | 34.2 µs |
writer_emit_change |
115 µs | 194 µs | 125 µs |
Run with:
cargo bench
Comparison with C libfst
Using the upstream fstReaderIterBlocks helper (compiled with clang -std=c99 -O2):
| Scenario | Rust (µs) | C helper (µs) | Speed-up |
|---|---|---|---|
| baseline, raw | 50.9 | 335 551 | ×6 590 |
| baseline, wrapped | 57.0 | 6 436 | ×113 |
| wide, raw | 130.3 | 2 933 | ×22.5 |
| wide, wrapped | 65.3 | 2 191 | ×33.6 |
Debug builds show similar ratios (×4.6–×5.6). Event counters matched exactly between both implementations.
Async, SIMD, and serde helpers
wavefst::async_support::{AsyncReader, AsyncWriter}buffer async sources/sinks usingtokiobefore delegating to the synchronous codecs. Useful when you cannot block the reactor thread.wavefst::serde_support(behindserde) snapshots hierarchy trees and value changes as owned data structures that plug directly intoserde_json, CBOR, etc.simdenables an SSE2 fast path for ASCII vector packing; non-x86 targets automatically use the scalar implementation.
Tooling
- Tests –
cargo test(add--features "async gzip serde simd"to exercise optional paths). - Benches –
cargo benchcompares reader/writer throughput across compression modes. - Docs –
doc/fst_format.mddescribes the binary format;doc/rust_crate_design.mdcovers the crate layout and design notes (both ASCII safe).
Roadmap
- Optional CLI (
wavefst-tool) for inspecting traces and converting to other formats. - Additional fixtures that exercise mixed compression, alias chains, and multi-block timelines.
- Configurable logging hooks (
tracing) for long-running ingest jobs.
Contributions, bug reports, and ideas are very welcome. File an issue or open a pull request with a reproduction trace if you hit a corner case.
License
Licensed under the modified MIT License. See LICENSE for details.
Dependencies
~6–10MB
~104K SLoC