cppyy_kit¶
Prototype in Python, run at C++ speed — mix Python and C++ with ease.
cppyy_kit is a suite of kits that drive real C++ robotics libraries from short Python via cppyy. No bindings to write, no code generation, no build step: the C++ library you already have installed is called directly, its own class and method names intact, while your Python does the orchestration. When a hot path needs C++ speed, you write that path in C++ inline — in the same file — and the kits handle the data marshaling and object lifetime across the boundary.
You get the productivity of a Python prototype and the performance of the C++ library underneath it, and the same code climbs an optimization ladder (freeze the startup cost, cache the compile, lower a hot leaf) without changing shape. Every number on this site is measured on one machine on one day and linked to the row that produced it in Benchmarks.
Three ways to mix Python and C++¶
Drive a whole C++ library from Python — the real BehaviorTree.CPP engine parses the XML, owns the tree, and ticks it; there is no Python binding for it, so this capability does not otherwise exist:
import bt_kit
bt = bt_kit.bringup_bt()
tree = bt.BehaviorTreeFactory().create_tree_from_text(xml) # the C++ factory
tree.tickWhileRunning() # the C++ engine ticks
Or drop to inline C++ for a hot kernel — the decorated function's docstring is
its C++ body; its annotations drive the NumPy marshaling; it compiles once and is
cached to a real .so thereafter (no first-use JIT after the first build):
import numpy as np
from cppyy_kit import cpp
@cpp
def sum_sq(data: cpp.arr("float")) -> float: # numpy -> (float* data, size_t data_size)
"double s = 0; for (std::size_t i = 0; i < data_size; ++i) s += data[i]*data[i]; return s;"
sum_sq(np.array([1, 2, 3], np.float32)) # 14.0 — no ctypes, no build, no bindings
Run those kernels on every core — plain Python threads run in true parallel once a
@cpp kernel releases the GIL around its compiled body, which pure-Python threads
cannot do. Add nogil=True and the body runs with the interpreter lock dropped; on a
16-core machine, eight independent jobs finish about 7.7× faster than with the GIL
held (≈150 ms → ≈20 ms). The
runnable example
has the full version:
import threading, numpy as np
from cppyy_kit import cpp
@cpp(nogil=True) # the compiled body runs with the GIL released
def crunch(out: "double*", slot: int, iters: int) -> None:
"double s = 0; for (std::size_t k = 1; k <= (std::size_t)iters; ++k) s += 1.0/(double(k)*1e-3 + 1.0); out[slot] = s;"
out = np.zeros(8) # 8 independent jobs, one slot each
threads = [threading.Thread(target=crunch, args=(out, i, 20_000_000)) for i in range(8)]
for t in threads: t.start()
for t in threads: t.join() # all 8 run at once, one per core
@cpp(nogil=True) wraps the compiled body in Py_BEGIN_ALLOW_THREADS /
Py_END_ALLOW_THREADS, so the interpreter lock is dropped while the C++ runs — no
trick, just the GIL released for the native work; only cppyy's argument/result
marshaling stays under the lock. The jobs are independent and write into distinct C++
slots, so none needs the GIL while computing.
The friction these three share — locating and loading the .sos, pinning callback
lifetimes, hiding cppyy's template and ownership sharp edges — is factored into the
cppyy_kit base, so each domain kit stays thin and its Python mirrors the library's
own API 1:1.
Measured results¶
The Benchmarks page is the single consolidated source (one
machine, one day, reproducible commands); each kit's REPORT.md carries the original
evidence. Each row below names its benchmark and links to it.
| Lever | Result |
|---|---|
| Accelerate — PCL cloud stays in C++ end to end | 15.1× latency / 7.4× CPU at 74-LOC parity, in the PCL pipeline benchmark |
| Freeze — zero-config Cling PCH of the library headers | rclcpp bringup ~1.73 s → 0.064 s (~27×) in the auto-PCH measurement, header parse eliminated |
Compile cache — content-hashed @cpp/cppdef → .so |
first-use JIT 632 → 91 ms on the PCL VoxelGrid kernel, paid once per machine |
| Lower (L2) — hot leaf authored as native C++ | inline Crocoddyl model 22.9× on the WBC action model, bit-identical |
TF ingest — C++ tf2 listener vs Python callback |
7.4–16.9× lower ingest CPU, in the TF ingest benchmark |
The kits¶
| Kit | What it drives | Headline |
|---|---|---|
| cppyy_kit (base) | the ROS-free machinery: load / callback / lifetime, @cpp, require, nogil, freeze & compile-cache |
first-use JIT paid once per machine: 632 → 91 ms on the PCL VoxelGrid kernel ↗ |
| rclcpp_kit | rclcpp (ROS 2 core): bringup, messages, tf, rosbag2, CDR | TF ingest 7.4–16.9× lower CPU |
| bt_kit | BehaviorTree.CPP v4 (no Python binding exists) | Groot2-compatible trees from Python |
| pcl_kit | Point Cloud Library (no maintained binding) | 15.1× latency / 7.4× CPU at LOC parity |
| ompl_kit | Open Motion Planning Library | Python validity-checker in the planner's inner loop, no codegen |
| nav2_kit | Nav2 algorithm cores, composed from Python | the real RegulatedPurePursuit with no lifecycle servers / no pluginlib |
| moveit_kit | the full MoveIt 2 C++ API | the whole C++ surface, not moveit_py's subset |
| control_kit | ros2_control | a Python controller in the real controller_manager |
| cv_kit | OpenCV C++ | zero-copy Image → cv::Mat, one CUDA branch point |
| dbow_kit | DBoW2 place recognition (no binding, not on conda-forge) | loop closure from short Python |
| wbc_kit | Crocoddyl custom action models | inline-C++ model, no build system |
Each kit is a package with a WHY.md (the rationale), REPORT.md (the evidence),
and SKILL.md (the LLM-facing cheat sheet); the anatomy is in
Architecture.
Demos & examples¶
Every headline links to the exact row that produced it in Benchmarks.
| Demo | What it proves | Headline number |
|---|---|---|
| Live webcam A vs B | a hand-written NCC tracker in one inline-C++ kernel vs the identical NumPy loop | 16.18× @ 640×480 |
| IK 5-solver bench | benchmark C++-only IK solvers (incl. unpackaged bio_ik/pick_ik) from one Python file | pure-Python 10–25× slower; bio_ik 991 solve/s |
| WBC inline-C++ model | a custom Crocoddyl action model authored inline, JIT-compiled, no CMake | 22.9× vs Python-derived, bit-identical |
| Retargeting teleop rig | webcam → body/hand tracking → TF → whole-body retarget onto G1/Talos, live, one Rerun viewer | glue kernel 341.5×, /tf marshaling 258.9× |
| Visual loop closure | ORB + DBoW2 + GTSAM front-end in short Python, pixels never leave C++ | 1080p ingest 135.8×; 19 loops, P/R 1.00/0.95 |
| Jitter bench | a ~1 kHz control loop orchestrated from Python on a stock kernel | ~2 µs median period, unprivileged |
| cppyy-accelerate skill | point a coding agent at slow Python; it moves the hot path to a kit | 16.3× (49.6 → 3.04 ms), bit-identical |
Where the speedups apply — and where they don't¶
The webcam gap is large because the hot per-frame stage is a hand-written per-pixel
NCC tracker with no OpenCV one-liner. When the per-frame work is only
library-provided ops (ORB, RANSAC — cv2 is already C++), the same A-vs-B comparison
narrows to ~1.1–1.2×
(webcam report).
In the retargeting rig, the measured cppyy wins are the /tf message marshaling and
the transform/retarget kernel. The IK solve runs on pinocchio's own Python bindings:
instantiating pinocchio::Model from headers under Cling trips boost 1.90's variant
template-arity limit (pinocchio's 25-type joint boost::variant), so that path cannot
be JIT-parsed
(retarget report).
The benchmarks ran on a shared development machine, so the ratios are more repeatable than the absolute times.
The optimization ladder¶
The same code climbs rungs as you need more speed — the kit API does not change:
- Prototype (L0). Plain Python driving the kit. Headers parsed and per-signature wrappers JIT-compiled on first use. Fastest to write.
- Accelerate. Move the hot path onto C++ via a kit,
@cpp, ornogil— 15.1× lower latency in the PCL pipeline benchmark, where the cloud stays in C++ end to end. - Freeze. A zero-config Cling PCH of the library headers is built once into
~/.cache/cppyy_kitand auto-loaded thereafter, eliminating the header parse — ~27× on rclcpp bringup (~1.73 s → 0.064 s) in the auto-PCH measurement. The compile cache does the same for@cpp/cppdefkernels: the first-use JIT — 632 → 91 ms on the PCL VoxelGrid kernel — is paid once per machine, not once per process. - Lower (L2). A proven-hot leaf is authored as a native C++ node — 22.9× on the WBC Crocoddyl action model (bit-identical cost), removing the per-call cppyy boundary.
Read the full ladder in Freeze & Cache and the 36 documented patterns behind it in The Patterns.
Powers rclcppyy¶
rclcpp_kit is the capability layer under
rclcppyy — the drop-in accelerator
that lets an existing rclpy program run ROS 2's C++ core (rclcpp, tf2, rosbag2, CDR
serialization) with minimal changes. rclcppyy 0.2.0 is now thin re-export shims over
rclcpp_kit, and installs from the same channel as ros-jazzy-rclcppyy.
Built for LLM agents¶
Agent-consumability is a design goal: every kit ships a SKILL.md (a
compact, LLM-facing cheat sheet of its real API), The Patterns
is the shared playbook a coding agent reads before writing a new kit or call, and the
cppyy-accelerate skill is a Claude-Code-consumable
PROFILE → MAP → APPLY → VERIFY procedure whose
worked example accelerates a naive voxel
downsampler 16.3× with bit-identical output.
Next steps¶
- Getting Started — install the packages, or develop from the repo.
- The Patterns — the canonical cppyy playbook.
- Benchmarks — every number here, one machine, one day, reproducible.
- Architecture — how the suite is put together.
Origin: extracted and expanded from rclcppyy, which it now powers.