Current section

Files

Jump to
adbc c_src adbc_nif_resource.hpp
Raw

c_src/adbc_nif_resource.hpp

#ifndef ADBC_NIF_RESOURCE_HPP
#define ADBC_NIF_RESOURCE_HPP
#include <adbc.h>
#include <atomic>
#include <erl_nif.h>
#include <memory>
#include <type_traits>
// Only for debugging:
#include <cstdio>
template <typename T> struct NoOpDeleter;
template <typename...> using void_t = void;
// Check for the existence of member variable 'release' using SFINAE
template <typename T, typename = void>
struct release_guard : std::false_type {};
template <typename T>
struct release_guard<T, typename std::enable_if<std::is_same<
decltype(T::release), void (*)(T *)>::value>::type>
: std::true_type {};
/// A NifRes<T> wraps a `T` to allow it to be shared between C++ and Erlang
/// while managing its lifetime properly.
///
/// Because the in-memory representation of a `*NifRes<T>` is the same as a
/// `*T`, the result from `my_nif_res->get_resource()` can be passed to a NIF
/// written in other languages as well (as long as they know the memory
/// representation of T).
///
/// Lifetime is managed automatically by:
/// - Returning an owned pointer from `allocate_resource` and `clone_ref`.
/// Behind the scenes, these calls manipulate Erlang's refcount of the resource
/// object.
/// - Whenever one of these owned pointers leaves the scope, the refcount is
/// decremented.
/// - Separately, a `destruct_resource` callback is provided to Erlang which
/// will be called by the GC when there are no references (from either Erlang or
/// C++ side) remaining.
template <typename T> struct NifRes {
using val_type_p = T *;
using val_type = T;
using res_type = NifRes<T>;
val_type val;
// only used when T = struct
void * private_data = nullptr;
static ErlNifResourceType *type;
/// Creates a new NifRes<T> using `enif_alloc_resource`, returning it as an
/// owned pointer. When this owned pointer leaves the scope,
/// `enif_release_resource` is automatically called.
static auto allocate_resource(ErlNifEnv *env, ERL_NIF_TERM &error)
-> std::unique_ptr<NifRes<T>, NoOpDeleter<res_type>> {
std::unique_ptr<NifRes<T>, NoOpDeleter<res_type>> res{
static_cast<res_type *>(
enif_alloc_resource(res_type::type, sizeof(res_type)))};
if (res == nullptr) {
error = erlang::nif::error(env, "cannot allocate Nif resource\n");
return res;
}
memset(&res->val, 0, sizeof(val_type));
res->private_data = nullptr;
return res;
}
/// Given a `term` that should be a NifRes<T>,
/// Obtain a pointer to the contained `val`
/// which is guaranteed to be valid for at least the lifetime of `env`.
///
/// In the case something which is not a NifRes<T> is passed,
/// `nullptr` is returned and the output-parameter `error` filled.
static res_type *get_resource(ErlNifEnv *env, ERL_NIF_TERM term,
ERL_NIF_TERM &error) {
res_type *self_res = nullptr;
if (!enif_get_resource(env, term, res_type::type,
reinterpret_cast<void **>(&self_res)) ||
self_res == nullptr) {
error = erlang::nif::error(env, "cannot access Nif resource");
}
return self_res;
}
/// Creates another reference to the same underlying NifRes to be used in
/// Erlang. (uses `enif_make_resource`)
ERL_NIF_TERM make_resource(ErlNifEnv *env) {
return enif_make_resource(env, this);
}
/// Creates another reference to the same underlying NifRes to be used in C++.
/// (Increments the reference count on the C++ side).
auto clone_ref() const -> std::unique_ptr<NifRes<T>, NoOpDeleter<res_type>> {
enif_keep_resource(this);
return std::unique_ptr<NifRes<T>>{this};
}
// Called whenever a _single_ reference to the resource goes out of scope.
// Decrements the reference count on the C++ side.
~NifRes() { enif_release_resource(this); }
// Callback which is called by the Erlang GC when the _last_ reference to the
// NifRes goes out of scope. If there is special cleanup that should happen
// for a particular child-class, create a template specialization for it.
template <typename R = T>
static auto destruct_resource(ErlNifEnv *env, void *args) ->
typename std::enable_if<release_guard<R>::value, void>::type {
auto res = (NifRes<T> *)args;
if (res) {
if (res->val.release) {
res->val.release(&res->val);
}
if (res->private_data) {
auto schema = (struct ArrowSchema*)res->private_data;
if (schema->release) {
schema->release(schema);
}
enif_free(schema);
}
}
}
template <typename R = T>
static auto destruct_resource(ErlNifEnv *env, void *args) ->
typename std::enable_if<!release_guard<R>::value, void>::type {}
};
// Used to construct a unique_ptr wrapping memory that is managed remotely.
// The value in this memory *does* need to be destructed
// but *should not* be deallocated using `delete`.
template <typename T> struct NoOpDeleter {
void operator()(T *val) {
// Do destruct
val->~T();
// Do not delete; we do not own the memory
}
};
#endif /* ADBC_NIF_RESOURCE_HPP */