Glaze has been designed to work as a generic interface for shared libraries and more. This is achieved through JSON pointer syntax access to memory.
Glaze allows a single header API (api.hpp) to be used for every shared library interface, greatly simplifying shared library handling.
Interfaces are simply Glaze object types. So whatever any JSON/binary interface can automatically be used as a library API.
The Glaze API system provides:
- Type-safe cross-compilation access: Access data structures and call functions across shared library boundaries with compile-time type checking
- Single universal API: One API header (
glaze/api/api.hpp) works for all shared libraries - JSON/Binary serialization: Built-in support for reading and writing data via JSON or BEVE (Binary Efficient Versatile Encoding)
- JSON pointer access: Navigate complex data structures using JSON pointer syntax (e.g.,
/path/to/field) - Member function invocation: Call member functions across API boundaries with full type safety
- Automatic pointer unwrapping: Transparent access through
std::unique_ptr,std::shared_ptr, and raw pointers
The core API is shown below. It is simple, yet incredibly powerful, allowing pretty much any C++ class to be manipulated across the API via JSON or binary, or even the class itself to be passed and safely cast on the other side.
namespace glz {
struct api {
api() noexcept = default;
api(const api&) noexcept = default;
api(api&&) noexcept = default;
api& operator=(const api&) noexcept = default;
api& operator=(api&&) noexcept = default;
virtual ~api() noexcept {}
// Get a typed pointer to a value at the given JSON pointer path
template <class T>
[[nodiscard]] T* get(const sv path) noexcept;
// Get a std::function from a member function or std::function across the API
template <class T>
[[nodiscard]] expected<T, error_code> get_fn(const sv path) noexcept;
// Call a member function across the API
template <class Ret, class... Args>
expected<func_return_t<Ret>, error_code> call(const sv path, Args&&... args) noexcept;
// Check if a path exists
[[nodiscard]] virtual bool contains(const sv path) noexcept = 0;
// Read data into the API object from JSON or BEVE
virtual bool read(const uint32_t format, const sv path, const sv data) noexcept = 0;
// Write data from the API object to JSON or BEVE
virtual bool write(const uint32_t format, const sv path, std::string& data) noexcept = 0;
// Get the last error message
[[nodiscard]] virtual const sv last_error() const noexcept { return error; }
// Low-level void* access (prefer templated get)
[[nodiscard]] virtual std::pair<void*, glz::hash_t> get(const sv path) noexcept = 0;
protected:
virtual bool caller(const sv path, const glz::hash_t type_hash, void*& ret,
std::span<void*> args) noexcept = 0;
virtual std::unique_ptr<void, void (*)(void*)> get_fn(const sv path,
const glz::hash_t type_hash) noexcept = 0;
std::string error{};
};
// Interface map type: maps API names to factory functions
using iface = std::map<std::string, std::function<std::shared_ptr<api>()>, std::less<>>;
// Function type for the glz_iface entry point
using iface_fn = std::shared_ptr<glz::iface> (*)();
}You can access data members using the get method with JSON pointer syntax:
// Assume we have an API object 'io' of type std::shared_ptr<glz::api>
auto* x = io->get<int>("/x"); // Get pointer to int
auto* y = io->get<double>("/y"); // Get pointer to double
auto* z = io->get<std::vector<double>>("/z"); // Get pointer to vector
// Use the values
if (x) {
std::cout << "x = " << *x << "\n";
}The API supports reading and writing entire objects or specific paths using JSON or BEVE:
// Write the entire object to JSON
std::string json_buffer;
io->write(glz::JSON, "", json_buffer);
// Write a specific path to JSON
std::string x_json;
io->write(glz::JSON, "/x", x_json);
// Read from JSON into the API object
std::string input_json = R"({"x": 42, "y": 3.14})";
io->read(glz::JSON, "", input_json);
// Read into a specific path
io->read(glz::JSON, "/x", "100");
// Use BEVE (binary format) for better performance
std::string beve_buffer;
io->write(glz::BEVE, "", beve_buffer);
io->read(glz::BEVE, "", beve_buffer);Member functions can be registered with the metadata, which allows the function to be called across the API.
struct my_api {
int x = 7;
double y = 5.5;
int func() { return 5; }
double sum(double a, double b) { return a + b; }
void increment(int& value) { ++value; }
};
template <>
struct glz::meta<my_api> {
using T = my_api;
static constexpr auto value = object(
"x", &T::x,
"y", &T::y,
"func", &T::func,
"sum", &T::sum,
"increment", &T::increment
);
static constexpr std::string_view name = "my_api";
};The call method invokes member functions across the API. It returns an expected<T, error_code> for proper error handling:
std::shared_ptr<glz::iface> iface{ glz_iface()() };
auto io = (*iface)["my_api"]();
// Call function with no arguments
auto result = io->call<int>("/func");
if (result) {
std::cout << "func returned: " << result.value() << "\n";
}
// Call function with arguments
auto sum_result = io->call<double>("/sum", 7.0, 2.0);
if (sum_result) {
std::cout << "sum = " << sum_result.value() << "\n"; // prints 9.0
}
// Call function with reference parameters
int value = 10;
auto inc_result = io->call<void>("/increment", value);
// value is now 11get_fn provides a means of getting a std::function from a member function across the API. This can be more efficient if you intend to call the same function multiple times:
// Get a std::function for a no-argument function
auto func = io->get_fn<std::function<int()>>("/func");
if (func) {
int result = func.value()();
std::cout << "result = " << result << "\n";
}
// Get a std::function for a multi-argument function
auto sum_fn = io->get_fn<std::function<double(double, double)>>("/sum");
if (sum_fn) {
double result = sum_fn.value()(7.0, 2.0);
std::cout << "sum = " << result << "\n";
}
// Get a std::function with reference parameters
auto inc_fn = io->get_fn<std::function<void(int&)>>("/increment");
if (inc_fn) {
int val = 5;
inc_fn.value()(val);
// val is now 6
}Glaze allows std::function objects to be stored as members of your API types. This enables you to expose callable objects (lambdas, function objects, etc.) across the API boundary.
struct my_api {
int x = 7;
double y = 5.5;
// Store a std::function as a member
std::function<double(const int&, const double&)> f =
[](const auto& i, const auto& d) { return i * d; };
std::function<void()> init = [] {
std::cout << "Initialization complete!\n";
};
};
template <>
struct glz::meta<my_api> {
using T = my_api;
static constexpr auto value = object(
"x", &T::x,
"y", &T::y,
"f", &T::f,
"init", &T::init
);
static constexpr std::string_view name = "my_api";
};You can then access and call these functions:
// Get and call a std::function
auto* f = io->get<std::function<double(const int&, const double&)>>("/f");
if (f) {
int x = 7;
double y = 5.5;
double result = (*f)(x, y); // result = 38.5
}
// Call a void function
auto* init = io->get<std::function<void()>>("/init");
if (init) {
(*init)(); // Prints "Initialization complete!"
}A valid interface concern is binary compatibility between types. Glaze uses compile-time hashing of types that is able to catch a wide range of changes to classes or types that would cause binary incompatibility. These compile-time hashes are checked when accessing across the interface and provide a safeguard, much like a std::any_cast, but working across compilations.
Key difference from std::any_cast: std::any_cast does not guarantee any safety between separately compiled code, whereas Glaze adds significant type checking across compilations and versions of compilers.
The type hash is a 128-bit value computed from multiple type characteristics, providing robust detection of incompatible changes. When you call get<T>() or call<Ret>(), the system verifies that the type hash matches before allowing access, returning nullptr or an error if there's a mismatch.
With a 128-bit hash, the probability of collision is astronomically low:
- With 10,000 registered types: approximately 1.47 × 10⁻³¹ chance of collision
- For comparison, the probability of winning the Mega Millions lottery (1/302,575,350 ≈ 3.3 × 10⁻⁹) is vastly higher
- You would need to win the lottery 2.25 × 10²² times (22 sextillion times) to equal the collision probability
This makes the 128-bit hash more than sufficient for any practical application.
By default custom type names from glz::name_v will be "Unnamed". It is best practice to give types the same name as it has in C++, including the namespace (at least the local namespace).
Concepts exist for naming const, pointer (*), and reference (&), versions of types as they are used. Many standard library containers are also supported.
expect(glz::name_v<std::vector<float>> == "std::vector<float>");To add a name for your class, include it in the glz::meta:
template <>
struct glz::meta<my_api> {
static constexpr std::string_view name = "my_api";
};Or, include it via local glaze meta:
struct my_api {
struct glaze {
static constexpr std::string_view name = "my_api";
};
};By default all types get a version of 0.0.1. The version tag allows the user to intentionally break API compatibility for a type when making changes that would not be caught by the compile time type checking.
template <>
struct glz::meta<my_api> {
static constexpr glz::version_t version{ 0, 0, 2 };
};Or, include it locally like name or value.
Glaze's type safety system performs comprehensive compile-time checks to detect binary incompatibilities. The following characteristics are hashed and checked when accessing types across the API:
namein meta: The type's registered name (e.g., "my_api")versionin meta: The semantic version of the type (major, minor, patch)sizeofthe type: The size of the type in bytes- Member variable names: All member variable names for object types (ensures field compatibility)
- Compiler type: Distinguishes between clang, gcc, and msvc (different compilers may have different ABIs)
These standard C++ type traits are hashed to ensure binary compatibility:
Triviality and Layout
std::is_trivialstd::is_standard_layout
Construction
std::is_default_constructiblestd::is_trivially_default_constructiblestd::is_nothrow_default_constructible
Copy and Move
std::is_trivially_copyablestd::is_move_constructiblestd::is_trivially_move_constructiblestd::is_nothrow_move_constructible
Destruction
std::is_destructiblestd::is_trivially_destructiblestd::is_nothrow_destructible
Other Characteristics
std::has_unique_object_representationsstd::is_polymorphicstd::has_virtual_destructorstd::is_aggregate
Any change to these characteristics between the library and the client will result in a hash mismatch, preventing unsafe access.
Glaze automatically unwraps pointer types when accessing members through the API. This means you can access the pointed-to value directly without manually dereferencing:
struct my_api {
int x = 7;
int* x_ptr = &x;
std::unique_ptr<double> uptr = std::make_unique<double>(5.5);
std::shared_ptr<std::string> sptr = std::make_shared<std::string>("hello");
};
template <>
struct glz::meta<my_api> {
using T = my_api;
static constexpr auto value = object(
"x", &T::x,
"x_ptr", &T::x_ptr,
"uptr", &T::uptr,
"sptr", &T::sptr
);
static constexpr std::string_view name = "my_api";
};Access the unwrapped values:
auto io = (*iface)["my_api"]();
// Access through raw pointer - returns pointer to the int, not pointer to pointer
auto* x = io->get<int>("/x_ptr");
if (x) {
std::cout << *x << "\n"; // prints 7
}
// Access through unique_ptr - returns pointer to the double
auto* y = io->get<double>("/uptr");
if (y) {
std::cout << *y << "\n"; // prints 5.5
}
// Access through shared_ptr - returns pointer to the string
auto* s = io->get<std::string>("/sptr");
if (s) {
std::cout << *s << "\n"; // prints "hello"
}This unwrapping works recursively, so std::unique_ptr<std::shared_ptr<T>> would also be unwrapped to access T directly.
The Glaze API system includes built-in support for many standard library types:
- Containers:
std::vector,std::array,std::deque,std::list - Associative containers:
std::map,std::unordered_map - Smart pointers:
std::unique_ptr,std::shared_ptr - Optional:
std::optional - Variant:
std::variant - Tuple:
std::tuple - Span:
std::span - Functional:
std::function - String:
std::string,std::string_view
All these types have proper name and hash support for type-safe cross-compilation access.
The API provides multiple mechanisms for error handling:
get, get_fn, and call return values that indicate success or failure:
// get returns nullptr on failure
auto* x = io->get<int>("/nonexistent");
if (!x) {
std::cerr << "Failed to get value\n";
}
// get_fn and call return expected<T, error_code>
auto result = io->call<int>("/func");
if (!result) {
std::cerr << "Call failed with error code\n";
}
auto fn = io->get_fn<std::function<void()>>("/init");
if (!fn) {
std::cerr << "Failed to get function\n";
}The last_error() method provides detailed error messages:
auto* x = io->get<int>("/wrong_path");
if (!x) {
std::cout << "Error: " << io->last_error() << "\n";
}
auto result = io->call<int>("/wrong_type");
if (!result) {
std::cout << "Error: " << io->last_error() << "\n";
}Use contains() to check if a path exists before accessing:
if (io->contains("/x")) {
auto* x = io->get<int>("/x");
// Safe to use x
}For more detailed information on specific topics:
- Building Shared Libraries: Complete guide to creating and using shared libraries with Glaze, including CMake configuration, platform-specific details, and best practices
- Advanced API Usage: In-depth coverage of advanced patterns, performance optimization, type hashing, and cross-compilation safety
Here's a complete example showing all major features:
#include "glaze/api/impl.hpp"
// Custom types
struct user {
std::string name;
int age;
};
template <>
struct glz::meta<user> {
using T = user;
static constexpr auto value = glz::object(
"name", &T::name,
"age", &T::age
);
static constexpr std::string_view name = "user";
};
// Main API type
struct user_management_api {
// Data members
std::vector<user> users;
int user_count = 0;
std::map<std::string, int> user_ids;
// Smart pointers
std::unique_ptr<std::string> api_key = std::make_unique<std::string>("secret");
// std::function members
std::function<bool(const user&)> validate_user = [](const user& u) {
return !u.name.empty() && u.age > 0;
};
// Member functions
void add_user(const user& u) {
users.push_back(u);
user_count++;
}
user& get_user(int index) {
return users.at(index);
}
int count_users() const {
return user_count;
}
std::vector<std::string> get_user_names() const {
std::vector<std::string> names;
for (const auto& u : users) {
names.push_back(u.name);
}
return names;
}
};
template <>
struct glz::meta<user_management_api> {
using T = user_management_api;
static constexpr auto value = glz::object(
"users", &T::users,
"user_count", &T::user_count,
"user_ids", &T::user_ids,
"api_key", &T::api_key,
"validate_user", &T::validate_user,
"add_user", &T::add_user,
"get_user", &T::get_user,
"count_users", &T::count_users,
"get_user_names", &T::get_user_names
);
static constexpr std::string_view name = "user_management_api";
static constexpr glz::version_t version{1, 0, 0};
};
// Export the API
glz::iface_fn glz_iface() noexcept {
return glz::make_iface<user_management_api>();
}Usage:
#include "glaze/api/lib.hpp"
int main() {
// Load the library
glz::lib_loader lib("./libs");
auto api = lib["user_management_api"]();
// Add a user using a member function
user new_user{"Alice", 30};
auto result = api->call<void>("/add_user", new_user);
// Get user count
auto count = api->call<int>("/count_users");
std::cout << "User count: " << count.value() << "\n";
// Access data directly
auto* users = api->get<std::vector<user>>("/users");
for (const auto& u : *users) {
std::cout << u.name << " (" << u.age << ")\n";
}
// Call std::function
auto* validator = api->get<std::function<bool(const user&)>>("/validate_user");
user test_user{"Bob", 25};
if ((*validator)(test_user)) {
std::cout << "User is valid\n";
}
// Read/write the entire API state
std::string json;
api->write(glz::JSON, "", json);
std::cout << "API state:\n" << json << "\n";
// Modify and read back
api->read(glz::JSON, "", R"({"user_count": 10})");
auto* new_count = api->get<int>("/user_count");
std::cout << "New count: " << *new_count << "\n";
return 0;
}