Skip to content

feat!: C ABI callback streaming, std::sync::mpsc removed from ffi (refs #482)#490

Merged
userFRM merged 1 commit into
mainfrom
feat/482b-c-abi-callback
May 6, 2026
Merged

feat!: C ABI callback streaming, std::sync::mpsc removed from ffi (refs #482)#490
userFRM merged 1 commit into
mainfrom
feat/482b-c-abi-callback

Conversation

@userFRM
Copy link
Copy Markdown
Owner

@userFRM userFRM commented May 6, 2026

Summary

Stage B of issue #482: rewrite the C ABI streaming layer in
ffi/src/streaming.rs to deliver events through callback registration
instead of polling. The user-supplied extern "C" fn flows through the
SSOT StreamingDispatcher (added in #489) for the queued path, or
directly on the FPSS reader thread for the inline path.

This is the C-ABI-only stage. Python and TypeScript bindings do not
depend on the FFI poll path -- they call thetadatadx::ThetaDataDx::start_streaming
directly with their own internal mpsc shims, so this change does not
break their compile. Their bindings migrate to the callback API in
follow-up PRs.

Removed C ABI symbols

  • tdx_unified_start_streaming
  • tdx_unified_next_event
  • tdx_fpss_next_event
  • tdx_fpss_event_free

All std::sync::mpsc::channel, Sender, Receiver, recv_timeout
references and the buffered-event closure path are gone. Verified:
rg "std::sync::mpsc" ffi/ --type rust returns zero matches.

New C ABI symbols

typedef void (*TdxFpssCallback)(const TdxFpssEvent* event, void* ctx);

int tdx_unified_set_callback(const TdxUnified* h, TdxFpssCallback cb, void* ctx);
int tdx_unified_set_inline_callback(const TdxUnified* h, TdxFpssCallback cb, void* ctx);
int tdx_fpss_set_callback(const TdxFpssHandle* h, TdxFpssCallback cb, void* ctx);
int tdx_fpss_set_inline_callback(const TdxFpssHandle* h, TdxFpssCallback cb, void* ctx);
  • Queued (set_callback): events flow FPSS reader -> bounded(8192) crossbeam queue -> dispatcher drain thread -> user fn. Reader never
    blocks on user code; overflow events are dropped and counted via
    tdx_*_dropped_events.
  • Inline (set_inline_callback): user fn fires directly on the
    FPSS reader thread, bypassing the queue. Microsecond-budget contract
    -- any allocation, I/O, or lock acquisition will stall the reader and
    cause the vendor session to drop. Identical semantics to
    start_streaming_inline.

Semantic migration

  • Poll loop -> callback registration. Old: caller spun on
    tdx_fpss_next_event(handle, timeout_ms) and freed each returned
    event. New: caller registers an extern "C" fn once via
    tdx_fpss_set_callback; the SDK invokes it for every event with the
    registered ctx opaque pointer.
  • Event lifetime. Old: *mut TdxFpssEvent was caller-owned and
    required tdx_fpss_event_free. New: the *const TdxFpssEvent
    pointer handed to the callback is valid only for the duration of that
    call; callers must copy any fields they want to outlive the
    callback.
  • tdx_fpss_connect defers the FPSS TLS connection until the first
    set_callback / set_inline_callback call. FpssClient::connect
    registers its event handler at construction time, so deferring the
    connect lets us avoid an internal queue and atomically bind the C
    callback to the FPSS reader.
  • tdx_*_reconnect re-uses the previously-registered callback so
    callers do not re-supply it. Returns -1 with a clear error if no
    callback was ever installed (the new ABI has no out-of-band buffer
    to fall back on).
  • tdx_*_dropped_events now reports StreamingDispatcher overflow
    drops (queue full when the reader tried to enqueue), not channel-
    disconnect drops. Returns 0 in inline mode (no queue exists).

C++ wrapper change

The C++ wrapper at sdks/cpp/include/thetadx.hpp was minimally
adjusted, not stubbed with #error/static_assert:

  • The generator's next_event method (MethodKind::NextEvent) was
    removed from the cpp_fpss target in sdk_surface.toml. Regenerated
    sdks/cpp/include/fpss.hpp.inc and sdks/cpp/src/fpss.cpp.inc
    no longer declare or define FpssClient::next_event.
  • FpssEventDeleter and FpssEventPtr are deleted from thetadx.hpp.
  • FpssClient::dropped_events() now reports dispatcher overflow drops
    (semantics changed; method preserved).
  • The sdks/cpp/examples/fpss_smoke.cpp example calls into the
    removed next_event API, so it now opens with #error "fpss_smoke.cpp depends on the removed next_event poll API. Re-enable when the C++ wrapper migrates to the callback C ABI in PR E (refs #482)." -- a static breakage signal so downstream consumers
    do not silently miss the API change.
  • The C++ wrapper migration to set_callback / set_inline_callback
    ships in a follow-up PR.

Tests

5 new unit tests in ffi/src/streaming.rs::tests cover the callback
wiring without needing a real FPSS server:

  • ffi_callback_inline_invokes_user_fn_on_caller_thread -- inline
    mode runs the user fn synchronously on the caller thread with the
    registered ctx.
  • ffi_callback_queued_runs_on_dispatcher_thread -- queued mode runs
    the user fn on the dispatcher's drain thread (different OS thread id
    than the producer).
  • ffi_callback_is_send_and_sync -- compile-time assertion that
    FfiCallback is Send + Sync.
  • fpss_dropped_events_zero_before_callback -- handle returns 0 when
    no callback has been installed.
  • unified_dropped_events_handles_null -- null-handle defensive
    return + dispatcher steady-state drop count.

No version bump

Cargo.toml stays at 8.0.29. The version bump rides the coherent
release that lands when all bindings (Python/TS/C++) are migrated.

Test plan

  • cargo fmt --all -- --check clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo test --workspace passing (475+ tests, including the 5 new
    callback unit tests)
  • cargo deny check clean
  • cargo run -p thetadatadx --bin generate_sdk_surfaces --features config-file -- --check clean
  • cargo check --manifest-path tools/mcp/Cargo.toml --locked
  • cargo clippy --manifest-path tools/mcp/Cargo.toml --all-targets -- -D warnings
  • cargo test --manifest-path tools/mcp/Cargo.toml --no-run
  • cargo check --manifest-path tools/server/Cargo.toml --locked
  • cargo check --manifest-path sdks/python/Cargo.toml --locked
  • cargo check --manifest-path sdks/typescript/Cargo.toml --locked
  • rg "std::sync::mpsc" ffi/ --type rust returns zero
  • CI green on the PR
  • Manual verification of set_callback / set_inline_callback
    from a C consumer once the C++ wrapper migration lands

#482)

Rewrites the C ABI streaming layer in ffi/src/streaming.rs to deliver
events through callback registration instead of polling. The user-
supplied extern "C" fn flows through the SSOT StreamingDispatcher
landed in #489 (queued mode) or directly on the FPSS reader thread
(inline mode).

Removed C ABI symbols:
- tdx_unified_start_streaming
- tdx_unified_next_event
- tdx_fpss_next_event
- tdx_fpss_event_free

New C ABI symbols (with TdxFpssCallback typedef):
- tdx_unified_set_callback / tdx_unified_set_inline_callback
- tdx_fpss_set_callback / tdx_fpss_set_inline_callback

tdx_fpss_connect now defers FPSS TLS connection until the first
set_callback call so callback registration and connect are atomic.
tdx_*_dropped_events now reports StreamingDispatcher overflow drops
(returns 0 in inline mode -- no queue). tdx_*_reconnect re-uses the
previously-registered callback so callers do not re-supply it.

The C++ wrapper's next_event / FpssEventPtr / FpssEventDeleter are
removed; thetadx.hpp now exposes only configuration / subscription /
lifecycle methods. The C++ wrapper migration to the callback API
ships in a follow-up PR. Python and TypeScript SDKs do not depend
on the FFI poll path -- they use thetadatadx::ThetaDataDx::start_streaming
directly with their own internal mpsc shims, so this change does not
break their compile.

No version bump.
@userFRM userFRM merged commit a25ce7b into main May 6, 2026
31 checks passed
@userFRM userFRM deleted the feat/482b-c-abi-callback branch May 6, 2026 12:25
userFRM added a commit that referenced this pull request May 6, 2026
Migrate the C++ wrapper to the callback C ABI shipped in PR #490.
`tdx::FpssClient` gains two header-only methods that wrap
`tdx_fpss_set_callback` / `tdx_fpss_set_inline_callback`:

    void set_callback(std::function<void(const FpssEvent&)> fn);
    void set_inline_callback(std::function<void(const FpssEvent&)> fn);

The Client owns a `unique_ptr<std::function<...>>` -- a stable
address survives moves of the owning client and is handed to the C
ABI as the `void* ctx`. A free `extern "C"` shim recovers the
function from `ctx` and invokes it with `const FpssEvent&`,
swallowing any propagating exception so unwinding cannot cross the
Rust/C boundary. The destructor's call to `tdx_fpss_shutdown` runs
before the function storage is freed, so the dispatcher / reader
threads cannot dereference stale state.

`fpss_smoke.cpp` is restored on the callback path -- `#error`
directive deleted, example rewritten to subscribe, register a
queued callback, print events for five seconds, and exit cleanly.
The CMake target builds clean.

`sdks/cpp/README.md` streaming section now documents callback
registration as the only entry point with a dedicated note on the
inline opt-in's microsecond-budget contract.

Version bump 8.0.29 -> 8.0.30 via `scripts/bump_version.py`. The
[Unreleased] CHANGELOG block becomes `## [8.0.30] - 2026-05-06` --
the single coherent #482 release that bundles the dispatcher core
(#489), C ABI callback (#490), and C++ wrapper migration. Python
(#492) and TypeScript (#493) entries land when those PRs merge.

Closes #482.
userFRM added a commit that referenced this pull request May 6, 2026
* feat!: C++ callback wrapper + v8.0.30 release (closes #482)

Migrate the C++ wrapper to the callback C ABI shipped in PR #490.
`tdx::FpssClient` gains two header-only methods that wrap
`tdx_fpss_set_callback` / `tdx_fpss_set_inline_callback`:

    void set_callback(std::function<void(const FpssEvent&)> fn);
    void set_inline_callback(std::function<void(const FpssEvent&)> fn);

The Client owns a `unique_ptr<std::function<...>>` -- a stable
address survives moves of the owning client and is handed to the C
ABI as the `void* ctx`. A free `extern "C"` shim recovers the
function from `ctx` and invokes it with `const FpssEvent&`,
swallowing any propagating exception so unwinding cannot cross the
Rust/C boundary. The destructor's call to `tdx_fpss_shutdown` runs
before the function storage is freed, so the dispatcher / reader
threads cannot dereference stale state.

`fpss_smoke.cpp` is restored on the callback path -- `#error`
directive deleted, example rewritten to subscribe, register a
queued callback, print events for five seconds, and exit cleanly.
The CMake target builds clean.

`sdks/cpp/README.md` streaming section now documents callback
registration as the only entry point with a dedicated note on the
inline opt-in's microsecond-budget contract.

Version bump 8.0.29 -> 8.0.30 via `scripts/bump_version.py`. The
[Unreleased] CHANGELOG block becomes `## [8.0.30] - 2026-05-06` --
the single coherent #482 release that bundles the dispatcher core
(#489), C ABI callback (#490), and C++ wrapper migration. Python
(#492) and TypeScript (#493) entries land when those PRs merge.

Closes #482.

* fix(cpp): address review on PR #494 -- memory safety on set_callback / move-assign

set_callback / set_inline_callback now stage the new std::function into a
local unique_ptr and only adopt it into callback_ after tdx_fpss_set_callback
returns 0. The C ABI rejects subsequent registrations with -1 while keeping
the previously installed (callback, ctx) live, so overwriting callback_
before checking the return code dangled the Rust-side ctx into freed
storage.

operator=(FpssClient&&) now drains the existing handle via tdx_fpss_shutdown
before dropping the old callback_. tdx_fpss_shutdown stops the FPSS reader
and joins the dispatcher drain thread before returning, so once it
completes no thread can still observe the old ctx pointer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant