From b2d14165077de22f85ab4c83964400d011529f44 Mon Sep 17 00:00:00 2001 From: Alexis Brodeur Date: Thu, 14 Aug 2025 15:07:56 -0400 Subject: [PATCH 1/4] Support `std::multimap` and `std::unordered_multimap` This commit enables support for `std::multimap` and `std::unordered_multimap` using `fine::decode` and `fine::encoder`. --- README.md | 2 + c_include/fine.hpp | 133 ++++++++++++++++++++++++++++++++++++-- test/c_src/finest.cpp | 41 ++++++++++++ test/lib/finest/nif.ex | 4 ++ test/test/finest_test.exs | 72 +++++++++++++++++++++ 5 files changed, 247 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fe797e1..52f98fa 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,8 @@ Fine provides implementations for the following types: | `std::vector` | x | x | `list(a)` | | `std::map` | x | x | `%{k => v}` | | `std::unordered_map` | x | x | `%{k => v}` | +| `std::multimap` | x | x | `list({k, v})` | +| `std::unordered_multimap` | x | x | `list({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 7a31677..ae103f1 100644 --- a/c_include/fine.hpp +++ b/c_include/fine.hpp @@ -381,14 +381,14 @@ inline Term make_new_binary(ErlNifEnv *env, const char *data, size_t size) { // // The given type must have a specialized Decoder implementation. template T decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - return Decoder::decode(env, term); + return Decoder>::decode(env, term); } // Encodes the given value as a Erlang term. // // The value type must have a specialized Encoder implementation. template ERL_NIF_TERM encode(ErlNifEnv *env, const T &value) { - return Encoder::encode(env, value); + return Encoder>::encode(env, value); } // We want decode to return the value, and since the argument types @@ -473,9 +473,10 @@ template <> struct Decoder { } if (!enif_get_local_pid(env, term, &pid)) { // If the term is a PID and it is not local, it means it's a remote PID. - throw std::invalid_argument( - "decode failed, expected a local pid, but got a remote one. NIFs can " - "only send messages to local PIDs and remote PIDs cannot be decoded"); + throw std::invalid_argument("decode failed, expected a local pid, but " + "got a remote one. NIFs can " + "only send messages to local PIDs and " + "remote PIDs cannot be decoded"); } return pid; } @@ -591,6 +592,25 @@ template struct Decoder> { } }; +template struct Decoder> { + static std::pair decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + int size; + const ERL_NIF_TERM *terms; + if (!enif_get_tuple(env, term, &size, &terms)) { + throw std::invalid_argument("decode failed, expected a tuple"); + } + + if (size != 2) { + throw std::invalid_argument( + "decode failed, expected tuple to have 2 elements, but had " + + std::to_string(size)); + } + + return std::make_pair(fine::decode(env, terms[0]), + fine::decode(env, terms[1])); + } +}; + template struct Decoder> { static std::vector decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { @@ -690,6 +710,68 @@ struct Decoder> { }; }; +template +struct Decoder> { + static std::multimap decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { + unsigned int length; + + if (!enif_get_list_length(env, term, &length)) { + throw std::invalid_argument("decode failed, expected a list"); + } + + std::multimap map; + + auto list = term; + + ERL_NIF_TERM head, tail; + while (enif_get_list_cell(env, list, &head, &tail)) { + auto entry = fine::decode>(env, head); + + map.emplace(std::move(entry)); + + list = tail; + } + + return map; + } +}; + +template +struct Decoder> { + static std::unordered_multimap + decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + unsigned int length; + + if (!enif_get_list_length(env, term, &length)) { + throw std::invalid_argument("decode failed, expected a list"); + } + + std::unordered_multimap map; + + auto list = term; + + ERL_NIF_TERM head, tail; + while (enif_get_list_cell(env, list, &head, &tail)) { + auto entry = fine::decode>(env, head); + + map.emplace(std::move(entry)); + + list = tail; + } + + 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; @@ -892,6 +974,14 @@ template struct Encoder> { } }; +template struct Encoder> { + static ERL_NIF_TERM encode(ErlNifEnv *env, const std::pair &pair) { + const auto first = fine::encode(env, pair.first); + const auto second = fine::encode(env, pair.second); + return enif_make_tuple(env, 2, first, second); + } +}; + template struct Encoder> { static ERL_NIF_TERM encode(ErlNifEnv *env, const std::vector &vector) { @@ -956,6 +1046,39 @@ struct Encoder> { } }; +template +struct Encoder> { + static ERL_NIF_TERM encode(ErlNifEnv *env, + const std::multimap &map) { + auto terms = std::vector(); + terms.reserve(map.size()); + + for (const auto &entry : map) { + terms.emplace_back(fine::encode(env, entry)); + } + + return enif_make_list_from_array(env, terms.data(), + static_cast(terms.size())); + } +}; + +template +struct Encoder> { + static ERL_NIF_TERM + encode(ErlNifEnv *env, + const std::unordered_multimap &map) { + auto terms = std::vector(); + terms.reserve(map.size()); + + for (const auto &entry : map) { + terms.emplace_back(fine::encode(env, entry)); + } + + return enif_make_list_from_array(env, terms.data(), + static_cast(terms.size())); + } +}; + template struct Encoder> { static ERL_NIF_TERM encode(ErlNifEnv *env, const ResourcePtr &resource) { return enif_make_resource(env, reinterpret_cast(resource.get())); diff --git a/test/c_src/finest.cpp b/test/c_src/finest.cpp index 8f1f6d6..c04ef4f 100644 --- a/test/c_src/finest.cpp +++ b/test/c_src/finest.cpp @@ -205,6 +205,7 @@ codec_map_atom_int64(ErlNifEnv *, std::map term) { return term; } FINE_NIF(codec_map_atom_int64, 0); + std::map, std::pmr::polymorphic_allocator>> codec_map_atom_int64_alloc( @@ -237,6 +238,46 @@ codec_unordered_map_atom_int64_alloc( } FINE_NIF(codec_unordered_map_atom_int64_alloc, 0); +std::multimap +codec_multimap_atom_int64(ErlNifEnv *, + std::multimap term) { + return term; +} +FINE_NIF(codec_multimap_atom_int64, 0); + +std::multimap< + fine::Atom, int64_t, std::less, + std::pmr::polymorphic_allocator>> +codec_multimap_atom_int64_alloc( + ErlNifEnv *, + std::multimap< + fine::Atom, int64_t, std::less, + std::pmr::polymorphic_allocator>> + term) { + return term; +} +FINE_NIF(codec_multimap_atom_int64_alloc, 0); + +std::unordered_multimap +codec_unordered_multimap_atom_int64( + ErlNifEnv *, std::unordered_multimap term) { + return term; +} +FINE_NIF(codec_unordered_multimap_atom_int64, 0); + +std::unordered_multimap< + fine::Atom, int64_t, std::hash, std::equal_to, + std::pmr::polymorphic_allocator>> +codec_unordered_multimap_atom_int64_alloc( + ErlNifEnv *, + std::unordered_multimap< + fine::Atom, int64_t, std::hash, std::equal_to, + std::pmr::polymorphic_allocator>> + term) { + return term; +} +FINE_NIF(codec_unordered_multimap_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 0e97d5d..6f3426a 100644 --- a/test/lib/finest/nif.ex +++ b/test/lib/finest/nif.ex @@ -40,6 +40,10 @@ defmodule Finest.NIF do 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_multimap_atom_int64(_term), do: err!() + def codec_multimap_atom_int64_alloc(_term), do: err!() + def codec_unordered_multimap_atom_int64(_term), do: err!() + def codec_unordered_multimap_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 37a1a90..c76dd97 100644 --- a/test/test/finest_test.exs +++ b/test/test/finest_test.exs @@ -233,6 +233,78 @@ defmodule FinestTest do end end + test "multimap" do + empty_multimap = [] + + small_multimap = [hello: 1, world: 2] + + large_multimap = + 0..64 |> Enum.map(fn x -> {:"a#{x}", x} end) |> Enum.to_list() + + for multimap <- [empty_multimap, small_multimap, large_multimap] do + assert Enum.sort(NIF.codec_multimap_atom_int64(multimap)) == Enum.sort(multimap) + assert Enum.sort(NIF.codec_multimap_atom_int64_alloc(multimap)) == Enum.sort(multimap) + assert Enum.sort(NIF.codec_unordered_multimap_atom_int64(multimap)) == Enum.sort(multimap) + + assert Enum.sort(NIF.codec_unordered_multimap_atom_int64_alloc(multimap)) == + Enum.sort(multimap) + end + + invalid_multimap = 10 + + assert_raise ArgumentError, "decode failed, expected a list", fn -> + NIF.codec_multimap_atom_int64(invalid_multimap) + end + + assert_raise ArgumentError, "decode failed, expected a list", fn -> + NIF.codec_multimap_atom_int64_alloc(invalid_multimap) + end + + assert_raise ArgumentError, "decode failed, expected a list", fn -> + NIF.codec_unordered_multimap_atom_int64(invalid_multimap) + end + + assert_raise ArgumentError, "decode failed, expected a list", fn -> + NIF.codec_unordered_multimap_atom_int64_alloc(invalid_multimap) + end + + multimap_with_invalid_key = [{"hello", 42}] + + assert_raise ArgumentError, "decode failed, expected an atom", fn -> + NIF.codec_multimap_atom_int64(multimap_with_invalid_key) + end + + assert_raise ArgumentError, "decode failed, expected an atom", fn -> + NIF.codec_multimap_atom_int64_alloc(multimap_with_invalid_key) + end + + assert_raise ArgumentError, "decode failed, expected an atom", fn -> + NIF.codec_unordered_multimap_atom_int64(multimap_with_invalid_key) + end + + assert_raise ArgumentError, "decode failed, expected an atom", fn -> + NIF.codec_unordered_multimap_atom_int64_alloc(multimap_with_invalid_key) + end + + multimap_with_invalid_value = [hello: 1.0] + + assert_raise ArgumentError, "decode failed, expected an integer", fn -> + NIF.codec_multimap_atom_int64(multimap_with_invalid_value) + end + + assert_raise ArgumentError, "decode failed, expected an integer", fn -> + NIF.codec_multimap_atom_int64_alloc(multimap_with_invalid_value) + end + + assert_raise ArgumentError, "decode failed, expected an integer", fn -> + NIF.codec_unordered_multimap_atom_int64(multimap_with_invalid_value) + end + + assert_raise ArgumentError, "decode failed, expected an integer", fn -> + NIF.codec_unordered_multimap_atom_int64_alloc(multimap_with_invalid_value) + end + end + test "resource" do resource = NIF.resource_create(self()) assert is_reference(resource) From 9bf9fe27c65321cb36698e628a55f9d4478413ab Mon Sep 17 00:00:00 2001 From: Alexis Brodeur Date: Mon, 18 Aug 2025 17:33:32 -0400 Subject: [PATCH 2/4] Remove unused IterCleanup --- c_include/fine.hpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/c_include/fine.hpp b/c_include/fine.hpp index ae103f1..0041d3f 100644 --- a/c_include/fine.hpp +++ b/c_include/fine.hpp @@ -762,14 +762,6 @@ struct Decoder> { return map; } - -private: - struct IterCleanup { - ErlNifEnv *env; - ErlNifMapIterator iter; - - ~IterCleanup() { enif_map_iterator_destroy(env, &iter); } - }; }; template struct Decoder> { From 2eefa6297033fbd6ba3f34f4e900c7b036736975 Mon Sep 17 00:00:00 2001 From: Alexis Brodeur Date: Mon, 18 Aug 2025 17:37:06 -0400 Subject: [PATCH 3/4] Rename multimap to keyword in tests --- test/test/finest_test.exs | 50 +++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/test/test/finest_test.exs b/test/test/finest_test.exs index c76dd97..6570c78 100644 --- a/test/test/finest_test.exs +++ b/test/test/finest_test.exs @@ -233,75 +233,75 @@ defmodule FinestTest do end end - test "multimap" do - empty_multimap = [] + test "keyword" do + empty_keyword = [] - small_multimap = [hello: 1, world: 2] + small_keyword = [hello: 1, world: 2] - large_multimap = + large_keyword = 0..64 |> Enum.map(fn x -> {:"a#{x}", x} end) |> Enum.to_list() - for multimap <- [empty_multimap, small_multimap, large_multimap] do - assert Enum.sort(NIF.codec_multimap_atom_int64(multimap)) == Enum.sort(multimap) - assert Enum.sort(NIF.codec_multimap_atom_int64_alloc(multimap)) == Enum.sort(multimap) - assert Enum.sort(NIF.codec_unordered_multimap_atom_int64(multimap)) == Enum.sort(multimap) + for keyword <- [empty_keyword, small_keyword, large_keyword] do + assert Enum.sort(NIF.codec_multimap_atom_int64(keyword)) == Enum.sort(keyword) + assert Enum.sort(NIF.codec_multimap_atom_int64_alloc(keyword)) == Enum.sort(keyword) + assert Enum.sort(NIF.codec_unordered_multimap_atom_int64(keyword)) == Enum.sort(keyword) - assert Enum.sort(NIF.codec_unordered_multimap_atom_int64_alloc(multimap)) == - Enum.sort(multimap) + assert Enum.sort(NIF.codec_unordered_multimap_atom_int64_alloc(keyword)) == + Enum.sort(keyword) end - invalid_multimap = 10 + invalid_keyword = 10 assert_raise ArgumentError, "decode failed, expected a list", fn -> - NIF.codec_multimap_atom_int64(invalid_multimap) + NIF.codec_multimap_atom_int64(invalid_keyword) end assert_raise ArgumentError, "decode failed, expected a list", fn -> - NIF.codec_multimap_atom_int64_alloc(invalid_multimap) + NIF.codec_multimap_atom_int64_alloc(invalid_keyword) end assert_raise ArgumentError, "decode failed, expected a list", fn -> - NIF.codec_unordered_multimap_atom_int64(invalid_multimap) + NIF.codec_unordered_multimap_atom_int64(invalid_keyword) end assert_raise ArgumentError, "decode failed, expected a list", fn -> - NIF.codec_unordered_multimap_atom_int64_alloc(invalid_multimap) + NIF.codec_unordered_multimap_atom_int64_alloc(invalid_keyword) end - multimap_with_invalid_key = [{"hello", 42}] + keyword_with_invalid_key = [{"hello", 42}] assert_raise ArgumentError, "decode failed, expected an atom", fn -> - NIF.codec_multimap_atom_int64(multimap_with_invalid_key) + NIF.codec_multimap_atom_int64(keyword_with_invalid_key) end assert_raise ArgumentError, "decode failed, expected an atom", fn -> - NIF.codec_multimap_atom_int64_alloc(multimap_with_invalid_key) + NIF.codec_multimap_atom_int64_alloc(keyword_with_invalid_key) end assert_raise ArgumentError, "decode failed, expected an atom", fn -> - NIF.codec_unordered_multimap_atom_int64(multimap_with_invalid_key) + NIF.codec_unordered_multimap_atom_int64(keyword_with_invalid_key) end assert_raise ArgumentError, "decode failed, expected an atom", fn -> - NIF.codec_unordered_multimap_atom_int64_alloc(multimap_with_invalid_key) + NIF.codec_unordered_multimap_atom_int64_alloc(keyword_with_invalid_key) end - multimap_with_invalid_value = [hello: 1.0] + keyword_with_invalid_value = [hello: 1.0] assert_raise ArgumentError, "decode failed, expected an integer", fn -> - NIF.codec_multimap_atom_int64(multimap_with_invalid_value) + NIF.codec_multimap_atom_int64(keyword_with_invalid_value) end assert_raise ArgumentError, "decode failed, expected an integer", fn -> - NIF.codec_multimap_atom_int64_alloc(multimap_with_invalid_value) + NIF.codec_multimap_atom_int64_alloc(keyword_with_invalid_value) end assert_raise ArgumentError, "decode failed, expected an integer", fn -> - NIF.codec_unordered_multimap_atom_int64(multimap_with_invalid_value) + NIF.codec_unordered_multimap_atom_int64(keyword_with_invalid_value) end assert_raise ArgumentError, "decode failed, expected an integer", fn -> - NIF.codec_unordered_multimap_atom_int64_alloc(multimap_with_invalid_value) + NIF.codec_unordered_multimap_atom_int64_alloc(keyword_with_invalid_value) end end From d67597a44b0c4ab3c2d466e1c44f2f41b4d0bf11 Mon Sep 17 00:00:00 2001 From: Alexis Brodeur Date: Mon, 18 Aug 2025 17:45:12 -0400 Subject: [PATCH 4/4] Correct formatting mistake --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 52f98fa..55dcc85 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,8 @@ Fine provides implementations for the following types: | `std::vector` | x | x | `list(a)` | | `std::map` | x | x | `%{k => v}` | | `std::unordered_map` | x | x | `%{k => v}` | -| `std::multimap` | x | x | `list({k, v})` | -| `std::unordered_multimap` | x | x | `list({k, v})` | +| `std::multimap` | x | x | `list({k, v})` | +| `std::unordered_multimap` | x | x | `list({k, v})` | | `fine::ResourcePtr` | x | x | `reference` | | `T` with [struct metadata](#structs) | x | x | `%a{}` | | `fine::Ok` | x | | `{:ok, ...}` |