Repository navigation
Bind the pipeline the batch source stands on - #157
Merged
Merged
Conversation
Closes #154. image_batch_source is the class meant for pipelines: asynchronous, batched, and running its own threads. Reaching it means binding the chain under it — the executors, the reader providers, the source — and the completion it hands back. Completion.wait, Completion.get and ImageBatchSource.read release the GIL. Held through a read, it would freeze every other Python thread for the duration and leave nothing asynchronous about the interface. read takes its destination by value and array is move-only, so the binding shares the caller's array rather than moving out of it, which would have left them holding an empty one. batch_source() assembles the four objects for a caller who wants the pipeline rather than the parts. Each source it builds owns its executor; sharing one between sources is done by passing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
martin-s-a
enabled auto-merge (squash)
September 20, 2026 18:54
martin-s-a
added a commit
to gigabit-clowns/rexlib
that referenced
this pull request
Sep 20, 2026
Four classes carry `REXLIB_API` on their methods but not on the class itself, so their typeinfo never leaves the shared object: - `thread_pool_executor` and `synchronous_executor` - `direct_image_reader_provider` and `caching_image_reader_provider` Their interfaces, `executor` and `image_reader_provider`, are both `REXLIB_API`. The implementations are not, which leaves each hierarchy exported halfway. ## Why it has never shown The library builds with hidden visibility, and a polymorphic class emits its typeinfo in the translation unit that defines its first out-of-line virtual — inside the library. Without `REXLIB_API` on the class, that symbol is `HIDDEN` and nothing outside can link against it. Ordinary C++ use never asks for it: constructing one of these, calling through it, destroying it, all go through the methods, and those are exported one by one. It only bites something that reaches for RTTI across the boundary, which is what a language binding does — pybind11 calls `typeid()` on every type it registers. It surfaced in gigabit-clowns/rexlib-python#157, as a load-time failure of the extension with no build warning ahead of it: ``` ImportError: _binding.cpython-314-x86_64-linux-gnu.so: undefined symbol: _ZTIN6rexlib20thread_pool_executorE ``` which demangles to `typeinfo for rexlib::thread_pool_executor`. ## The change, and the evidence One keyword on each of the four classes. Measured on `thread_pool_executor.cpp`, compiled with the same `-fvisibility=hidden -fvisibility-inlines-hidden` the build uses: | | `_ZTIN6rexlib20thread_pool_executorE` | |---|---| | before | `HIDDEN` | | after | `DEFAULT` | The symbol was emitted either way; what changed is whether it crosses the boundary. `image_source` and `image_batch_source` are left alone deliberately. Neither is polymorphic, so their typeinfo is a weak symbol any consumer emits for itself, and nothing is missing. The per-method `REXLIB_API` on these classes is now redundant but left in place: `image_read_format_manager` and `image_reader_provider` both already carry the class attribute and repeat it on members, so removing it here would be a larger diff against a convention this does not settle. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
rexlib #463 exports the implementations behind the interfaces this branch binds, so the typeinfo the cast needs is visible outside the library. The pin is moved rather than left to `git submodule update --remote` so that a clone of this branch builds against the same rexlib CI does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
oierlauzi
approved these changes
Sep 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #154. Item 3b of the backlog, and the half with the threads in it.
image_batch_sourceis the class meant for pipelines: asynchronous, batched,running its own threads. The slow path stays what it is, for one-off reads.
Reaching that meant binding the chain beneath it —
Executorand its twokinds,
ImageReaderProviderand its two,ImageSource— andCompletion,which is what
readhands back.ExecutorandCompletionlive in a new_binding.concurrency, followingsrc/core/concurrency/.The GIL, which is the whole point
Completion.wait,Completion.getandImageBatchSource.readtakepy::call_guard<py::gil_scoped_release>().This is not a refinement. Held through a read, the GIL freezes every other
Python thread for its duration, and there is nothing asynchronous left about
the interface — ordering a batch and blocking the interpreter is what the slow
path already does. pybind11 converts the arguments before the guard is
constructed and the return value after it is destroyed, so nothing touches
Python without it.
readgets the same treatment because it opens files through the providerbefore returning, even though it returns before the reads are done.
The destination is shared, not moved
readtakes its destination by value andrexlib::arrayis move-only. Boundplainly, pybind11 would move the array out of the caller's Python object and
hand them back an empty one — silently, and only noticed later. The binding
takes
array&and passesdestination.share(), which is a second handle ontothe same storage. A test pins it: the destination still reports its shape
after the read.
Assembly
batch_source(workers=…, cache=…, manager=…, executor=…)wires the formats, aprovider, an executor and the source, so a caller reaches the pipeline without
naming four objects. Every argument has a default and every one can be
overridden; the parts are all exported for anyone who wants to assemble them
by hand, and a test does exactly that.
Each source built this way owns its executor, which is the settled decision:
sharing a pool between sources is the caller's business, done by passing one
executor to both, and per-instance is the direction that stays cheap to
reverse.
cache=Nswaps the direct provider for a caching one.The
query_extentsandquery_core_extentsoverloads over a provider arrivehere too, having waited for the provider to exist.
Checked
g++ -std=c++20 -fsyntax-only -Wall -Wextra -Wpedanticagainst the pinnedsubmodule.
ruff check .passes; 56 tests collect undertests/em/image/.together, an empty batch resolving without touching the source, a wrong
batch size refused, the synchronous executor, a caching provider serving a
repeated read, and assembly by hand.
🤖 Generated with Claude Code