C++ binding
aviso ships a C++ binding as a stable C ABI plus a header-only C++ facade over
it. The aviso-ffi crate builds a libaviso_ffi library and generates a C
header (aviso.h); the hand-written aviso.hpp adds RAII handles,
std::string and std::optional ergonomics, and a throwing aviso::Error.
The point of the C ABI is packaging. A C++ application links the prebuilt library and compiles the facade with its own toolchain, so the binding works under C++17 and needs no Rust toolchain in the consumer’s build.
What you can do today
- Build a client from a base URL, with a Bearer token, HTTP Basic credentials, or a credential the client finds for itself.
- Publish notifications with
notify; see Publishing. - Listen to a stream with a callback handler, with filtering and replay; see Listening.
- Listen with several watches through one handler, each with its own filter; see Several watches at once.
- Attach triggers (echo, log, command, webhook, Teams, post) to a listener; see Triggers.
- Read schemas with
schemaandschema_for, and run the operator-only admin callswipe_stream,wipe_all, anddelete_notification; see Operations. - Call verbs asynchronously: each except
notify_manyhas a*_asyncform returning astd::future; see Async. - Read structured errors: every failure throws an
aviso::Errorwhosewhat()is a human-readable message and whoseerror()returns the kind, HTTP status, and request id.
Credentials
When you hold the credential in your code, name it on the builder.
bearer_auth sends a token and basic_auth sends a username and password:
aviso::Client client = aviso::ClientBuilder("https://aviso.example.org")
.bearer_auth(token)
.build();
An empty token throws from build(). A credential named this way is sent to
whatever address you gave, plain http:// included; writing it into the call
is choosing where it goes.
When the credential is supplied by the environment or by a file instead, call
discover_auth and let the client find it:
aviso::Client client = aviso::ClientBuilder("https://aviso.example.org")
.discover_auth()
.build();
It looks in the environment, then the auth: block of
~/.config/aviso/config.yaml, then ~/.config/aviso/credentials.yaml, and
stops at the first one that has a credential. Finding nothing leaves the
client anonymous. A source that exists but cannot be read throws from
build().
A credential found this way is not sent to a plain http:// address unless
it is loopback, and build() throws instead. Nothing in the code named the
credential, so a mistyped host would otherwise send it in the clear. Use an
https address, or pass the credential yourself with bearer_auth or
basic_auth.
Starting from the config file
If the machine already has ~/.config/aviso/config.yaml set up for the
aviso command, from_file starts the builder from it: server address,
timeouts and TLS settings from the file, plus a credential found the same way
discover_auth finds one.
aviso::Client client = aviso::ClientBuilder::from_file().build();
Setters called afterwards replace what the file said, and base_url lets you
supply an address the file did not have:
aviso::Client client = aviso::ClientBuilder::from_file()
.base_url("https://other.example.org")
.bearer_auth(token)
.build();
A missing default file sets nothing. from_file(path) reads a specific file,
which must exist. Either way, a file that cannot be read throws from
build(), and so does a found credential paired with a plain http://
address that is not loopback, whether the address came from the file or from
a later base_url call. Naming the credential with bearer_auth or
basic_auth lifts that.
Starting from the environment
from_environment behaves like from_file, except that the address may also
come from AVISO_BASE_URL, which takes precedence over the file. The aviso
command and the Python binding use the same order, so a program built this
way needs no arguments on a machine configured for either:
aviso::Client client = aviso::ClientBuilder::from_environment().build();
If neither the environment nor the file provides an address, build() throws
a configuration error that names both sources.
Inspecting the resolved configuration
When a connection fails, the first question is which server and which
credential the client is using. describe() reports this for any builder,
without building a client:
aviso::ClientBuilder builder = aviso::ClientBuilder::from_environment();
std::cout << builder.describe();
base_url https://aviso.example.org/ (environment AVISO_BASE_URL)
auth bearer (credentials file /home/me/.config/aviso/credentials.yaml)
timeout 30s (config file /home/me/.config/aviso/config.yaml)
heartbeat_interval none (default)
ca_bundle none (default)
danger_accept_invalid_certs false (default)
Each line shows a setting, its value and, in parentheses, its source. The
report contains no secrets: the credential is described by kind and source,
never by value, and any user:password@ is removed from the address. It can
therefore be included as it is in a log or a support request. A discovered
credential that build() would refuse is shown with the reason on its auth
line. A builder created with ClientBuilder(url) that has no named credential
and has not called discover_auth reports auth as anonymous.
describe() throws aviso::Error for an error the builder already holds,
such as a null address, or for a config file that exists but cannot be read.
A first call
#include "aviso.hpp"
#include <iostream>
int main() {
try {
aviso::Client client = aviso::ClientBuilder("http://localhost:8000")
.basic_auth("user", "pass")
.build();
std::cout << client.schema() << '\n';
} catch (const aviso::Error& error) {
std::cerr << "aviso error: " << error.what() << '\n';
return 1;
}
}
How calls behave
Every call blocks until the server responds and throws aviso::Error on any
failure, so wrap them in try / catch. JSON-shaped data crosses as strings:
notify takes its payload as a JSON string and the calls return their responses
as JSON strings, which you parse with whatever JSON library your application
already uses.
These calls must not run inside a listener or async callback (a runtime thread);
doing so throws an aviso::Error with kind AvisoErrorKind_InvalidUsage rather
than deadlocking. See Listening for the callback surface.
Some objects are used up by the call that consumes them: a ClientBuilder by
build(), a WatchRequest by the watch or WatchSet it is given to, a
Trigger by add_trigger, and a WatchSet by watch_many. Moving from a
Client, Watch or any of these leaves the source empty as well. Calling a
method on a used-up or moved-from object throws an aviso::Error with kind
AvisoErrorKind_InvalidUsage, whose message names the object.
Reading an error
error.what() is a message for humans. error.error() is the part to branch
on: an ErrorInfo with a kind, the http_status when the server answered,
and the server’s request_id to quote when asking for help.
The kind tells you what to do next. AvisoErrorKind_Transport means the
server or network was unreachable and a retry may work. AvisoErrorKind_Http
means the server answered and said no; look at http_status and the message.
A credential the server rejects on a request arrives this way, as a 401 or
403. AvisoErrorKind_Auth covers the other credential problems: none was
available, one would have travelled in the clear to a non-loopback http
address, or a watch was refused even after refreshing the credential. None of
these is worth a retry. AvisoErrorKind_InvalidInput means the calling
code passed something the binding could see was wrong before any request went
out, such as identifier JSON that is not an object.
} catch (const aviso::Error& error) {
const aviso::ErrorInfo& info = error.error();
if (info.kind == AvisoErrorKind_Transport) {
// retry later
} else if (info.kind == AvisoErrorKind_Http && info.http_status == 401) {
// the server rejected the credential; do not retry
} else if (info.kind == AvisoErrorKind_Http && info.http_status == 404) {
// the event type is not on this server
}
}
examples/cpp/resilience/03_error_handling.cpp
provokes seven different errors on purpose and shows a branch for each.
Building against the library
Eighteen worked, tested examples live in
examples/cpp,
grouped by purpose: basics/, resilience/, triggers/ and async/. Each
one links the library and compiles the facade through CMake, the way your own
program would, and CI runs every one against a real server. For local
development, build the library from the workspace first:
cargo build -p aviso-ffi
cmake -S examples/cpp -B build/cpp
cmake --build build/cpp
./build/cpp/01_schema # nothing configured: says so, exits 0
AVISO_BASE_URL=http://localhost:8000 ./build/cpp/01_schema
The examples connect through from_environment(), so they need no further
setup on a machine where the aviso command is configured; 01_schema
prints the resolved configuration before its first request. The
two CMake cache variables AVISO_FFI_INCLUDE_DIR and AVISO_FFI_LIB_DIR
default to the in-tree locations; point them at a prebuilt drop to build
against shipped artifacts with no Rust toolchain. The
examples README
lists every file with what it shows.
Installing with cargo-c
For a standard system install rather than an in-tree build,
cargo-c drops the library, a pkg-config
.pc, and the headers in the usual layout:
cargo install cargo-c # once
cargo cinstall -p aviso-ffi --release --prefix=/usr/local --libdir=/usr/local/lib
A consumer then discovers it through pkg-config, with no in-tree paths and no Rust toolchain:
find_package(PkgConfig REQUIRED)
pkg_check_modules(AVISO_FFI REQUIRED IMPORTED_TARGET aviso_ffi)
target_link_libraries(my_app PRIVATE PkgConfig::AVISO_FFI)
The install places the headers under include/aviso_ffi/, and the .pc puts
that directory on the include path, so #include "aviso.hpp" works unchanged.
The C header and the facade
aviso.h is generated from the Rust surface by cbindgen and is the source of
truth for the ABI; aviso.hpp is hand-written over it and is the recommended
surface for C++ callers. C callers can use aviso.h directly: every fallible
call returns an owning AvisoOutcome that you inspect, take a value out of, and
free.