diff --git a/README.md b/README.md index 56016dc..fe797e1 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ Fine provides implementations for the following types: | `std::tuple` | x | x | `{a, b, ..., c}` | | `std::vector` | x | x | `list(a)` | | `std::map` | x | x | `%{k => v}` | +| `std::unordered_map` | x | x | `%{k => v}` | | `fine::ResourcePtr` | x | x | `reference` | | `T` with [struct metadata](#structs) | x | x | `%a{}` | | `fine::Ok` | x | | `{:ok, ...}` | diff --git a/c_include/fine.hpp b/c_include/fine.hpp index 5dca38f..7a31677 100644 --- a/c_include/fine.hpp +++ b/c_include/fine.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -652,6 +653,43 @@ struct Decoder> { }; }; +template +struct Decoder> { + static std::unordered_map + decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + std::unordered_map 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(env, key_term); + auto value = fine::decode(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 struct Decoder> { static ResourcePtr decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { void *ptr; @@ -875,10 +913,37 @@ struct Encoder> { const std::map &map) { auto keys = std::vector(); auto values = std::vector(); + 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 +struct Encoder> { + static ERL_NIF_TERM + encode(ErlNifEnv *env, + const std::unordered_map &map) { + auto keys = std::vector(); + auto values = std::vector(); + 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; @@ -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{}(atom.to_string()); } }; diff --git a/test/c_src/finest.cpp b/test/c_src/finest.cpp index cf91f7b..8f1f6d6 100644 --- a/test/c_src/finest.cpp +++ b/test/c_src/finest.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -216,6 +217,26 @@ codec_map_atom_int64_alloc( } FINE_NIF(codec_map_atom_int64_alloc, 0); +std::unordered_map +codec_unordered_map_atom_int64(ErlNifEnv *, + std::unordered_map term) { + return term; +} +FINE_NIF(codec_unordered_map_atom_int64, 0); + +std::unordered_map< + fine::Atom, int64_t, std::hash, std::equal_to, + std::pmr::polymorphic_allocator>> +codec_unordered_map_atom_int64_alloc( + ErlNifEnv *, + std::unordered_map< + fine::Atom, int64_t, std::hash, std::equal_to, + std::pmr::polymorphic_allocator>> + term) { + return term; +} +FINE_NIF(codec_unordered_map_atom_int64_alloc, 0); + fine::ResourcePtr codec_resource(ErlNifEnv *, fine::ResourcePtr term) { return term; diff --git a/test/lib/finest/nif.ex b/test/lib/finest/nif.ex index d97279f..0e97d5d 100644 --- a/test/lib/finest/nif.ex +++ b/test/lib/finest/nif.ex @@ -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!() diff --git a/test/test/finest_test.exs b/test/test/finest_test.exs index 26363ba..37a1a90 100644 --- a/test/test/finest_test.exs +++ b/test/test/finest_test.exs @@ -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