Skip to content

Allow using std::string_view instead of ErlNifBinary #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ Fine provides implementations for the following types:
| `bool` | x | x |
| `ErlNifPid` | x | x |
| `ErlNifBinary` | x | x |
| `std::string_view` | x | x |
| `std::string` | x | x |
| `fine::Atom` | x | x |
| `std::nullopt_t` | x | |
Expand Down Expand Up @@ -169,9 +170,15 @@ Fine provides implementations for the following types:
> talking about UTF-8 encoded strings or arbitrary binaries.
>
> However, when dealing with large binaries, it is preferable for the
> NIF to accept `ErlNifBinary` as arguments and deal with the raw data
> explicitly, which is zero-copy. That said, keep in mind that `ErlNifBinary`
> is read-only and only valid during the NIF call lifetime.
> NIF to accept `std::string_view` (or `ErlNifBinary`) as arguments and
> deal with the raw data explicitly, which is zero-copy. That said,
> keep in mind that those objects are read-only pointers to the data
> and they are valid only during the NIF call lifetime.
>
> Since Elixir binaries are not zero-terminated, the use of
> `std::string` is recommended when interfacing with C APIs, although
> this will require an additional allocation, which cannot be
> circumvented.
>
> Similarly, when returning large binaries, prefer creating the term
> with `enif_make_new_binary` and returning `fine::Term`, as shown below.
Expand Down
36 changes: 30 additions & 6 deletions include/fine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,28 @@
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <erl_nif.h>
#include <map>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <variant>
#include <vector>

#include <erl_nif.h>

#if defined(_MSVC_LANG)
#define CPP_VERSION _MSVC_LANG
#else
#define CPP_VERSION __cplusplus
#endif

#if CPP_VERSION < 201703L
#error "elixir-nx/fine only supports C++ 17 and later"
#endif

#if ERL_NIF_MAJOR_VERSION > 2 || \
(ERL_NIF_MAJOR_VERSION == 2 && ERL_NIF_MINOR_VERSION >= 17)
#define FINE_ERL_NIF_CHAR_ENCODING ERL_NIF_UTF8
Expand Down Expand Up @@ -396,11 +408,17 @@ template <> struct Decoder<ErlNifBinary> {
}
};

template <> struct Decoder<std::string_view> {
static std::string_view decode(ErlNifEnv *env, const ERL_NIF_TERM &term) {
auto binary = fine::decode<ErlNifBinary>(env, term);
return std::string_view(reinterpret_cast<const char *>(binary.data),
binary.size);
}
};

template <> struct Decoder<std::string> {
static std::string decode(ErlNifEnv *env, const ERL_NIF_TERM &term) {
auto binary = fine::decode<ErlNifBinary>(env, term);
return std::string(
{reinterpret_cast<const char *>(binary.data), binary.size});
return std::string(fine::decode<std::string_view>(env, term));
}
};

Expand Down Expand Up @@ -659,8 +677,8 @@ template <> struct Encoder<ErlNifBinary> {
}
};

template <> struct Encoder<std::string> {
static ERL_NIF_TERM encode(ErlNifEnv *env, const std::string &string) {
template <> struct Encoder<std::string_view> {
static ERL_NIF_TERM encode(ErlNifEnv *env, const std::string_view &string) {
ERL_NIF_TERM term;
auto data = enif_make_new_binary(env, string.length(), &term);
if (data == nullptr) {
Expand All @@ -671,6 +689,12 @@ template <> struct Encoder<std::string> {
}
};

template <> struct Encoder<std::string> {
static ERL_NIF_TERM encode(ErlNifEnv *env, const std::string &string) {
return fine::encode<std::string_view>(env, string);
}
};

template <> struct Encoder<Atom> {
static ERL_NIF_TERM encode(ErlNifEnv *env, const Atom &atom) {
if (atom.term) {
Expand Down
5 changes: 5 additions & 0 deletions test/c_src/finest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ ErlNifBinary codec_binary(ErlNifEnv *, ErlNifBinary term) {
}
FINE_NIF(codec_binary, 0);

std::string_view codec_string_view(ErlNifEnv *, std::string_view term) {
return term;
}
FINE_NIF(codec_string_view, 0);

std::string codec_string(ErlNifEnv *, std::string term) { return term; }
FINE_NIF(codec_string, 0);

Expand Down
1 change: 1 addition & 0 deletions test/lib/finest/nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ defmodule Finest.NIF do
def codec_bool(_term), do: err!()
def codec_pid(_term), do: err!()
def codec_binary(_term), do: err!()
def codec_string_view(_term), do: err!()
def codec_string(_term), do: err!()
def codec_atom(_term), do: err!()
def codec_nullopt(), do: err!()
Expand Down
10 changes: 10 additions & 0 deletions test/test/finest_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ defmodule FinestTest do
end
end

test "string_view" do
assert NIF.codec_string_view("hello world") == "hello world"
assert NIF.codec_string_view(<<0, 1, 2>>) == <<0, 1, 2>>
assert NIF.codec_string_view(<<>>) == <<>>

assert_raise ArgumentError, "decode failed, expected a binary", fn ->
NIF.codec_string(1)
end
end

test "string" do
assert NIF.codec_string("hello world") == "hello world"
assert NIF.codec_string(<<0, 1, 2>>) == <<0, 1, 2>>
Expand Down