From 4232ecc53679b7194aeff42b3e5bb6157f1340f5 Mon Sep 17 00:00:00 2001 From: Alexis Brodeur Date: Thu, 14 Aug 2025 13:27:31 -0400 Subject: [PATCH 1/2] Support std::unordered_map This change ensures that `std::unordered_map`s are supported alongside `std::map`s. --- README.md | 1 + c_include/fine.hpp | 73 ++++++++++++++++++++++++++++++++++++--- test/c_src/finest.cpp | 21 +++++++++++ test/lib/finest/nif.ex | 2 ++ test/test/finest_test.exs | 73 ++++++++++++++++++++++++++++++++++----- 5 files changed, 158 insertions(+), 12 deletions(-) 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..8c50ea8 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} -> {String.to_atom("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 + + for not_a_map <- [10, [], nil, Map, "map"] do + 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 From e7dee6eea720ab6679193006bd6fc35a61f9277e Mon Sep 17 00:00:00 2001 From: Alexis Brodeur Date: Thu, 14 Aug 2025 15:13:38 -0400 Subject: [PATCH 2/2] Make tests more straighforward --- test/test/finest_test.exs | 80 +++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/test/test/finest_test.exs b/test/test/finest_test.exs index 8c50ea8..37a1a90 100644 --- a/test/test/finest_test.exs +++ b/test/test/finest_test.exs @@ -169,7 +169,7 @@ defmodule FinestTest do large_map = 0..64 |> Enum.with_index() - |> Map.new(fn {key, value} -> {String.to_atom("a#{key}"), value} end) + |> 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 @@ -178,58 +178,58 @@ defmodule FinestTest do assert NIF.codec_unordered_map_atom_int64_alloc(map) == map end - for not_a_map <- [10, [], nil, Map, "map"] do - assert_raise ArgumentError, "decode failed, expected a map", fn -> - NIF.codec_map_atom_int64(not_a_map) - end + invalid_map = 10 - 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(invalid_map) + end - 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_map_atom_int64_alloc(invalid_map) + end - assert_raise ArgumentError, "decode failed, expected a map", fn -> - NIF.codec_unordered_map_atom_int64_alloc(not_a_map) - end + assert_raise ArgumentError, "decode failed, expected a map", fn -> + NIF.codec_unordered_map_atom_int64(invalid_map) end - 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 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(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_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(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 + assert_raise ArgumentError, "decode failed, expected an atom", fn -> + NIF.codec_unordered_map_atom_int64_alloc(map_with_invalid_key) end - 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 + 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_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(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 + assert_raise ArgumentError, "decode failed, expected an integer", fn -> + NIF.codec_unordered_map_atom_int64_alloc(map_with_invalid_value) end end