Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ Fine provides implementations for the following types:
| `std::tuple<Args...>` | x | x | `{a, b, ..., c}` |
| `std::vector<T>` | x | x | `list(a)` |
| `std::map<K, V>` | x | x | `%{k => v}` |
| `std::unordered_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, ...}` |
Expand Down
73 changes: 69 additions & 4 deletions c_include/fine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <string>
#include <string_view>
#include <type_traits>
#include <unordered_map>
#include <variant>
#include <vector>

Expand Down Expand Up @@ -652,6 +653,43 @@ struct Decoder<std::map<K, V, Compare, Alloc>> {
};
};

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

ERL_NIF_TERM key_term, value_term;
ErlNifMapIterator iter;
if (!enif_map_iterator_create(env, term, &iter,
ERL_NIF_MAP_ITERATOR_FIRST)) {
throw std::invalid_argument("decode failed, expected a map");
}

// Define RAII cleanup for the iterator
auto cleanup = IterCleanup{env, iter};

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

return map;
}

private:
struct IterCleanup {
ErlNifEnv *env;
ErlNifMapIterator iter;

~IterCleanup() { enif_map_iterator_destroy(env, &iter); }
};
};

template <typename T> struct Decoder<ResourcePtr<T>> {
static ResourcePtr<T> decode(ErlNifEnv *env, const ERL_NIF_TERM &term) {
void *ptr;
Expand Down Expand Up @@ -875,10 +913,37 @@ struct Encoder<std::map<K, V, Compare, Alloc>> {
const std::map<K, V, Compare, Alloc> &map) {
auto keys = std::vector<ERL_NIF_TERM>();
auto values = std::vector<ERL_NIF_TERM>();
keys.reserve(map.size());
values.reserve(map.size());

for (const auto &[key, value] : map) {
keys.emplace_back(fine::encode(env, key));
values.emplace_back(fine::encode(env, value));
}

ERL_NIF_TERM map_term;
if (!enif_make_map_from_arrays(env, keys.data(), values.data(), keys.size(),
&map_term)) {
throw std::runtime_error("encode failed, failed to make a map");
}

return map_term;
}
};

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

for (const auto &[key, value] : map) {
keys.push_back(fine::encode(env, key));
values.push_back(fine::encode(env, value));
keys.emplace_back(fine::encode(env, key));
values.emplace_back(fine::encode(env, value));
}

ERL_NIF_TERM map_term;
Expand Down Expand Up @@ -1189,13 +1254,13 @@ inline int load(ErlNifEnv *env, void **, ERL_NIF_TERM) {

namespace std {
template <> struct hash<::fine::Term> {
size_t operator()(const ::fine::Term &term) noexcept {
size_t operator()(const ::fine::Term &term) const noexcept {
return enif_hash(ERL_NIF_INTERNAL_HASH, term, 0);
}
};

template <> struct hash<::fine::Atom> {
size_t operator()(const ::fine::Atom &atom) noexcept {
size_t operator()(const ::fine::Atom &atom) const noexcept {
return std::hash<std::string_view>{}(atom.to_string());
}
};
Expand Down
21 changes: 21 additions & 0 deletions test/c_src/finest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <optional>
#include <stdexcept>
#include <thread>
#include <unordered_map>

#include <erl_nif.h>
#include <fine.hpp>
Expand Down Expand Up @@ -216,6 +217,26 @@ codec_map_atom_int64_alloc(
}
FINE_NIF(codec_map_atom_int64_alloc, 0);

std::unordered_map<fine::Atom, int64_t>
codec_unordered_map_atom_int64(ErlNifEnv *,
std::unordered_map<fine::Atom, int64_t> term) {
return term;
}
FINE_NIF(codec_unordered_map_atom_int64, 0);

std::unordered_map<
fine::Atom, int64_t, std::hash<fine::Atom>, std::equal_to<fine::Atom>,
std::pmr::polymorphic_allocator<std::pair<const fine::Atom, int64_t>>>
codec_unordered_map_atom_int64_alloc(
ErlNifEnv *,
std::unordered_map<
fine::Atom, int64_t, std::hash<fine::Atom>, std::equal_to<fine::Atom>,
std::pmr::polymorphic_allocator<std::pair<const fine::Atom, int64_t>>>
term) {
return term;
}
FINE_NIF(codec_unordered_map_atom_int64_alloc, 0);

fine::ResourcePtr<TestResource>
codec_resource(ErlNifEnv *, fine::ResourcePtr<TestResource> term) {
return term;
Expand Down
2 changes: 2 additions & 0 deletions test/lib/finest/nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ defmodule Finest.NIF do
def codec_vector_int64_alloc(_term), do: err!()
def codec_map_atom_int64(_term), do: err!()
def codec_map_atom_int64_alloc(_term), do: err!()
def codec_unordered_map_atom_int64(_term), do: err!()
def codec_unordered_map_atom_int64_alloc(_term), do: err!()
def codec_resource(_term), do: err!()
def codec_struct(_term), do: err!()
def codec_struct_exception(_term), do: err!()
Expand Down
67 changes: 62 additions & 5 deletions test/test/finest_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,76 @@ defmodule FinestTest do
end

test "map" do
assert NIF.codec_map_atom_int64(%{hello: 1, world: 2}) == %{hello: 1, world: 2}
assert NIF.codec_map_atom_int64_alloc(%{hello: 1, world: 2}) == %{hello: 1, world: 2}
small_map = %{hello: 1, world: 2}

empty_map = %{}

# Large maps have more than 32 elements:
# https://www.erlang.org/doc/system/maps.html#how-large-maps-are-implemented
large_map =
0..64
|> Enum.with_index()
|> Map.new(fn {key, value} -> {:"a#{key}", value} end)

for map <- [small_map, empty_map, large_map] do
assert NIF.codec_map_atom_int64(map) == map
assert NIF.codec_map_atom_int64_alloc(map) == map
assert NIF.codec_unordered_map_atom_int64(map) == map
assert NIF.codec_unordered_map_atom_int64_alloc(map) == map
end

invalid_map = 10

assert_raise ArgumentError, "decode failed, expected a map", fn ->
NIF.codec_map_atom_int64(invalid_map)
end

assert_raise ArgumentError, "decode failed, expected a map", fn ->
NIF.codec_map_atom_int64_alloc(invalid_map)
end

assert_raise ArgumentError, "decode failed, expected a map", fn ->
NIF.codec_map_atom_int64(10)
NIF.codec_unordered_map_atom_int64(invalid_map)
end

assert_raise ArgumentError, "decode failed, expected a map", fn ->
NIF.codec_unordered_map_atom_int64_alloc(invalid_map)
end

map_with_invalid_key = %{"hello" => 1}

assert_raise ArgumentError, "decode failed, expected an atom", fn ->
NIF.codec_map_atom_int64(%{"hello" => 1})
NIF.codec_map_atom_int64(map_with_invalid_key)
end

assert_raise ArgumentError, "decode failed, expected an atom", fn ->
NIF.codec_map_atom_int64_alloc(map_with_invalid_key)
end

assert_raise ArgumentError, "decode failed, expected an atom", fn ->
NIF.codec_unordered_map_atom_int64(map_with_invalid_key)
end

assert_raise ArgumentError, "decode failed, expected an atom", fn ->
NIF.codec_unordered_map_atom_int64_alloc(map_with_invalid_key)
end

map_with_invalid_value = %{hello: :world}

assert_raise ArgumentError, "decode failed, expected an integer", fn ->
NIF.codec_map_atom_int64(map_with_invalid_value)
end

assert_raise ArgumentError, "decode failed, expected an integer", fn ->
NIF.codec_map_atom_int64_alloc(map_with_invalid_value)
end

assert_raise ArgumentError, "decode failed, expected an integer", fn ->
NIF.codec_unordered_map_atom_int64(map_with_invalid_value)
end

assert_raise ArgumentError, "decode failed, expected an integer", fn ->
NIF.codec_map_atom_int64(%{hello: 1.0})
NIF.codec_unordered_map_atom_int64_alloc(map_with_invalid_value)
end
end

Expand Down