Skip to content

Provide fully templated encoders and decoders #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
79 changes: 57 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ NIFs in C++.

- STL compatible Erlang-backend mutex and rwlock.

- Compatible with STL container allocators and polymorphic memory
resources.

## Motivation

Some projects make extensive use of NIFs, where using the C API results
Expand Down Expand Up @@ -131,28 +134,28 @@ auto message = fine::decode<std::string>(env, term);

Fine provides implementations for the following types:

| Type | Encoder | Decoder |
| ------------------------------------ | ------- | ------- |
| `fine::Term` | x | x |
| `int64_t` | x | x |
| `uint64_t` | x | x |
| `double` | x | x |
| `bool` | x | x |
| `ErlNifPid` | x | x |
| `ErlNifBinary` | x | x |
| `std::string_view` | x | x |
| `std::string` | x | x |
| `fine::Atom` | x | x |
| `std::nullopt_t` | x | |
| `std::optional<T>` | x | x |
| `std::variant<Args...>` | x | x |
| `std::tuple<Args...>` | x | x |
| `std::vector<T>` | x | x |
| `std::map<K, V>` | x | x |
| `fine::ResourcePtr<T>` | x | x |
| `T` with [struct metadata](#structs) | x | x |
| `fine::Ok<Args...>` | x | |
| `fine::Error<Args...>` | x | |
| C++ Type | Encoder | Decoder | Elixir Type |
| ------------------------------------ | ------- | ------- | --------------------------- |
| `fine::Term` | x | x | `term` |
| `int64_t` | x | x | `integer` |
| `uint64_t` | x | x | `non_neg_integer` |
| `double` | x | x | `float` |
| `bool` | x | x | `boolean` |
| `ErlNifPid` | x | x | `pid` |
| `ErlNifBinary` | x | x | `binary` |
| `std::string_view` | x | x | `binary` |
| `std::string` | x | x | `binary` |
| `fine::Atom` | x | x | `atom` |
| `std::nullopt_t` | x | | `nil` |
| `std::optional<T>` | x | x | `a \| nil` |
| `std::variant<Args...>` | x | x | `a \| b \| ... \| c` |
| `std::tuple<Args...>` | x | x | `{a, b, ..., c}` |
| `std::vector<T>` | x | x | `list(a)` |
| `std::map<K, V>` | x | x | `%{k => v}` |
| `fine::ResourcePtr<T>` | x | x | `reference` |
| `T` with [struct metadata](#structs) | x | x | `%a{}` |
| `fine::Ok<Args...>` | x | | `{:ok, ...}` |
| `fine::Error<Args...>` | x | | `{:error, ...}` |

> #### ERL_NIF_TERM {: .warning}
>
Expand Down Expand Up @@ -557,6 +560,38 @@ const char* my_object__name(struct my_object*);

fine::SharedMutex my_object_rwlock("my_lib", "my_object", my_object__name(my_object));
```
## Allocators

For compatibility with the STL, fine supports stateless allocators when
decoding values, while also supporting stateful allocators when encoding
values. The following shows how a custom `MyAllocator` allocator and
`my_memory_resource` memory resource can be used in conjunction with fine:

```c++
template<typename T>
struct MyAllocator { ... };

std::pmr::memory_resource* my_memory_resource = ...;

std::vector<std::pmr::string, MyAllocator<std::pmr::string>> repeat_string(
ErlNifEnv *,
std::basic_string<char, std::char_traits<char>, MyAllocator<char>>
string,
std::uint64_t repeat) {
std::vector<std::pmr::string, MyAllocator<std::pmr::string>> strings;

for (std::uint64_t i = 0; i != repeat; ++i) {
strings.emplace_back(std::pmr::string(string, my_memory_resource));
}

return strings;
}
FINE_NIF(repeat_string, 0);
```

Attempting to decode STL containers making use of `std::pmr::polymorphic_allocator`
will result in the `std::pmr::get_default_resource()` memory resource being
used.

<!-- Docs -->

Expand Down
57 changes: 36 additions & 21 deletions include/fine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,7 @@ Term make_resource_binary(ErlNifEnv *env, ResourcePtr<T> resource,
//
// This is useful when returning large binary from a NIF and the source
// buffer does not outlive the return.
inline fine::Term make_new_binary(ErlNifEnv *env, const char *data,
size_t size) {
inline Term make_new_binary(ErlNifEnv *env, const char *data, size_t size) {
ERL_NIF_TERM term;
auto term_data = enif_make_new_binary(env, size, &term);
if (term_data == nullptr) {
Expand Down Expand Up @@ -432,9 +431,12 @@ template <> struct Decoder<std::string_view> {
}
};

template <> struct Decoder<std::string> {
static std::string decode(ErlNifEnv *env, const ERL_NIF_TERM &term) {
return std::string(fine::decode<std::string_view>(env, term));
template <typename Alloc>
struct Decoder<std::basic_string<char, std::char_traits<char>, Alloc>> {
using string = std::basic_string<char, std::char_traits<char>, Alloc>;

static string decode(ErlNifEnv *env, const ERL_NIF_TERM &term) {
return string(fine::decode<std::string_view>(env, term));
}
};

Expand Down Expand Up @@ -521,35 +523,38 @@ template <typename... Args> struct Decoder<std::tuple<Args...>> {
}
};

template <typename T> struct Decoder<std::vector<T>> {
static std::vector<T> decode(ErlNifEnv *env, const ERL_NIF_TERM &term) {
template <typename T, typename Alloc> struct Decoder<std::vector<T, Alloc>> {
static std::vector<T, Alloc> decode(ErlNifEnv *env,
const ERL_NIF_TERM &term) {
unsigned int length;

if (!enif_get_list_length(env, term, &length)) {
throw std::invalid_argument("decode failed, expected a list");
}

std::vector<T> vector;
std::vector<T, Alloc> vector;
vector.reserve(length);

auto list = term;

ERL_NIF_TERM head, tail;
while (enif_get_list_cell(env, list, &head, &tail)) {
auto elem = fine::decode<T>(env, head);
vector.push_back(elem);
vector.emplace_back(std::move(elem));
list = tail;
}

return vector;
}
};

template <typename K, typename V> struct Decoder<std::map<K, V>> {
static std::map<K, V> decode(ErlNifEnv *env, const ERL_NIF_TERM &term) {
auto map = std::map<K, V>();
template <typename K, typename V, typename Compare, typename Alloc>
struct Decoder<std::map<K, V, Compare, Alloc>> {
static std::map<K, V, Compare, Alloc> decode(ErlNifEnv *env,
const ERL_NIF_TERM &term) {
std::map<K, V, Compare, Alloc> map;

ERL_NIF_TERM key, value;
ERL_NIF_TERM key_term, value_term;
ErlNifMapIterator iter;
if (!enif_map_iterator_create(env, term, &iter,
ERL_NIF_MAP_ITERATOR_FIRST)) {
Expand All @@ -559,8 +564,12 @@ template <typename K, typename V> struct Decoder<std::map<K, V>> {
// Define RAII cleanup for the iterator
auto cleanup = IterCleanup{env, iter};

while (enif_map_iterator_get_pair(env, &iter, &key, &value)) {
map[fine::decode<K>(env, key)] = fine::decode<V>(env, value);
while (enif_map_iterator_get_pair(env, &iter, &key_term, &value_term)) {
auto key = fine::decode<K>(env, key_term);
auto value = fine::decode<V>(env, value_term);

map.insert_or_assign(std::move(key), std::move(value));

enif_map_iterator_next(env, &iter);
}

Expand Down Expand Up @@ -705,8 +714,11 @@ template <> struct Encoder<std::string_view> {
}
};

template <> struct Encoder<std::string> {
static ERL_NIF_TERM encode(ErlNifEnv *env, const std::string &string) {
template <typename Alloc>
struct Encoder<std::basic_string<char, std::char_traits<char>, Alloc>> {
static ERL_NIF_TERM
encode(ErlNifEnv *env,
const std::basic_string<char, std::char_traits<char>, Alloc> &string) {
return fine::encode<std::string_view>(env, string);
}
};
Expand Down Expand Up @@ -775,8 +787,9 @@ template <typename... Args> struct Encoder<std::tuple<Args...>> {
}
};

template <typename T> struct Encoder<std::vector<T>> {
static ERL_NIF_TERM encode(ErlNifEnv *env, const std::vector<T> &vector) {
template <typename T, typename Alloc> struct Encoder<std::vector<T, Alloc>> {
static ERL_NIF_TERM encode(ErlNifEnv *env,
const std::vector<T, Alloc> &vector) {
auto terms = std::vector<ERL_NIF_TERM>();
terms.reserve(vector.size());

Expand All @@ -789,8 +802,10 @@ template <typename T> struct Encoder<std::vector<T>> {
}
};

template <typename K, typename V> struct Encoder<std::map<K, V>> {
static ERL_NIF_TERM encode(ErlNifEnv *env, const std::map<K, V> &map) {
template <typename K, typename V, typename Compare, typename Alloc>
struct Encoder<std::map<K, V, Compare, Alloc>> {
static ERL_NIF_TERM encode(ErlNifEnv *env,
const std::map<K, V, Compare, Alloc> &map) {
auto keys = std::vector<ERL_NIF_TERM>();
auto values = std::vector<ERL_NIF_TERM>();

Expand Down
58 changes: 58 additions & 0 deletions test/c_src/finest.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <cstring>
#include <exception>
#include <memory_resource>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#include <memory_resource>

#include <optional>
#include <stdexcept>
#include <thread>
Expand Down Expand Up @@ -76,6 +77,44 @@ struct ExError {
static constexpr auto is_exception = true;
};

template <typename T> struct Allocator {
using value_type = std::decay_t<T>;

Allocator() noexcept = default;

template <typename U> Allocator(const Allocator<U> &) noexcept {}

value_type *allocate(std::size_t n, const void *hint = nullptr) {
(void)hint;

void *ptr = enif_alloc(sizeof(T) * n);
if (ptr == nullptr) {
throw std::bad_alloc();
}
return reinterpret_cast<value_type *>(ptr);
}

void deallocate(value_type *ptr, std::size_t n) {
(void)n;

enif_free(ptr);
}

template <typename U, typename... Args> void construct(U *p, Args &&...args) {
new (p) U(std::forward<Args>(args)...);
}

template <typename U> void destruct(U *p) { std::destroy_at(p); }

friend bool operator==(const Allocator &, const Allocator &) noexcept {
return true;
}

friend bool operator!=(const Allocator &, const Allocator &) noexcept {
return false;
}
};

int64_t add(ErlNifEnv *, int64_t x, int64_t y) { return x + y; }
FINE_NIF(add, 0);

Expand Down Expand Up @@ -295,6 +334,25 @@ std::nullopt_t shared_mutex_shared_lock_test(ErlNifEnv *) {
}
FINE_NIF(shared_mutex_shared_lock_test, 0);

template <typename T> using NifVector = std::vector<T, Allocator<T>>;

template <typename T>
using NifBasicString = std::basic_string<T, std::char_traits<T>, Allocator<T>>;

using NifString = NifBasicString<char>;

NifVector<NifString> allocators(ErlNifEnv *, NifString string,
std::uint64_t repeat) {
NifVector<NifString> strings;

for (std::uint64_t i = 0; i != repeat; ++i) {
strings.emplace_back(string);
}

return strings;
}
FINE_NIF(allocators, 0);

} // namespace finest

FINE_INIT("Elixir.Finest.NIF");
2 changes: 2 additions & 0 deletions test/lib/finest/nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,7 @@ defmodule Finest.NIF do
def shared_mutex_unique_lock_test(), do: err!()
def shared_mutex_shared_lock_test(), do: err!()

def allocators(_string, _repeat), do: err!()

defp err!(), do: :erlang.nif_error(:not_loaded)
end
7 changes: 7 additions & 0 deletions test/test/finest_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,11 @@ defmodule FinestTest do
NIF.shared_mutex_shared_lock_test()
end
end

describe "allocators" do
test "allocators" do
assert NIF.allocators("abc", 16) ==
["abc"] |> Stream.cycle() |> Stream.take(16) |> Enum.to_list()
Comment on lines +312 to +314
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw. I would add it to this test:

test "vector" do
assert NIF.codec_vector_int64([1, 2, 3]) == [1, 2, 3]

something like this:

assert NIF.codec_vector_int64_alloc([1, 2, 3]) == [1, 2, 3] 

Since we focus on encoding/decoding working as expected :)

end
end
end