A libclang/libtooling-based C++ introspection engine. It parses real C++23 with
a Clang frontend and, from a single AST walk, emits pybind11/nanobind binding
TUs, .pyi type stubs, Sphinx C++ API documentation (cpp/c-domain directives,
including macros — no Doxygen), and a structured public-API JSON IR.
Apiary began as einsums-pybind, the binding generator for the
Einsums tensor library, and is now a
standalone tool. The annotation contract lives in
include/apiary/Annotations.hpp (the APIARY_* macros).
Einsums remains the reference consumer: its einsums_add_module(... PYBIND)
helper is a thin wrapper over the generic apiary_add_bindings /
apiary_aggregate_extension functions (Quick start), driving
them to aggregate its modules under a single import einsums. New consumers
call those functions directly — there is no Einsums coupling.
Annotate the C++ you want to bind:
// mylib/include/mylib/Greeter.hpp
#include <apiary/Annotations.hpp>
namespace mylib {
class APIARY_EXPOSE Greeter {
public:
APIARY_EXPOSE Greeter();
APIARY_EXPOSE explicit Greeter(std::string greeting);
APIARY_EXPOSE std::string say(std::string const &name) const;
};
} // namespace mylibWire it up with Apiary's CMake helpers — the same ones whether Apiary is
installed (find_package) or vendored (add_subdirectory):
find_package(Apiary REQUIRED) # or: add_subdirectory(external/apiary)
target_link_libraries(mylib PUBLIC apiary::annotations) # the APIARY_* header
# Probe the build compiler once for the system/stdlib include paths libtooling
# needs (sets APIARY_SYSTEM_FLAGS).
apiary_detect_toolchain(CXX_STANDARD 20)
# Generate the binding TU (+ .pyi + docs JSON). DEPENDS_TARGETS supplies the
# transitive -I/-D from your library's usage requirements.
apiary_add_bindings(
BINDING DOCS_JSON
HEADERS ${CMAKE_SOURCE_DIR}/mylib/include/mylib/Greeter.hpp
SOURCE_INCLUDES mylib/Greeter.hpp
REGISTER_FUNCTION apiary_register_mylib
DEPENDS_TARGETS mylib
OUTPUT_NAME mylib
CXX_STANDARD 20
OUT_BINDING _tu OUT_STUB _stub OUT_DOCS_JSON _docs
)
# Assemble one or more modules into a single Python extension. You supply MAIN
# (the PYBIND11_MODULE body); the helper generates the register header it
# includes and creates the pybind11 target.
apiary_aggregate_extension(
NAME _core
MAIN ${CMAKE_SOURCE_DIR}/mylib/src/main.cpp
BINDINGS ${_tu}
MODULES mylib
MODULES_HEADER ${CMAKE_BINARY_DIR}/gen/include/mylib/Modules.hpp
MODULES_INCLUDE_DIR ${CMAKE_BINARY_DIR}/gen/include
STUBS ${_stub} STUBS_TARGET mylib_stubs
FRAG_DIR ${CMAKE_BINARY_DIR}/gen PKG_DIR ${CMAKE_BINARY_DIR}/mylib
)
target_link_libraries(_core PRIVATE mylib)main.cpp includes the generated header and calls the aggregator:
#include <mylib/Modules.hpp> // generated: declares apiary_register_all()
PYBIND11_MODULE(_core, m) { apiary_register_all(m); }Build and import — the codegen runs as a build edge (re-fires on header changes):
cmake -S . -B build && cmake --build build
PYTHONPATH=build python3 -c "import _core; print(_core.Greeter('hi').say('world'))"That's the whole consumer surface — apiary_detect_toolchain,
apiary_add_bindings, apiary_aggregate_extension. Call apiary_add_bindings
once per module and pass them all to a single apiary_aggregate_extension to
aggregate many modules under one extension.
Every macro is a C++11 attribute that places between the class-key and the class name (or before a function return type). Multiple macros stack. Under non-Clang compilers all macros expand to nothing, so production builds carry no overhead.
| Macro | Purpose |
|---|---|
APIARY_EXPOSE |
Mark a declaration for binding. Without this, the codegen ignores it. |
APIARY_HIDE |
Suppress binding for an otherwise-exposed declaration (e.g. an inherited member). |
| Macro | Purpose |
|---|---|
APIARY_RENAME("py_name") |
Override the Python identifier used for the binding. |
APIARY_MODULE("submodule") |
Place the binding inside a Python submodule. Dotted names ("tensor.algebra") request nested submodules. |
APIARY_EXCEPTION |
Bind the class as a Python exception via py::register_exception<T> instead of py::class_<>. C++ class must derive from std::exception (or compatible). pybind11-only. |
| Macro | Purpose |
|---|---|
APIARY_HOLDER(std::shared_ptr) |
Override the pybind11 holder type. Default is std::unique_ptr. |
APIARY_BUFFER_PROTOCOL |
Flip on pybind11's buffer protocol. Pair with BUFFER_FROM. |
APIARY_BUFFER_FROM(helper) |
Free function helper(T&) returning py::buffer_info; codegen wraps it in a .def_buffer() lambda. |
APIARY_IMPLICIT_FROM(Source) |
Emit py::implicitly_convertible<Source, Class>() after the binding. |
APIARY_DYNAMIC_ATTR |
Allow Python instances to carry arbitrary attributes. |
APIARY_NOCOPY |
Skip generation of the copy-ctor binding. |
APIARY_NOMOVE |
Skip generation of the move-ctor binding. |
APIARY_NO_BASES |
Force-skip emission of base-class arguments. Usually unnecessary — the emitter auto-skips bases that aren't themselves bound. |
APIARY_READONLY |
On a field — bind as def_readonly instead of def_readwrite. |
| Macro | Purpose |
|---|---|
APIARY_RVP(reference_internal) |
Set return_value_policy. Argument is the unqualified policy name. |
APIARY_KEEP_ALIVE(0, 1) |
Emit py::keep_alive<nurse, patient>(). |
APIARY_RELEASE_GIL |
Wrap the call in py::call_guard<py::gil_scoped_release>(). |
APIARY_OPERATOR("__add__") |
Bind the method as a Python operator instead of a named function. |
APIARY_GETTER("name") and APIARY_SETTER("name") get
merged into one .def_property("name", &get, &set) when the codegen
sees a matching name on a getter/setter pair. A @getter with no
matching @setter becomes a .def_property_readonly.
Doxygen comments (/// or /** */) above an exposed declaration become
the Python docstring automatically. Override explicitly with
APIARY_DOC("text").
Templated classes need an explicit instantiation directive — pybind11 binds concrete types, not templates.
Cross-product (APIARY_INSTANTIATE): each parameter list
is keyed by the exact C++ template-parameter name. The codegen matches
by name, not position, so the order in the macro is free. Python names
are auto-derived from the values.
template <typename T, int rank>
class APIARY_EXPOSE
APIARY_INSTANTIATE(Matrix,
T(float, double),
rank(1, 2))
Matrix { ... };
// Produces: Matrix_float_1, Matrix_float_2, Matrix_double_1, Matrix_double_2Single instantiation (APIARY_INSTANTIATE_AS): pin one
concrete type to a chosen Python name. Use this when one template
parameter depends on another (e.g. Alloc = std::allocator<T>), which
a flat cross-product can't express.
APIARY_INSTANTIATE_AS("Matrix2d_double",
Matrix<double, 2, std::allocator<double>>)Cross-product with name template (APIARY_INSTANTIATE_TEMPLATE):
same matching rules; placeholders in the name template use the C++
template-parameter names too.
template <typename Element, int Rank>
class APIARY_EXPOSE
APIARY_INSTANTIATE_TEMPLATE("Block_{Element}_{Rank}",
Block,
Element(float, double),
Rank(1, 2))
Block { ... };
// Produces: Block_float_1, Block_float_2, Block_double_1, Block_double_2Placeholder values are sanitized to valid Python identifiers
(std::complex<double> → std_complex_double).
APIARY_INSTANTIATE_AS also works on templated free functions —
each directive defines one instantiation. Multiple directives sharing a
Python name turn into a pybind11 overload set; the codegen picks the
right one at call site via Python's argument types.
template <typename T>
APIARY_EXPOSE
APIARY_INSTANTIATE_AS("scale", mylib::Array<float>)
APIARY_INSTANTIATE_AS("scale", mylib::Array<double>)
void scale(typename T::ValueType factor, T *A);When two or more INSTANTIATE_AS lines share a Python name AND their
argument signatures are identical (only the return type or value-type
differs), the codegen automatically collapses them into a single Python
entry that takes a dtype="..." kwarg and dispatches at runtime:
template <typename T>
APIARY_EXPOSE
APIARY_INSTANTIATE_AS("zeros", float)
APIARY_INSTANTIATE_AS("zeros", double)
APIARY_INSTANTIATE_AS("zeros", std::complex<float>)
APIARY_INSTANTIATE_AS("zeros", std::complex<double>)
Array<T> zeros(std::string name, std::vector<size_t> dims);
// Python: zeros("X", [4, 4], dtype="float64")Recognized dtype aliases (numpy convention): float32/f4/f/single
(float), float64/f8/d (double), complex64/c8/F
(complex), complex128/complex/c16/D (complex).
The default dtype is float64 if double is in the group, otherwise
the first instantiation's first alias.
For functions templated on leading bool parameters (e.g. template <bool TransA, bool TransB, typename T>), pair
APIARY_TEMPLATE_KWARGS with APIARY_INSTANTIATE_BOOLS.
The codegen expands 2^N combinations internally and emits one Python
entry per dtype taking each bool as a kw-only argument:
template <bool TransA, bool TransB, typename T>
APIARY_EXPOSE
APIARY_TEMPLATE_KWARGS("trans_a", "trans_b")
APIARY_INSTANTIATE_BOOLS("gemm", mylib::Array<float>, float)
APIARY_INSTANTIATE_BOOLS("gemm", mylib::Array<double>, double)
void gemm(U alpha, T const &A, T const &B, U beta, T *C);
// Python: gemm(1.0, A, B, 0.0, C, trans_a=True, trans_b=False)The first INSTANTIATE_BOOLS argument is the Python name (shared
across the bool fan-out); the rest are the non-bool template args.
The 2^N bool combinations are generated automatically; the codegen
then emits a single Python def per dtype with an internal if-chain
dispatcher.
Use APIARY_INSTANTIATE_MEMBER_AS to bind a templated method
with its own template parameters (independent of the enclosing class's
parameters). Multiple directives stack; same-signature ones with
recognized dtypes auto-merge into a dtype= dispatcher exactly like
free-function INSTANTIATE_AS.
template <typename T>
class APIARY_EXPOSE Workspace { ... };
class Workspace {
template <typename U>
APIARY_EXPOSE
APIARY_INSTANTIATE_MEMBER_AS("declare_array",
U=mylib::Array<float>)
APIARY_INSTANTIATE_MEMBER_AS("declare_array",
U=mylib::Array<double>)
U &declare_array(std::string name, std::vector<size_t> dims);
};APIARY_INSTANTIATE_MEMBER (no _AS) is the same idea for
members whose own parameters depend on the class's. Argument is a
Name=Type pair like Dim=std::vector<size_t>.
Templated classes often have parameter-pack constructors whose arity depends on a template parameter. The codegen needs to know how many arguments to bind per instantiation.
template <typename T, size_t rank>
struct APIARY_EXPOSE
APIARY_INSTANTIATE_AS("Matrix_double_2",
Matrix<double, 2, std::allocator<double>>)
Matrix {
template <typename... Dims>
APIARY_EXPOSE
APIARY_VARIADIC_FROM(rank, size_t) // pack -> rank-many size_t args
Matrix(std::string name, Dims... dims);
};For Matrix_double_2, this binds a ctor with signature
(std::string, size_t, size_t). For Matrix_double_3 it would be
(std::string, size_t, size_t, size_t).
The first arg names the template parameter that gives the count; the second is the concrete C++ type each expanded slot should take. The last function parameter is assumed to be the pack.
The codegen emits Clang-style file:line errors and exits non-zero on problems. Common cases:
| Error | Cause |
|---|---|
unknown parameter keyword 'X' (template parameters are: ...) |
Either a typo, or an upstream #define mangled the keyword before stringification. |
class name '<X>' in directive payload does not match |
Same cause: macro expansion changed the class name token. |
expected N parameter list(s), got M |
Number of Param(...) groups doesn't match the template signature. |
parameter keyword '<X>' specified more than once |
Duplicate group. |
missing parameter list for template parameter '<X>' |
A template parameter has no matching group. |
The strict name-match for INSTANTIATE / INSTANTIATE_TEMPLATE is the
load-bearing guard against random macro expansion. If some upstream
header has #define Element WHATEVER, the codegen sees WHATEVER(...),
fails the match against the real Element parameter, and emits a
diagnostic instead of producing wrong bindings.
-
Configure —
apiary_add_bindings()emits anadd_custom_commandper set of headers, andapiary_aggregate_extension()writes the register header (<prefix>all()) and creates thepybind11_add_moduletarget. (In Einsums,einsums_finalize_pybind()calls these once per opted-in module.) -
Build — ninja resolves the dependency chain:
- the
apiarytool builds first; - your C++ library/targets build;
- for each unit,
apiaryruns over its headers and emits<name>_pybind.cpp, containing avoid <register-function>(py::module_ &m); - the consumer's
MAINand every generated TU compile; - the extension links them into one Python module.
- the
Touching an annotated header re-fires only that unit's codegen edge (via the
add_custom_command's DEPENDS ${HEADERS}) and re-links the extension.
- Default constructors with conditional
requiresclauses that are compile-timedeleted for some instantiations may emit spurious bindings. UseAPIARY_HIDEto suppress per-method. - Cross-product with dependent parameters can't be expressed
(
Alloc = std::allocator<T>). Fall back to oneAPIARY_INSTANTIATE_ASper concrete type. - System header detection assumes Clang's
-print-resource-diris available and (on macOS)xcrun --show-sdk-path. A conda env withclangdev+llvmdevsatisfies both. Other setups may need to setAPIARY_CLANG_RESOURCE_DIR/APIARY_SYSROOTmanually before the first configure. requires requires { … }clauses block doxygen attachment — clang'sgetRawCommentForDecldoesn't associate///comments with a function template that has a nested requires-expression. Flatten to a singlerequires (A && B && …)clause and the comment (and thus the Python docstring) will attach.- Stub-side metafunction expansion isn't supported — return types
involving
RemoveComplexT<T>and friends fall back toAnyin the generated.pyi. The runtime binding is correct; only the static type information is reduced.
apiary can emit code against either pybind11 (default) or
nanobind. Pass --target {pybind11,nanobind} on the command line:
apiary --target nanobind --module myext header.hpp -- ...The output differs in:
- Headers —
<pybind11/...>vs<nanobind/...>, with nanobind's STL bindings split per-type (<nanobind/stl/string.h>,<nanobind/stl/vector.h>, etc.) - Module macro —
PYBIND11_MODULEvsNB_MODULE - Namespace —
py::vsnb:: - Return value policy —
py::return_value_policy::reference_internalvsnb::rv_policy::reference_internal - Buffer protocol — pybind11 emits
.def_buffer()lambdas; nanobind doesn't have an equivalent directive (usenb::ndarray<>for tensor protocol instead).APIARY_BUFFER_FROMdirectives are silently dropped under the nanobind target.
The apiary_aggregate_extension helper uses pybind11 today.
Switching the pipeline to nanobind requires also swapping
pybind11_add_module for nanobind_add_module and the matching
find_package(nanobind). The --target flag is what makes the rest of
that switch a one-line change in the cmake hook.
Every codegen invocation also produces a Python type-stub fragment,
emitted alongside the generated .cpp:
build/gen/mylib_core_pybind.cpp # bindings
build/gen/mylib_core.pyi # stub fragment
A finalize step (scripts/aggregate_stubs.py, located at APIARY_SCRIPTS_DIR)
runs as the STUBS_TARGET custom target apiary_aggregate_extension wires up,
after the extension is linked.
It splits each fragment by the # %%submodule: <name> sentinels the
emitter inserts and merges them into per-submodule files in the
package directory:
build/lib/mylib/
├── _core.cpython-…so # the C extension
├── _core.pyi # top-level entities
├── linalg.pyi # entities tagged @module("linalg")
├── graph.pyi # entities tagged @module("graph")
├── __init__.py / .pyi # runtime + stub re-exporting _core
└── py.typed # PEP 561 marker
Type translation runs per-instantiation:
# scale (free function with INSTANTIATE_AS for four dtypes)
@overload
def scale(factor: float, A: ArrayF) -> None: ...
@overload
def scale(factor: float, A: ArrayD) -> None: ...
@overload
def scale(factor: complex, A: ArrayC) -> None: ...
@overload
def scale(factor: complex, A: ArrayZ) -> None: ...
# zeros (auto-detected dtype dispatcher)
def zeros(name: str, dims: list[int], dtype: str = "float64") \
-> ArrayF | ArrayD | ArrayC | ArrayZ: ...
# gemm (TEMPLATE_KWARGS bool fan-out)
@overload
def gemm(alpha: float, A: ArrayF, B: ArrayF, beta: float,
C: ArrayF, *, trans_a: bool = False, trans_b: bool = False) -> None: ...
@overload
def gemm(alpha: complex, A: ArrayC, B: ArrayC, beta: complex,
C: ArrayC, *, trans_a: bool = False, trans_b: bool = False) -> None: ...Doxygen comments above an exposed declaration become Python docstrings.
@getter / @setter pairs become @property declarations.
Rich-comparison dunders (__eq__, __lt__, …) are widened to take
object to satisfy LSP — a stub typed __eq__(self, other: Vec3)
would otherwise trip pyright's reportIncompatibleMethodOverride.
When a function in one module takes a type from another module (e.g. a
function taking an Array<T> whose binding lives in a different module),
the visitor records the external annotated class with is_external=true
purely for name resolution. The C++ emitter ignores externals (their
binding lives in the owning module's TU); the .pyi emitter uses
them to map Array<float, std::allocator<float>> →
ArrayF so cross-module signatures resolve without needing a
shared registry across codegen invocations.
For each per-instantiation parameter / return type, the .pyi emitter:
- Substitutes template names on the raw C++ type (preserving forms
like
typename T::ValueTypefor re-resolution). - Tries the canonical (typedef-expanded) form via clang's
getCanonicalType()if the as-written form fails — catches alias templates likeArray<T>↔BasicArray<T, std::allocator<T>>. - Inlines
typename Class<args>::ValueTypereferences with the class's first type argument. - Substitutes any known cpp_to_py-mapped class instantiation in
nested types (so
std::tuple<Array<float>, ...>reduces totuple[ArrayF, ...]). - Falls back to
Anywhen none of the above produces a Python-valid identifier — pyright will surface the gap rather than the stub silently mistyping.
Each Python submodule needs a tiny .py shell next to _core.so. The
recommended pattern uses PEP 562 __getattr__ so the C extension
isn't loaded until first attribute access:
# mylib/graph.py
import importlib as _importlib
def __getattr__(name):
if name.startswith("_"):
raise AttributeError(name)
core = _importlib.import_module("._core.graph", "mylib")
attr = getattr(core, name)
globals()[name] = attr # cache for subsequent lookups
return attrThe generated <sub>.pyi describes the static surface; the .py shell
is just a runtime trampoline.
Annotated headers can use #if/#else/#endif against any
configure-time define, including everything that
your project's config-define generator writes into <mylib/Config.hpp>. The
codegen tool runs Clang's full preprocessor and only sees the active
branch:
#include <mylib/Config.hpp>
#include <mylib/cuda/DeviceAllocator.hpp>
template <typename T, size_t rank, typename Alloc>
struct APIARY_EXPOSE
APIARY_INSTANTIATE_AS("Matrix_double_2",
Matrix<double, 2, std::allocator<double>>)
#if defined(MYLIB_HAVE_CUDA)
APIARY_INSTANTIATE_AS("Matrix_double_2_cuda",
Matrix<double, 2, cuda::DeviceAllocator<double>>)
#endif
Matrix { ... };When MYLIB_WITH_CUDA=ON, the GPU instantiation is added; toggling it
off and reconfiguring drops it. The generated Defines.hpp files are
in every codegen edge's DEPENDS, so re-configure → re-fire codegen
automatically. Also forwarded: every INTERFACE_COMPILE_DEFINITIONS
reachable from the module's MODULE_DEPENDENCIES (gets -D flags on the
codegen invocation).
- Visibility warnings when linking the extension (weak symbols across the consumer's library and the generated TU) are cosmetic on macOS; symbols still resolve correctly.
src/
main.cpp CLI driver: ClangTool + per-TU IR accumulation +
emit pass. Drives the post-IR passes
(compute_python_overloads, compute_properties)
before invoking the C++ and .pyi emitters.
Tracks total error count, exits non-zero.
Visitor.hpp/.cpp RecursiveASTVisitor that walks declarations,
filters by apiary: annotation, builds
the Module IR. Captures annotated classes from
outside the current module's headers as
``is_external`` for cross-module name resolution.
IR.hpp/.cpp BoundClass / BoundMethod / BoundField / BoundEnum /
BoundFunction / BoundParam / BoundInstantiation /
BoundProperty / PythonOverload. Plus a
deterministic textual dump (``--dump-ir``).
AnnotationParser.hpp/.cpp
Splits raw "apiary:<directive>:<args>"
payloads into structured Directive records.
Knows about free-form-tail directives (doc,
instantiate, holder) where the tail may contain
':'.
InstantiateParser.hpp/.cpp
Parses INSTANTIATE / INSTANTIATE_TEMPLATE
payloads into ParamGroup lists, respects nested
``<>`` and ``()``. Provides cross_product and
sanitize_python_name helpers.
TypeTranslator.hpp/.cpp
Wraps Clang's PrintingPolicy for fully-qualified
pretty C++ types. Also provides
``translate_python_type`` (and the string-only
variant ``translate_python_type_string``) that
maps fundamentals + std containers to their
Python equivalents.
DocExtractor.hpp/.cpp
Pulls doxygen text from
ASTContext::getRawCommentForDeclNoCache, strips
leading ``///``/``*`` markers and decoration
banner lines (``//////``, ``=====``, ``-----``).
PythonOverloads.hpp/.cpp
Post-IR pass that decides how each free
function's raw instantiation list collapses
into Python entries: NonTemplate,
SingleInstantiation, OverloadSet,
DtypeDispatcher, TemplateKwargsDispatcher.
Both emitters consume the precomputed view.
Properties.hpp/.cpp Post-IR pass that walks each class's methods
and collapses @getter/@setter pairs into
BoundClass.properties.
Emitter.hpp/.cpp IR -> pybind11 C++ (or nanobind, via
``--target``). Two output modes:
PYBIND11_MODULE(name, m) (standalone fixtures /
goldens) or void register_<Module>(py::module_ &m)
(autogen aggregator path). In-process
clang::format::reformat() picks up the project's
.clang-format.
PyiEmitter.hpp/.cpp IR -> Python .pyi stubs. Walks the same IR plus
the post-pass views, emits per-submodule blocks
delimited by ``# %%submodule: <name>`` sentinels
for the aggregator to split.
scripts/
aggregate_stubs.py Reads every ``*.pyi`` fragment in
``--frag-dir`` and merges by submodule sentinel
into per-submodule files in ``--pkg-dir``.
Writes a shared header per output and the
PEP-561 ``py.typed`` marker.
tests/
fixtures/ Annotated headers used by the emitter tests.
golden/ Expected emitter output (regen with REGEN=1).
run_smoke.sh IR-dump substring assertions covering
BoundClass/BoundMethod/BoundFunction shapes,
property merge, submodule routing, Python
type translation, and default-value sanitization.
run_golden.sh pybind11 emitter golden-file diff.
- Define the macro in
include/apiary/Annotations.hpp. - If its tail can contain
:, add it todirective_takes_free_form_tailinAnnotationParser.cpp. - Handle it in the emitter (most directives are read via
DirectiveViewinEmitter.cpp's class-body / method-emission helpers). - Add a fixture under
tests/fixtures/and regenerate goldens (REGEN=1 tests/run_golden.sh ...). - Exercise it end-to-end with a Python smoke test (build an extension that uses the directive, then assert on the result).
class APIARY_EXPOSE Resource {
public:
/// Read-only-from-Python access to the underlying name.
APIARY_GETTER("name")
std::string const &get_name() const;
/// Pythonic name setter.
APIARY_SETTER("name")
void set_name(std::string const &n);
};Generated stub:
class Resource:
@property
def name(self) -> str:
"""Read-only-from-Python access to the underlying name."""
...
@name.setter
def name(self, value: str) -> None: ...class APIARY_EXPOSE Vec3 {
public:
/// Component-wise equality.
APIARY_EXPOSE APIARY_OPERATOR("__eq__")
bool operator==(Vec3 const &other) const;
};Generated stub (note the object widening for LSP compliance):
class Vec3:
def __eq__(self, other: object) -> bool:
"""Component-wise equality."""
...namespace APIARY_MODULE("graph") cg {
APIARY_EXPOSE
class Graph { ... };
APIARY_EXPOSE
void execute(Graph &g);
} // namespace cgBoth Graph and execute end up in mylib.graph. The aggregator
writes them into build/lib/mylib/graph.pyi. Anything outside the
namespace block (or any entity tagged with its own
APIARY_MODULE("…")) routes to the chosen submodule.
#include <mylib/Config.hpp>
APIARY_EXPOSE
APIARY_INSTANTIATE_AS("Matrix_double_2",
Matrix<double, 2, std::allocator<double>>)
#if defined(MYLIB_HAVE_CUDA)
APIARY_INSTANTIATE_AS("Matrix_double_2_cuda",
Matrix<double, 2, cuda::DeviceAllocator<double>>)
#endif
template <typename T, size_t rank, typename Alloc>
class Matrix { ... };Toggle MYLIB_WITH_CUDA and reconfigure — the codegen picks up the
Defines.hpp mtime change and re-fires automatically; the CUDA
instantiation appears (or disappears) in the generated bindings + stubs.