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 <string>
#include <utility>
namespace greeter {
/// 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)) {}
/// Return ``"<greeting>, <name>!"``.
APIARY_EXPOSE std::string say(std::string const &name) const { return _greeting + ", " + name + "!"; }
/// 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``.
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 PYBIND11_MODULE body. apiary_aggregate_extension() generates the
// <greeter/Modules.hpp> header (declaring greeter_register_all() + each
// module's register function); we just include it and call the aggregator.
#include <greeter/Modules.hpp>
#include <pybind11/pybind11.h>
PYBIND11_MODULE(greeter, 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) # apiary::apiary, apiary::annotations, apiary_* helpers
find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
find_package(pybind11 CONFIG REQUIRED)
# --- 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")
# Generate the binding TU (+ .pyi + docs JSON) for this module's headers.
apiary_add_bindings(
BINDING DOCS_JSON
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
# PYBIND11_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 "${CMAKE_CURRENT_BINARY_DIR}"
)
# Name the extension ``greeter`` and land it where ``import greeter`` finds it
# (PYTHONPATH=build), then link the library it binds.
set_target_properties(greeter_ext PROPERTIES
OUTPUT_NAME greeter
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
target_link_libraries(greeter_ext PRIVATE greeter)
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:
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!"
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