Skip to content

Commit 225a801

Browse files
userFRMclaude
andauthored
fix(cpp): pin ThetaDataDxContract size and field offsets directly (#875)
The shared ThetaDataDxContract is embedded by value in every data event, but the generated C++ layout asserts pinned it only indirectly: each embedding event asserts the field after `contract` lands at offset 40, and the tagged wrapper asserts its embedded slot. A same-total-size permutation of the contract's internal fields would pass every existing assert yet silently misread contract.right, contract.strike, and contract.expiration on each event. Emit direct static_asserts so the contract struct's total size AND every field offset are compile-time pinned to the Rust #[repr(C)] layout, the same way the tick mirrors are pinned in tick_layout_asserts.hpp.inc. The asserted numbers come from the generator's own align_to layout walk over a single contract field model, never hand-typed, so Rust and C stay locked together on regeneration. Co-authored-by: preview <noreply@anthropic.com>
1 parent ce512f8 commit 225a801

3 files changed

Lines changed: 105 additions & 1 deletion

File tree

crates/thetadatadx/build_support_bin/fpss_events/cpp_asserts.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,37 @@
99
use std::fmt::Write as _;
1010

1111
use super::common::snake_case;
12-
use super::layout::{c_struct_offsets, c_struct_size, fpss_event_offsets, fpss_event_size};
12+
use super::layout::{
13+
c_struct_offsets, c_struct_size, contract_field_offsets, contract_size, fpss_event_offsets,
14+
fpss_event_size,
15+
};
1316
use super::schema::{sorted_control_events, sorted_data_events, EventDef, Schema};
1417

18+
/// Emit direct size + per-field offset asserts for the shared
19+
/// `ThetaDataDxContract`. Every data event embeds this struct by value, so an
20+
/// internal field permutation that preserved the 40-byte total (swapping the
21+
/// `has_*` flags, say) would slip past the embedding events' offset asserts
22+
/// yet silently misread `contract.right` / `.strike` / `.expiration` on every
23+
/// tick. Pinning the contract's own size and each field offset closes that
24+
/// gap the same way the tick mirrors are pinned. Numbers come from the
25+
/// generator's layout walk, not hand-typed constants.
26+
fn render_contract_asserts(out: &mut String) {
27+
writeln!(
28+
out,
29+
"static_assert(sizeof(ThetaDataDxContract) == {},\n \"ThetaDataDxContract total size drifted\");",
30+
contract_size()
31+
)
32+
.unwrap();
33+
for (field_name, offset) in contract_field_offsets() {
34+
writeln!(
35+
out,
36+
"static_assert(offsetof(ThetaDataDxContract, {field_name}) == {offset},\n \"ThetaDataDxContract::{field_name} offset drifted\");"
37+
)
38+
.unwrap();
39+
}
40+
out.push('\n');
41+
}
42+
1543
fn render_event_asserts(out: &mut String, event_name: &str, def: &EventDef) {
1644
let alias = format!("ThetaDataDxStream{event_name}");
1745
let field = snake_case(event_name);
@@ -54,6 +82,10 @@ pub(super) fn render_cpp_fpss_layout_asserts(schema: &Schema) -> String {
5482
"/* @generated DO NOT EDIT — regenerated by build.rs from fpss_event_schema.toml */\n\n",
5583
);
5684

85+
// Shared `ThetaDataDxContract` — pinned directly so an internal field
86+
// permutation can't hide behind the embedding events' offset asserts.
87+
render_contract_asserts(&mut out);
88+
5789
// Per-variant struct asserts — data first, then control, mirroring
5890
// the Rust struct emission order in `ffi_rust.rs`.
5991
for (event_name, def) in sorted_data_events(schema) {

crates/thetadatadx/build_support_bin/fpss_events/layout.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,57 @@ pub(super) fn c_field_layout(ty: &str) -> CFieldLayout {
2727
}
2828
}
2929

30+
/// Physical field layout of the shared `ThetaDataDxContract` struct, in
31+
/// C declaration order. The `Contract` column type is opaque to
32+
/// [`c_field_layout`] (it contributes a single 40-byte/align-8 slot to the
33+
/// embedding event structs), so the contract's own internal fields are
34+
/// modelled here once and consumed by both the size and per-field offset
35+
/// helpers. Keeping this list as the single source means the asserted
36+
/// numbers are computed by the same `align_to` walk that places every other
37+
/// FPSS field, never hand-typed. Order and widths mirror the C mirror in
38+
/// `ffi_c.rs::render_contract_struct_c` and the Rust `#[repr(C)]` struct in
39+
/// `ffi_rust.rs::render_contract_struct_rust`.
40+
fn contract_fields() -> [(&'static str, CFieldLayout); 9] {
41+
[
42+
// `const char* symbol` — borrowed NUL-terminated string pointer.
43+
("symbol", CFieldLayout { size: 8, align: 8 }),
44+
("sec_type", CFieldLayout { size: 4, align: 4 }),
45+
// Tagged-optional `bool has_*` flags + their payloads. C has no
46+
// `Option<T>`, so presence rides a leading `bool`.
47+
("has_expiration", CFieldLayout { size: 1, align: 1 }),
48+
("expiration", CFieldLayout { size: 4, align: 4 }),
49+
("has_right", CFieldLayout { size: 1, align: 1 }),
50+
// `char right` — `'C'` / `'P'` (NUL when `has_right` is false).
51+
("right", CFieldLayout { size: 1, align: 1 }),
52+
("has_strike", CFieldLayout { size: 1, align: 1 }),
53+
("strike", CFieldLayout { size: 8, align: 8 }),
54+
("strike_thousandths", CFieldLayout { size: 4, align: 4 }),
55+
]
56+
}
57+
58+
/// Byte offset of each `ThetaDataDxContract` field, computed by the same
59+
/// `align_to` walk as every other FPSS struct so the emitted asserts pin the
60+
/// contract's internal layout directly rather than inferring it from the
61+
/// embedding events.
62+
pub(super) fn contract_field_offsets() -> Vec<(&'static str, usize)> {
63+
let fields = contract_fields();
64+
let mut offsets = Vec::with_capacity(fields.len());
65+
let mut size = 0;
66+
for (name, layout) in fields {
67+
size = align_to(size, layout.align);
68+
offsets.push((name, size));
69+
size += layout.size;
70+
}
71+
offsets
72+
}
73+
74+
/// Total size, in bytes, of the shared `ThetaDataDxContract` struct, computed
75+
/// from [`contract_fields`]. Must equal the opaque 40-byte slot
76+
/// [`c_field_layout`] reserves for an embedded `Contract` column.
77+
pub(super) fn contract_size() -> usize {
78+
c_layout(contract_fields().into_iter().map(|(_, layout)| layout))
79+
}
80+
3081
/// Expand a schema column list into the physical C field layout. The
3182
/// only column type that splits into >1 physical fields is `Vec<u8>`
3283
/// which becomes `(<name>: *const u8, <name>_len: size_t)`.

sdks/cpp/include/fpss_layout_asserts.hpp.inc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
/* @generated DO NOT EDIT — regenerated by build.rs from fpss_event_schema.toml */
22

3+
static_assert(sizeof(ThetaDataDxContract) == 40,
4+
"ThetaDataDxContract total size drifted");
5+
static_assert(offsetof(ThetaDataDxContract, symbol) == 0,
6+
"ThetaDataDxContract::symbol offset drifted");
7+
static_assert(offsetof(ThetaDataDxContract, sec_type) == 8,
8+
"ThetaDataDxContract::sec_type offset drifted");
9+
static_assert(offsetof(ThetaDataDxContract, has_expiration) == 12,
10+
"ThetaDataDxContract::has_expiration offset drifted");
11+
static_assert(offsetof(ThetaDataDxContract, expiration) == 16,
12+
"ThetaDataDxContract::expiration offset drifted");
13+
static_assert(offsetof(ThetaDataDxContract, has_right) == 20,
14+
"ThetaDataDxContract::has_right offset drifted");
15+
static_assert(offsetof(ThetaDataDxContract, right) == 21,
16+
"ThetaDataDxContract::right offset drifted");
17+
static_assert(offsetof(ThetaDataDxContract, has_strike) == 22,
18+
"ThetaDataDxContract::has_strike offset drifted");
19+
static_assert(offsetof(ThetaDataDxContract, strike) == 24,
20+
"ThetaDataDxContract::strike offset drifted");
21+
static_assert(offsetof(ThetaDataDxContract, strike_thousandths) == 32,
22+
"ThetaDataDxContract::strike_thousandths offset drifted");
23+
324
static_assert(offsetof(ThetaDataDxStreamMarketValue, contract) == 0,
425
"ThetaDataDxStreamMarketValue::contract offset drifted");
526
static_assert(offsetof(ThetaDataDxStreamMarketValue, ms_of_day) == 40,

0 commit comments

Comments
 (0)