C++ wrapper for fdb_c API

* Header-only
* C objects wrapped in refcounting C++ objects
* Accept lambdas as future callback
* Type-safe access to Future values
* throw/nothrow member functions
This commit is contained in:
Junhyun Shim 2022-03-02 15:12:36 +01:00
parent bc9380d5e2
commit b23c51ffcc
2 changed files with 557 additions and 1 deletions

View File

@ -99,6 +99,10 @@ if(NOT WIN32 AND NOT IS_ARM_MAC)
test/unit/fdb_api.cpp
test/unit/fdb_api.hpp)
add_library(fdb_cpp INTERFACE)
target_sources(fdb_cpp INTERFACE test/fdb.hpp)
target_include_directories(fdb_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/test)
target_link_libraries(fdb_cpp INTERFACE fmt::fmt)
if(OPEN_FOR_IDE)
add_library(fdb_c_performance_test OBJECT test/performance_test.c test/test.h)
add_library(fdb_c_ryw_benchmark OBJECT test/ryw_benchmark.c test/test.h)
@ -143,7 +147,7 @@ if(NOT WIN32 AND NOT IS_ARM_MAC)
# do not set RPATH for mako
set_property(TARGET mako PROPERTY SKIP_BUILD_RPATH TRUE)
target_link_libraries(mako PRIVATE fdb_c fdbclient)
target_link_libraries(mako PRIVATE fdb_c fdbclient fmt::fmt Threads::Threads fdb_cpp)
if(NOT OPEN_FOR_IDE)
# Make sure that fdb_c.h is compatible with c90

552
bindings/c/test/fdb.hpp Normal file
View File

@ -0,0 +1,552 @@
#pragma once
#ifndef FDB_API_VERSION
#define FDB_API_VERSION 710
#endif
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <fmt/format.h>
// introduce the option enums
#include <fdb_c_options.g.h>
namespace fdb {
// hide C API to discourage mixing C/C++ API
namespace native {
#include <foundationdb/fdb_c.h>
}
using ByteString = std::basic_string<uint8_t>;
using BytesRef = std::basic_string_view<uint8_t>;
using CharsRef = std::string_view;
using KeyRef = BytesRef;
using ValueRef = BytesRef;
inline uint8_t const* to_byte_ptr(char const* ptr) noexcept {
return reinterpret_cast<uint8_t const*>(ptr);
}
// get bytestring view from charstring: e.g. std::basic_string{_view}<char>
template <template <class...> class StringLike, class Char>
BytesRef to_bytes_ref(const StringLike<Char>& s) noexcept {
static_assert(sizeof(Char) == 1);
return BytesRef(reinterpret_cast<uint8_t const*>(s.data()),
s.size());
}
// get charstring view from bytestring: e.g. std::basic_string{_view}<uint8_t>
template <template <class...> class StringLike, class Char>
CharsRef to_chars_ref(const StringLike<Char>& s) noexcept {
static_assert(sizeof(Char) == 1);
return CharsRef(reinterpret_cast<char const*>(s.data()),
s.size());
}
[[maybe_unused]] constexpr const bool overflow_check = false;
inline int intsize(BytesRef b) {
if constexpr (overflow_check) {
if (b.size() > static_cast<size_t>(std::numeric_limits<int>::max()))
throw std::overflow_error("byte strlen goes beyond int bounds");
}
return static_cast<int>(b.size());
}
class Error {
public:
using code_type = native::fdb_error_t;
Error() noexcept : err(0) {}
explicit Error(code_type err) noexcept : err(err) {}
char const* what() noexcept { return native::fdb_get_error(err); }
explicit operator bool() const noexcept { return err != 0; }
bool is(code_type other) const noexcept { return err != other; }
code_type code() const noexcept { return err; }
bool retryable() const noexcept {
return native::fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE, err) != 0;
}
private:
code_type err;
};
/* Traits of value types held by ready futures.
Holds type and value extraction function. */
namespace future_var {
struct None {
struct type {};
static Error extract(native::FDBFuture*, type&) noexcept {
return Error(0);
}
};
struct Int64 {
using type = int64_t;
static Error extract(native::FDBFuture* f, type& out) noexcept {
return Error(native::fdb_future_get_int64(f, &out));
}
};
struct Key {
using type = std::pair<uint8_t const*, int>;
static Error extract(native::FDBFuture* f, type& out) noexcept {
auto& [out_key, out_key_length] = out;
return Error(native::fdb_future_get_key(f, &out_key, &out_key_length));
}
};
struct Value {
using type = std::tuple<bool, uint8_t const*, int>;
static Error extract(native::FDBFuture* f, type& out) noexcept {
auto& [out_present, out_value, out_value_length] = out;
auto out_present_native = native::fdb_bool_t{};
auto err = native::fdb_future_get_value(f, &out_present_native,
&out_value, &out_value_length);
out_present = (out_present_native != 0);
return Error(err);
}
};
struct StringArray {
using type = std::pair<const char**, int>;
static Error extract(native::FDBFuture* f, type& out) noexcept {
auto& [out_strings, out_count] = out;
return Error(native::fdb_future_get_string_array(f, &out_strings, &out_count));
}
};
struct KeyValueArray {
using type = std::tuple<native::FDBKeyValue const *, int, bool>;
static Error extract(native::FDBFuture* f, type& out) noexcept {
auto& [out_kv, out_count, out_more] = out;
auto out_more_native = native::fdb_bool_t{};
auto err = native::fdb_future_get_keyvalue_array(f, &out_kv, &out_count, &out_more_native);
out_more = (out_more_native != 0);
return Error(err);
}
};
} // namespace future_var
[[noreturn]] inline void throw_error(std::string_view preamble, Error err) {
auto msg = std::string(preamble);
msg.append(err.what());
throw std::runtime_error(msg);
}
inline int max_api_version() {
return native::fdb_get_max_api_version();
}
inline Error select_api_version_nothrow(int version) {
return Error(native::fdb_select_api_version(version));
}
inline void select_api_version(int version) {
if (auto err = select_api_version_nothrow(version)) {
throw_error(fmt::format("ERROR: fdb_select_api_version({}): ", version), err);
}
}
namespace network {
inline Error set_option_nothrow(FDBNetworkOption option, BytesRef str) noexcept {
return Error(native::fdb_network_set_option(option, str.data(), intsize(str)));
}
inline Error set_option_nothrow(FDBNetworkOption option, int64_t value) noexcept {
return Error(native::fdb_network_set_option(
option,
reinterpret_cast<const uint8_t*>(&value),
static_cast<int>(sizeof(value))));
}
inline void set_option(FDBNetworkOption option, BytesRef str) {
if (auto err = set_option_nothrow(option, str)) {
throw_error(
fmt::format("ERROR: fdb_network_set_option({}): ",
static_cast<std::underlying_type_t<FDBNetworkOption>>(option)),
err);
}
}
inline void set_option(FDBNetworkOption option, int64_t value) {
if (auto err = set_option_nothrow(option, value)) {
throw_error(
fmt::format("ERROR: fdb_network_set_option({}, {}): ",
static_cast<std::underlying_type_t<FDBNetworkOption>>(option),
value),
err);
}
}
inline Error setup_nothrow() noexcept {
return Error(native::fdb_setup_network());
}
inline void setup() {
if (auto err = setup_nothrow())
throw_error("ERROR: fdb_network_setup(): ", err);
}
inline Error run() {
return Error(native::fdb_run_network());
}
inline Error stop() {
return Error(native::fdb_stop_network());
}
} // namespace network
class TX;
class Database;
class Result {
friend class TX;
std::shared_ptr<native::FDBResult> r;
Result(native::FDBResult* result) {
if (result) r = std::shared_ptr<native::FDBResult>(result, &native::fdb_result_destroy);
}
public:
using KeyValueArray = future_var::KeyValueArray::type;
Error get_keyvalue_array_nothrow(KeyValueArray& out) const noexcept {
auto out_more_native = native::fdb_bool_t{};
auto& [out_kv, out_count, out_more] = out;
auto err_raw = native::fdb_result_get_keyvalue_array(r.get(), &out_kv,
&out_count, &out_more_native);
out_more = out_more_native != 0;
return Error(err_raw);
}
KeyValueArray get_keyvalue_array() const {
auto ret = KeyValueArray{};
if (auto err = get_keyvalue_array_nothrow(ret))
throw_error("ERROR: result_get_keyvalue_array(): ", err);
return ret;
}
};
class Future {
protected:
friend class TX;
std::shared_ptr<native::FDBFuture> f;
Future(native::FDBFuture* future)
{
if (future) f = std::shared_ptr<native::FDBFuture>(future, &native::fdb_future_destroy);
}
// wrap any capturing lambda as callback passable to fdb_future_set_callback().
// destroy after invocation.
template <class Fn>
static void callback(native::FDBFuture*, void* param) {
auto fp = static_cast<Fn*>(param);
try {
(*fp)();
} catch (const std::exception& e) {
fmt::print(stderr, "ERROR: Exception thrown in user callback: {}", e.what());
}
delete fp;
}
// set as callback user-defined completion handler of signature void(Future)
template <class FutureType, class UserFunc>
void then(UserFunc&& fn) {
auto cb = [fut=FutureType(*this), fn=std::forward<UserFunc>(fn)]() {
fn(fut);
};
using cb_type = std::decay_t<decltype(cb)>;
auto fp = new cb_type(std::move(cb));
native::fdb_future_set_callback(f.get(), &callback<cb_type>, fp);
}
public:
Future() noexcept : Future(nullptr) {}
Future(const Future&) noexcept = default;
Future& operator=(const Future&) noexcept = default;
bool valid() const noexcept { return f != nullptr; }
explicit operator bool() const noexcept { return valid(); }
bool ready() const noexcept {
assert(valid());
return native::fdb_future_is_ready(f.get()) != 0;
}
Error block_until_ready() const noexcept {
assert(valid());
return Error(native::fdb_future_block_until_ready(f.get()));
}
Error error() const noexcept {
assert(valid());
return Error(native::fdb_future_get_error(f.get()));
}
void cancel() noexcept {
native::fdb_future_cancel(f.get());
}
template <class VarTraits>
typename VarTraits::type get() const {
assert(valid());
assert(!error());
auto out = typename VarTraits::Type{};
if (auto err = VarTraits::extract(f.get(), out)) {
throw_error("future_get: ", err);
}
return out;
}
template <class VarTraits>
Error get_nothrow(typename VarTraits::type& var) const noexcept {
assert(valid());
assert(!error());
auto out = typename VarTraits::Type{};
return VarTraits::extract(f.get(), out);
}
template <class UserFunc>
void then(UserFunc&& fn) {
then<Future>(std::forward<UserFunc>(fn));
}
};
template <typename VarTraits>
class TypedFuture : public Future {
friend class TX;
using self_type = TypedFuture<VarTraits>;
using Future::Future;
// hide type-unsafe inherited functions
using Future::get;
using Future::get_nothrow;
using Future::then;
TypedFuture(const Future& f) noexcept : Future(f) {}
public:
using contained_type = typename VarTraits::type;
Future erase_type() const noexcept {
return static_cast<Future const&>(*this);
}
contained_type get() const {
return get<VarTraits>();
}
Error get_nothrow(contained_type& out) const noexcept {
return get_nothrow<VarTraits>(out);
}
template <class UserFunc>
void then(UserFunc&& fn) {
Future::then<self_type>(std::forward<UserFunc>(fn));
}
};
namespace key_select {
struct inclusive { static constexpr const bool value = true; };
struct exclusive { static constexpr const bool value = false; };
}
class TX {
friend class Database;
std::shared_ptr<native::FDBTransaction> tr;
explicit TX(native::FDBTransaction* tr_raw) {
if (tr_raw) tr = std::shared_ptr<native::FDBTransaction>(tr_raw, &native::fdb_transaction_destroy);
}
public:
TX() noexcept : TX(nullptr) {}
TX(const TX&) noexcept = default;
TX& operator=(const TX&) noexcept = default;
bool valid() const noexcept { return tr != nullptr; }
explicit operator bool() const noexcept { return valid(); }
Error set_option_nothrow(FDBTransactionOption option, int64_t value) noexcept {
return Error(native::fdb_transaction_set_option(
tr.get(), option,
reinterpret_cast<const uint8_t*>(&value),
static_cast<int>(sizeof(value))));
}
Error set_option_nothrow(FDBTransactionOption option, BytesRef str) noexcept {
return Error(native::fdb_transaction_set_option(
tr.get(), option, str.data(), intsize(str)));
}
void set_option(FDBTransactionOption option, int64_t value) {
if (auto err = set_option_nothrow(option, value)) {
throw_error(fmt::format("transaction_set_option({}, {}) returned error: ",
static_cast<std::underlying_type_t<FDBTransactionOption>>(option),
value),
err);
}
}
void set_option(FDBTransactionOption option, BytesRef str) {
if (auto err = set_option_nothrow(option, str)) {
throw_error(
fmt::format("transaction_set_option({}) returned error: ",
static_cast<std::underlying_type_t<FDBTransactionOption>>(option)),
err);
}
}
TypedFuture<future_var::Int64> get_read_version() {
return native::fdb_transaction_get_read_version(tr.get());
}
Error get_committed_version_nothrow(int64_t& out) {
return Error(native::fdb_transaction_get_committed_version(tr.get(), &out));
}
int64_t get_committed_version() {
auto out = int64_t{};
if (auto err = get_committed_version_nothrow(out)) {
throw_error("get_committed_version: ", err);
}
return out;
}
// Func should have signature void(Future f, bool need_retry)
// Func should first check that retry == false and f.error() == 0
// before attempting to extract value from f
TypedFuture<future_var::Value> get(KeyRef key, bool snapshot) {
return native::fdb_transaction_get(tr.get(), key.data(), intsize(key), snapshot);
}
// Usage: tx.get_range<key_select::inclusive, key_select::exclusive>(begin, end, ...);
// gets key-value pairs in key range [begin, end)
template <class FirstInclusive, class LastInclusive>
TypedFuture<future_var::KeyValueArray> get_range(KeyRef begin, KeyRef end,
int limit, int target_bytes, FDBStreamingMode mode,
int iteration, bool snapshot, bool reverse) {
if constexpr (FirstInclusive::value && LastInclusive::value) {
return native::fdb_transaction_get_range(tr.get(),
FDB_KEYSEL_FIRST_GREATER_OR_EQUAL(begin.data(), intsize(begin)),
FDB_KEYSEL_LAST_LESS_OR_EQUAL(end.data(), intsize(end)),
limit, target_bytes, mode, iteration, snapshot, reverse);
} else if constexpr (FirstInclusive::value && !LastInclusive::value) {
return native::fdb_transaction_get_range(tr.get(),
FDB_KEYSEL_FIRST_GREATER_OR_EQUAL(begin.data(), intsize(begin)),
FDB_KEYSEL_LAST_LESS_THAN(end.data(), intsize(end)),
limit, target_bytes, mode, iteration, snapshot, reverse);
} else if constexpr (!FirstInclusive::value && LastInclusive::value) {
return native::fdb_transaction_get_range(tr.get(),
FDB_KEYSEL_FIRST_GREATER_THAN(begin.data(), intsize(begin)),
FDB_KEYSEL_LAST_LESS_OR_EQUAL(end.data(), intsize(end)),
limit, target_bytes, mode, iteration, snapshot, reverse);
} else {
return native::fdb_transaction_get_range(tr.get(),
FDB_KEYSEL_FIRST_GREATER_THAN(begin.data(), intsize(begin)),
FDB_KEYSEL_LAST_LESS_THAN(end.data(), intsize(end)),
limit, target_bytes, mode, iteration, snapshot, reverse);
}
}
Result read_blob_granules(KeyRef begin, KeyRef end,
int64_t begin_version, int64_t read_version,
native::FDBReadBlobGranuleContext context) {
return Result(native::fdb_transaction_read_blob_granules(tr.get(),
begin.data(), intsize(begin),
end.data(), intsize(end),
begin_version, read_version, context));
}
TypedFuture<future_var::None> commit() {
return native::fdb_transaction_commit(tr.get());
}
TypedFuture<future_var::None> on_error(Error err) {
return native::fdb_transaction_on_error(tr.get(), err.code());
}
void reset() {
return native::fdb_transaction_reset(tr.get());
}
void set(KeyRef key, ValueRef value) {
native::fdb_transaction_set(tr.get(), key.data(), intsize(key), value.data(), intsize(value));
}
void clear(KeyRef key) {
native::fdb_transaction_clear(tr.get(), key.data(), intsize(key));
}
void clear_range(KeyRef begin, KeyRef end) {
native::fdb_transaction_clear_range(tr.get(), begin.data(), intsize(begin),
end.data(), intsize(end));
}
};
class Database {
std::shared_ptr<native::FDBDatabase> db;
public:
Database(const Database&) noexcept = default;
Database& operator=(const Database&) noexcept = default;
Database(const std::string& cluster_file_path) : db(nullptr) {
auto db_raw = static_cast<native::FDBDatabase*>(nullptr);
if (auto err = Error(native::fdb_create_database(cluster_file_path.c_str(), &db_raw)))
throw_error(
fmt::format("Failed to create database with '{}': ", cluster_file_path),
err);
db = std::shared_ptr<native::FDBDatabase>(db_raw, &native::fdb_database_destroy);
}
Database() noexcept : db(nullptr) {}
Error set_option_nothrow(FDBDatabaseOption option, int64_t value) noexcept {
return Error(native::fdb_database_set_option(
db.get(), option,
reinterpret_cast<const uint8_t*>(&value),
static_cast<int>(sizeof(value))));
}
Error set_option_nothrow(FDBDatabaseOption option, BytesRef str) noexcept {
return Error(native::fdb_database_set_option(
db.get(), option, str.data(), intsize(str)));
}
void set_option(FDBDatabaseOption option, int64_t value) {
if (auto err = set_option_nothrow(option, value)) {
throw_error(
fmt::format("database_set_option({}, {}) returned error: ",
static_cast<std::underlying_type_t<FDBDatabaseOption>>(option),
value),
err);
}
}
void set_option(FDBDatabaseOption option, BytesRef str) {
if (auto err = set_option_nothrow(option, str)) {
throw_error(
fmt::format("database_set_option({}) returned error: ",
static_cast<std::underlying_type_t<FDBDatabaseOption>>(option)),
err);
}
}
TX create_tx() {
if (!db) throw std::runtime_error("create_transaction from null database");
auto tx_native = static_cast<native::FDBTransaction*>(nullptr);
auto err = Error(native::fdb_database_create_transaction(db.get(), &tx_native));
if (err) throw_error("Failed to create transaction: ", err);
return TX(tx_native);
}
};
} // namespace fdb