Skip to content

bridge-cpp: host callables - std::function crosses the boundary - #4089

Merged
codeshaunted merged 2 commits into
canaryfrom
avery/cpp-callbacks
Jul 20, 2026
Merged

bridge-cpp: host callables - std::function crosses the boundary#4089
codeshaunted merged 2 commits into
canaryfrom
avery/cpp-callbacks

Conversation

@codeshaunted

@codeshaunted codeshaunted commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Bridge-week step 12. Re-landed from -full with the wire layer rewritten against protobuf.

What

function run_agent(query: string, tool: (string) -> string) -> string { tool(query) }
std::string out = b::run_agent("hi", [](std::string q) { return q + "!"; }, ...);
  • A callable argument registers a type-erased dispatcher in the process-global host_value_registry and rides as InboundValue.handle{HOST_VALUE_CALLABLE}. The engine's dispatch callback runs it fire-and-return on a detached thread, decoding BamlToHostCall args (required by declared order, supplied optionals by name into arg<T> slots) and completing via complete_host_call on every exit path.
  • Exception rehydration by identity (python parity): a native exception thrown in your callback comes back to you as the original exception object — custom types and fields survive (MyDomainError.code == 42 after the round trip). baml::host_throw<ValidationError> crosses as the real typed BAML class, structurally caught by BAML's catch (e: ValidationError). A baml::error rethrown through a callable transcodes its payload so class identity survives.
  • Emitter: top-level Ty::Function params become std::function<Ret(Slots...)> with optional callable params as arg<T> slots; encode_callable carries the declared wire names for by-name optional dispatch. Nested callables stay unsupported.
  • Zero Rust changes: register_host_dispatch_callback / register_host_release_callback / complete_host_call were already in the v1 ABI table.

Deviations from python (documented in the test header)

  • No async callables: C++ has no runtime awaitable detection; every std::function is the sync path (blocking its dispatch thread is fine — dispatches are concurrent).
  • The release/weakref test is xfail in python and has no C++ observation point.

Tests

test_host_callables.cc: 17 engine round-trip cases — simple/multi-arg/int/class-value callables, capturing lambdas, distinct registry keys, repeated invocation (incl. zero-N), native-exception identity round trips (std::runtime_error, std::out_of_range, custom type with fields), host_throw caught in BAML and propagating back typed, BAML-side catch of HostCallable with demangled class_name, and the optional-args-in-callables matrix (all-unset host defaults / by-name partial / all-set).

All 12 sdk_test_cpp fixture jobs pass locally. Parity workflow to follow.

Summary by CodeRabbit

  • New Features
    • Added C++ SDK support for passing callable functions into BAML workflows, including multi-argument callbacks, return values, captured state, and structured argument payloads.
    • Added optional callable-argument delivery semantics (host defaults, partial named overrides, full named overrides).
    • Introduced typed host-callable exception support so thrown errors preserve type and payload across the C++/BAML boundary.
  • Tests
    • Added an end-to-end C++ test suite covering host-callable round-tripping, exception propagation, and callable invocation edge cases.

Bridge-week step 12, re-landed from -full with the wire layer rewritten
against protobuf (the original predates the pb migration). A callable
argument registers a type-erased dispatcher in the process-global
host_value_registry and rides as InboundValue.handle{HOST_VALUE_CALLABLE};
the engine's dispatch callback runs it fire-and-return on a detached
thread, decoding BamlToHostCall args (required by declared order,
supplied optionals by name into arg<T> slots) and completing via
complete_host_call on every exit path.

Host exceptions cross back in two shapes (python parity):
baml::host_throw<T> as the typed BAML class value (structurally caught
by BAML's catch), and any other exception as baml.errors.HostCallable
whose _handle rehydrates the ORIGINAL exception object by identity in
throw_from_result - custom types and fields survive the round trip. A
baml::error rethrown through a callable transcodes its payload
outbound->inbound so class identity survives.

Emitter: top-level Ty::Function params become
std::function<Ret(Slots...)> (optional callable params as arg<T> slots)
encoded via encode_callable with the declared wire names; nested
callables stay unsupported. 17 engine round-trip cases ported.
@vercel

vercel Bot commented Jul 19, 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 19, 2026 8:06am
promptfiddle Ready Ready Preview, Comment Jul 19, 2026 8:06am
promptfiddle2 Ready Ready Preview, Comment Jul 19, 2026 8:06am

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 19, 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: 0a99a854-4a03-40db-ac55-cdc820185bcf

📥 Commits

Reviewing files that changed from the base of the PR and between b512eb4 and 673fedf.

📒 Files selected for processing (1)
  • baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h

📝 Walkthrough

Walkthrough

The C++ bridge and SDK generator now support BAML host-callable parameters, asynchronous dispatch, optional arguments, typed host throws, native exception rehydration, and expanded end-to-end coverage.

Changes

Host callable bridge

Layer / File(s) Summary
Host-value registry and throw contracts
baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h, baml_language/sdks/cpp/bridge_cpp/include/baml/errors.h, baml_language/sdks/cpp/bridge_cpp/include/baml/baml.h
Adds registry-backed host values, typed host_throw<T> errors, exception metadata payloads, and value transcoding.
Callable dispatch and exception rehydration
baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h, baml_language/sdks/cpp/bridge_cpp/include/baml/codec.h
Decodes callable arguments, invokes registered dispatchers asynchronously, completes calls, releases handles, and restores stored native exceptions.
Generated callable bindings
baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs
Generates std::function callable types, preserves wire parameter names, emits host-callable encoding, and vendors required bridge headers.
End-to-end validation
baml_language/sdk_tests/crates/cpp/function_calls/customizable/tests/test_host_callables.cc
Tests callable returns, captured lambdas, structured arguments, repeated calls, optional arguments, typed BAML throws, and native exception identity.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GeneratedSDK
  participant BAMLRuntime
  participant DispatchTrampoline
  participant HostCallable
  participant Codec
  GeneratedSDK->>Codec: encode_callable
  Codec->>BAMLRuntime: send callable handle
  BAMLRuntime->>DispatchTrampoline: dispatch call and arguments
  DispatchTrampoline->>HostCallable: decode and invoke
  HostCallable-->>DispatchTrampoline: return value or exception
  DispatchTrampoline-->>BAMLRuntime: complete host call
  BAMLRuntime-->>Codec: decode result or rethrow stored exception
Loading

Possibly related PRs

Poem

A bunny hopped through callable land,
With typed throws tucked close at hand.
Lambdas leapt and errors flew,
Optional args knew what to do.
The bridge rang true from end to end!

🚥 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 accurately summarizes the main change: C++ host callable support using std::function across the bridge.
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-callbacks

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h (1)

508-518: 🚀 Performance & Scalability | 🔵 Trivial

Thread-per-invocation may not scale for high-frequency callbacks.

Each engine→host call spawns and detaches a fresh std::thread. For tight loops (e.g. call_repeatedly with large N) this is a per-call thread-creation cost and unbounded concurrent thread count. If this path ever sees high call volume, consider a bounded worker pool / dispatch queue rather than a raw thread per call. Not blocking given the fire-and-return contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h` around
lines 508 - 518, The fire-and-return path currently creates and detaches one
std::thread per engine→host call, which can cause excessive creation overhead
and unbounded concurrency. Replace the per-invocation thread in the dispatcher
block with a bounded worker pool or dispatch queue that preserves asynchronous
execution and routes dispatcher exceptions through bridge_failure.
baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs (1)

1085-1130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a focused unit test for translate_callable_ty — no test covers this helper today; a small case with required + optional callable params, unnamed-name defaults, and void return would lock in the emitted std::function<...> string and wire_names.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs` around lines 1085 - 1130, The
helper translate_callable_ty lacks focused coverage for mixed callable parameter
modes and void returns. Add a unit test that exercises required and optional
parameters, verifies unnamed parameters produce empty wire_names entries, and
asserts both the emitted std::function type string and returned wire_names.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h`:
- Around line 480-519: Update baml_cpp_host_dispatch_trampoline so all bridge
setup and dispatch operations, including bytes.assign, find_dispatcher, and
std::thread construction, are enclosed in a try/catch that cannot let exceptions
cross the C ABI. Route any caught setup failure through bridge_failure to
complete the call_id, while preserving the existing missing-dispatcher handling
and worker-thread catch for dispatch failures.

---

Nitpick comments:
In `@baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h`:
- Around line 508-518: The fire-and-return path currently creates and detaches
one std::thread per engine→host call, which can cause excessive creation
overhead and unbounded concurrency. Replace the per-invocation thread in the
dispatcher block with a bounded worker pool or dispatch queue that preserves
asynchronous execution and routes dispatcher exceptions through bridge_failure.

In `@baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs`:
- Around line 1085-1130: The helper translate_callable_ty lacks focused coverage
for mixed callable parameter modes and void returns. Add a unit test that
exercises required and optional parameters, verifies unnamed parameters produce
empty wire_names entries, and asserts both the emitted std::function type string
and returned wire_names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4b1aba11-31c0-480b-b325-9cbf0c9223db

📥 Commits

Reviewing files that changed from the base of the PR and between fe33043 and b512eb4.

📒 Files selected for processing (6)
  • baml_language/sdk_tests/crates/cpp/function_calls/customizable/tests/test_host_callables.cc
  • baml_language/sdks/cpp/bridge_cpp/include/baml/baml.h
  • baml_language/sdks/cpp/bridge_cpp/include/baml/codec.h
  • baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h
  • baml_language/sdks/cpp/bridge_cpp/include/baml/errors.h
  • baml_language/sdks/cpp/sdkgen_cpp/src/lib.rs

Comment thread baml_language/sdks/cpp/bridge_cpp/include/baml/detail/host_value.h
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown

Binary size checks failed

4 violations · ✅ 3 passed

⚠️ Please fix the size gate issues or acknowledge them by updating baselines.

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 25.3 MB 10.7 MB file 24.5 MB +788.3 KB (+3.2%) FAIL
packed-program Linux 🔒 17.0 MB 7.0 MB file 17.0 MB -4.1 KB (-0.0%) OK
baml-cli macOS 🔒 19.6 MB 9.3 MB file 18.9 MB +678.0 KB (+3.6%) FAIL
packed-program macOS 🔒 13.2 MB 6.2 MB file 13.2 MB +0 B (+0.0%) OK
baml-cli Windows 🔒 21.1 MB 9.5 MB file 20.4 MB +676.4 KB (+3.3%) FAIL
packed-program Windows 🔒 14.1 MB 6.2 MB file 14.1 MB -512 B (-0.0%) OK
bridge_wasm WASM 16.2 MB 🔒 4.4 MB gzip 4.3 MB +130.5 KB (+3.1%) FAIL

🔒 = 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.

Details & how to fix

Violations:

  • baml-cli (Linux) file_bytes: 25.3 MB exceeds limit of 25.2 MB (exceeded by +53.1 KB, policy: max_file_bytes)
  • baml-cli (Linux) file_delta_pct: +3.2% exceeds limit of 3.0% (exceeded by +0.2pp, policy: max_delta_pct)
  • baml-cli (macOS) file_bytes: 19.6 MB exceeds limit of 19.5 MB (exceeded by +110.9 KB, policy: max_file_bytes)
  • baml-cli (macOS) file_delta_pct: +3.6% exceeds limit of 3.0% (exceeded by +0.6pp, policy: max_delta_pct)
  • baml-cli (Windows) file_bytes: 21.1 MB exceeds limit of 21.0 MB (exceeded by +63.5 KB, policy: max_file_bytes)
  • baml-cli (Windows) file_delta_pct: +3.3% exceeds limit of 3.0% (exceeded by +0.3pp, policy: max_delta_pct)
  • bridge_wasm (WASM) gzip_bytes: 4.4 MB exceeds limit of 4.4 MB (exceeded by +2.3 KB, policy: max_gzip_bytes)
  • bridge_wasm (WASM) gzip_delta_pct: +3.1% exceeds limit of 3.0% (exceeded by +0.1pp, policy: max_delta_pct)

Add/update baselines:

.ci/size-gate/aarch64-apple-darwin.toml:

[artifacts.baml-cli]
file_bytes = 19578112
stripped_bytes = 19578160
gzip_bytes = 9340868

.ci/size-gate/wasm32-unknown-unknown.toml:

[artifacts.bridge_wasm]
file_bytes = 16160508
gzip_bytes = 4402886

.ci/size-gate/x86_64-pc-windows-msvc.toml:

[artifacts.baml-cli]
file_bytes = 21103104
stripped_bytes = 21103104
gzip_bytes = 9548436

.ci/size-gate/x86_64-unknown-linux-gnu.toml:

[artifacts.baml-cli]
file_bytes = 25295952
stripped_bytes = 25295944
gzip_bytes = 10740130

Generated by cargo size-gate · workflow run

… the C ABI

Review finding: bytes.assign (bad_alloc), the registry lock, and the
std::thread constructor (system_error under thread exhaustion) all ran
outside a guard in the extern-C dispatch trampoline - an escape is
UB/terminate and the in-flight call never completes. Setup faults now
route through bridge_failure so the engine unblocks, with an outermost
swallow for the pathological case where the failure path itself throws.
@codeshaunted
codeshaunted added this pull request to the merge queue Jul 20, 2026
Merged via the queue into canary with commit cfcb5a7 Jul 20, 2026
62 of 63 checks passed
@codeshaunted
codeshaunted deleted the avery/cpp-callbacks branch July 20, 2026 18:16
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