C++ Bindings

Yokan provides comprehensive header-only C++ bindings that wrap the C API with modern C++ idioms, RAII resource management, and exception handling.

Overview

The C++ bindings are located in include/yokan/cxx/ and provide C++ classes equivalent to the C opaque pointers:

  • yokan::Client - Wrapper for yk_client_t

  • yokan::Database - Wrapper for yk_database_handle_t

  • yokan::Collection - Wrapper for document collections

  • yokan::Exception - C++ exception for error handling

Warning

Some C++ functions have parameters in a different order than their C equivalents. In particular, functions that take a mode have this mode as the last parameter to allow C++ optional parameters.

Quick Start

Here’s a minimal example using the C++ API:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/client.hpp>
#include <yokan/cxx/database.hpp>
#include <iostream>
#include <string>

namespace tl = thallium;

int main() {
    try {
        // Initialize Thallium engine
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);

        // Create a client using the Margo instance from Thallium
        yokan::Client client(engine.get_margo_instance());

        // Look up the server address
        tl::endpoint server_ep = engine.lookup("na+sm://localhost:1234");

        // Connect to a database using the resolved address
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), 42);

        // Put a value
        std::string key = "greeting";
        std::string value = "Hello, Yokan!";
        db.put(key.data(), key.size(), value.data(), value.size());

        // Get the value back
        std::vector<char> buffer(256);
        size_t vsize = buffer.size();
        db.get(key.data(), key.size(), buffer.data(), &vsize);

        std::string retrieved(buffer.data(), vsize);
        std::cout << "Retrieved: " << retrieved << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

This demonstrates:

  • Automatic resource management (RAII)

  • Exception-based error handling

  • Clean, modern C++ API

Client and Database Handles

Creating Clients and Databases

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/client.hpp>
#include <yokan/cxx/database.hpp>
#include <iostream>

namespace tl = thallium;

int main() {
    try {
        // Initialize Thallium engine
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);

        // Create a client using the Margo instance from Thallium
        yokan::Client client(engine.get_margo_instance());
        std::cout << "Client created" << std::endl;

        // Look up the server address
        tl::endpoint server_ep = engine.lookup("na+sm://localhost:1234");

        // Connect to a database by address and provider ID
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(),        // Server address (hg_addr_t)
            42,                          // Provider ID
            true                         // Check validity
        );
        std::cout << "Database handle created" << std::endl;

        // The client and database are reference counted
        // They'll be automatically cleaned up when they go out of scope

        // Copy the database handle (increments reference count)
        yokan::Database db_copy = db;
        std::cout << "Database handle copied" << std::endl;

        // Move the database handle (transfers ownership)
        yokan::Database db_moved = std::move(db_copy);
        std::cout << "Database handle moved" << std::endl;

        // db_copy is now invalid, db_moved and db are valid
        // All will be cleaned up automatically

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

The yokan::Client and yokan::Database classes:

  • Use RAII for automatic cleanup

  • Are copyable (reference counted)

  • Are movable for efficient transfer

  • Throw yokan::Exception on errors

Basic Operations

Put, Get, Exists, and Erase

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        // Initialize Thallium engine
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);

        // Create a client using the Margo instance from Thallium
        yokan::Client client(engine.get_margo_instance());

        // Look up the server address
        tl::endpoint server_ep = engine.lookup(argv[1]);

        // Create database handle
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // Put operation
        std::string key = "user:1001";
        std::string value = "Alice Johnson";
        db.put(key.data(), key.size(), value.data(), value.size());
        std::cout << "Stored: " << key << " = " << value << std::endl;

        // Get operation
        std::vector<char> buffer(256);
        size_t vsize = buffer.size();
        db.get(key.data(), key.size(), buffer.data(), &vsize);
        std::string retrieved(buffer.data(), vsize);
        std::cout << "Retrieved: " << retrieved << std::endl;

        // Exists operation
        bool exists = db.exists(key.data(), key.size());
        std::cout << "Key exists: " << (exists ? "yes" : "no") << std::endl;

        // Length operation
        size_t length = db.length(key.data(), key.size());
        std::cout << "Value length: " << length << " bytes" << std::endl;

        // Erase operation
        db.erase(key.data(), key.size());
        std::cout << "Key erased" << std::endl;

        // Verify erasure
        bool exists_after = db.exists(key.data(), key.size());
        std::cout << "Key exists after erase: " << (exists_after ? "yes" : "no") << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

All basic operations:

  • Accept const void* and size_t for binary data

  • Support strings through .data() and .size()

  • Take an optional mode parameter (defaults to YOKAN_MODE_DEFAULT)

  • Throw yokan::Exception on errors

Batch Operations

Multi-key Operations

For efficiency, use batch operations when working with multiple keys:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <vector>
#include <string>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        // Initialize Thallium engine
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);

        // Create a client using the Margo instance from Thallium
        yokan::Client client(engine.get_margo_instance());

        // Look up the server address
        tl::endpoint server_ep = engine.lookup(argv[1]);

        // Create database handle
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // Prepare multiple key/value pairs
        std::vector<std::string> keys = {"user:1", "user:2", "user:3"};
        std::vector<std::string> values = {"Alice", "Bob", "Carol"};

        // Convert to raw pointers for batch API
        std::vector<const void*> key_ptrs;
        std::vector<size_t> key_sizes;
        std::vector<const void*> value_ptrs;
        std::vector<size_t> value_sizes;

        for(const auto& k : keys) {
            key_ptrs.push_back(k.data());
            key_sizes.push_back(k.size());
        }
        for(const auto& v : values) {
            value_ptrs.push_back(v.data());
            value_sizes.push_back(v.size());
        }

        // Put multiple key/value pairs at once
        db.putMulti(keys.size(),
                    key_ptrs.data(), key_sizes.data(),
                    value_ptrs.data(), value_sizes.data());
        std::cout << "Stored " << keys.size() << " key/value pairs" << std::endl;

        // Get multiple values at once
        std::vector<std::vector<char>> buffers(keys.size(), std::vector<char>(256));
        std::vector<void*> buffer_ptrs;
        std::vector<size_t> buffer_sizes;

        for(auto& buf : buffers) {
            buffer_ptrs.push_back(buf.data());
            buffer_sizes.push_back(buf.size());
        }

        db.getMulti(keys.size(),
                    key_ptrs.data(), key_sizes.data(),
                    buffer_ptrs.data(), buffer_sizes.data());

        std::cout << "Retrieved values:" << std::endl;
        for(size_t i = 0; i < keys.size(); i++) {
            std::string value(buffers[i].data(), buffer_sizes[i]);
            std::cout << "  " << keys[i] << " = " << value << std::endl;
        }

        // Check existence of multiple keys
        std::vector<bool> existence = db.existsMulti(keys.size(),
                                                      key_ptrs.data(), key_sizes.data());

        std::cout << "Existence check:" << std::endl;
        for(size_t i = 0; i < keys.size(); i++) {
            std::cout << "  " << keys[i] << ": " << (existence[i] ? "exists" : "missing") << std::endl;
        }

        // Erase multiple keys
        db.eraseMulti(keys.size(), key_ptrs.data(), key_sizes.data());
        std::cout << "Erased " << keys.size() << " keys" << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Batch operations reduce network round-trips and improve throughput significantly.

String and Vector Helpers

The C++ API provides convenient helpers for working with std::string and std::vector:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);
        yokan::Client client(engine.get_margo_instance());
        tl::endpoint server_ep = engine.lookup(argv[1]);
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // Working with std::string directly
        std::string key = "user:1001";
        std::string value = "Alice Johnson";

        // Put using string helper
        db.put(key.data(), key.size(), value.data(), value.size());

        // Get with automatic sizing using std::vector
        std::vector<char> result;

        // First get the size
        size_t vsize = db.length(key.data(), key.size());

        // Resize vector and get data
        result.resize(vsize);
        db.get(key.data(), key.size(), result.data(), &vsize);

        // Convert to string
        std::string retrieved(result.data(), vsize);
        std::cout << "Retrieved: " << retrieved << std::endl;

        // Working with multiple keys using vectors
        std::vector<std::string> keys = {"user:1001", "user:1002", "user:1003"};
        std::vector<std::string> values = {"Alice", "Bob", "Carol"};

        // Convert to pointer/size arrays for batch operations
        std::vector<const void*> key_ptrs;
        std::vector<size_t> key_sizes;
        std::vector<const void*> val_ptrs;
        std::vector<size_t> val_sizes;

        for(const auto& k : keys) {
            key_ptrs.push_back(k.data());
            key_sizes.push_back(k.size());
        }

        for(const auto& v : values) {
            val_ptrs.push_back(v.data());
            val_sizes.push_back(v.size());
        }

        // Batch put
        db.putMulti(keys.size(), key_ptrs.data(), key_sizes.data(),
                    val_ptrs.data(), val_sizes.data());

        std::cout << "Stored " << keys.size() << " key-value pairs" << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

List Operations

The C++ API provides wrappers for list operations that work with callbacks:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);
        yokan::Client client(engine.get_margo_instance());
        tl::endpoint server_ep = engine.lookup(argv[1]);
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // Insert test data
        std::vector<std::string> keys = {"user:001", "user:002", "user:003", "user:004"};
        std::vector<std::string> values = {"Alice", "Bob", "Carol", "Dave"};

        for(size_t i = 0; i < keys.size(); i++) {
            db.put(keys[i].data(), keys[i].size(),
                   values[i].data(), values[i].size());
        }

        std::cout << "Inserted " << keys.size() << " records" << std::endl;

        // List key-value pairs using iter
        std::string from_key = "user:";
        std::string filter = "";

        std::cout << "\nListing key-value pairs:" << std::endl;

        // Create callback using C++ lambda
        auto keyval_callback = [](size_t index, const void* key, size_t ksize,
                                   const void* val, size_t vsize) -> yk_return_t {
            std::string k(static_cast<const char*>(key), ksize);
            std::string v(static_cast<const char*>(val), vsize);
            std::cout << "  [" << index << "] " << k << " = " << v << std::endl;
            return YOKAN_SUCCESS;
        };

        db.iter(from_key.data(), from_key.size(),
                filter.data(), filter.size(),
                0, /* count = 0 means all */
                keyval_callback);

        std::cout << "\nIteration completed successfully!" << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Alternatively, use the packed variants for lower-level control:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);
        yokan::Client client(engine.get_margo_instance());
        tl::endpoint server_ep = engine.lookup(argv[1]);
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // Insert test data
        for(int i = 1; i <= 5; i++) {
            std::string key = "item:" + std::to_string(i);
            std::string value = "value" + std::to_string(i);
            db.put(key.data(), key.size(), value.data(), value.size());
        }

        // List keys using packed format
        std::string from_key = "item:";
        std::string filter = "";

        // Allocate buffer for packed keys
        std::vector<char> packed_keys(1024);
        std::vector<size_t> key_sizes(10);
        size_t count = 10;

        db.listKeysPacked(from_key.data(), from_key.size(),
                          filter.data(), filter.size(),
                          count,
                          packed_keys.data(), packed_keys.size(),
                          key_sizes.data());

        std::cout << "Listed " << count << " keys:" << std::endl;

        // Parse packed keys
        size_t offset = 0;
        for(size_t i = 0; i < count; i++) {
            std::string key(packed_keys.data() + offset, key_sizes[i]);
            std::cout << "  " << key << std::endl;
            offset += key_sizes[i];
        }

        // List key-value pairs using packed format
        std::vector<char> packed_vals(1024);
        std::vector<size_t> val_sizes(10);
        count = 10;

        db.listKeyValsPacked(from_key.data(), from_key.size(),
                             filter.data(), filter.size(),
                             count,
                             packed_keys.data(), packed_keys.size(),
                             key_sizes.data(),
                             packed_vals.data(), packed_vals.size(),
                             val_sizes.data());

        std::cout << "\nListed " << count << " key-value pairs:" << std::endl;

        // Parse packed key-value pairs
        size_t key_offset = 0;
        size_t val_offset = 0;
        for(size_t i = 0; i < count; i++) {
            std::string key(packed_keys.data() + key_offset, key_sizes[i]);
            std::string value(packed_vals.data() + val_offset, val_sizes[i]);
            std::cout << "  " << key << " = " << value << std::endl;
            key_offset += key_sizes[i];
            val_offset += val_sizes[i];
        }

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Working with Modes

Modes modify operation semantics and can be combined using bitwise OR:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        // Initialize Thallium engine
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);

        // Create a client using the Margo instance from Thallium
        yokan::Client client(engine.get_margo_instance());

        // Look up the server address
        tl::endpoint server_ep = engine.lookup(argv[1]);

        // Create database handle
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // APPEND mode: Append to existing values
        std::string log_key = "application_log";
        std::string entry1 = "Entry 1\n";
        std::string entry2 = "Entry 2\n";
        std::string entry3 = "Entry 3\n";

        db.put(log_key.data(), log_key.size(), entry1.data(), entry1.size());
        db.put(log_key.data(), log_key.size(), entry2.data(), entry2.size(),
               YOKAN_MODE_APPEND);
        db.put(log_key.data(), log_key.size(), entry3.data(), entry3.size(),
               YOKAN_MODE_APPEND);

        std::vector<char> log_buffer(1024);
        size_t log_size = log_buffer.size();
        db.get(log_key.data(), log_key.size(), log_buffer.data(), &log_size);
        std::string log(log_buffer.data(), log_size);
        std::cout << "Appended log:\n" << log << std::endl;

        // CONSUME mode: Get and erase atomically
        std::string task_key = "pending_task";
        std::string task_value = "Process data";
        db.put(task_key.data(), task_key.size(), task_value.data(), task_value.size());

        std::vector<char> task_buffer(256);
        size_t task_size = task_buffer.size();
        db.get(task_key.data(), task_key.size(), task_buffer.data(), &task_size,
               YOKAN_MODE_CONSUME);
        std::string task(task_buffer.data(), task_size);
        std::cout << "Consumed task: " << task << std::endl;

        bool still_exists = db.exists(task_key.data(), task_key.size());
        std::cout << "Task still exists: " << (still_exists ? "yes" : "no") << std::endl;

        // NEW_ONLY mode: Only put if key doesn't exist
        std::string counter_key = "counter";
        std::string initial_value = "1";

        db.put(counter_key.data(), counter_key.size(),
               initial_value.data(), initial_value.size(),
               YOKAN_MODE_NEW_ONLY);
        std::cout << "Initial counter set" << std::endl;

        try {
            std::string new_value = "2";
            db.put(counter_key.data(), counter_key.size(),
                   new_value.data(), new_value.size(),
                   YOKAN_MODE_NEW_ONLY);
            std::cout << "Second put shouldn't succeed!" << std::endl;
        } catch(const yokan::Exception& ex) {
            std::cout << "Expected: Can't overwrite with NEW_ONLY" << std::endl;
        }

        // Combining modes
        std::string multi_mode_key = "combined";
        std::string multi_value = "test";
        db.put(multi_mode_key.data(), multi_mode_key.size(),
               multi_value.data(), multi_value.size());

        std::vector<char> combined_buffer(256);
        size_t combined_size = combined_buffer.size();
        db.get(multi_mode_key.data(), multi_mode_key.size(),
               combined_buffer.data(), &combined_size,
               YOKAN_MODE_CONSUME | YOKAN_MODE_NO_RDMA);
        std::cout << "Used combined modes: CONSUME | NO_RDMA" << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Available modes include:

  • YOKAN_MODE_DEFAULT - Default behavior

  • YOKAN_MODE_INCLUSIVE - Include lower bound in lists

  • YOKAN_MODE_APPEND - Append to values

  • YOKAN_MODE_CONSUME - Get and erase atomically

  • YOKAN_MODE_WAIT - Wait for keys to appear

  • YOKAN_MODE_NOTIFY - Notify waiting clients

  • YOKAN_MODE_NEW_ONLY - Only put if key doesn’t exist

  • YOKAN_MODE_EXIST_ONLY - Only put if key exists

  • YOKAN_MODE_NO_RDMA - Disable RDMA for small data

Exception Handling

The C++ API throws yokan::Exception for all errors:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        // Initialize Thallium engine
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);

        // Create a client using the Margo instance from Thallium
        yokan::Client client(engine.get_margo_instance());

        // Look up the server address
        tl::endpoint server_ep = engine.lookup(argv[1]);

        // Create database handle
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // Example 1: Handling missing keys
        std::cout << "=== Example 1: Missing key ===" << std::endl;
        try {
            std::string key = "nonexistent";
            std::vector<char> buffer(256);
            size_t size = buffer.size();
            db.get(key.data(), key.size(), buffer.data(), &size);
        } catch(const yokan::Exception& ex) {
            std::cout << "Expected error - key not found: " << ex.what() << std::endl;
        }

        // Example 2: Safe get with existence check
        std::cout << "\n=== Example 2: Safe get ===" << std::endl;
        std::string key = "safe_key";
        if(db.exists(key.data(), key.size())) {
            std::vector<char> buffer(256);
            size_t size = buffer.size();
            db.get(key.data(), key.size(), buffer.data(), &size);
            std::string value(buffer.data(), size);
            std::cout << "Value: " << value << std::endl;
        } else {
            std::cout << "Key doesn't exist, using default" << std::endl;
        }

        // Example 3: Exception information
        std::cout << "\n=== Example 3: Exception details ===" << std::endl;
        try {
            std::string missing_key = "another_missing_key";
            std::vector<char> buffer(256);
            size_t size = buffer.size();
            db.get(missing_key.data(), missing_key.size(), buffer.data(), &size);
        } catch(const yokan::Exception& ex) {
            std::cout << "Exception message: " << ex.what() << std::endl;
            // The exception message contains error details
        }

        // Example 4: RAII ensures cleanup even with exceptions
        std::cout << "\n=== Example 4: RAII cleanup ===" << std::endl;
        {
            yokan::Database scoped_db = client.makeDatabaseHandle(
                server_ep.get_addr(), std::atoi(argv[2]));

            try {
                // Even if this throws...
                std::string key = "will_fail";
                std::vector<char> buffer(256);
                size_t size = buffer.size();
                scoped_db.get(key.data(), key.size(), buffer.data(), &size);
            } catch(const yokan::Exception&) {
                // ...the database handle is still cleaned up
                std::cout << "Exception caught, but resources are safe" << std::endl;
            }
            // scoped_db is automatically cleaned up here
        }
        std::cout << "Scoped database handle cleaned up" << std::endl;

        // Example 5: Multiple operations with error handling
        std::cout << "\n=== Example 5: Batch error handling ===" << std::endl;
        std::vector<std::string> keys = {"key1", "key2", "key3"};

        for(const auto& k : keys) {
            try {
                std::vector<char> buffer(256);
                size_t size = buffer.size();
                db.get(k.data(), k.size(), buffer.data(), &size);
                std::string value(buffer.data(), size);
                std::cout << k << " = " << value << std::endl;
            } catch(const yokan::Exception&) {
                std::cout << k << " = <not found>" << std::endl;
            }
        }

        std::cout << "\n=== All examples completed ===" << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Fatal error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Best practices:

  1. Wrap operations in try/catch blocks

  2. Use RAII for automatic cleanup

  3. Check backend capabilities before using advanced modes

  4. Handle missing keys gracefully

RAII and Resource Management

Copy and Move Semantics

The C++ classes use reference counting for safe copying:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <memory>

namespace tl = thallium;

void demonstrateCopying(yokan::Database db) {
    // Database handle is copied (reference counted)
    // Original handle remains valid
    std::string key = "copy_test";
    std::string value = "copied";
    db.put(key.data(), key.size(), value.data(), value.size());
    std::cout << "Operation in copied handle successful" << std::endl;
    // db is destroyed here, but reference count decremented
}

yokan::Database demonstrateMoving(yokan::Client& client, hg_addr_t addr, uint16_t provider_id) {
    // Create database handle
    yokan::Database db = client.makeDatabaseHandle(addr, provider_id);

    // Return by value uses move semantics
    return db;
    // Original db is moved, not copied
}

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);
        yokan::Client client(engine.get_margo_instance());
        tl::endpoint server_ep = engine.lookup(argv[1]);

        {
            // Create database handle - RAII ensures cleanup
            yokan::Database db = client.makeDatabaseHandle(
                server_ep.get_addr(), std::atoi(argv[2]));

            // Demonstrate copying
            yokan::Database db_copy = db; // Reference counted copy
            demonstrateCopying(db_copy);

            // Both db and db_copy are still valid
            std::cout << "Original handle still valid" << std::endl;

            // Demonstrate moving
            yokan::Database db_moved = demonstrateMoving(
                client, server_ep.get_addr(), std::atoi(argv[2]));

            std::string key = "move_test";
            std::string value = "moved";
            db_moved.put(key.data(), key.size(), value.data(), value.size());

            // Using smart pointers for dynamic allocation
            auto db_ptr = std::make_unique<yokan::Database>(
                client.makeDatabaseHandle(server_ep.get_addr(), std::atoi(argv[2])));

            key = "smart_ptr_test";
            value = "smart pointer";
            db_ptr->put(key.data(), key.size(), value.data(), value.size());

            // All resources will be automatically cleaned up when going out of scope
            // No need for explicit cleanup calls
        }

        std::cout << "All resources automatically cleaned up" << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Resources are automatically cleaned up when the last reference is destroyed, preventing leaks and simplifying code.

Advanced Patterns

Custom Memory Management

For high-performance applications, preallocate buffers:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <vector>
#include <string>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);
        yokan::Client client(engine.get_margo_instance());
        tl::endpoint server_ep = engine.lookup(argv[1]);
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // Strategy 1: Preallocate buffer for known size
        std::string key = "large_value";

        // Get the value size first
        size_t value_size = db.length(key.data(), key.size());

        // Preallocate exact size
        std::vector<char> value_buffer(value_size);
        db.get(key.data(), key.size(), value_buffer.data(), &value_size);

        std::cout << "Retrieved " << value_size << " bytes" << std::endl;

        // Strategy 2: Reuse buffer for multiple gets
        std::vector<char> reusable_buffer(4096); // 4KB buffer

        std::vector<std::string> keys_to_fetch = {"user:1", "user:2", "user:3"};

        for(const auto& k : keys_to_fetch) {
            size_t vsize = reusable_buffer.size();
            db.get(k.data(), k.size(), reusable_buffer.data(), &vsize);

            std::string value(reusable_buffer.data(), vsize);
            std::cout << k << " = " << value << std::endl;
        }

        // Strategy 3: Batch operations with preallocated arrays
        const size_t batch_size = 100;

        std::vector<const void*> key_ptrs(batch_size);
        std::vector<size_t> key_sizes(batch_size);
        std::vector<void*> val_ptrs(batch_size);
        std::vector<size_t> val_sizes(batch_size);

        // Preallocate value buffers
        std::vector<std::vector<char>> value_buffers(batch_size);
        for(size_t i = 0; i < batch_size; i++) {
            value_buffers[i].resize(256); // 256 bytes per value
            val_ptrs[i] = value_buffers[i].data();
            val_sizes[i] = value_buffers[i].size();
        }

        // Prepare keys
        std::vector<std::string> batch_keys(batch_size);
        for(size_t i = 0; i < batch_size; i++) {
            batch_keys[i] = "batch:" + std::to_string(i);
            key_ptrs[i] = batch_keys[i].data();
            key_sizes[i] = batch_keys[i].size();
        }

        // Batch get with preallocated memory
        db.getMulti(batch_size, key_ptrs.data(), key_sizes.data(),
                    val_ptrs.data(), val_sizes.data());

        std::cout << "Fetched " << batch_size << " values with preallocated buffers" << std::endl;

        // Strategy 4: Memory pool for high-frequency operations
        struct MemoryPool {
            std::vector<char> buffer;
            size_t used = 0;

            MemoryPool(size_t size) : buffer(size) {}

            char* allocate(size_t n) {
                if(used + n > buffer.size()) {
                    throw std::runtime_error("Pool exhausted");
                }
                char* ptr = buffer.data() + used;
                used += n;
                return ptr;
            }

            void reset() { used = 0; }
        };

        MemoryPool pool(10240); // 10KB pool

        for(int i = 0; i < 10; i++) {
            std::string k = "pool:" + std::to_string(i);
            size_t vsize = 512;
            char* buf = pool.allocate(vsize);

            db.get(k.data(), k.size(), buf, &vsize);

            std::string value(buf, vsize);
            std::cout << "Fetched from pool: " << value << std::endl;
        }

        pool.reset(); // Reuse pool

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    } catch(const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Binary Data Handling

Working with binary (non-text) data:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <vector>
#include <cstring>
#include <cstdint>

namespace tl = thallium;

// Example struct for binary data
struct UserRecord {
    uint64_t id;
    char name[32];
    double balance;
    uint32_t age;
};

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);
        yokan::Client client(engine.get_margo_instance());
        tl::endpoint server_ep = engine.lookup(argv[1]);
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // Store binary struct
        UserRecord user;
        user.id = 12345;
        std::strncpy(user.name, "Alice Johnson", sizeof(user.name));
        user.balance = 1234.56;
        user.age = 30;

        std::string key = "user:12345";
        db.put(key.data(), key.size(), &user, sizeof(UserRecord));

        std::cout << "Stored binary record" << std::endl;

        // Retrieve binary struct
        UserRecord retrieved_user;
        size_t vsize = sizeof(UserRecord);
        db.get(key.data(), key.size(), &retrieved_user, &vsize);

        std::cout << "Retrieved user:" << std::endl;
        std::cout << "  ID: " << retrieved_user.id << std::endl;
        std::cout << "  Name: " << retrieved_user.name << std::endl;
        std::cout << "  Balance: $" << retrieved_user.balance << std::endl;
        std::cout << "  Age: " << retrieved_user.age << std::endl;

        // Store binary blob (e.g., image data)
        std::vector<uint8_t> blob_data(1024);
        for(size_t i = 0; i < blob_data.size(); i++) {
            blob_data[i] = static_cast<uint8_t>(i % 256);
        }

        std::string blob_key = "image:001";
        db.put(blob_key.data(), blob_key.size(),
               blob_data.data(), blob_data.size());

        std::cout << "\nStored binary blob of " << blob_data.size() << " bytes" << std::endl;

        // Retrieve blob
        std::vector<uint8_t> retrieved_blob(1024);
        size_t blob_size = retrieved_blob.size();
        db.get(blob_key.data(), blob_key.size(),
               retrieved_blob.data(), &blob_size);

        // Verify data
        bool match = (blob_data == retrieved_blob);
        std::cout << "Binary data matches: " << (match ? "yes" : "no") << std::endl;

        // Store array of integers
        std::vector<int32_t> int_array = {10, 20, 30, 40, 50};
        std::string array_key = "array:001";

        db.put(array_key.data(), array_key.size(),
               int_array.data(), int_array.size() * sizeof(int32_t));

        // Retrieve array
        std::vector<int32_t> retrieved_array(5);
        size_t array_size = retrieved_array.size() * sizeof(int32_t);
        db.get(array_key.data(), array_key.size(),
               retrieved_array.data(), &array_size);

        std::cout << "\nRetrieved array: ";
        for(const auto& val : retrieved_array) {
            std::cout << val << " ";
        }
        std::cout << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Template-Based Wrappers

Create type-safe wrappers using templates:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>
#include <sstream>
#include <type_traits>

namespace tl = thallium;

// Type-safe wrapper for Yokan database
template<typename KeyType, typename ValueType>
class TypedDatabase {
private:
    yokan::Database db;

    // Serialize key to binary
    template<typename T>
    std::vector<char> serialize(const T& obj) {
        if constexpr (std::is_same_v<T, std::string>) {
            return std::vector<char>(obj.begin(), obj.end());
        } else {
            std::vector<char> buffer(sizeof(T));
            std::memcpy(buffer.data(), &obj, sizeof(T));
            return buffer;
        }
    }

    // Deserialize value from binary
    template<typename T>
    T deserialize(const std::vector<char>& data) {
        if constexpr (std::is_same_v<T, std::string>) {
            return std::string(data.begin(), data.end());
        } else {
            T obj;
            std::memcpy(&obj, data.data(), sizeof(T));
            return obj;
        }
    }

public:
    TypedDatabase(yokan::Database db) : db(std::move(db)) {}

    void put(const KeyType& key, const ValueType& value) {
        auto key_data = serialize(key);
        auto val_data = serialize(value);
        db.put(key_data.data(), key_data.size(),
               val_data.data(), val_data.size());
    }

    ValueType get(const KeyType& key) {
        auto key_data = serialize(key);

        // Get value size first
        size_t vsize = db.length(key_data.data(), key_data.size());

        // Allocate and get value
        std::vector<char> val_data(vsize);
        db.get(key_data.data(), key_data.size(), val_data.data(), &vsize);

        return deserialize<ValueType>(val_data);
    }

    bool exists(const KeyType& key) {
        auto key_data = serialize(key);
        return db.exists(key_data.data(), key_data.size());
    }

    void erase(const KeyType& key) {
        auto key_data = serialize(key);
        db.erase(key_data.data(), key_data.size());
    }
};

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);
        yokan::Client client(engine.get_margo_instance());
        tl::endpoint server_ep = engine.lookup(argv[1]);
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), std::atoi(argv[2]));

        // Create typed database wrapper for string->string
        TypedDatabase<std::string, std::string> string_db(db);

        string_db.put("name", "Alice");
        std::string name = string_db.get("name");
        std::cout << "Name: " << name << std::endl;

        // Create typed database wrapper for int->double
        TypedDatabase<int, double> numeric_db(db);

        numeric_db.put(42, 3.14159);
        double value = numeric_db.get(42);
        std::cout << "Value for key 42: " << value << std::endl;

        // Check existence
        bool exists = numeric_db.exists(42);
        std::cout << "Key 42 exists: " << (exists ? "yes" : "no") << std::endl;

        // Erase
        numeric_db.erase(42);
        exists = numeric_db.exists(42);
        std::cout << "Key 42 exists after erase: " << (exists ? "yes" : "no") << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Migration API

The C++ API provides a clean interface for database migration:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 5) {
        std::cerr << "Usage: " << argv[0]
                  << " <source_addr> <source_provider_id>"
                  << " <dest_addr> <dest_provider_id>" << std::endl;
        return 1;
    }

    try {
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);
        yokan::Client client(engine.get_margo_instance());

        // Connect to source database
        tl::endpoint source_ep = engine.lookup(argv[1]);
        yokan::Database source_db = client.makeDatabaseHandle(
            source_ep.get_addr(), std::atoi(argv[2]));

        // Connect to destination database
        tl::endpoint dest_ep = engine.lookup(argv[3]);
        yokan::Database dest_db = client.makeDatabaseHandle(
            dest_ep.get_addr(), std::atoi(argv[4]));

        // Populate source database with test data
        std::cout << "Populating source database..." << std::endl;
        for(int i = 0; i < 10; i++) {
            std::string key = "key:" + std::to_string(i);
            std::string value = "value:" + std::to_string(i);
            source_db.put(key.data(), key.size(), value.data(), value.size());
        }

        // Manual migration using list and put operations
        std::cout << "\nStarting manual migration..." << std::endl;

        size_t total_migrated = 0;

        // Use iter to iterate and copy
        auto migration_callback = [&](size_t index, const void* key, size_t ksize,
                                       const void* val, size_t vsize) -> yk_return_t {
            // Put key-value pair into destination database
            dest_db.put(key, ksize, val, vsize);
            total_migrated++;
            return YOKAN_SUCCESS;
        };

        // Migrate all keys (empty prefix means all keys)
        std::string prefix = "";
        std::string filter = "";

        source_db.iter(prefix.data(), prefix.size(),
                       filter.data(), filter.size(),
                       0, // count = 0 means all
                       migration_callback);

        std::cout << "Migrated " << total_migrated << " key-value pairs" << std::endl;

        // Verify migration by checking destination database
        std::cout << "\nVerifying migration..." << std::endl;

        for(int i = 0; i < 10; i++) {
            std::string key = "key:" + std::to_string(i);

            // Check if key exists in destination
            bool exists = dest_db.exists(key.data(), key.size());

            if(exists) {
                // Get value from destination
                std::vector<char> buffer(256);
                size_t vsize = buffer.size();
                dest_db.get(key.data(), key.size(), buffer.data(), &vsize);

                std::string value(buffer.data(), vsize);
                std::cout << "  " << key << " = " << value << std::endl;
            } else {
                std::cout << "  " << key << " NOT FOUND!" << std::endl;
            }
        }

        std::cout << "\nMigration verification complete!" << std::endl;

        // Note: For production use with large databases, consider:
        // 1. Batch operations for better performance
        // 2. Error handling and retry logic
        // 3. Progress tracking
        // 4. Using REMI for provider-level migration (requires server access)

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

See Database migration for detailed migration documentation.

Document Collections

Work with JSON documents using the Collection API:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <yokan/cxx/collection.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <server_addr> <provider_id>" << std::endl;
        return 1;
    }

    try {
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);
        yokan::Client client(engine.get_margo_instance());
        tl::endpoint server_ep = engine.lookup(argv[1]);
        uint16_t provider_id = std::atoi(argv[2]);

        // Create database handle
        yokan::Database db = client.makeDatabaseHandle(
            server_ep.get_addr(), provider_id);

        // Create a collection
        // Note: Collection constructor takes name first, then Database object
        std::string collection_name = "users";
        yokan::Collection collection(collection_name.c_str(), db);

        std::cout << "Working with collection: " << collection_name << std::endl;

        // Prepare JSON documents
        std::vector<std::string> documents = {
            R"({"name": "Alice Johnson", "age": 30, "email": "alice@example.com", "role": "engineer"})",
            R"({"name": "Bob Smith", "age": 35, "email": "bob@example.com", "role": "manager"})",
            R"({"name": "Carol White", "age": 28, "email": "carol@example.com", "role": "engineer"})"
        };

        // Prepare IDs for storing
        std::vector<yk_id_t> ids = {1001, 1002, 1003};

        // Prepare pointers for storeMulti
        std::vector<const void*> doc_ptrs;
        std::vector<size_t> doc_sizes;

        for(const auto& doc : documents) {
            doc_ptrs.push_back(doc.data());
            doc_sizes.push_back(doc.size());
        }

        // Store multiple documents with explicit IDs
        collection.storeMulti(documents.size(),
                             doc_ptrs.data(),
                             doc_sizes.data(),
                             ids.data());

        std::cout << "\nStored " << documents.size() << " documents" << std::endl;
        for(size_t i = 0; i < ids.size(); i++) {
            std::cout << "  ID " << ids[i] << std::endl;
        }

        // Load a document by ID
        yk_id_t id_to_load = 1001;
        std::vector<char> buffer(512);
        size_t doc_size = buffer.size();

        collection.load(id_to_load, buffer.data(), &doc_size);
        std::string retrieved_doc(buffer.data(), doc_size);

        std::cout << "\nRetrieved document " << id_to_load << ":" << std::endl;
        std::cout << retrieved_doc << std::endl;

        // Get document length
        size_t length = collection.length(id_to_load);
        std::cout << "\nDocument " << id_to_load << " length: " << length << " bytes" << std::endl;

        // Update a document
        std::string updated_doc = R"({"name": "Alice Johnson", "age": 31, "email": "alice.j@example.com", "role": "senior engineer"})";

        collection.update(id_to_load, updated_doc.data(), updated_doc.size());
        std::cout << "\nUpdated document " << id_to_load << std::endl;

        // Load updated document
        doc_size = buffer.size();
        collection.load(id_to_load, buffer.data(), &doc_size);
        std::string updated_retrieved(buffer.data(), doc_size);
        std::cout << "Updated document content: " << updated_retrieved << std::endl;

        // Erase a document
        yk_id_t id_to_erase = 1002;
        collection.erase(id_to_erase);
        std::cout << "\nErased document " << id_to_erase << std::endl;

        // List documents using iter with C++ callback
        std::cout << "\nListing all remaining documents:" << std::endl;

        auto callback = [](size_t index, yk_id_t id, const void* doc, size_t size) -> yk_return_t {
            std::string document(static_cast<const char*>(doc), size);
            std::cout << "  [" << index << "] ID " << id << ": " << document << std::endl;
            return YOKAN_SUCCESS;
        };

        yk_id_t start_id = 0; // Start from beginning
        std::string filter = ""; // No filter

        collection.iter(start_id,
                       filter.data(), filter.size(),
                       0, // max = 0 means all
                       callback);

        std::cout << "\nCollection operations completed successfully!" << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Collections provide:

  • JSON document storage

  • Document IDs

  • Batch operations

  • Query capabilities

Performance Considerations

  1. Use batch operations to reduce network round-trips

  2. Preallocate buffers for get operations when sizes are known

  3. Use move semantics to avoid unnecessary copying

  4. Choose appropriate backends based on workload characteristics

  5. Use NO_RDMA mode for small key/value pairs

  6. Consider pool configuration for concurrent operations

  7. Profile your application to identify bottlenecks

Comparison with C API

C++ Advantages:

  • RAII automatic cleanup

  • Exception-based error handling

  • Type safety

  • Modern C++ idioms

  • Easier to use correctly

When to use C API:

  • C-only environments

  • ABI stability requirements

  • Specific performance needs

  • Library interoperability

Integration Examples

With Thallium

The C++ bindings work seamlessly with Thallium:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <thallium/serialization/stl/string.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>

namespace tl = thallium;

// Custom RPC that uses Yokan
void store_user(const tl::request& req, yokan::Database& db,
                const std::string& username, const std::string& email) {
    try {
        std::string key = "user:" + username;
        db.put(key.data(), key.size(), email.data(), email.size());

        std::cout << "Stored user: " << username << " -> " << email << std::endl;
        req.respond(true);

    } catch(const yokan::Exception& ex) {
        std::cerr << "Yokan error: " << ex.what() << std::endl;
        req.respond(false);
    }
}

void get_user(const tl::request& req, yokan::Database& db,
              const std::string& username) {
    try {
        std::string key = "user:" + username;

        // Get email
        std::vector<char> buffer(256);
        size_t vsize = buffer.size();
        db.get(key.data(), key.size(), buffer.data(), &vsize);

        std::string email(buffer.data(), vsize);
        std::cout << "Retrieved user: " << username << " -> " << email << std::endl;

        req.respond(email);

    } catch(const yokan::Exception& ex) {
        std::cerr << "Yokan error: " << ex.what() << std::endl;
        req.respond(std::string(""));
    }
}

int main(int argc, char** argv) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <protocol> <provider_id>" << std::endl;
        return 1;
    }

    try {
        // Initialize Thallium engine in server mode
        tl::engine engine(argv[1], THALLIUM_SERVER_MODE);

        // Create Yokan client using Thallium's Margo instance
        yokan::Client client(engine.get_margo_instance());

        // Create database handle (connecting to local provider)
        yokan::Database db = client.makeDatabaseHandle(
            engine.self().get_addr(), std::atoi(argv[2]));

        std::cout << "Server running at " << engine.self() << std::endl;

        // Register Thallium RPCs that use Yokan
        engine.define("store_user",
                     [&db](const tl::request& req, const std::string& username,
                           const std::string& email) {
                         store_user(req, db, username, email);
                     });

        engine.define("get_user",
                     [&db](const tl::request& req, const std::string& username) {
                         get_user(req, db, username);
                     });

        // Enable remote shutdown
        engine.enable_remote_shutdown();

        // Wait for finalize
        engine.wait_for_finalize();

    } catch(const yokan::Exception& ex) {
        std::cerr << "Yokan error: " << ex.what() << std::endl;
        return 1;
    } catch(const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

With Bedrock

Using Yokan with Bedrock’s C++ API:

/*
 * (C) 2024 The University of Chicago
 *
 * See COPYRIGHT in top-level directory.
 */
#include <thallium.hpp>
#include <bedrock/Client.hpp>
#include <yokan/cxx/database.hpp>
#include <yokan/cxx/client.hpp>
#include <iostream>
#include <string>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <bedrock_config.json>" << std::endl;
        return 1;
    }

    try {
        // Initialize Thallium engine
        tl::engine engine("na+sm", THALLIUM_CLIENT_MODE);

        // Create Bedrock client
        bedrock::Client bedrock_client(engine);

        // Load Bedrock configuration
        std::string config_file = argv[1];

        std::cout << "Loading Bedrock configuration from: " << config_file << std::endl;

        // Query Bedrock for Yokan provider information
        // (In a real application, you would parse the config or use Bedrock API
        //  to discover providers. This is a simplified example.)

        // For this example, assume we know the server address and provider ID
        std::string server_addr = "na+sm://12345-0";
        uint16_t provider_id = 1;

        // Create Yokan client
        yokan::Client yokan_client(engine.get_margo_instance());

        // Look up server
        tl::endpoint server_ep = engine.lookup(server_addr);

        // Create database handle
        yokan::Database db = yokan_client.makeDatabaseHandle(
            server_ep.get_addr(), provider_id);

        std::cout << "Connected to Yokan provider via Bedrock" << std::endl;

        // Use Yokan database
        std::string key = "bedrock:test";
        std::string value = "Yokan + Bedrock integration";

        db.put(key.data(), key.size(), value.data(), value.size());
        std::cout << "Stored: " << key << " = " << value << std::endl;

        // Retrieve value
        std::vector<char> buffer(256);
        size_t vsize = buffer.size();
        db.get(key.data(), key.size(), buffer.data(), &vsize);

        std::string retrieved(buffer.data(), vsize);
        std::cout << "Retrieved: " << retrieved << std::endl;

        // Example Bedrock configuration (for reference):
        std::cout << "\nExample Bedrock configuration snippet:" << std::endl;
        std::cout << R"({
    "libraries": {
        "yokan": "libyokan-bedrock-module.so"
    },
    "providers": [
        {
            "name": "my_yokan_db",
            "type": "yokan",
            "provider_id": 1,
            "config": {
                "database": {
                    "type": "map"
                }
            }
        }
    ]
})" << std::endl;

    } catch(const yokan::Exception& ex) {
        std::cerr << "Yokan error: " << ex.what() << std::endl;
        return 1;
    } catch(const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

See 02_advanced_setup for Bedrock configuration details.

Building Applications

CMake Integration

Use pkg-config to find Yokan:

cmake_minimum_required(VERSION 3.10)
project(my_yokan_app CXX)

set(CMAKE_CXX_STANDARD 14)

find_package (yokan REQUIRED)

add_executable(my_app main.cpp)
target_link_libraries(my_app yokan::client yokan::server)

Manual Compilation

g++ -std=c++14 my_app.cpp $(pkg-config --cflags --libs yokan-client) -o my_app