Examples¶
These pages embed the real, buildable projects under examples/ in the
repository — the same files the CI builds — so nothing here can drift from
working code.
The greeter example¶
examples/greeter/ is a self-contained downstream project that consumes
apiary the way any library would: find_package(Apiary) plus the apiary_*
CMake helpers, with no Einsums coupling. It binds a header-only C++ library — a
class with a renamed Python type, two constructors, a method, a getter/setter
property, and a free function — into an importable greeter extension.
1. Annotate the C++¶
The only apiary-specific things in the header are the APIARY_* macros from
<apiary/Annotations.hpp>. They expand to nothing in a normal compile; the
apiary tool reads them when it walks the AST. APIARY_EXPOSE marks what to
bind, APIARY_RENAME sets the Python name, and an APIARY_GETTER /
APIARY_SETTER pair becomes a single read/write property.
#pragma once
#include <apiary/Annotations.hpp>
#include <cctype>
#include <functional>
#include <optional>
#include <stdexcept>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
namespace greeter {
/// Raised by ``Greeter.say`` when there is nobody to greet.
///
/// ``APIARY_EXCEPTION`` binds it as a Python exception type: C++ code throws
/// it, and Python catches ``greeter.GreetingError``.
class APIARY_EXPOSE APIARY_EXCEPTION GreetingError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
/// A friendly greeter — the obligatory binding-tool "hello world".
///
/// Demonstrates the common annotations: a renamed, non-copyable class with
/// constructors, a method, and a read/write property built from a
/// getter/setter pair.
class APIARY_EXPOSE APIARY_RENAME("Greeter") Greeter {
public:
/// Greet with the default word ("Hello").
APIARY_EXPOSE Greeter() : _greeting("Hello") {}
/// Greet with a custom word.
APIARY_EXPOSE explicit Greeter(std::string greeting) : _greeting(std::move(greeting)) {}
/// Greet with a custom word, made emphatic when ``emphatic`` is true.
APIARY_EXPOSE Greeter(std::string greeting, bool emphatic) : _greeting(std::move(greeting)) {
if (emphatic) {
_greeting += "!!";
}
}
/// Return ``"<greeting>, <name>!"``. Raises GreetingError when ``name`` is empty.
APIARY_EXPOSE std::string say(std::string const &name) const {
if (name.empty()) {
throw GreetingError("nobody to greet");
}
return _greeting + ", " + name + "!";
}
/// ``say(name)``, in capitals when ``loud`` is true.
APIARY_EXPOSE std::string say_aloud(std::string const &name, bool loud) const {
std::string out = say(name);
if (loud) {
for (char &c : out) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
}
return out;
}
/// The greeting word, exposed as a read/write Python property ``greeting``.
APIARY_GETTER("greeting") std::string const &greeting() const { return _greeting; }
APIARY_SETTER("greeting") void set_greeting(std::string value) { _greeting = std::move(value); }
private:
std::string _greeting;
};
/// A free function — bound as a module-level function ``shout``.
/// Two greeters taking turns. Owns both, and hands them out by reference.
class APIARY_EXPOSE Conversation {
public:
APIARY_EXPOSE Conversation() : _opener("Hello"), _closer("Goodbye") {}
/// Both speakers, as references into this conversation: changing one
/// changes the conversation, and each keeps the conversation alive.
APIARY_EXPOSE APIARY_RVP(reference_internal) std::tuple<Greeter &, Greeter &> speakers() { return {_opener, _closer}; }
/// Both speakers again, through const pointers.
APIARY_EXPOSE APIARY_RVP(reference_internal) std::pair<Greeter const *, Greeter const *> speakers_view() const {
return {&_opener, &_closer};
}
/// What the two say to ``name``, one after the other.
APIARY_EXPOSE std::string exchange(std::string const &name) const { return _opener.say(name) + " " + _closer.say(name); }
private:
Greeter _opener;
Greeter _closer;
};
/// Who ``speaker`` is, or ``"nobody"`` when there is no speaker (None).
APIARY_EXPOSE inline std::string introduce(Greeter const *speaker) {
return speaker == nullptr ? std::string{"nobody"} : speaker->greeting();
}
/// A greeting for ``name``, or for the room when there is no name (None).
APIARY_EXPOSE inline std::string welcome(std::optional<std::string> const &name) {
return Greeter("Welcome").say(name.value_or("everyone"));
}
/// Greet each of ``names`` that ``keep`` accepts, one greeting per line.
APIARY_EXPOSE inline std::string greet_some(std::vector<std::string> const &names, std::function<bool(std::string const &)> const &keep) {
Greeter const hello;
std::string out;
for (std::string const &name : names) {
if (keep(name)) {
out += hello.say(name) + "\n";
}
}
return out;
}
APIARY_EXPOSE inline std::string shout(std::string const &text) {
std::string out = text;
for (char &c : out) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
return out + "!";
}
} // namespace greeter
2. Supply the module body¶
You write only the PYBIND11_MODULE shell. apiary_aggregate_extension
generates <greeter/Modules.hpp> (which declares greeter_register_all()
and each module’s register function); the entry point includes it and calls the
aggregator.
// The extension's module body. apiary_aggregate_extension() generates the
// <greeter/Modules.hpp> header (declaring greeter_register_all() + each
// module's register function, and APIARY_EXTENSION_MODULE, which is
// PYBIND11_MODULE or NB_MODULE for the configured backend); we just include it
// and call the aggregator.
#include <greeter/Modules.hpp>
APIARY_EXTENSION_MODULE(_core, m) {
m.doc() = "Apiary example extension";
greeter_register_all(m);
}
3. Wire it up in CMake¶
Three helpers do the work: apiary_detect_toolchain probes the
compiler for the include paths libtooling needs, apiary_add_bindings
generates the binding translation unit (plus a .pyi stub and docs JSON), and
apiary_aggregate_extension assembles everything into one extension. Linking
apiary::annotations puts the macro header on the include path.
cmake_minimum_required(VERSION 3.20)
project(apiary_greeter_example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Apiary REQUIRED COMPONENTS tool) # apiary::apiary, apiary::annotations, apiary_* helpers
# The binding library: APIARY_BACKEND, pybind11 by default or nanobind, found
# here with the Python it builds for. Everything below follows from it, and
# src/module.cpp is written for either. -DAPIARY_STABLE_ABI=ON makes a nanobind
# build one stable-ABI module for every Python from 3.12 on.
apiary_find_backend()
# --- The C++ library being bound (header-only here) -----------------------
add_library(greeter INTERFACE)
target_include_directories(greeter INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include")
# Linking apiary::annotations puts the APIARY_* macro header on the include
# path (it expands to nothing in a normal compile) AND, because
# apiary_add_bindings collects this target's usage requirements, makes the
# header resolvable when the apiary tool parses the sources.
target_link_libraries(greeter INTERFACE apiary::annotations)
# --- Apiary codegen -------------------------------------------------------
# Probe the build compiler once for the include paths libtooling needs.
apiary_detect_toolchain(CXX_STANDARD 20)
set(_gen "${CMAKE_CURRENT_BINARY_DIR}/gen")
set(_pkg "${CMAKE_CURRENT_BINARY_DIR}/greeter") # the importable package dir
# Generate the binding TU (+ .pyi + docs JSON) for this module's headers.
apiary_add_bindings(
BINDING DOCS_JSON
MODULE greeter
HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/include/greeter/Greeter.hpp"
SOURCE_INCLUDES greeter/Greeter.hpp
REGISTER_FUNCTION greeter_register_core
DEPENDS_TARGETS greeter
OUTPUT_DIR "${_gen}"
OUTPUT_NAME greeter_core
CXX_STANDARD 20
OUT_BINDING _tu OUT_STUB _stub OUT_DOCS_JSON _docs
)
# Assemble the module(s) into one extension. MAIN supplies the module body
# and includes the generated register header.
apiary_aggregate_extension(
NAME greeter_ext
MAIN "${CMAKE_CURRENT_SOURCE_DIR}/src/module.cpp"
BINDINGS ${_tu}
MODULES core
REGISTER_PREFIX greeter_register_
MODULES_HEADER "${_gen}/include/greeter/Modules.hpp"
MODULES_INCLUDE_DIR "${_gen}/include"
STUBS ${_stub} STUBS_TARGET greeter_stubs
FRAG_DIR "${_gen}" PKG_DIR "${_pkg}"
PY_HELPERS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/python/greeter"
)
# The extension is the package's ``_core``, beside the stubs that describe it:
# ``greeter/_core.pyi`` and ``greeter/__init__.pyi``, which a type checker
# reads for ``import greeter`` (PYTHONPATH=build). Then link the library it
# binds.
set_target_properties(greeter_ext PROPERTIES
OUTPUT_NAME _core
LIBRARY_OUTPUT_DIRECTORY "${_pkg}"
)
target_link_libraries(greeter_ext PRIVATE greeter)
# A hand-written module beside the generated one, ``greeter._extras``.
# apiary_add_extension_module builds it with the same library and defines which
# that is, so src/extras.cpp, written against <apiary/Python.hpp>, names neither.
apiary_add_extension_module(greeter_extras src/extras.cpp)
set_target_properties(greeter_extras PROPERTIES
OUTPUT_NAME _extras
LIBRARY_OUTPUT_DIRECTORY "${_pkg}"
)
# Stage the package's Python side beside the extension.
add_custom_target(greeter_python ALL
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/python/greeter" "${_pkg}"
COMMENT "greeter: staging Python package into ${_pkg}"
)
add_dependencies(greeter_python greeter_ext)
4. Use it from Python¶
The renamed class, the property synthesized from the getter/setter pair, and the free function are all just ordinary Python:
import sys
import greeter
g = greeter.Greeter()
assert g.say("world") == "Hello, world!", g.say("world")
g2 = greeter.Greeter("Hi")
assert g2.say("Apiary") == "Hi, Apiary!"
# Read/write property built from the getter/setter pair.
assert g2.greeting == "Hi"
g2.greeting = "Hey"
assert g2.say("you") == "Hey, you!"
# Free function.
assert greeter.shout("loud") == "LOUD!"
# A C++ exception marked APIARY_EXCEPTION arrives as its own Python type.
assert issubclass(greeter.GreetingError, Exception)
try:
g.say("")
except greeter.GreetingError as e:
assert str(e) == "nobody to greet", str(e)
else:
raise AssertionError("say('') did not raise GreetingError")
# None reaches a pointer parameter as nullptr and an optional one as nullopt.
assert greeter.introduce(g2) == "Hey"
assert greeter.introduce(None) == "nobody"
assert greeter.welcome("Ada") == "Welcome, Ada!"
assert greeter.welcome(None) == "Welcome, everyone!"
# A bool parameter, a constructor's bool and a predicate's return all accept
# what pybind11's bool accepts: True and False, anything with __bool__ (an int,
# None, numpy.bool_), and nothing else - a list has no __bool__ slot.
class Truthy:
def __init__(self, truth):
self.truth = truth
def __bool__(self):
return self.truth
for yes in (True, 1, Truthy(True)):
assert g.say_aloud("ada", yes) == "HELLO, ADA!", (yes, g.say_aloud("ada", yes))
for no in (False, 0, None, Truthy(False)):
assert g.say_aloud("ada", no) == "Hello, ada!", (no, g.say_aloud("ada", no))
expect_type_error = False
try:
g.say_aloud("ada", [1])
except TypeError:
expect_type_error = True
assert expect_type_error, "a list was accepted as a bool"
assert greeter.Greeter("Yo", 1).say("x") == "Yo!!, x!"
names = ["ada", "bob", "cy"]
assert greeter.greet_some(names, lambda n: n != "bob") == "Hello, ada!\nHello, cy!\n"
assert greeter.greet_some(names, lambda n: len(n)) == "Hello, ada!\nHello, bob!\nHello, cy!\n"
assert greeter.greet_some(names, lambda n: Truthy(n == "cy")) == "Hello, cy!\n"
try:
import numpy as np
except ImportError:
np = None
if np is not None:
assert g.say_aloud("ada", np.bool_(True)) == "HELLO, ADA!"
assert greeter.greet_some(names, lambda n: np.asarray(len(n)) > 2) == "Hello, ada!\nHello, bob!\n"
# A tuple of references hands back the conversation's own speakers, not copies:
# a change made through one shows up in the conversation.
conv = greeter.Conversation()
opener, closer = conv.speakers()
assert conv.exchange("you") == "Hello, you! Goodbye, you!", conv.exchange("you")
opener.greeting = "Hi"
assert conv.exchange("you") == "Hi, you! Goodbye, you!", conv.exchange("you")
# The same through a pair of const pointers.
seen_opener, seen_closer = conv.speakers_view()
assert (seen_opener.greeting, seen_closer.greeting) == ("Hi", "Goodbye")
# Each speaker keeps its conversation alive (reference_internal), so dropping
# the last Python reference to the conversation leaves both usable.
del conv, seen_opener, seen_closer
gc.collect()
assert opener.say("x") == "Hi, x!" and closer.say("x") == "Goodbye, x!"
# greeter._extras is written by hand against <apiary/Python.hpp>: the same source
# under either library.
from greeter import _extras
assert _extras.backend in ("pybind11", "nanobind"), _extras.backend
assert _extras.call(lambda n: n.upper(), "ada") == "ADA"
def refuse(name):
raise ValueError("not " + name)
reported = _extras.call(refuse, "ada")
assert reported.startswith("raised: ") and "not ada" in reported, reported
assert _extras.platform() == sys.platform, _extras.platform()
print("greeter example: OK")
Build and run¶
With an installed Apiary on CMAKE_PREFIX_PATH (or swap the find_package
for an add_subdirectory of the apiary tree):
$ cmake -S examples/greeter -B examples/greeter/build \
-DCMAKE_PREFIX_PATH=<apiary-install-prefix>
$ cmake --build examples/greeter/build
$ PYTHONPATH=examples/greeter/build python3 examples/greeter/test_greeter.py
greeter example: OK