Fork of jamesdbrock/hffix.
Header-only, in-place, no-alloc FIX parser. Aims at lower latency and
tighter tails than upstream: SoA field index, SWAR digit parsing,
O(1) data-tag lookup, counter-based asserts under NDEBUG, no
exceptions anywhere in the library, zero allocations.
Adds a Google Benchmark suite under benchmarks/ (upstream ships
none); see Benchmarks.
Changes from upstream:
is_tag_a_data_lengthis an O(1) bitmap lookup (upstream walks ~75 ints per field).- SWAR timestamp parser instead of digit-by-digit.
- Reader iterator carries
message_endsoincrement()doesn't chase the parent reader. basic_indexed_message+build_field_indexwith caller-suppliedfield_index_buffer<N>. Insertion-order SoA index (tags[N]+pos_len[N]). Two error flags:truncated()(more fields thanN) andoverflowed()(message > 4 GiB; field position/length each need to fit inuint32_t).authoritative()combines them. See Field indexing for usage and sizing.- Compile-time group dispatch:
reader.group<tag::NoMDEntries>()looks the delimiter up inhffix::groups::group_def<CountTag>. All FIX 5.0 SP2 + FIXT 1.1 groups (~500) generated from the spec byfixspec-gen-fieldsintohffix_groups.hpp. Runtimegroup(count_tag, first_tag)for ad-hoc cases and for the handful of NoXxx names whose delimiter differs across messages. Iterator is bounded by the NoXxx count. message_writerisnoexcept.push_back_*set an internal error flag on overflow and become no-ops; caller callspush_back_trailer()once and checks theboolreturn. The throwing variant is gone.- Batched parse over a single buffer:
hffix::messages(buf, n)range andhffix::for_each_message(begin, end, fn)callback. Yield only complete + valid readers, skip invalid frames vianext_message_readerresync, and surface the tail pointer (iterator::remainder()/ return value) for stream re-feeding. See Batched parse. - Reader programmer-error checks (
begin()on an invalid message,next_message_reader()on incomplete, etc.) expand toHFFIX_ASSERT(cond, msg). UnderNDEBUGit bumps an atomic counter (hffix::assert_failure_count()); in debug builds it traps. Override the macro before include for a custom handler. - Boost.DateTime dropped. Writer takes
std::chrono::sys_timeor a rawint64_tepoch (push_back_timestamp_epoch_nanos/_millis); reader returnsstd::optional<int64_t>fromas_epoch_nanos/as_epoch_millis. Epoch-materializing reader paths rejectyearoutside[1970, 2200]to avoid silentint64_toverflow inchrono's nanosecond arithmetic; see Timestamp range. field_value::as_string()removed; useas_string_view()orbegin()/end(). Library is strictly no-alloc end-to-end.- Non-validating
field_value::as_int<T>()/as_decimal<T>()renamed toas_int_unchecked<T>()/as_decimal_unchecked<T>(). Non-digit input produces undefined output; no overflow detection. Usetry_as_int(T&)/try_as_decimalfor any wire-untrusted value. - CMake + Conan 2. GoogleTest + Google Benchmark.
- C++20 required; legacy
__cplusplusguards removed. fixgenwrites synthetic FIX 5.0 SP2 datasets over FIXT 1.1. MD snapshots: 3–10 book levels per side. MD incrementals: 1–200 entries. Timestamps carry millisecond jitter so the SWAR parser sees varied digits. NewOrderSingle / ExecutionReport / Logon / Logout / Heartbeat are sampled alongside MD;ClOrdIDis a 20-char alphanumeric.fixspec-gen-fieldsrewritten from Haskell to C++ (pugixml) and switched to QuickFIX-format spec XML as input (FIX50SP2.xml+FIXT11.xmlship infixspec/). Drops the Haskell/Cabal toolchain; ships as a regular CMake target alongside the rest of the utils..clang-format,scripts/format{,-check}.sh.
CMake 3.20+, Conan 2.x, C++20 toolchain.
pip install 'conan>=2.0,<3.0'
conan profile detect --force
conan install . --output-folder=build --build=missing \
-s build_type=Release \
-s compiler.cppstd=gnu20 -s:b compiler.cppstd=gnu20
cmake --preset conan-release
cmake --build build -j
ctest --test-dir buildCLI binaries land in build/utils/:
build/utils/fixgen -o data.fix -n 500000
build/utils/fixprint < data.fix | headconanfile.py declares hffix2/1.0.0 as a header-only package with
CMake target hffix2::hffix and CMake file name hffix2.
conan create . --build=missing \
-s compiler.cppstd=gnu20 -s:b compiler.cppstd=gnu20 \
-c tools.build:skip_test=TrueDownstream conanfile.txt:
[requires]
hffix2/1.0.0
[generators]
CMakeDeps
CMakeToolchain
Downstream CMakeLists.txt:
find_package(hffix2 REQUIRED)
target_link_libraries(my_app PRIVATE hffix2::hffix)Dataset and bench run are separate targets. The dataset is NOT committed (would be multi-GB); generate it locally before running benchmarks:
cmake --build build --target bench_data # writes benchmarks/data/synthetic.fix
cmake --build build --target bench # runs benchmarks against itbench does NOT depend on bench_data. Re-invoke the latter only when
seed, size, or generator change. Size is controlled via
-DHFFIX_BENCH_MESSAGES=N at configure time (default 500 000).
The bench suite registers:
BM_Parse_ScanMessages:next_message_reader()only, no field work.BM_Parse_IterAllFields: visit every field of every message.BM_Parse_FindCommonFields:find_with_hintfor 5 header tags.BM_Parse_FindManyFields_Iter:find_with_hintfor 15 tags mixed across the message (10 present in MD bodies, 5 always absent → force-scans every field).BM_Parse_FindLotsFields_Iter: same idea, 30 tags (10 present, 20 absent); iterator-style lookup worst case.BM_Parse_FindManyFields_*_HighHit: 15-tag set with ~55-65% per-tag hit rate against fixgen's mix (iter + indexed variants). Honest baseline against the absent-bias above.BM_Parse_FindCommonFields_Indexed/BM_Parse_FindManyFields_Indexed/BM_Parse_FindLotsFields_Indexed: same lookup workloads viabuild_field_index+idx.find_with_hint.BM_Parse_BuildFieldIndex: index build only, no lookups.BM_Parse_FindN_Iter/<n>/BM_Parse_FindN_Indexed/<n>: per-lookup amortization forn ∈ {3,5,7,8,10,13,15,20,30}.BM_Parse_Sequential_Iter/BM_Parse_Random_Iter/BM_Parse_Sequential_Indexed/BM_Parse_Random_Indexed: 10 tags read in on-wire order vs reversed. Exercisesfind_with_hintcarry across calls (sequential = forward, random = wrap-heavy).BM_Parse_TailLatency_{Sequential,Random}_{Iter,Indexed}: same pattern, per-message timing viaclock::now()probes. Reports p95/p99/p999/max. Probe adds ~20-30 ns/sample uniformly; relative deltas comparable, absolute p* not.BM_Parse_GroupAccess_Sugared/BM_Parse_GroupAccess_Manual: sameMDEntryPx+MDEntrySizeper-entry lookups, viareader.group<>()and via hand-rolled delimiter scan. Throughput parity check on the sugar.- Single-message hot path:
BM_WriteLogon,BM_WriteNewOrder,BM_WriteNewOrder_EpochNanos,BM_WriteNewOrder_Closure,BM_ReadMessageScan,BM_ReadMessageFindFields,BM_RoundTrip,BM_ParseTimestampMillis,BM_ParseTimestampNanos.
Single-threaded, -O3 -flto=full, native arch flag enabled. Synthetic
dataset (utils/fixgen, default 500 000 messages, avg 1222 B/msg):
mixed MD snapshots, MD incrementals up to 200 entries, NewOrderSingle,
ExecutionReport, session messages. Mean of 5 repetitions at
--benchmark_min_time=1s.
Iterator path (upstream API surface, like-for-like):
| Benchmark | upstream | fork |
|---|---|---|
ScanMessages |
21.1 M msgs/s | 21.4 M (+2%) |
IterAllFields |
122 M f/s | 218 M (+78%) |
FindCommonFields(5) |
16.3 M msgs/s | 18.1 M (+11%) |
FindManyFields(15) |
0.143 M msgs/s | 0.251 M (+76%) |
FindLotsFields(30) |
0.0415 M msgs/s | 0.0762 M (+84%) |
Indexed (field_index_buffer<N> path):
| Benchmark | fork idx |
|---|---|
BuildFieldIndex (no lookups) |
1.42 M msgs/s |
FindCommonFields(5) idx |
1.43 M msgs/s (-92% vs iter) |
FindManyFields(15) idx |
1.05 M msgs/s (+319% vs iter) |
FindLotsFields(30) idx |
0.668 M msgs/s (+777% vs iter) |
Realistic hit-rate (15 tags, ~55-65% per-tag hit on fixgen's mix; no absent-bias inflation):
| Benchmark | fork |
|---|---|
FindManyFields_Iter_HighHit |
0.166 M msgs/s |
FindManyFields_Indexed_HighHit |
0.949 M msgs/s (+472% vs iter) |
Group access (10-entry MD increment payload, MDEntryPx + MDEntrySize
per entry):
| Benchmark | fork |
|---|---|
GroupAccess_Sugared (reader.group<>()) |
0.807 M msgs/s |
GroupAccess_Manual (hand-rolled delimiter scan) |
0.826 M msgs/s |
Sugar within 2% of the manual baseline; difference inside run-to-run noise.
Single-message hot path (in-cache):
| Benchmark | upstream | fork |
|---|---|---|
BM_WriteLogon |
52.0 ns | 24.1 ns (-54%) |
BM_WriteNewOrder |
74.0 ns | 46.8 ns (-37%) |
BM_WriteNewOrder_EpochNanos |
— | 49.6 ns |
BM_WriteNewOrder_Closure |
— | 46.3 ns |
BM_ReadMessageScan |
90.5 ns | 59.4 ns (-34%) |
BM_ReadMessageFindFields |
91.4 ns | 55.0 ns (-40%) |
BM_RoundTrip |
169 ns | 99.6 ns (-41%) |
BM_ParseTimestampMillis |
— | 7.29 ns |
BM_ParseTimestampNanos |
— | 7.17 ns |
_EpochNanos parity with _NewOrder (49.6 vs 46.8 ns) shows
push_back_timestamp_epoch_nanos has no overhead vs the chrono
time_point overload. _Closure (try_write_message lambda) parity
shows the closure helper is free.
Per-lookup-count break-even on a single NewOrder message in cache:
| Lookups | iterator | indexed |
|---|---|---|
| 3 | 17.3 ns | 62.2 ns |
| 5 | 44.6 ns | 64.7 ns |
| 8 | 120 ns | 72.2 ns |
| 13 | 353 ns | 92.4 ns |
| 15 | 449 ns | 99.7 ns |
Index amortization on the dataset (throughput in M msgs/s;
ratio = indexed / iterator):
| Lookups | iterator | indexed | idx/iter |
|---|---|---|---|
| 3 | 21.3 M | 1.42 M | 0.07× |
| 5 | 18.2 M | 1.41 M | 0.08× |
| 7 | 1.42 M | 1.41 M | 0.99× |
| 8 | 1.33 M | 1.40 M | 1.05× |
| 10 | 1.19 M | 1.37 M | 1.16× |
| 13 | 0.358 M | 1.19 M | 3.33× |
| 15 | 0.248 M | 1.09 M | 4.40× |
| 20 | 0.139 M | 0.900 M | 6.48× |
| 30 | 0.0730 M | 0.671 M | 9.19× |
Break-even at ~8 lookups on the dataset; at 30 lookups indexed wins ~9× over iterator-style search.
Sequential vs random tag read (10 tags, in on-wire order vs reversed):
| Benchmark | upstream | fork |
|---|---|---|
Sequential_Iter |
0.755 M msgs/s | 1.22 M (+62%) |
Random_Iter |
0.138 M msgs/s | 0.236 M (+71%) |
Sequential_Indexed |
— | 1.43 M msgs/s |
Random_Indexed |
— | 1.10 M msgs/s |
Random reads cost ~5× sequential under the iterator path; the indexed path closes the gap to ~1.3× (random ~77% of sequential).
Tail latency per message. Includes ~20-30 ns clock::now() probe per
sample; same overhead in every variant, so relative deltas are
meaningful, absolute p* are not.
| Benchmark | p95 | p99 | p999 | max |
|---|---|---|---|---|
Sequential_Iter (fork) |
3780 ns | 4530 ns | 4840 ns | 29800 ns |
Random_Iter (fork) |
22500 ns | 27100 ns | 29100 ns | 85100 ns |
Sequential_Indexed (fork) |
4020 ns | 4820 ns | 5150 ns | 34100 ns |
Random_Indexed (fork) |
5140 ns | 6180 ns | 7220 ns | 81700 ns |
Sequential_Iter (upstream) |
6690 ns | 8030 ns | 8490 ns | 45700 ns |
Random_Iter (upstream) |
39200 ns | 47100 ns | 50100 ns | 202000 ns |
Random_Iter tails blow up (max ~85 µs fork, ~202 µs upstream)
because every lookup forces a full message rescan on a 1.2 KB MD frame.
Random_Indexed keeps the tail within ~2× of Sequential_Indexed.
build_field_index walks a message once and fills a caller-supplied
field_index_buffer<N> with two parallel arrays: int tags[i] and a
packed uint64_t pos_len[i] = (uint32_t value_pos << 32) | uint32_t value_len.
indexed_message::find_with_hint scans the tags array and unpacks
pos_len only at the match site. Pays off above ~6 lookups per
message; see Benchmarks.
Two flags. Check both before treating find_with_hint() "absent" as real.
overflowed(): message > 4 GiB, or any single field's position / length exceedsuint32_t. The packedpos_lenhalves cannot address past that. No index is built, tag/pos_len arrays empty. Iterator API still works on the message.truncated(): message has more fields thanfield_index_buffer<N>::capacity. Index holds the firstNin insertion order; fields past slotNare invisible tofind_with_hint(). BumpN, fall back to iterator, or accept the false-negative explicitly.
idx.authoritative() is shorthand for !truncated() && !overflowed().
Gate every find_with_hint() on it.
hffix::field_index_buffer<512> idx_buffer;
for (auto r = hffix::message_reader(buf, buf + n);
r.is_complete() && r.is_valid();
r = r.next_message_reader()) {
auto idx = hffix::build_field_index(r, idx_buffer);
if (!idx.authoritative()) [[unlikely]] {
hffix::message_reader::const_iterator it = r.begin();
if (!r.find_with_hint(hffix::tag::Price, it)) continue;
auto price = it->value();
continue;
}
std::size_t h = 0;
auto price = idx.find_with_hint(hffix::tag::Price, h);
if (price.begin() == price.end()) continue;
}Skipping the authoritative() check treats find_with_hint() == empty
as "absent" even when the field exists past slot N. Silent false
negative.
field_index_buffer<N> is stack-allocated, 12*N bytes (int tags[N]
+ uint64_t pos_len[N]) plus up to 32 bytes of alignas(32) padding.
Oversize freely; assert !truncated() in dev.
- Session messages (Logon, Heartbeat, ResendRequest):
N=16. - Single-instrument order flow (NewOrderSingle, ExecutionReport):
15–30 fields,
N=64. - Market data snapshots / incrementals: depends on
NoMDEntries.N=512covers most feeds; full-book refreshes can exceed it. - Multi-leg orders with nested groups:
N=128+.
message_writer is noexcept end-to-end. Each push_back_* checks
the remaining buffer; on overflow it flips an internal error flag and
becomes a no-op for the rest of the message. Caller checks push_back_trailer()
once at the end (returns bool). No exception escapes.
#include <hffix.hpp>
char buf[1024];
hffix::message_writer w(buf);
w.push_back_header("FIXT.1.1");
w.push_back_string(hffix::tag::MsgType, "D");
w.push_back_int(hffix::tag::OrderQty, 100);
w.push_back_decimal(hffix::tag::Price, 50001, -2);
if (!w.push_back_trailer()) {
// buffer was too small somewhere; nothing was sent.
return;
}
char* end = w.message_end();Each push_back_*: one bounds check against the field's worst-case
byte count, then unchecked writes through *_unchecked digit helpers
and memcpy. Poison check and bounds check are both
predicted-not-taken.
Closure helper:
char* end;
bool ok = hffix::try_write_message(buf, buf + sizeof(buf), end,
[&](hffix::message_writer& w) {
w.push_back_header("FIXT.1.1");
w.push_back_string(hffix::tag::MsgType, "D");
w.push_back_int(hffix::tag::OrderQty, 100);
});Two error channels.
1. Recoverable parse errors: surfaced as state, never as
exceptions. On the reader: is_complete() / is_valid(). On the
writer: a sticky has_error() flag plus a bool return from
push_back_trailer(). Validating parsers return bool via the try_as_*
family. None of these trap. Counter++ in a metric, drop the frame,
log, and continue.
2. Programmer-error preconditions: expand to HFFIX_ASSERT(cond, msg). Default behavior depends on NDEBUG:
| Build | NDEBUG |
HFFIX_ASSERT failure |
|---|---|---|
| Release | defined | Atomic counter bump (lock-free, no abort, no allocation). |
| Debug | not defined | Trap: __builtin_trap() on gcc/clang, __debugbreak() on MSVC, std::abort() elsewhere. |
Read the counter:
auto fails = hffix::assert_failure_count();
hffix::reset_assert_failure_count();Benchmarks and the fuzz harness ship their own assert_override.hpp
that always std::abort()s, so a violation under those binaries is a
crash regardless of NDEBUG (libFuzzer needs SIGABRT to detect
assertion failures). Tests rely on the debug-build trap.
Sites:
next_message_reader()on an incomplete reader.calculate_check_sum()/message_end()/begin()/end()/message_type()/check_sum()on an invalid reader.iterator::operator+(n)withn < 0.
These are caller-contract checks, not wire-data checks. A well-formed call site will not fire them no matter what bytes arrive on the socket.
Define HFFIX_ASSERT before <hffix.hpp>:
extern void hffix_on_assert(char const* msg) noexcept;
#define HFFIX_ASSERT(cond, msg) \
do { if (!(cond)) [[unlikely]] hffix_on_assert(msg); } while (0)
#include <hffix.hpp>The library's definition is gated on #ifndef HFFIX_ASSERT, so the
override wins. In a monorepo, set it once in a project-wide header
that precedes <hffix.hpp> in every TU.
Epoch-materializing parsers reject year < 1970 or year > 2200:
field_value::as_epoch_nanos()→std::optional<int64_t>field_value::as_epoch_millis()→std::optional<int64_t>field_value::as_timestamp(std::chrono::time_point&)field_value::as_timestamp_nano(std::chrono::time_point&)
Out-of-range YYYY returns nullopt / false. The int-out
overloads (as_timestamp(int& y, int& m, ...), as_date,
as_timeonly, as_timeonly_nano) skip epoch math and are
unaffected.
FIX YYYY is 4 ASCII digits, so wire input can carry 0000..9999.
For year = 9999, building a time_point<_, nanoseconds> triggers
a seconds → nanoseconds widening (* 1'000'000'000). Result
overflows int64_t and wraps silently; the time_point holds
garbage.
<chrono> does not check arithmetic overflow. duration_cast,
operator+ on durations, and the common_type promotion that fires
on seconds + nanoseconds are plain int64_t ops with wrap on
overflow. The real chrono::sys_time<nanoseconds> wall is
2262-04-11 23:47:16.854775807 UTC (known "year 2262 problem"). Cap
2200 sits below it with margin for the proleptic-Gregorian
day-of-year / era-boundary corner cases.
For dates outside [1970, 2200], use the int-out as_timestamp
overload. It skips the epoch path and bounds only
month ∈ [1, 12], day ∈ [1, 31]. Caller handles date arithmetic.
basic_message_readeris logically immutable after construction. Concurrent reads from multiple threads on the sameconst&are safe. Copies are independent (the copy constructor re-runsinit()), cheap, and safe to make across threads.basic_message_reader_const_iteratorreferences its parent reader and the underlying buffer. Iterators dereferenced concurrently on the same buffer are safe; do not move them across threads while one of them is being advanced.basic_indexed_messageis read-only afterbuild_field_indexreturns. Concurrentfind_with_hint/has/value_atcalls on the same instance are safe.find_with_hintmutates the caller-suppliedhint, so do not share the same hint variable across threads.message_writeris single-owner. Do not share an instance across threads or invokepush_back_*concurrently on the same writer. The buffer the writer points into must not be read by another thread untilpush_back_trailer()returnstrue.- The library uses one global,
constinitlookup table (details::g_length_tag_table). Static-init order is safe; reads are concurrent-clean. - No file-scope state mutates at runtime; no hidden allocations; no thread-local state.
The de-facto deployment model is one reader/writer per worker thread,
each operating on a per-thread buffer fed from a single feed handler.
Sharing a parsed indexed_message to fan out across consumer
threads is supported as long as the underlying buffer is not mutated.
A single TCP recv() or Aeron poll typically delivers N concatenated
FIX frames. Two helpers iterate them without manual cursor bookkeeping:
#include <hffix.hpp>
// Range form. Yields complete + valid readers; skips invalid frames via
// next_message_reader's "8=FIX" resync; stops at the first incomplete
// frame.
void handle(char const* buf, std::size_t n) {
auto range = hffix::messages(buf, n);
auto it = range.begin();
for (; it != range.end(); ++it) {
process(*it);
}
char const* tail_begin = it.remainder(); // partial frame to keep
// copy [tail_begin, buf + n) into the head of the next read buffer
}// Callback form. Returns the unconsumed tail pointer directly.
char const* tail = hffix::for_each_message(buf, buf + n,
[&](hffix::message_reader const& r) {
process(r);
});Both forms prefetch the next message's header before invoking the
callback. Per-message overhead is the same as a hand-rolled
while (r.is_complete()) { ... r = r.next_message_reader(); } loop;
the helpers just hide the is_complete / is_valid ladder and the
tail-pointer extraction.
Pick the range form when you want to compose with std::ranges /
algorithms or break out of iteration. Pick the callback form when you
have a single dispatch closure and want the tail pointer in one
expression.
reader.group<tag::NoXxx>() returns a basic_group_view over the
group's entries. The delimiter tag (first tag of the entry block) is
looked up at compile time in hffix::groups::group_def<CountTag>:
hffix::message_reader r(buf, w.message_end());
if (!r.is_valid()) return;
for (auto const& entry : r.group<hffix::tag::NoMDEntries>()) {
auto it = entry.begin();
if (entry.find_with_hint(hffix::tag::MDEntryPx, it))
auto px = it->value();
if (entry.find_with_hint(hffix::tag::MDEntrySize, it))
auto sz = it->value().as_int_unchecked<int>();
}All FIX 5.0 SP2 + FIXT 1.1 groups are pre-registered via the generated
hffix_groups.hpp. To override a delimiter (for venue extensions or a
group whose delimiter differs across messages), declare your own
registration at namespace scope:
HFFIX_REGISTER_GROUP(NoQuoteEntries, QuoteEntryID);Unknown CountTag triggers a static_assert. The runtime overload
reader.group(count_tag, first_tag) is also available for ad-hoc
groups without registration.
The iterator is bounded by the NoXxx count; trailing non-group fields
do not leak into the last entry. Nested groups: call
entry.group<Inner>() on a basic_group_entry.
For many lookups per entry, build a per-entry index:
hffix::field_index_buffer<32> entry_idx;
for (auto const& entry : r.group<hffix::tag::NoMDEntries>()) {
auto idx = hffix::build_field_index(entry, entry_idx);
std::size_t h = 0;
auto px = idx.find_with_hint(hffix::tag::MDEntryPx, h);
// ...
}Standalone CMake project under fuzz/ builds a libFuzzer + ASan +
UBSan binary that hammers message_reader, for_each_message,
repeating groups, build_field_index, and the try_as_* parser
family.
The fuzz target depends on fuzz_dataset, which copies seed files
from tests/data/ into build/fuzz/dataset/ before the
run. Mutated inputs that libFuzzer persists land in the same build-tree
directory. Crashes / timeouts are written next
to the working directory; replay with
build/fuzz/fuzz_reader ./crash-<hash>.
| Option | Default | Effect |
|---|---|---|
HFFIX_BUILD |
ON top-level / OFF as subdir | Tests, benchmarks, CLI. |
HFFIX_BUILD_DOCS |
OFF | Doxygen HTML (requires doxygen in PATH). |
HFFIX_BUILD_FIXSPEC_GEN |
matches HFFIX_BUILD |
Builds fixspec-gen-fields. |
HFFIX_NATIVE_ARCH |
ON | -mcpu=native / -march=native. |
HFFIX_LTO |
ON | CMAKE_INTERPROCEDURAL_OPTIMIZATION (Clang/AppleClang forced to -flto=full). |
HFFIX_SANITIZE |
OFF | -fsanitize=address,undefined. Use with -DCMAKE_BUILD_TYPE=Debug and -DHFFIX_LTO=OFF. |
HFFIX_SANITIZE_THREAD |
OFF | -fsanitize=thread. Mutually exclusive with HFFIX_SANITIZE. |
HFFIX_BENCH_MESSAGES |
500000 | Dataset size for bench_data. |
HFFIX_BENCH_MIN_TIME |
1s |
--benchmark_min_time for bench. |
HFFIX_BENCH_REPETITIONS |
5 | --benchmark_repetitions for bench. |
Debug + sanitizers:
conan install . --output-folder=build-asan --build=missing \
-s build_type=Debug -s compiler.cppstd=gnu20 -s:b compiler.cppstd=gnu20
cmake --preset conan-debug -DHFFIX_SANITIZE=ON -DHFFIX_LTO=OFF -DHFFIX_NATIVE_ARCH=OFF
cmake --build build-asan -j
ctest --test-dir build-asanSwap -DHFFIX_SANITIZE=ON for -DHFFIX_SANITIZE_THREAD=ON for TSan.
HFFIX_SANITIZE and HFFIX_SANITIZE_THREAD are mutually exclusive.
Source: QuickFIX-format XML specs (fixspec/FIX50SP2.xml +
fixspec/FIXT11.xml). Single tool invocation emits both headers:
cmake --build build --target fixspec-gen-fields
build/utils/fixspec-gen-fields fixspec/FIX50SP2.xml fixspec/FIXT11.xml \
-o include/hffix_fields.hpp \
-go include/hffix_groups.hppFIX 5.0 splits session (FIXT.1.1) from application (FIX.5.0 SP2) into
two files; pass both. Extra venue-specific XMLs can be appended as
further positional args; fields/messages with identical definitions
are deduplicated, name conflicts on the same tag emit aliases, group
delimiters that differ across messages keep the first-encountered
one and the rest stay reachable via the runtime overload
reader.group(count_tag, first_tag).
hffix_groups.hpp carries HFFIX_REGISTER_GROUP(NoXxx, FirstTag);
declarations for every <group> in the input specs (~500 for
FIX 5.0 SP2 + FIXT 1.1). The bundled copy is committed; regenerate
to track a spec update or pull in a venue extension.
Downstream projects that consume hffix via add_subdirectory(hffix) or
find_package(hffix) can call the bundled CMake helper:
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE hffix2::hffix)
hffix_generate_fields(
TARGET my_app
SPEC_XML ${PROJECT_SOURCE_DIR}/my_spec/FIX50SP2.xml
${PROJECT_SOURCE_DIR}/my_spec/FIXT11.xml
)The helper builds the generator if needed, regenerates
hffix_fields.hpp and hffix_groups.hpp from the supplied specs,
adds the dependency to my_app, and prepends the output dir to its
include path with BEFORE so the regenerated headers shadow the
bundled ones.