Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
73 changes: 65 additions & 8 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} -> {String.to_atom("a#{key}"), value} end)
Comment thread
brodeuralexis marked this conversation as resolved.
Outdated

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

for not_a_map <- [10, [], nil, Map, "map"] do
Comment thread
brodeuralexis marked this conversation as resolved.
Outdated
assert_raise ArgumentError, "decode failed, expected a map", fn ->
NIF.codec_map_atom_int64(not_a_map)
end

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

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

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

assert_raise ArgumentError, "decode failed, expected an atom", fn ->
NIF.codec_map_atom_int64(%{"hello" => 1})
for map_with_invalid_key <- [%{"hello" => 1}, %{2 => 2}, %{{:a, "tuple"} => 3}] do
assert_raise ArgumentError, "decode failed, expected an atom", fn ->
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
end

assert_raise ArgumentError, "decode failed, expected an integer", fn ->
NIF.codec_map_atom_int64(%{hello: 1.0})
for map_with_invalid_value <- [%{hello: :world}, %{foo: "bar"}, %{cafe: {:record, 0xBABE}}] do
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_unordered_map_atom_int64_alloc(map_with_invalid_value)
end
end
end

Expand Down