Skip to content

Commit 6a7f9ac

Browse files
awesomebytesclaude
andcommitted
docs/kits: fold TF + vision-v2 lessons into COMMON_PATTERNS
- NEW §22 Overload mis-resolution: cppyy can pick a compilable-but-wrong overload that crashes at runtime with no traceback (tf lookupTransform routed into the Clock timeout path -> bus error); prefer a single-signature base (tf2::BufferCore) or a cppdef wrapper. Distinct hazard class from §9. - §6: universal-reference-default template ctor (NodeT&& node = NodeT()) as a 3rd "build it in a C++ factory" instance (tf). - §9: the Cling ORC static-initializer wall -- headers parse but EXECUTION fails to materialize a namespace-scope internal-linkage static (gtsam DefaultKeyFormatter); no dep fixes it, take the Python-binding fallback (vision v2). - §11: std::string inside a returned std::vector can surface as Python bytes (tf frame names) -- decode at the boundary. - §13: the efficiency face -- "let C++ own the loop, cross on demand": a library spinning its own C++ thread (tf TransformListener) is an ideal cppyy target, ~7-14x less ingest CPU, compounding with traffic. - §14: a C++ object owning an executor + std::thread needs ordered teardown before rclcpp::shutdown (tf, 3rd instance). - §20: layered-blocker probing discipline (boost -> tbb -> ORC, know when the bottom is a Cling limit and stop) + distrust env console-script shims, prefer the native binary (vision rerun spawn). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 431185d commit 6a7f9ac

1 file changed

Lines changed: 59 additions & 3 deletions

File tree

docs/kits/COMMON_PATTERNS.md

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,12 @@ all container/buffer work inside a `cppyy.cppdef` helper.
143143
int as `void*`; pass `uintptr_t`), and you **must keep the source buffer alive**
144144
for the Mat's lifetime (a lifetime guard, not a copy). Distinguish "buffer you can
145145
alias" (zero-copy) from "storage you must own" (one copy).
146-
- **`std::make_shared<T>()` can be flaky from Python** (overload-cache sensitivity,
147-
seen in control_kit) — build the object in a small C++ factory helper instead.
146+
- **Build the object in a small C++ factory when Python can't construct it.** Three
147+
instances now: `std::make_shared<T>()` flaky from Python (overload-cache
148+
sensitivity, control_kit); `make_shared` of a specific class (control_kit); and a
149+
**template ctor with a universal-reference default** — `Cls(NodeT&& node =
150+
NodeT())` reports "class has no public constructors" from Python (tf). One-line
151+
`cppdef` factory returning the object sidesteps all three.
148152

149153
### 7. On-demand templates: include the `impl` headers
150154
A precompiled `.so` only carries the specializations its authors compiled. To let
@@ -185,6 +189,15 @@ during transaction revert** (no Python traceback).
185189
packages all generate one. Never `cppyy.include` it; find the *clean* base header
186190
and load the class/plugin directly. (ros2_control's headers happen to be
187191
Cling-clean — this wall is per-package, so probe.)
192+
- **The ORC static-initializer wall: parse succeeds, *execution* fails (vision/gtsam).**
193+
A header can `cppyy.include` cleanly yet the first use fault later, when Cling's ORC
194+
JIT must materialize a **namespace-scope internal-linkage static** it can't emit —
195+
gtsam's `static const KeyFormatter DefaultKeyFormatter` (`Key.h`, a non-exported
196+
`std::function` global; each TU emits its own init). No dependency change fixes it
197+
(it is a Cling limitation, not a missing dep). This is a distinct failure *stage*
198+
from a parse error — worth suspecting when includes pass but a symbol won't
199+
materialize; the honest fallback is §20 (the library's own Python binding for batch
200+
steps).
188201
- **Mitigation:** probe risky glue out-of-process first —
189202
`cppyy_kit.probe_cppdef(code, include_paths=, headers=, libraries=)` compiles it
190203
in a throwaway subprocess and returns `(ok, message)` without risking the main
@@ -216,6 +229,9 @@ But three neighbours are silent traps:
216229
the alias.
217230
- **Type-constant `#define` macros are invisible to cppyy** (vision: `CV_8UC1`,
218231
`CV_8U`). Re-expose the few you need as real `const int` in a `cppdef` block.
232+
- **A `std::string` inside a returned `std::vector<std::string>` can surface as
233+
Python `bytes`, not `str`** (tf: `getAllFrameNames()`). Decode at the kit
234+
boundary (`b.decode()`).
219235

220236
### 12. Mirror, don't sugar
221237
Patch/return the library's real classes so methods keep their C++ names (add
@@ -237,6 +253,14 @@ loop, a spin) never contends.
237253
instead: a plain-function `std::thread` in a `cppdef` helper (note `std::async`
238254
does **not** JIT in Cling — use `std::thread`). control_kit's blocking
239255
controller-switch does exactly this.
256+
- **The efficiency face — "let C++ own the loop; cross only on demand" (tf).** The
257+
flip side of the GIL rule is a *design win*: a library that already spins its own
258+
C++ thread is an **ideal** cppyy target. `tf2_ros::TransformListener(spin_thread=
259+
true)` ingests `/tf` on its own `std::thread`, entirely off the GIL; Python only
260+
crosses on `lookup`. Measured **~7–14× less ingest CPU** than an equivalent Python
261+
listener (whose callback runs under the GIL), and the win **compounds with
262+
traffic**. Prefer wrapping the library's own loop over re-implementing it in a
263+
Python thread.
240264

241265
### 14. Teardown: release global-state C++ objects before Python finalizes
242266
A cppyy process ends by running two teardown mechanisms with **no ordering
@@ -269,6 +293,13 @@ with no Python traceback**, *after* all useful work is done.
269293
process-global C++ state (bt, pcl) register nothing: their objects are
270294
per-instance and RAII-released on scope exit, and the JIT'd namespaces are
271295
Cling's to tear down. `test/test_clean_exit.py` is the regression tripwire.
296+
- **A C++ object owning an executor + `std::thread` is the same hazard (tf, 3rd
297+
instance).** `rclcppyy.tf`'s C++ TransformListener owns a spinning executor +
298+
thread; `register_teardown` a callback that drops it (its dtor cancels the
299+
executor and joins the thread) so it releases **before** `shutdown_rclcpp` (LIFO
300+
order is correct). Exit 0 confirmed. Same rule as the pluginlib instance/loader
301+
`reset()` in §19: anything owning threads/executors/global state gets an ordered
302+
teardown.
272303

273304
### 15. First-use JIT: make it visible, move it with `warmup()`
274305
The first time a given C++ signature is crossed, cppyy JIT-compiles a call wrapper
@@ -390,7 +421,19 @@ Before investing in a kit, a couple of one-line greps tell you what's separable:
390421
first. When blocked *and* the work is **batch** (not a hot loop), the library's own
391422
**Python binding is a legitimate fallback** for that step — cppyy is not the only
392423
tool, and a kit can mix (drive the hot C++ path via cppyy, use the binding for a
393-
one-shot batch step).
424+
one-shot batch step). (2nd instance of this rule after the gtsam batch step below.)
425+
- **Probe layered blockers one at a time, and know when to stop.** gtsam via cppyy
426+
is the worked example: fixing the boost blocker (add headers) only exposed a
427+
`GTSAM_USE_TBB` → tbb-headers blocker, which when fixed exposed the Cling **ORC
428+
static-init wall** (§9) — a *Cling limitation no dependency fixes*. Peel one layer,
429+
re-probe out-of-process; when the bottom layer is a Cling limitation rather than a
430+
missing dep, stop and take the Python-binding fallback for that batch step. Don't
431+
keep adding dependencies against a wall that isn't a dependency problem.
432+
- **Distrust environment shims, prefer the native binary.** A library's console-
433+
script entry point can be broken in an env while its native binary works (vision:
434+
the `rerun` console script vs spawning the viewer binary by its executable path).
435+
When a Python-package CLI shim misbehaves, resolve and invoke the real executable
436+
directly rather than assuming the library is broken.
394437

395438
### 21. Vendored-source direct-compile (when there's no package)
396439
For a small, well-understood subset of a library that ships no conda package
@@ -399,6 +442,19 @@ with a direct `$CXX` invocation into a `.so` — this beats fighting the library
399442
CMake/ExternalProject. It generalizes the L2 lowering recipe (`build_l2_node`
400443
`build_dbow2`): a reproducible build script, artifact gitignored, env-version tagged.
401444

445+
### 22. Overload mis-resolution: a compilable-but-WRONG overload that crashes
446+
Distinct from the parse/execution faults (§9): with a **thicket of overloads**, cppyy
447+
can pick one that **compiles and runs but is the wrong one**, crashing at runtime
448+
(bus error, no Python traceback). tf: `tf2_ros::Buffer::lookupTransform(target,
449+
source, TimePoint)` resolved into the `rclcpp::Time`+timeout `canTransform` path,
450+
which called `rclcpp::Clock::now()` and bus-errored. The trap is worst when a class
451+
mixes a `using`-imported base form with timeout/clock forms of the same name.
452+
- **Rule:** prefer the **single-signature base class** (here `tf2::BufferCore`, one
453+
unambiguous `lookupTransform`) over the overload-heavy derived one; or wrap the
454+
exact call you want in a `cppdef` free function so C++ overload resolution — not
455+
cppyy's — picks it. Probe a suspicious overloaded call out-of-process; a
456+
wrong-overload crash gives you nothing to read in Python.
457+
402458
---
403459

404460
## Today vs L1 ("freeze") — L1 now WORKS

0 commit comments

Comments
 (0)