Skip to content

bridge-cpp: run host callables inline instead of one thread per crossing#4097

Open
codeshaunted wants to merge 1 commit into
canaryfrom
avery/cpp-inline-host-dispatch
Open

bridge-cpp: run host callables inline instead of one thread per crossing#4097
codeshaunted wants to merge 1 commit into
canaryfrom
avery/cpp-inline-host-dispatch

Conversation

@codeshaunted

@codeshaunted codeshaunted commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

What

The C++ trampoline spawned a detached std::thread for every BAML → host callable crossing:

std::thread([dispatcher = std::move(dispatcher), ...]() mutable {
  dispatcher(call_id, std::move(bytes));
}).detach();

This mirrors bridge_python::host_value, which spawns a tokio task. A tokio task is ~free; an OS thread is not. On an M-series Mac std::thread spawn+detach alone measures 7.7us, and a crossing that does no work costs ~29us — all overhead, on a path hot enough to matter (an embedded game loop issues thousands of these per second).

This PR runs the dispatcher inline on the engine's worker instead.

Why it's safe

host_impls installs the in-flight entry (InflightGuard::new(call_id)) before calling fire_dispatch, so completing from inside the dispatch callback resolves an already-registered oneshot. The awaiting future simply polls ready.

Inline dispatch does occupy the worker for the duration of the callable, which would strand the runtime if the callable synchronously re-enters the engine. So fire_dispatch now announces the blocking section with block_in_place on a multi-thread runtime, migrating the remaining tasks off this worker.

Sync re-entrancy is not part of the supported contract (host_dispatch.rs: "No synchronous re-entrancy"), but it did work before this change by accident of the thread spawn — so it is preserved rather than regressed. block_in_place panics on a current-thread runtime and is meaningless outside a runtime context, so both fall back to a direct call.

Measurements

aarch64-apple-darwin, a BAML function making 6 crossings, 400 iterations:

per crossing
before (thread per call) 29.08 us
after (inline + block_in_place) 8.47 us
3.4x

Verified separately, before and after: a host callable that calls back into BAML synchronously returns the correct result. With inline dispatch but without the block_in_place half, that same test deadlocks — which is what the second half of this change buys.

Test notes

cargo test -p sys_native host_dispatch passes; workspace clippy (--all-targets --all-features -D warnings) is clean.

Note that sdks/cpp/bridge_cpp/tests/run.sh does not build on canary at the moment, independent of this change: runtime_smoke.cc:42 reads started.envelope, but envelope is a member of call_state, not call_registry::started. Untouched here.

Summary by CodeRabbit

  • Performance

    • Improved host-call dispatch efficiency by reducing unnecessary thread creation overhead.
    • Added safer handling for dispatches running within multi-threaded asynchronous runtimes.
  • Reliability

    • Improved failure handling during host-call execution to ensure in-flight calls are completed correctly when dispatch errors occur.

The C++ trampoline spawned a detached std::thread for every BAML -> host
callable crossing, mirroring bridge_python's spawned tokio task. A tokio
task is ~free; an OS thread is not. On an M-series Mac the spawn alone
measures 7.7us, and a crossing that does no work costs ~29us -- all of it
overhead, on a path hot enough to matter (an embedded game loop issues
thousands of these per second).

Run the dispatcher inline on the engine's worker instead. This is safe
because host_impls installs the in-flight entry *before* firing the
dispatch, so completing from inside the callback resolves an
already-registered oneshot.

Inline dispatch does occupy the worker for the duration of the callable,
which would strand the runtime if the callable synchronously re-enters
the engine. So fire_dispatch now announces the blocking section with
block_in_place on a multi-thread runtime, migrating the remaining tasks
off this worker. Re-entrancy keeps working -- it is not part of the
supported contract, but it worked before this change and still does.
block_in_place panics on a current-thread runtime and is meaningless
outside a runtime context, so both fall back to a direct call.

Measured on aarch64-apple-darwin, 6 crossings per call, 400 iterations:

  before   29.08 us/crossing
  after     8.47 us/crossing   (3.4x)

Re-entrant callable (host callable that calls back into BAML) verified
working before and after.
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jul 21, 2026 2:15am
promptfiddle Ready Ready Preview, Comment Jul 21, 2026 2:15am
promptfiddle2 Ready Ready Preview, Comment Jul 21, 2026 2:15am

Request Review

@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7297e150-355a-4ee9-9ed5-c76a6b82c7c3

📥 Commits

Reviewing files that changed from the base of the PR and between 070b3a3 and 1b6940c.

📒 Files selected for processing (2)
  • baml_language/crates/sys_native/src/host_dispatch.rs
  • baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h

📝 Walkthrough

Walkthrough

Host dispatch now runs inline instead of spawning detached threads. The native Rust path uses block_in_place on multi-thread Tokio runtimes and direct invocation elsewhere, while the C++ trampoline catches dispatcher failures and completes the bridge call.

Changes

Inline host dispatch

Layer / File(s) Summary
C++ inline trampoline
baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h
Host dispatch runs inline, removes thread support, updates documentation, and completes in-flight calls when dispatcher setup or execution fails.
Tokio runtime dispatch bridge
baml_language/crates/sys_native/src/host_dispatch.rs
Dispatch uses tokio::task::block_in_place on multi-thread runtimes and direct invocation in other contexts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EngineWorker
  participant HostTrampoline
  participant HostDispatchFn
  participant TokioRuntime
  participant InFlightCall
  EngineWorker->>HostTrampoline: invoke host-callable trampoline
  HostTrampoline->>HostDispatchFn: dispatch inline
  HostDispatchFn->>TokioRuntime: inspect current runtime
  TokioRuntime->>HostDispatchFn: use block_in_place or direct invocation
  HostDispatchFn->>InFlightCall: complete call
  HostTrampoline->>InFlightCall: complete bridge failure on dispatcher exception
Loading

Possibly related PRs

  • BoundaryML/baml#4089: Updates overlapping host-callable dispatch and exception handling in the same C++ trampoline.

Suggested reviewers: 2kai2kai2

Poem

I’m a rabbit in the worker’s lane,
No thread to spawn, no extra chain.
Inline hops through runtime light,
Catches faults and makes things right.
Dispatch runs, the bridge completes—
Carrots for all the tidy beats! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: executing host callables inline instead of spawning a thread per crossing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch avery/cpp-inline-host-dispatch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 25.3 MB 10.7 MB file 25.3 MB +16.7 KB (+0.1%) OK
packed-program Linux 🔒 17.0 MB 7.0 MB file 17.0 MB +8.4 KB (+0.0%) OK
baml-cli macOS 🔒 19.6 MB 9.3 MB file 19.5 MB +33.3 KB (+0.2%) OK
packed-program macOS 🔒 13.2 MB 6.1 MB file 13.2 MB +16.7 KB (+0.1%) OK
baml-cli Windows 🔒 21.1 MB 9.5 MB file 21.1 MB +32.8 KB (+0.2%) OK
packed-program Windows 🔒 14.2 MB 6.3 MB file 14.1 MB +17.6 KB (+0.1%) OK
bridge_wasm WASM 16.2 MB 🔒 4.4 MB gzip 4.4 MB -502 B (-0.0%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

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