Skip to content

Glaze Interfaces (Generic Library API)

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.

Overview

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 API Interface

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> (*)();
}

Basic Usage

Accessing Data Members

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";
}

Reading and Writing Data

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

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";
};

Calling Member Functions

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 11

Getting std::function Objects

get_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
}

std::function Support

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!"
}

Type Safety

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.

Hash Collision Safety

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.

Name

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";
  };
};

Version

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.

What Is Checked?

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:

Type Identity

  • name in meta: The type's registered name (e.g., "my_api")
  • version in meta: The semantic version of the type (major, minor, patch)
  • sizeof the type: The size of the type in bytes
  • Member variable names: All member variable names for object types (ensures field compatibility)

Compiler Information

  • Compiler type: Distinguishes between clang, gcc, and msvc (different compilers may have different ABIs)

Type Traits

These standard C++ type traits are hashed to ensure binary compatibility:

Triviality and Layout - std::is_trivial - std::is_standard_layout

Construction - std::is_default_constructible - std::is_trivially_default_constructible - std::is_nothrow_default_constructible

Copy and Move - std::is_trivially_copyable - std::is_move_constructible - std::is_trivially_move_constructible - std::is_nothrow_move_constructible

Destruction - std::is_destructible - std::is_trivially_destructible - std::is_nothrow_destructible

Other Characteristics - std::has_unique_object_representations - std::is_polymorphic - std::has_virtual_destructor - std::is_aggregate

Any change to these characteristics between the library and the client will result in a hash mismatch, preventing unsafe access.

Automatic Pointer Unwrapping

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.

Supported Standard Library Types

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.

Error Handling

The API provides multiple mechanisms for error handling:

Return Values

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";
}

Error Messages

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";
}

Path Checking

Use contains() to check if a path exists before accessing:

if (io->contains("/x")) {
  auto* x = io->get<int>("/x");
  // Safe to use x
}

Further Reading

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

Example: Complete API Definition

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;
}