fix-codec-rs
A high-performance FIX (Financial Information Exchange) protocol encoder/decoder library written in Rust, designed for HFT and low-latency trading systems.
We target to encode and decode level, no session or application level for this protocol.
Tested with FIX version 4.2 and 4.4
Features
- Zero-copy decoding — field values are byte slices into the original buffer, no allocation on the hot path
- Reusable decoder/encoder — single instance across thousands of messages, amortizes allocation cost
- SmallVec inline storage — 95%+ of messages fit in inline stack storage (32-field default), avoiding heap allocation entirely
- SIMD-accelerated scanning — uses
memchrfor fast=and SOH delimiter search - Lazy sorted index — O(log n)
find()via binary search, built only on first use - Repeating groups — full support for nested groups, both FIX 4.2 and FIX 4.4 specifications
- Auto checksum/body length — automatic tag 9 and tag 10 computation during encoding (toggleable)
- 500+ tag constants — comprehensive coverage of FIX 4.2 and FIX 4.4 tag definitions
Installation
Add to your Cargo.toml:
[]
= { = "https://github.com/ledongthuc/fix-codec-rs" }
Usage
Decoding
use Decoder;
use tag;
Decoding with Validation
use Decoder;
let mut decoder = new;
let raw = b"8=FIX.4.2\x019=73\x0135=D\x0149=CLIENT\x0156=BROKER\x0134=1\x0152=20240101-12:00:00\x0111=ORD001\x0155=AAPL\x0154=1\x0138=100\x0144=150.00\x0140=2\x0110=128\x01";
let msg = decoder.decode.unwrap;
// Validate body length (tag 9) and checksum (tag 10)
msg.validate_body_length.unwrap;
msg.validate_checksum.unwrap;
Encoding
use Decoder;
use Encoder;
let mut decoder = new;
let mut encoder = new;
let raw = b"8=FIX.4.2\x019=73\x0135=D\x0149=CLIENT\x0156=BROKER\x0134=1\x0152=20240101-12:00:00\x0111=ORD001\x0155=AAPL\x0154=1\x0138=100\x0144=150.00\x0140=2\x0110=128\x01";
let msg = decoder.decode.unwrap;
// Encode back to wire format — tag 9 and tag 10 are recomputed automatically
let mut out = Vecnew;
encoder.encode.unwrap;
Encoding with Auto-Calculation Disabled
use Encoder;
let mut encoder = new;
// Preserve original tag 9 and tag 10 values without recomputing
encoder.disable_auto_calculate_body_length;
encoder.disable_auto_calculate_checksum;
Pre-sizing for Large Messages
use Decoder;
use Encoder;
// Pre-allocate for messages with up to 64 fields (avoids reallocation)
let mut decoder = with_capacity;
// Pre-allocate 1024-byte output buffer
let mut encoder = with_capacity;
Repeating Groups
use Decoder;
use group;
use tag;
let mut decoder = new;
// Market data snapshot with 2 MD entries
let raw = b"8=FIX.4.2\x019=100\x0135=W\x0149=SERVER\x0156=CLIENT\x01268=2\x01269=0\x01270=150.25\x01271=500\x01269=1\x01270=150.30\x01271=300\x0110=200\x01";
let msg = decoder.decode.unwrap;
// Iterate MD entries using the built-in FIX 4.2 group spec
for entry in msg.groups
Nested Groups
use Decoder;
use group;
use tag;
let mut decoder = new;
let raw = b"8=FIX.4.4\x019=...\x0135=AE\x01453=2\x01448=FIRM_A\x01447=D\x01452=1\x01539=1\x01524=TRADER1\x01448=FIRM_B\x01447=D\x01452=2\x0110=000\x01";
let msg = decoder.decode.unwrap;
for party in msg.groups
Custom Group Spec
use GroupSpec;
use tag;
// Define a custom repeating group
const MY_GROUP: GroupSpec = GroupSpec ;
Benchmark
Benchmarks run with Criterion.rs on Apple M-series (arm64). Run your own with cargo bench.
Decode throughput
| Message | Fields | Size | Throughput |
|---|---|---|---|
| Tiny (1 field) | 1 | ~9 B | ~600 ns/msg |
| New Order Single | 8 | ~73 B | ~800 ns/msg |
| Execution Report | 12 | ~104 B | ~1.0 µs/msg |
| Market Data Snapshot | 20 | ~100 B | ~1.1 µs/msg |
find() strategy: binary search vs linear scan
| Strategy | 1 lookup | 4 lookups | 8 lookups |
|---|---|---|---|
| Binary search | lower | lower | lower |
| Linear scan | lower | higher | higher |
Binary search (via lazy sorted index) is the default. Break-even point is typically around 2–3 lookups per message.
Encode throughput
| Message | Throughput |
|---|---|
| New Order Single | ~700 ns/msg |
| Execution Report | ~900 ns/msg |
Roundtrip (decode + encode)
| Message | Throughput |
|---|---|
| New Order Single | ~1.5 µs/msg |
| Market Data Snapshot | ~2.0 µs/msg |
Run full benchmarks:
# Open HTML report
Design Notes
Zero-copy — Message<'a> and Group<'a> hold references into the original input buffer. No string copies. Values are &[u8] slices; callers parse numeric/string values as needed.
Reusable decoder — Decoder holds a SmallVec internally. Reuse the same instance across messages to avoid repeated allocation. The decoder clears internal state on each decode() call.
SmallVec inline storage — field offset storage fits 32 entries inline on the stack. Messages with more than 32 fields spill to the heap automatically.
Lazy sorted index — Message::find() builds a sorted tag index on first call using OnceCell. Subsequent find() calls on the same message use binary search. If you only iterate with fields(), no sort ever happens.
Group specs are 'static — built-in GroupSpec values reference static tag slices. Zero overhead at runtime.
Supported FIX Versions
| Version | Tag Coverage | Group Specs |
|---|---|---|
| FIX 4.2 | 450+ tags | 19 groups |
| FIX 4.4 | 500+ tags | 37 groups |
Tag constants are in fix_codec_rs::tag. Group specs are in fix_codec_rs::group::fix42 and fix_codec_rs::group::fix44.
Dev Setup
Prerequisites
- Rust toolchain (stable, edition 2024): https://rustup.rs
Clone and build
Run tests
Run benchmarks
Criterion generates an HTML report at target/criterion/report/index.html.
Contributing
Contributions are welcome. Please follow the process below:
- Fork the repository and create a feature branch from
main. - Write tests for any new behavior. All existing tests must pass.
- Run
cargo clippy -- -D warningsandcargo fmtbefore submitting. - Open a pull request with a clear description of the change and motivation.
For bug reports, open a GitHub issue with a minimal reproducing example — ideally a failing test case.
License
MIT