A FIX/FAST protocol engine for Rust, built as a Cargo workspace of focused crates: the wire layer, the session layer, and a client-side engine.
IronFix implements FIX tag=value messaging and the FAST encoding primitives from the ground up β there is no upstream protocol library underneath it. The tag=value decoder is zero-copy and dictionary-free: it scans bytes and hands out field slices that borrow the input buffer, with schema validation available as a separate, opt-in pass.
What works today, end to end:
- Decoding and encoding FIX tag=value messages, including
BodyLength(tag 9) andCheckSum(tag 10) handling and length-prefixed Length/Data field pairs. Every malformed-input path is a typed error, never a panic. - Framing over a Tokio codec (
FixCodec) with a bounded read buffer and an unconditionally verified trailer. - A client-side session (
Initiator): TCP dial, Logon handshake, heartbeats and TestRequests at the negotiated interval, CompID validation, sequence-gap detection,ResendRequest/SequenceReset/ gap fill,PossDupFlagandOrigSendingTimehandling,ResetSeqNumFlag, session-levelReject, andFIXT.1.1BeginString withApplVerIDfor FIX 5.0 sessions. Heartbeat/TestRequest handling is partial:HeartBtInt = 0(legal FIX for "do not send heartbeats") is currently taken literally as a zero-length interval, and a pending TestRequest is cleared only by a Heartbeat echoing the matchingTestReqIDβ seedoc/fix_operations.md. - A typestate session FSM and checked sequence arithmetic in
ironfix-session. - A QuickFIX XML dictionary loader and a
Validatorinironfix-dictionary. - FAST primitives in
ironfix-fast: stop-bit integer and string encode/decode and presence maps, all round-trip tested. The copy/delta/increment/tail/default operators are a classification vocabulary (theOperatorenum with predicates such asuses_dictionary/requires_pmap), not wired-in codec operators β the decoder and encoder do not apply them yet.
This list is deliberately explicit. If a capability is not named under "What works today" and appears below, it does not exist in the code β do not plan around it.
- No Acceptor.
ironfix-enginehasInitiatoronly. The server-side examples hand-roll their own accept loop withDecoder/Encoderdirectly. EngineBuilderhas no terminal method. It collects sessions and timeouts but there is nobuild(); the working entry point isInitiator::new(config, app).connect(addr).- The engine never uses
MessageStore. Resend-from-store is not wired up β an inboundResendRequestis answered with a gap fill, not with the original messages.MemoryStoreis the only store implementation; there is noFileStoreand no memory-mapped store. - No TLS.
ironfix-transportcontainsFixCodecand nothing else β no TCP connector, no acceptor, norustls.InitiatorcallsTcpStream::connectdirectly. - Async only. Everything that touches a socket runs on Tokio. There is no synchronous mode, no kernel-bypass path, and no busy-polling transport.
- Only FIX 4.4 has an embedded dictionary (
ironfix-dictionary/spec/FIX44.xml, vendored from QuickFIX). Other versions requireDictionary::from_quickfix_xmlwith your own XML. TheValidatoris not invoked by the engine or the codec; you call it yourself. - The derive macros are stubs.
#[derive(FixMessage)]and#[derive(FixField)]both expand totodo!(). Neitherironfix-derivenorironfix-codegenhas an in-workspace consumer. - FAST is standalone. There is no FAST template XML parser, no UDP multicast receiver, and no wiring into the session or engine path.
- No recorded benchmark baseline. A criterion harness now exists (see
Benchmarks below):
ironfix-tagvalue,ironfix-fastandironfix-transporteach carry abenches/target run bymake bench. It ships no saved baseline and no published figures, so every latency and throughput target indoc/remains a design goal, unmeasured until you run it on hardware you name.
The session layer is version-parameterised by BeginString, and there is a
runnable client/server example pair for each version below. "Dictionary" means a
dictionary is embedded in the crate and loadable without extra files.
| Version | BeginString | Example pair | Dictionary embedded |
|---|---|---|---|
| FIX 4.0 | FIX.4.0 |
yes | no |
| FIX 4.1 | FIX.4.1 |
yes | no |
| FIX 4.2 | FIX.4.2 |
yes | no |
| FIX 4.3 | FIX.4.3 |
yes | no |
| FIX 4.4 | FIX.4.4 |
yes | yes |
| FIX 5.0 | FIXT.1.1 |
yes | no |
| FIX 5.0 SP1 | FIXT.1.1 |
yes | no |
| FIX 5.0 SP2 | FIXT.1.1 |
yes | no |
| Crate | Description |
|---|---|
ironfix-core |
Fundamental types, traits, and error definitions; depends on no other IronFix crate |
ironfix-dictionary |
QuickFIX XML loading, schema types, and the opt-in Validator |
ironfix-tagvalue |
Zero-copy tag=value decoding and encoding; dictionary-free |
ironfix-session |
Session-layer protocol logic: typestate FSM, sequences, heartbeats. No I/O |
ironfix-store |
The MessageStore trait and MemoryStore |
ironfix-transport |
FixCodec, a Tokio codec that frames FIX messages. No TCP helpers, no TLS |
ironfix-fast |
FAST encoding/decoding primitives: stop-bit, presence maps, operators |
ironfix-codegen |
Build-time Rust generation from a Dictionary (no in-workspace consumer) |
ironfix-derive |
Procedural macros β currently stubs that expand to todo!() |
ironfix-engine |
The composition root: Initiator, Connection, the Application trait |
ironfix-example |
Umbrella facade (prelude) plus the runnable examples |
There is no ironfix facade crate. The umbrella re-exports live in
ironfix-example.
Connect as an initiator, send a NewOrderSingle, then log out. The engine owns
the socket: it dials, frames, performs the Logon handshake, and stamps the
header, MsgSeqNum and trailer on everything you send.
use std::sync::Arc;
use std::time::Duration;
use ironfix_engine::{Initiator, NoOpApplication, OutboundMessage};
use ironfix_engine::SessionConfig;
use ironfix_core::MsgType;
use ironfix_core::types::CompId;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = SessionConfig::new(
CompId::new("SENDER")?,
CompId::new("TARGET")?,
"FIX.4.4",
)
.with_heartbeat_interval(Duration::from_secs(30));
// Replace NoOpApplication with your own `Application` impl to receive
// on_logon / from_app / from_admin callbacks.
let initiator = Initiator::new(config, Arc::new(NoOpApplication))
.with_connect_timeout(Duration::from_secs(5));
let connection = initiator.connect("127.0.0.1:9876").await?;
let mut order = OutboundMessage::new(MsgType::NewOrderSingle);
order
.push_str(11, "ORD001")
.push_str(55, "AAPL")
.push_char(54, '1')
.push_uint(38, 100)
.push_str(44, "150.50")
.push_char(40, '2');
connection.send(order).await?;
connection.logout().await?;
connection.wait_closed().await;
Ok(())
}Or terminate an [EngineBuilder] directly: into_initiator() produces the
client above, and into_acceptor() produces a server-side [Acceptor] that
establishes sessions on inbound connections. Both engines hand the socket to the
same session reactor once the Logon handshake completes.
use ironfix_core::types::CompId;
use ironfix_engine::EngineBuilder;
use ironfix_session::SessionConfig;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
// Create a session configuration (sender = you, target = the counterparty).
let config = SessionConfig::new(
CompId::new("CLIENT")?,
CompId::new("VENUE")?,
"FIX.4.4",
);
// The builder terminates in a runnable engine. `into_initiator` produces a
// client that dials the counterparty; `into_acceptor` produces a server that
// establishes sessions on inbound connections.
let initiator = EngineBuilder::new()
.add_session(config)
.into_initiator()?;
let connection = initiator.connect("127.0.0.1:9876").await?;
connection.logout().await?;
connection.wait_closed().await;
# Ok(())
# }See ironfix-example/examples/fix44_engine_client.rs for the same flow with a
real Application implementation, and fix44_server.rs for the matching
Acceptor-based server.
This project includes a Makefile with common tasks to simplify development.
Here's a list of useful commands:
make build # Compile the workspace
make release # Build the workspace in release mode
make run EXAMPLE=fix44_server # Run one example (the workspace has no binaries)make test # Run all tests
make fmt # Format code
make fmt-check # Check formatting without applying
make lint # Run clippy with warnings as errors
make lint-fix # Auto-fix lint issues
make fix # Auto-fix Rust compiler suggestions
make check # Run fmt-check + lint + test + doc + check-spanish
make pre-push # Run fix + fmt + lint-fix + test + check-spanish + readme + docmake doc # Fail on any undocumented public item (clippy -D missing_docs)
make doc-open # Build and open Rust documentation
make create-doc # Generate internal docs
make readme # No-op: README.md is hand-maintained in this workspace
make publish # Explain how to publish; see publish-all
make publish-all # Publish all crates in dependency order, fail-fastPublishing reads CARGO_REGISTRY_TOKEN from the environment β it is never
passed on a command line. Preview the sequence without touching crates.io with
make publish-all DRY_RUN=1.
make coverage # Generate code coverage report (XML)
make coverage-html # Generate HTML coverage report
make open-coverage # Open HTML report
make bench # Run the criterion benchmarks
make bench-quick # Reduced sample count; a smoke run, not a measurement
make bench-build # Compile the benchmarks without running them
make bench-show # Open the criterion HTML report
make bench-save # Save the current run as a named baseline
make bench-compare # Compare the current run against a saved baseline
make bench-clean # Remove benchmark dataBenchmarks cover the hot paths: tag=value decode/encode and checksum
(ironfix-tagvalue), the FAST stop-bit and presence-map primitives
(ironfix-fast), and the FixCodec framing loop (ironfix-transport).
βΉοΈ The harness ships no recorded baseline and no published figures. Every
performance number in doc/ is a design target; treat any figure as unmeasured
until you have produced it yourself with make bench on hardware you name.
make git-log # Show commits on current branch vs main
make check-spanish # Enforce English-only sources and docs (runs in CI)
make zip # Create zip without target/ and temp files
make tree # Visualize project tree (excludes common clutter)make workflow-build # Simulate build workflow
make workflow-lint # Simulate lint workflow
make workflow-test # Simulate test workflow
make workflow-coverage # Simulate coverage workflow
make workflow # Run all workflowsβΉοΈ Requires act for local workflow simulation and cargo-tarpaulin for coverage.
All examples live in ironfix-example/examples/. Each FIX version has a
client/server pair; run the server first, then the client in another terminal.
# Start a FIX 4.4 server
cargo run --example fix44_server
# In another terminal, start the client
cargo run --example fix44_clientPer-version pairs β note that the servers hand-roll their accept loop and
session handling, because there is no Acceptor in ironfix-engine:
fix40_server/fix40_clientβ FIX 4.0 (port 9870)fix41_server/fix41_clientβ FIX 4.1 (port 9871)fix42_server/fix42_clientβ FIX 4.2 (port 9872)fix43_server/fix43_clientβ FIX 4.3 (port 9873)fix44_server/fix44_clientβ FIX 4.4 (port 9876)fix50_server/fix50_clientβ FIX 5.0 over FIXT.1.1 (port 9880)fix50sp1_server/fix50sp1_clientβ FIX 5.0 SP1 (port 9881)fix50sp2_server/fix50sp2_clientβ FIX 5.0 SP2 (port 9882)
Engine and concurrency examples:
fix44_engine_clientβ the same client flow driven byInitiator, which owns the socket, framing, Logon and heartbeats. Pairs withfix44_server.fix44_server_channelβ server-side message hand-off over a channel.
FAST examples:
fast_server/fast_clientβ FAST encode/decode over a socketfast_server_spscβ FAST server with single-producer/single-consumer hand-off
doc/fix_operations.mdβ the FIX operations specification this engine conforms to, plus the implementation checklist. This is the authority for message layouts, required tags and session semantics.doc/architecture.mdβ a forward-looking design target, not a description of the current code. Read its status banner before relying on anything in it.
We welcome contributions to this project! If you would like to contribute, please follow these steps:
- Fork the repository.
- Create a new branch for your feature or bug fix.
- Make your changes and ensure that the project still builds and all tests pass.
- Commit your changes and push your branch to your forked repository.
- Submit a pull request to the main repository.
If you have any questions, issues, or would like to provide feedback, please feel free to contact the project maintainer:
- Author: JoaquΓn BΓ©jar GarcΓa
- Email: jb@taunais.com
- Telegram: @joaquin_bejar
- Repository: https://github.com/joaquinbejar/IronFix
- Documentation: https://docs.rs/ironfix-engine
We appreciate your interest and look forward to your contributions!
License: MIT
Repositories by the same author that this project depends on, and repositories that depend on it.
| Repository | Description |
|---|---|
| fauxchange | Exchange-in-a-box: local options exchange simulator with realistic matching, FIX/WS/REST APIs and historical replay. |
| otc-rfq | OTC Request-for-Quote engine with REST, SBE streaming and FIX/WebSocket/gRPC venue connectivity. |