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.

examples/greeter/include/greeter/Greeter.hpp
#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.

examples/greeter/src/module.cpp
// 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.

examples/greeter/CMakeLists.txt
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:

examples/greeter/test_greeter.py
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