Skip to content

Commit 4232ecc

Browse files
committed
Support std::unordered_map
This change ensures that `std::unordered_map`s are supported alongside `std::map`s.
1 parent a69298e commit 4232ecc

5 files changed

Lines changed: 158 additions & 12 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ Fine provides implementations for the following types:
152152
| `std::tuple<Args...>` | x | x | `{a, b, ..., c}` |
153153
| `std::vector<T>` | x | x | `list(a)` |
154154
| `std::map<K, V>` | x | x | `%{k => v}` |
155+
| `std::unordered_map<K, V>` | x | x | `%{k => v}` |
155156
| `fine::ResourcePtr<T>` | x | x | `reference` |
156157
| `T` with [struct metadata](#structs) | x | x | `%a{}` |
157158
| `fine::Ok<Args...>` | x | | `{:ok, ...}` |

c_include/fine.hpp

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <string>
1313
#include <string_view>
1414
#include <type_traits>
15+
#include <unordered_map>
1516
#include <variant>
1617
#include <vector>
1718

@@ -652,6 +653,43 @@ struct Decoder<std::map<K, V, Compare, Alloc>> {
652653
};
653654
};
654655

656+
template <typename K, typename V, typename Hash, typename Pred, typename Alloc>
657+
struct Decoder<std::unordered_map<K, V, Hash, Pred, Alloc>> {
658+
static std::unordered_map<K, V, Hash, Pred, Alloc>
659+
decode(ErlNifEnv *env, const ERL_NIF_TERM &term) {
660+
std::unordered_map<K, V, Hash, Pred, Alloc> map;
661+
662+
ERL_NIF_TERM key_term, value_term;
663+
ErlNifMapIterator iter;
664+
if (!enif_map_iterator_create(env, term, &iter,
665+
ERL_NIF_MAP_ITERATOR_FIRST)) {
666+
throw std::invalid_argument("decode failed, expected a map");
667+
}
668+
669+
// Define RAII cleanup for the iterator
670+
auto cleanup = IterCleanup{env, iter};
671+
672+
while (enif_map_iterator_get_pair(env, &iter, &key_term, &value_term)) {
673+
auto key = fine::decode<K>(env, key_term);
674+
auto value = fine::decode<V>(env, value_term);
675+
676+
map.insert_or_assign(std::move(key), std::move(value));
677+
678+
enif_map_iterator_next(env, &iter);
679+
}
680+
681+
return map;
682+
}
683+
684+
private:
685+
struct IterCleanup {
686+
ErlNifEnv *env;
687+
ErlNifMapIterator iter;
688+
689+
~IterCleanup() { enif_map_iterator_destroy(env, &iter); }
690+
};
691+
};
692+
655693
template <typename T> struct Decoder<ResourcePtr<T>> {
656694
static ResourcePtr<T> decode(ErlNifEnv *env, const ERL_NIF_TERM &term) {
657695
void *ptr;
@@ -875,10 +913,37 @@ struct Encoder<std::map<K, V, Compare, Alloc>> {
875913
const std::map<K, V, Compare, Alloc> &map) {
876914
auto keys = std::vector<ERL_NIF_TERM>();
877915
auto values = std::vector<ERL_NIF_TERM>();
916+
keys.reserve(map.size());
917+
values.reserve(map.size());
918+
919+
for (const auto &[key, value] : map) {
920+
keys.emplace_back(fine::encode(env, key));
921+
values.emplace_back(fine::encode(env, value));
922+
}
923+
924+
ERL_NIF_TERM map_term;
925+
if (!enif_make_map_from_arrays(env, keys.data(), values.data(), keys.size(),
926+
&map_term)) {
927+
throw std::runtime_error("encode failed, failed to make a map");
928+
}
929+
930+
return map_term;
931+
}
932+
};
933+
934+
template <typename K, typename V, typename Hash, typename Pred, typename Alloc>
935+
struct Encoder<std::unordered_map<K, V, Hash, Pred, Alloc>> {
936+
static ERL_NIF_TERM
937+
encode(ErlNifEnv *env,
938+
const std::unordered_map<K, V, Hash, Pred, Alloc> &map) {
939+
auto keys = std::vector<ERL_NIF_TERM>();
940+
auto values = std::vector<ERL_NIF_TERM>();
941+
keys.reserve(map.size());
942+
values.reserve(map.size());
878943

879944
for (const auto &[key, value] : map) {
880-
keys.push_back(fine::encode(env, key));
881-
values.push_back(fine::encode(env, value));
945+
keys.emplace_back(fine::encode(env, key));
946+
values.emplace_back(fine::encode(env, value));
882947
}
883948

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

11901255
namespace std {
11911256
template <> struct hash<::fine::Term> {
1192-
size_t operator()(const ::fine::Term &term) noexcept {
1257+
size_t operator()(const ::fine::Term &term) const noexcept {
11931258
return enif_hash(ERL_NIF_INTERNAL_HASH, term, 0);
11941259
}
11951260
};
11961261

11971262
template <> struct hash<::fine::Atom> {
1198-
size_t operator()(const ::fine::Atom &atom) noexcept {
1263+
size_t operator()(const ::fine::Atom &atom) const noexcept {
11991264
return std::hash<std::string_view>{}(atom.to_string());
12001265
}
12011266
};

test/c_src/finest.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <optional>
66
#include <stdexcept>
77
#include <thread>
8+
#include <unordered_map>
89

910
#include <erl_nif.h>
1011
#include <fine.hpp>
@@ -216,6 +217,26 @@ codec_map_atom_int64_alloc(
216217
}
217218
FINE_NIF(codec_map_atom_int64_alloc, 0);
218219

220+
std::unordered_map<fine::Atom, int64_t>
221+
codec_unordered_map_atom_int64(ErlNifEnv *,
222+
std::unordered_map<fine::Atom, int64_t> term) {
223+
return term;
224+
}
225+
FINE_NIF(codec_unordered_map_atom_int64, 0);
226+
227+
std::unordered_map<
228+
fine::Atom, int64_t, std::hash<fine::Atom>, std::equal_to<fine::Atom>,
229+
std::pmr::polymorphic_allocator<std::pair<const fine::Atom, int64_t>>>
230+
codec_unordered_map_atom_int64_alloc(
231+
ErlNifEnv *,
232+
std::unordered_map<
233+
fine::Atom, int64_t, std::hash<fine::Atom>, std::equal_to<fine::Atom>,
234+
std::pmr::polymorphic_allocator<std::pair<const fine::Atom, int64_t>>>
235+
term) {
236+
return term;
237+
}
238+
FINE_NIF(codec_unordered_map_atom_int64_alloc, 0);
239+
219240
fine::ResourcePtr<TestResource>
220241
codec_resource(ErlNifEnv *, fine::ResourcePtr<TestResource> term) {
221242
return term;

test/lib/finest/nif.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ defmodule Finest.NIF do
3838
def codec_vector_int64_alloc(_term), do: err!()
3939
def codec_map_atom_int64(_term), do: err!()
4040
def codec_map_atom_int64_alloc(_term), do: err!()
41+
def codec_unordered_map_atom_int64(_term), do: err!()
42+
def codec_unordered_map_atom_int64_alloc(_term), do: err!()
4143
def codec_resource(_term), do: err!()
4244
def codec_struct(_term), do: err!()
4345
def codec_struct_exception(_term), do: err!()

test/test/finest_test.exs

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,19 +160,76 @@ defmodule FinestTest do
160160
end
161161

162162
test "map" do
163-
assert NIF.codec_map_atom_int64(%{hello: 1, world: 2}) == %{hello: 1, world: 2}
164-
assert NIF.codec_map_atom_int64_alloc(%{hello: 1, world: 2}) == %{hello: 1, world: 2}
163+
small_map = %{hello: 1, world: 2}
164+
165+
empty_map = %{}
166+
167+
# Large maps have more than 32 elements:
168+
# https://www.erlang.org/doc/system/maps.html#how-large-maps-are-implemented
169+
large_map =
170+
0..64
171+
|> Enum.with_index()
172+
|> Map.new(fn {key, value} -> {String.to_atom("a#{key}"), value} end)
173+
174+
for map <- [small_map, empty_map, large_map] do
175+
assert NIF.codec_map_atom_int64(map) == map
176+
assert NIF.codec_map_atom_int64_alloc(map) == map
177+
assert NIF.codec_unordered_map_atom_int64(map) == map
178+
assert NIF.codec_unordered_map_atom_int64_alloc(map) == map
179+
end
180+
181+
for not_a_map <- [10, [], nil, Map, "map"] do
182+
assert_raise ArgumentError, "decode failed, expected a map", fn ->
183+
NIF.codec_map_atom_int64(not_a_map)
184+
end
185+
186+
assert_raise ArgumentError, "decode failed, expected a map", fn ->
187+
NIF.codec_map_atom_int64_alloc(not_a_map)
188+
end
165189

166-
assert_raise ArgumentError, "decode failed, expected a map", fn ->
167-
NIF.codec_map_atom_int64(10)
190+
assert_raise ArgumentError, "decode failed, expected a map", fn ->
191+
NIF.codec_unordered_map_atom_int64(not_a_map)
192+
end
193+
194+
assert_raise ArgumentError, "decode failed, expected a map", fn ->
195+
NIF.codec_unordered_map_atom_int64_alloc(not_a_map)
196+
end
168197
end
169198

170-
assert_raise ArgumentError, "decode failed, expected an atom", fn ->
171-
NIF.codec_map_atom_int64(%{"hello" => 1})
199+
for map_with_invalid_key <- [%{"hello" => 1}, %{2 => 2}, %{{:a, "tuple"} => 3}] do
200+
assert_raise ArgumentError, "decode failed, expected an atom", fn ->
201+
NIF.codec_map_atom_int64(map_with_invalid_key)
202+
end
203+
204+
assert_raise ArgumentError, "decode failed, expected an atom", fn ->
205+
NIF.codec_map_atom_int64_alloc(map_with_invalid_key)
206+
end
207+
208+
assert_raise ArgumentError, "decode failed, expected an atom", fn ->
209+
NIF.codec_unordered_map_atom_int64(map_with_invalid_key)
210+
end
211+
212+
assert_raise ArgumentError, "decode failed, expected an atom", fn ->
213+
NIF.codec_unordered_map_atom_int64_alloc(map_with_invalid_key)
214+
end
172215
end
173216

174-
assert_raise ArgumentError, "decode failed, expected an integer", fn ->
175-
NIF.codec_map_atom_int64(%{hello: 1.0})
217+
for map_with_invalid_value <- [%{hello: :world}, %{foo: "bar"}, %{cafe: {:record, 0xBABE}}] do
218+
assert_raise ArgumentError, "decode failed, expected an integer", fn ->
219+
NIF.codec_map_atom_int64(map_with_invalid_value)
220+
end
221+
222+
assert_raise ArgumentError, "decode failed, expected an integer", fn ->
223+
NIF.codec_map_atom_int64_alloc(map_with_invalid_value)
224+
end
225+
226+
assert_raise ArgumentError, "decode failed, expected an integer", fn ->
227+
NIF.codec_unordered_map_atom_int64(map_with_invalid_value)
228+
end
229+
230+
assert_raise ArgumentError, "decode failed, expected an integer", fn ->
231+
NIF.codec_unordered_map_atom_int64_alloc(map_with_invalid_value)
232+
end
176233
end
177234
end
178235

0 commit comments

Comments
 (0)