From d4f0c7a7a5833f03430d6b7d446fd5a741179dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Wed, 19 Feb 2025 18:26:56 +0900 Subject: [PATCH] Move Fine to a dependency --- Makefile | 2 +- Makefile.win | 2 +- c_src/pythonx/fine.hpp | 1047 ------------------------------------- c_src/pythonx/fine.md | 443 ---------------- c_src/pythonx/pythonx.cpp | 2 +- lib/pythonx/nif.ex | 50 +- mix.exs | 6 +- mix.lock | 1 + 8 files changed, 34 insertions(+), 1519 deletions(-) delete mode 100644 c_src/pythonx/fine.hpp delete mode 100644 c_src/pythonx/fine.md diff --git a/Makefile b/Makefile index 2a41bc7..f862af6 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ NIF_PATH = $(PRIV_DIR)/libpythonx.so C_SRC = $(shell pwd)/c_src/pythonx CPPFLAGS = -shared -fPIC -std=c++17 -Wall -Wextra -Wno-unused-parameter -Wno-comment -CPPFLAGS += -I$(ERTS_INCLUDE_DIR) +CPPFLAGS += -I$(ERTS_INCLUDE_DIR) -I$(FINE_INCLUDE_DIR) ifdef DEBUG CPPFLAGS += -g diff --git a/Makefile.win b/Makefile.win index 31ac5f7..370a420 100644 --- a/Makefile.win +++ b/Makefile.win @@ -3,7 +3,7 @@ NIF_PATH=$(PRIV_DIR)\libpythonx.dll C_SRC=$(MAKEDIR)\c_src\pythonx CPPFLAGS=/LD /std:c++17 /W4 /wd4100 /wd4458 /O2 /EHsc -CPPFLAGS=$(CPPFLAGS) /I"$(ERTS_INCLUDE_DIR)" +CPPFLAGS=$(CPPFLAGS) /I"$(ERTS_INCLUDE_DIR)" /I"$(FINE_INCLUDE_DIR)" SOURCES=$(C_SRC)\*.cpp HEADERS=$(C_SRC)\*.hpp diff --git a/c_src/pythonx/fine.hpp b/c_src/pythonx/fine.hpp deleted file mode 100644 index bbb3ace..0000000 --- a/c_src/pythonx/fine.hpp +++ /dev/null @@ -1,1047 +0,0 @@ -#ifndef FINE_HPP -#define FINE_HPP -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace fine { - -// Forward declarations - -template T decode(ErlNifEnv *env, const ERL_NIF_TERM &term); -template ERL_NIF_TERM encode(ErlNifEnv *env, const T &value); - -template struct Decoder; -template struct Encoder; - -namespace __private__ { -std::vector &get_erl_nif_funcs(); -int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info); -} // namespace __private__ - -// Definitions - -namespace __private__ { -inline ERL_NIF_TERM make_atom(ErlNifEnv *env, const char *msg) { - ERL_NIF_TERM atom; - if (enif_make_existing_atom(env, msg, &atom, ERL_NIF_LATIN1)) { - return atom; - } else { - return enif_make_atom(env, msg); - } -} -} // namespace __private__ - -// A representation of an atom term. -class Atom { -public: - Atom(std::string name) : name(name), term(std::nullopt) { - if (!Atom::initialized) { - Atom::atoms.push_back(this); - } - } - - std::string to_string() const { return this->name; } - - bool operator==(const Atom &other) const { return this->name == other.name; } - - bool operator==(const char *other) const { return this->name == other; } - - bool operator<(const Atom &other) const { return this->name < other.name; } - -private: - static void init_atoms(ErlNifEnv *env) { - for (auto atom : Atom::atoms) { - atom->term = fine::__private__::make_atom(env, atom->name.c_str()); - } - - Atom::atoms.clear(); - Atom::initialized = true; - } - - friend struct Encoder; - - friend int __private__::load(ErlNifEnv *env, void **priv_data, - ERL_NIF_TERM load_info); - - // We accumulate all globally defined atom objects and create the - // terms upfront as part of init (called from the NIF load callback). - inline static std::vector atoms = {}; - inline static bool initialized = false; - - std::string name; - std::optional term; -}; - -namespace __private__::atoms { -inline auto ok = Atom("ok"); -inline auto error = Atom("error"); -inline auto nil = Atom("nil"); -inline auto true_ = Atom("true"); -inline auto false_ = Atom("false"); -inline auto __struct__ = Atom("__struct__"); -inline auto __exception__ = Atom("__exception__"); -inline auto message = Atom("message"); -inline auto ElixirArgumentError = Atom("Elixir.ArgumentError"); -inline auto ElixirRuntimeError = Atom("Elixir.RuntimeError"); -} // namespace __private__::atoms - -// Represents any term. -// -// This type should be used instead of ERL_NIF_TERM in the NIF signature -// and encode/decode APIs. -class Term { - // ERL_NIF_TERM is typedef-ed as an integer type. At the moment of - // writing it is unsigned long int. This means that we cannot define - // separate Decoder and Decoder, - // (which could potentially match uint64_t). The same applies to - // Encoder. For this reason we need a wrapper object for terms, so - // they can be unambiguously distinguished. We define implicit - // bidirectional conversion between Term and ERL_NIF_TERM, so that - // Term is effectively just a typing tag for decoder and encoder - // (and the nif signature). - -public: - Term(const ERL_NIF_TERM &term) : term(term) {} - - operator ERL_NIF_TERM() const { return this->term; } - -private: - ERL_NIF_TERM term; -}; - -// Represents a `:ok` tagged tuple, useful as a NIF result. -template class Ok { -public: - Ok(const Args &...items) : items(items...) {} - -private: - friend struct Encoder>; - - std::tuple items; -}; - -// Represents a `:error` tagged tuple, useful as a NIF result. -template class Error { -public: - Error(const Args &...items) : items(items...) {} - -private: - friend struct Encoder>; - - std::tuple items; -}; - -namespace __private__ { -template struct ResourceWrapper { - T resource; - bool initialized; - - static void dtor(ErlNifEnv *env, void *ptr) { - auto resource_wrapper = reinterpret_cast *>(ptr); - - if (resource_wrapper->initialized) { - if constexpr (has_destructor::value) { - resource_wrapper->resource.destructor(env); - } - resource_wrapper->resource.~T(); - } - } - - template - struct has_destructor : std::false_type {}; - - template - struct has_destructor< - U, - typename std::enable_if().destructor(std::declval())), - void>::value>::type> : std::true_type {}; -}; -} // namespace __private__ - -// A smart pointer that retains ownership of a resource object. -template class ResourcePtr { - // For more context see [1] and [2]. - // - // [1]: https://stackoverflow.com/a/3279550 - // [2]: https://stackoverflow.com/a/5695855 - -public: - // Make default constructor public, so that classes with ResourcePtr - // field can also have default constructor. - ResourcePtr() : ptr(nullptr) {} - - ResourcePtr(const ResourcePtr &other) : ptr(other.ptr) { - if (this->ptr != nullptr) { - enif_keep_resource(reinterpret_cast(this->ptr)); - } - } - - ResourcePtr(ResourcePtr &&other) : ResourcePtr() { swap(other, *this); } - - ~ResourcePtr() { - if (this->ptr != nullptr) { - enif_release_resource(reinterpret_cast(this->ptr)); - } - } - - ResourcePtr &operator=(ResourcePtr other) { - swap(*this, other); - return *this; - } - - T &operator*() const { return this->ptr->resource; } - - T *operator->() const { return &this->ptr->resource; } - - T *get() const { return &this->ptr->resource; } - - friend void swap(ResourcePtr &left, ResourcePtr &right) { - using std::swap; - swap(left.ptr, right.ptr); - } - -private: - // This constructor assumes the pointer is already accounted for in - // the resource reference count. Since it is private, we guarantee - // this in all the callers. - ResourcePtr(__private__::ResourceWrapper *ptr) : ptr(ptr) {} - - // Friend functions that use the resource_type static member or the - // private constructor. - - template - friend ResourcePtr make_resource(Args &&...args); - - friend class Registration; - - friend struct Decoder>; - - inline static ErlNifResourceType *resource_type = nullptr; - - __private__::ResourceWrapper *ptr; -}; - -// Allocates a new resource object, invoking its constructor with the -// given arguments. -template -ResourcePtr make_resource(Args &&...args) { - auto type = ResourcePtr::resource_type; - - if (type == nullptr) { - throw std::runtime_error( - "calling make_resource with unexpected type. Make sure" - " to register your resource type with the FINE_RESOURCE macro"); - } - - void *allocation_ptr = - enif_alloc_resource(type, sizeof(__private__::ResourceWrapper)); - - auto resource_wrapper = - reinterpret_cast<__private__::ResourceWrapper *>(allocation_ptr); - - // We create ResourcePtr right away, to make sure the resource is - // properly released in case the constructor below throws - auto resource = ResourcePtr(resource_wrapper); - - // We use a wrapper struct with an extra field to track if the - // resource has actually been initialized. This way if the constructor - // below throws, we can skip the destructor calls in the Erlang dtor - resource_wrapper->initialized = false; - - // Invoke the constructor with prefect forwarding to initialize the - // object at the VM-allocated memory - new (&resource_wrapper->resource) T(std::forward(args)...); - - resource_wrapper->initialized = true; - - return resource; -} - -// Creates a binary term pointing to the given buffer. -// -// The buffer is managed by the resource object and should be deallocated -// once the resource is destroyed. -template -Term make_resource_binary(ErlNifEnv *env, ResourcePtr resource, - const char *data, size_t size) { - return enif_make_resource_binary( - env, reinterpret_cast(resource.get()), data, size); -} - -// Decodes the given Erlang term as a value of the specified type. -// -// The given type must have a specialized Decoder implementation. -template T decode(ErlNifEnv *env, const ERL_NIF_TERM &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); -} - -// We want decode to return the value, and since the argument types -// are always the same, we need template specialization, so that the -// caller can explicitly specify the desired type. However, in order -// to implement decode for a type such as std::vector we need -// partial specialization, and that is not supported for functions. -// To solve this, we specialize a struct instead and have the decode -// logic in a static member function. -// -// In case of encode, the argument type differs, so we could use -// function overloading. That said, we pick struct specialization as -// well for consistency with decode. This approach also prevents from -// implicit argument conversion, which is arguably good in this case, -// as it makes the encoding explicit. - -template struct Decoder {}; - -template struct Encoder {}; - -template <> struct Decoder { - static Term decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - return Term(term); - } -}; - -template <> struct Decoder { - static int64_t decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - int64_t integer; - if (!enif_get_int64(env, term, - reinterpret_cast(&integer))) { - throw std::invalid_argument("decode failed, expected an integer"); - } - return integer; - } -}; - -template <> struct Decoder { - static uint64_t decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - uint64_t integer; - if (!enif_get_uint64(env, term, - reinterpret_cast(&integer))) { - throw std::invalid_argument( - "decode failed, expected an unsigned integer"); - } - return integer; - } -}; - -template <> struct Decoder { - static double decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - double number; - if (!enif_get_double(env, term, &number)) { - throw std::invalid_argument("decode failed, expected a float"); - } - return number; - } -}; - -template <> struct Decoder { - static bool decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - char atom_string[6]; - auto length = enif_get_atom(env, term, atom_string, 6, ERL_NIF_LATIN1); - - if (length == 5 && strcmp(atom_string, "true") == 0) { - return true; - } - - if (length == 6 && strcmp(atom_string, "false") == 0) { - return false; - } - - throw std::invalid_argument("decode failed, expected a boolean"); - } -}; - -template <> struct Decoder { - static ErlNifPid decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - ErlNifPid pid; - if (!enif_get_local_pid(env, term, &pid)) { - throw std::invalid_argument("decode failed, expected a local pid"); - } - return pid; - } -}; - -template <> struct Decoder { - static ErlNifBinary decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - ErlNifBinary binary; - if (!enif_inspect_binary(env, term, &binary)) { - throw std::invalid_argument("decode failed, expected a binary"); - } - return binary; - } -}; - -template <> struct Decoder { - static std::string decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - auto binary = fine::decode(env, term); - return std::string( - {reinterpret_cast(binary.data), binary.size}); - } -}; - -template <> struct Decoder { - static Atom decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - unsigned int length; - if (!enif_get_atom_length(env, term, &length, ERL_NIF_LATIN1)) { - throw std::invalid_argument("decode failed, expected an atom"); - } - - auto buffer = std::make_unique(length + 1); - - // Note that enif_get_atom writes the NULL byte at the end - if (!enif_get_atom(env, term, buffer.get(), length + 1, ERL_NIF_LATIN1)) { - throw std::invalid_argument("decode failed, expected an atom"); - } - - return Atom(std::string(buffer.get(), length)); - } -}; - -template struct Decoder> { - static std::optional decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - char atom_string[4]; - if (enif_get_atom(env, term, atom_string, 4, ERL_NIF_LATIN1) == 4) { - if (strcmp(atom_string, "nil") == 0) { - return std::nullopt; - } - } - - return fine::decode(env, term); - } -}; - -template struct Decoder> { - static std::variant decode(ErlNifEnv *env, - const ERL_NIF_TERM &term) { - return do_decode(env, term); - } - -private: - template - static std::variant do_decode(ErlNifEnv *env, - const ERL_NIF_TERM &term) { - try { - return fine::decode(env, term); - } catch (std::invalid_argument) { - if constexpr (sizeof...(Rest) > 0) { - return do_decode(env, term); - } else { - throw std::invalid_argument( - "decode failed, none of the variant types could be decoded"); - } - } - } -}; - -template struct Decoder> { - static std::tuple decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - constexpr auto expected_size = sizeof...(Args); - - 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 != expected_size) { - throw std::invalid_argument("decode failed, expected tuple to have " + - std::to_string(expected_size) + - "elements, but had " + std::to_string(size)); - } - - return do_decode(env, terms, std::make_index_sequence()); - } - -private: - template - static std::tuple do_decode(ErlNifEnv *env, - const ERL_NIF_TERM *terms, - std::index_sequence) { - return std::make_tuple(fine::decode(env, terms[Indices])...); - } -}; - -template struct Decoder> { - static std::vector 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::vector vector; - vector.reserve(length); - - auto list = term; - - ERL_NIF_TERM head, tail; - while (enif_get_list_cell(env, list, &head, &tail)) { - auto elem = fine::decode(env, head); - vector.push_back(elem); - list = tail; - } - - return vector; - } -}; - -template struct Decoder> { - static std::map decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - auto map = std::map(); - - ERL_NIF_TERM key, value; - ErlNifMapIterator iter; - if (!enif_map_iterator_create(env, term, &iter, - ERL_NIF_MAP_ITERATOR_FIRST)) { - throw std::invalid_argument("decode failed, expeted a map"); - } - - // Define RAII cleanup for the iterator - auto cleanup = IterCleanup{env, iter}; - - while (enif_map_iterator_get_pair(env, &iter, &key, &value)) { - map[fine::decode(env, key)] = fine::decode(env, 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; - auto type = ResourcePtr::resource_type; - - if (!enif_get_resource(env, term, type, &ptr)) { - throw std::invalid_argument( - "decode failed, expected a reference to resource"); - } - - enif_keep_resource(ptr); - - return ResourcePtr( - reinterpret_cast<__private__::ResourceWrapper *>(ptr)); - } -}; - -template -struct Decoder> { - static T decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - ERL_NIF_TERM struct_value; - if (!enif_get_map_value(env, term, - encode(env, __private__::atoms::__struct__), - &struct_value)) { - throw std::invalid_argument("decode failed, expected a struct"); - } - - // Make sure __struct__ matches - const auto &struct_atom = *T::module; - if (enif_compare(struct_value, encode(env, struct_atom)) != 0) { - throw std::invalid_argument("decode failed, expected a " + - struct_atom.to_string() + " struct"); - } - - T ex_struct; - - constexpr auto fields = T::fields(); - - std::apply( - [&](auto... field) { - (set_field(env, term, ex_struct, std::get<0>(field), - std::get<1>(field)), - ...); - }, - fields); - - return ex_struct; - } - -private: - template - static void set_field(ErlNifEnv *env, ERL_NIF_TERM term, T &ex_struct, - U T::*field_ptr, const Atom *atom) { - ERL_NIF_TERM value; - if (!enif_get_map_value(env, term, encode(env, *atom), &value)) { - throw std::invalid_argument( - "decode failed, expected the struct to have " + atom->to_string() + - " field"); - } - - ex_struct.*(field_ptr) = fine::decode(env, value); - } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const Term &term) { return term; } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const int64_t &integer) { - return enif_make_int64(env, integer); - } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const uint64_t &integer) { - return enif_make_uint64(env, integer); - } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const double &number) { - return enif_make_double(env, number); - } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const bool &boolean) { - return fine::encode(env, boolean ? __private__::atoms::true_ - : __private__::atoms::false_); - } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const ErlNifPid &pid) { - return enif_make_pid(env, &pid); - } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const ErlNifBinary &binary) { - return enif_make_binary(env, const_cast(&binary)); - } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const std::string &string) { - ERL_NIF_TERM term; - auto data = enif_make_new_binary(env, string.length(), &term); - if (data == nullptr) { - throw std::runtime_error("encode failed, failed to allocate new binary"); - } - memcpy(data, string.data(), string.length()); - return term; - } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const Atom &atom) { - if (atom.term) { - return atom.term.value(); - } else { - return fine::__private__::make_atom(env, atom.name.c_str()); - } - } -}; - -template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const std::nullopt_t &nullopt) { - return fine::encode(env, __private__::atoms::nil); - } -}; - -template struct Encoder> { - static ERL_NIF_TERM encode(ErlNifEnv *env, const std::optional &optional) { - if (optional) { - return fine::encode(env, optional.value()); - } else { - return fine::encode(env, __private__::atoms::nil); - } - } -}; - -template struct Encoder> { - static ERL_NIF_TERM encode(ErlNifEnv *env, - const std::variant &variant) { - return do_encode(env, variant); - } - -private: - template - static ERL_NIF_TERM do_encode(ErlNifEnv *env, - const std::variant &variant) { - if (auto value = std::get_if(&variant)) { - return fine::encode(env, *value); - } - - if constexpr (sizeof...(Rest) > 0) { - return do_encode(env, variant); - } else { - throw std::runtime_error("unreachable"); - } - } -}; - -template struct Encoder> { - static ERL_NIF_TERM encode(ErlNifEnv *env, const std::tuple &tuple) { - return do_encode(env, tuple, std::make_index_sequence()); - } - -private: - template - static ERL_NIF_TERM do_encode(ErlNifEnv *env, - const std::tuple &tuple, - std::index_sequence) { - constexpr auto size = sizeof...(Args); - return enif_make_tuple(env, size, - fine::encode(env, std::get(tuple))...); - } -}; - -template struct Encoder> { - static ERL_NIF_TERM encode(ErlNifEnv *env, const std::vector &vector) { - auto terms = std::vector(); - terms.reserve(vector.size()); - - for (const auto &item : vector) { - terms.push_back(fine::encode(env, item)); - } - - 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::map &map) { - auto keys = std::vector(); - auto values = std::vector(); - - for (const auto &[key, value] : map) { - keys.push_back(fine::encode(env, key)); - values.push_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 ResourcePtr &resource) { - return enif_make_resource(env, reinterpret_cast(resource.get())); - } -}; - -template -struct Encoder> { - static ERL_NIF_TERM encode(ErlNifEnv *env, const T &ex_struct) { - const auto &struct_atom = *T::module; - constexpr auto fields = T::fields(); - constexpr auto is_exception = get_is_exception(); - - constexpr auto num_fields = std::tuple_size::value; - constexpr auto num_extra_fields = is_exception ? 2 : 1; - - ERL_NIF_TERM keys[num_extra_fields + num_fields]; - ERL_NIF_TERM values[num_extra_fields + num_fields]; - - keys[0] = fine::encode(env, __private__::atoms::__struct__); - values[0] = fine::encode(env, struct_atom); - - if constexpr (is_exception) { - keys[1] = fine::encode(env, __private__::atoms::__exception__); - values[1] = fine::encode(env, __private__::atoms::true_); - } - - put_key_values(env, ex_struct, keys + num_extra_fields, - values + num_extra_fields, - std::make_index_sequence()); - - ERL_NIF_TERM map; - if (!enif_make_map_from_arrays(env, keys, values, - num_extra_fields + num_fields, &map)) { - throw std::runtime_error("encode failed, failed to make a map"); - } - - return map; - } - -private: - template - static void put_key_values(ErlNifEnv *env, const T &ex_struct, - ERL_NIF_TERM keys[], ERL_NIF_TERM values[], - std::index_sequence) { - constexpr auto fields = T::fields(); - - std::apply( - [&](auto... field) { - ((keys[Indices] = fine::encode(env, *std::get<1>(field)), - values[Indices] = - fine::encode(env, ex_struct.*(std::get<0>(field)))), - ...); - }, - fields); - } - - static constexpr bool get_is_exception() { - if constexpr (has_is_exception::value) { - return T::is_exception; - } else { - return false; - } - } - - template - struct has_is_exception : std::false_type {}; - - template - struct has_is_exception> - : std::true_type {}; -}; - -template struct Encoder> { - static ERL_NIF_TERM encode(ErlNifEnv *env, const Ok &ok) { - auto tag = __private__::atoms::ok; - - if constexpr (sizeof...(Args) > 0) { - return fine::encode(env, std::tuple_cat(std::tuple(tag), ok.items)); - } else { - return fine::encode(env, tag); - } - } -}; - -template struct Encoder> { - static ERL_NIF_TERM encode(ErlNifEnv *env, const Error &error) { - auto tag = __private__::atoms::error; - - if constexpr (sizeof...(Args) > 0) { - return fine::encode(env, std::tuple_cat(std::tuple(tag), error.items)); - } else { - return fine::encode(env, tag); - } - } -}; - -namespace __private__ { -class ExceptionError : public std::exception { -public: - ERL_NIF_TERM reason; - - ExceptionError(ERL_NIF_TERM reason) : reason(reason) {} - const char *what() const noexcept { return "erlang exception raised"; } -}; -} // namespace __private__ - -// Raises an Elixir exception with the given value as reason. -template void raise(ErlNifEnv *env, const T &value) { - auto term = encode(env, value); - throw __private__::ExceptionError(term); -} - -// Mechanism for accumulating information via static object definitions. -class Registration { -public: - template - static Registration register_resource(const char *name) { - Registration::resources.push_back({&fine::ResourcePtr::resource_type, - name, - __private__::ResourceWrapper::dtor}); - return {}; - } - - static Registration register_nif(ErlNifFunc erl_nif_func) { - Registration::erl_nif_funcs.push_back(erl_nif_func); - return {}; - } - -private: - static bool init_resources(ErlNifEnv *env) { - for (const auto &[resource_type_ptr, name, dtor] : - Registration::resources) { - auto flags = ERL_NIF_RT_CREATE; - auto type = enif_open_resource_type(env, NULL, name, dtor, flags, NULL); - - if (type) { - *resource_type_ptr = type; - } else { - return false; - } - } - - Registration::resources.clear(); - - return true; - } - - friend std::vector &__private__::get_erl_nif_funcs(); - - friend int __private__::load(ErlNifEnv *env, void **priv_data, - ERL_NIF_TERM load_info); - - inline static std::vector> - resources = {}; - - inline static std::vector erl_nif_funcs = {}; -}; - -// NIF definitions - -namespace __private__ { -inline ERL_NIF_TERM raise_error_with_message(ErlNifEnv *env, Atom module, - std::string message) { - ERL_NIF_TERM keys[3] = {fine::encode(env, __private__::atoms::__struct__), - fine::encode(env, __private__::atoms::__exception__), - fine::encode(env, __private__::atoms::message)}; - ERL_NIF_TERM values[3] = { - fine::encode(env, module), - fine::encode(env, __private__::atoms::true_), - fine::encode(env, message), - }; - - ERL_NIF_TERM map; - if (!enif_make_map_from_arrays(env, keys, values, 3, &map)) { - return enif_raise_exception(env, encode(env, message)); - } - - return enif_raise_exception(env, map); -} - -template -ERL_NIF_TERM nif_impl(ErlNifEnv *env, const ERL_NIF_TERM argv[], - Return (*fun)(ErlNifEnv *, Args...), - std::index_sequence) { - try { - auto result = fun(env, decode(env, argv[Indices])...); - return encode(env, result); - } catch (const ExceptionError &error) { - return enif_raise_exception(env, error.reason); - } catch (const std::invalid_argument &error) { - return raise_error_with_message( - env, __private__::atoms::ElixirArgumentError, error.what()); - } catch (const std::runtime_error &error) { - return raise_error_with_message(env, __private__::atoms::ElixirRuntimeError, - error.what()); - } catch (...) { - return raise_error_with_message(env, __private__::atoms::ElixirRuntimeError, - "unknown exception thrown within NIF"); - } -} -} // namespace __private__ - -template -ERL_NIF_TERM nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[], - Return (*fun)(ErlNifEnv *, Args...)) { - const auto num_args = sizeof...(Args); - - if (num_args != argc) { - return enif_raise_exception( - env, encode(env, std::string("wrong number of arguments"))); - } - - return __private__::nif_impl(env, argv, fun, - std::make_index_sequence()); -} - -template -constexpr unsigned int nif_arity(Ret (*)(Args...)) { - return sizeof...(Args) - 1; -} - -namespace __private__ { -inline std::vector &get_erl_nif_funcs() { - return Registration::erl_nif_funcs; -} - -inline int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info) { - Atom::init_atoms(env); - - if (!Registration::init_resources(env)) { - return -1; - } - - return 0; -} -} // namespace __private__ - -// Macros - -#define FINE_NIF(name, flags) \ - static ERL_NIF_TERM name##_nif(ErlNifEnv *env, int argc, \ - const ERL_NIF_TERM argv[]) { \ - return fine::nif(env, argc, argv, name); \ - } \ - auto __nif_registration_##name = fine::Registration::register_nif( \ - {#name, fine::nif_arity(name), name##_nif, flags}); \ - static_assert(true, "require a semicolon after the macro") - -// Note that we use static, in case FINE_REASOURCE is used in another -// translation unit on the same line. - -#define FINE_RESOURCE(class_name) \ - static auto __FINE_CONCAT__(__resource_registration_, __LINE__) = \ - fine::Registration::register_resource(#class_name); \ - static_assert(true, "require a semicolon after the macro") - -// An extra level of indirection is necessary to make sure __LINE__ -// is expanded before concatenation. -#define __FINE_CONCAT__(a, b) __FINE_CONCAT_IMPL__(a, b) -#define __FINE_CONCAT_IMPL__(a, b) a##b - -// This is a modified version of ERL_NIF_INIT that points to the -// registered NIF functions and also sets the load callback. - -#define FINE_INIT(name) \ - ERL_NIF_INIT_PROLOGUE \ - ERL_NIF_INIT_GLOB \ - ERL_NIF_INIT_DECL(NAME); \ - ERL_NIF_INIT_DECL(NAME) { \ - auto &nif_funcs = fine::__private__::get_erl_nif_funcs(); \ - auto num_funcs = static_cast(nif_funcs.size()); \ - auto funcs = nif_funcs.data(); \ - auto load = fine::__private__::load; \ - static ErlNifEntry entry = {ERL_NIF_MAJOR_VERSION, \ - ERL_NIF_MINOR_VERSION, \ - name, \ - num_funcs, \ - funcs, \ - load, \ - NULL, \ - NULL, \ - NULL, \ - ERL_NIF_VM_VARIANT, \ - 1, \ - sizeof(ErlNifResourceTypeInit), \ - ERL_NIF_MIN_ERTS_VERSION}; \ - ERL_NIF_INIT_BODY; \ - return &entry; \ - } \ - ERL_NIF_INIT_EPILOGUE \ - static_assert(true, "require a semicolon after the macro") - -} // namespace fine - -#endif diff --git a/c_src/pythonx/fine.md b/c_src/pythonx/fine.md deleted file mode 100644 index a6517e7..0000000 --- a/c_src/pythonx/fine.md +++ /dev/null @@ -1,443 +0,0 @@ -# Fine - -Fine is a C++ library streamlining Erlang NIFs implementation, focused -on Elixir. - -Erlang provides C API for implementing native functions -([`erl_nif`](https://www.erlang.org/doc/apps/erts/erl_nif.html)). -Fine is not a replacement of the C API, instead it is designed as a -complementary API, enhancing the developer experience when implementing -NIFs in C++. - -## Features - -- Automatic encoding/decoding of NIF arguments and return value, - inferred from function signatures. - -- Smart pointer enabling safe management of resource objects. - -- Registering NIFs and resource types via simple annotations. - -- Support for encoding/decoding Elixir structs based on compile time - metadata. - -- Propagating C++ exceptions as Elixir exceptions, with support for - raising custom Elixir exceptions. - -- Creating all static atoms at load time. - -## Motivation - -Some projects make extensive use of NIFs, where using the C API results -in a lot of boilerplate code and a set of ad-hoc helper functions that -get copied from project to project. The main idea behind Fine is to -reduce the friction of getting from Elixir to C++ and vice versa, so -that developers can focus on writing the actual native code. - -## Requirements - -Currently Fine requires C++17. The supported compilers include GCC, -Clang and MSVC. - -## Usage - -A minimal NIF adding two numbers can be implemented like so: - -```cpp -#include - -int64_t add(ErlNifEnv *env, int64_t x, int64_t y) { - return x + y; -} - -FINE_NIF(add, 0); - -FINE_INIT("Elixir.MyLib.NIF"); -``` - -### Encoding/Decoding - -Terms are automatically encoded and decoded at the NIF boundary based -on the function signature. In some cases, you may also want to invoke -encode/decode directly: - -```cpp -// Encode -auto message = std::string("hello world"); -auto term = fine::encode(env, message); - -// Decode -auto message = fine::decode(env, term); -``` - -Fine provides implementations for the following types: - -| Type | Encoder | Decoder | -| ------------------------------------ | ------- | ------- | -| `fine::Term` | x | x | -| `int64_t` | x | x | -| `uint64_t` | x | x | -| `double` | x | x | -| `bool` | x | x | -| `ErlNifPid` | x | x | -| `ErlNifBinary` | x | x | -| `std::string` | x | x | -| `fine::Atom` | x | x | -| `std::nullopt_t` | x | | -| `std::optional` | x | x | -| `std::variant` | x | x | -| `std::tuple` | x | x | -| `std::vector` | x | x | -| `std::map` | x | x | -| `fine::ResourcePtr` | x | x | -| `T` with [struct metadata](#structs) | x | x | -| `fine::Ok` | x | | -| `fine::Error` | x | | - -You can extend encoding/decoding to work on custom types by defining -the following specializations: - -```cpp -// Note that the specialization must be defined in the `fine` namespace. -namespace fine { - template <> struct Decoder { - static MyType decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - // ... - } - }; - - template <> struct Encoder { - static ERL_NIF_TERM encode(ErlNifEnv *env, const MyType &value) { - // ... - } - }; -} -``` - -> #### ERL_NIF_TERM {: .warning} -> -> In some cases, you may want to define a NIF that accepts or returns -> a term and effectively skip the encoding/decoding. However, the NIF -> C API defines `ERL_NIF_TERM` as an alias for an integer type, which -> may introduce an ambiguity for encoding/decoding. For this reason -> Fine provides a wrapper type `fine::Term` and it should be used in -> the NIF signature in those cases. `fine::Term` defines implicit -> conversion to and from `ERL_NIF_TERM`, so it can be used with all -> `enif_*` functions with no changes. - -> #### Binaries {: .info} -> -> `std::string` is just a sequence of `char`s and therefore it makes -> for a good counterpart for Elixir binaries, regardless if we are -> 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. -> -> Similarly, when returning large binaries, prefer creating the term -> with `enif_make_new_binary` and returning `fine::Term`, as shown below. -> -> ```cpp -> fine::Term read_data(ErlNifEnv *env) { -> const char *buffer = ...; -> uint64_t size = ...; -> -> ERL_NIF_TERM binary_term; -> auto binary_data = enif_make_new_binary(env, size, &binary_term); -> memcpy(binary_data, buffer, size); -> -> return binary_term; -> } -> ``` -> -> You can also return `ErlNifBinary` allocated with `enif_alloc_binary`, -> but keep in mind that returning the binary converts it to term, which -> in turn transfers the ownership, so you should not use that `ErlNifBinary` -> after the NIF finishes. - - -### Resource objects - -Resource objects is a mechanism for passing pointers to C++ data -structures to and from NIFs, and around your Elixir code. On the Elixir -side those pointer surface as reference terms (`#Reference<...>`). - -Fine provides a construction function `fine::make_resource(...)`, -similar to `std::make_unique` and `std::make_shared` available in the -C++ standard library. This function creates a new object of the type -`T`, invoking its constructor with the given arguments and it returns -a smart pointer of type `fine::ResourcePtr`. The pointer is -automatically decoded and encoded as a reference term. It can also be -passed around C++ code, automatically managing the reference count -(similarly to `std::shared_ptr`). - -You need to indicate that a given class can be used as a resource type -via the `FINE_RESOURCE` macro. - -```cpp -#include - -class Generator { -public: - Generator(uint64_t seed) { /* ... */ } - int64_t random_integer() { /* ... */ } - // ... -}; - -FINE_RESOURCE(Generator); - -fine::ResourcePtr create_generator(ErlNifEnv *env, uint64_t seed) { - return fine::make_resource(seed); -} - -FINE_NIF(create_generator, 0); - -int64_t random_integer(ErlNifEnv *env, fine::ResourcePtr generator) { - return generator->random_integer(); -} - -FINE_NIF(random_integer, 0); - -FINE_INIT("Elixir.MyLib.NIF"); -``` - -Once neither Elixir nor C++ holds a reference to the resource object, -it gets destroyed. By default only the `T` type destructor is called. -However, in some cases you may want to interact with NIF APIs as part -of the destructor. In that case, you can implement a `destructor` -callback on `T`, which receives the relevant `ErlNifEnv`: - -```cpp -class Generator { - // ... - - void destructor(ErlNifEnv *env) { - // Example: send a message to some process using env - } -}; -``` - -If defined, the `destructor` callback is called first, and then the -`T` destructor is called as usual. - -Oftentimes NIFs deal with classes from third-party packages, in which -case, you may not control how the objects are created and you cannot -add callbacks such as `destructor` to the implementation. If you run -into any of these limitations, you can define your own wrapper class, -holding an object of the third-party class and implementing the desired -construction/destruction on top. - -You can use `fine::make_resource_binary(env, resource, data, size)` -to create a binary term with memory managed by the resource. - -### Structs - -Elixir structs can be passed to and from NIFs. To do that, you need to -define a corresponding C++ class that includes metadata fields used -for automatic encoding and decoding. The metadata consists of: - -- `module` - the Elixir struct name as an atom reference - -- `fields` - a mapping between Elixir struct and C++ class fields - -- `is_exception` (optional) - when defined as true, indicates the - Elixir struct is an exception - -For example, given an Elixir struct `%MyLib.Point{x: integer, y: integer}`, -you could operate on it in the NIF, like this: - -```cpp -#include - -namespace atoms { - auto ElixirMyLibPoint = fine::Atom("Elixir.MyLib.Point"); - auto x = fine::Atom("x"); - auto y = fine::Atom("y"); -} - -struct ExPoint { - int64_t x; - int64_t y; - - static constexpr auto module = &atoms::ElixirMyLibPoint; - - static constexpr auto fields() { - return std::make_tuple(std::make_tuple(&ExPoint::x, &atoms::x), - std::make_tuple(&ExPoint::y, &atoms::y)); - } -}; - -ExPoint point_reflection(ErlNifEnv *env, ExPoint point) { - return ExPoint{-point.x, -point.y}; -} - -FINE_NIF(point_reflection, 0); - -FINE_INIT("Elixir.MyLib.NIF"); -``` - -Structs can be particularly convenient when using NIF resource objects. -When working with resources, it is common to have an Elixir struct -corresponding to the resource. In the previous `Generator` example, -you may define an Elixir struct such as `%MyLib.Generator{resource: reference}`. -Instead of passing and returning the reference from the NIF, you can -pass and return the struct itself: - -```cpp -#include - -class Generator { -public: - Generator(uint64_t seed) { /* ... */ } - int64_t random_integer() { /* ... */ } - // ... -}; - -namespace atoms { - auto ElixirMyLibGenerator = fine::Atom("Elixir.MyLib.Generator"); - auto resource = fine::Atom("resource"); -} - -struct ExGenerator { - fine::ResourcePtr resource; - - static constexpr auto module = &atoms::ElixirMyLibPoint; - - static constexpr auto fields() { - return std::make_tuple( - std::make_tuple(&ExGenerator::resource, &atoms::resource), - ); - } -}; - -ExGenerator create_generator(ErlNifEnv *env, uint64_t seed) { - return ExGenerator{fine::make_resource(seed)}; -} - -FINE_NIF(create_generator, 0); - -int64_t random_integer(ErlNifEnv *env, ExGenerator ex_generator) { - return ex_generator.resource->random_integer(); -} - -FINE_NIF(random_integer, 0); - -FINE_INIT("Elixir.MyLib.NIF"); -``` - -### Exceptions - -All C++ exceptions thrown within the NIF are caught and raised as -Elixir exceptions. - -```cpp -throw std::runtime_error("something went wrong"); -// ** (RuntimeError) something went wrong - -throw std::invalid_argument("expected x, got y"); -// ** (ArgumentError) expected x, got y - -throw OtherError(...); -// ** (RuntimeError) unknown exception -``` - -Additionally, you can use `fine::raise(env, value)` to raise exception, -where `value` is encoded into a term and used as the exception. This -is not particularly useful with regular types, however it can be used -to raise custom Elixir exceptions. Consider the following exception: - -```elixir -defmodule MyLib.MyError do - defexception [:data] - - @impl true - def message(error) do - "got error with data #{data}" - end -end -``` - -First, we need to implement the corresponding C++ class: - -```cpp -namespace atoms { - auto ElixirMyLibMyError = fine::Atom("Elixir.MyLib.MyError"); - auto data = fine::Atom("data"); -} - -struct ExMyError { - int64_t data; - - static constexpr auto module = &atoms::ElixirMyLibMyError; - - static constexpr auto fields() { - return std::make_tuple( - std::make_tuple(&ExMyError::data, &atoms::data)); - } - - static constexpr auto is_exception = true; -}; -``` - -Then, we can raise it anywhere in a NIF: - -```cpp -fine::raise(env, ExMyError{42}) -// ** (MyLib.MyError) got error with data 42 -``` - -### Atoms - -It is preferable to define atoms as static variables, this way the -corresponding terms are created once, at NIF load time. - -```cpp -namespace atoms { - auto hello_world = fine::Atom("hello_world"); -} -``` - -### Result types - -When it comes to NIFs, errors often indicate unexpected failures and -raising an exception makes sense, however you may also want to handle -certain errors gracefully by returning `:ok`/`:error` tuples, similarly -to usual Elixir functions. Fine provides `Ok<...>` and `Error<...>` -types for this purpose. - -```cpp -fine::Ok<>() -// :ok - -fine::Ok(1) -// {:ok, 1} - -fine::Error<>() -// :error - -fine::Error("something went wrong") -// {:error, "something went wrong"} -``` - -You can use `std::variant` to express a union of possible result types -a NIF may return: - -```cpp -std::variant, fine::Error> find_meaning(ErlNifEnv *env) { - if (...) { - return fine::Error("something went wrong"); - } - - return fine::Ok(42); -} -``` - -Note that if you use a particular union frequently, it may be convenient -to define a type alias with `using`/`typedef` to keep signatures brief. - -## Prior work - -Some of the ideas have been previously explored by Serge Aleynikov (@saleyn) -and Daniel Goertzen (@goertzenator) ([source](https://github.com/saleyn/nifpp)). diff --git a/c_src/pythonx/pythonx.cpp b/c_src/pythonx/pythonx.cpp index 6be76c4..59f6e06 100644 --- a/c_src/pythonx/pythonx.cpp +++ b/c_src/pythonx/pythonx.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -9,7 +10,6 @@ #include #include -#include "fine.hpp" #include "python.hpp" extern "C" void pythonx_handle_io_write(const char *message, diff --git a/lib/pythonx/nif.ex b/lib/pythonx/nif.ex index 2f08ecd..47a4256 100644 --- a/lib/pythonx/nif.ex +++ b/lib/pythonx/nif.ex @@ -12,30 +12,30 @@ defmodule Pythonx.NIF do end end - def init(_python_dl_path, _python_home_path, _sys_paths), do: :erlang.nif_error(:not_loaded) - def terminate(), do: :erlang.nif_error(:not_loaded) - def janitor_decref(_ptr), do: :erlang.nif_error(:not_loaded) - def none_new(), do: :erlang.nif_error(:not_loaded) - def false_new(), do: :erlang.nif_error(:not_loaded) - def true_new(), do: :erlang.nif_error(:not_loaded) - def long_from_int64(_integer), do: :erlang.nif_error(:not_loaded) - def long_from_string(_string, _base), do: :erlang.nif_error(:not_loaded) - def float_new(_float), do: :erlang.nif_error(:not_loaded) - def bytes_from_binary(_binary), do: :erlang.nif_error(:not_loaded) - def unicode_from_string(_string), do: :erlang.nif_error(:not_loaded) - def unicode_to_string(_object), do: :erlang.nif_error(:not_loaded) - def dict_new(), do: :erlang.nif_error(:not_loaded) - def dict_set_item(_object, _key, _value), do: :erlang.nif_error(:not_loaded) - def tuple_new(_size), do: :erlang.nif_error(:not_loaded) - def tuple_set_item(_object, _index, _value), do: :erlang.nif_error(:not_loaded) - def list_new(_size), do: :erlang.nif_error(:not_loaded) - def list_set_item(_object, _index, _value), do: :erlang.nif_error(:not_loaded) - def set_new(), do: :erlang.nif_error(:not_loaded) - def set_add(_object, _key), do: :erlang.nif_error(:not_loaded) - def object_repr(_object), do: :erlang.nif_error(:not_loaded) - def format_exception(_error), do: :erlang.nif_error(:not_loaded) - def decode_once(_object), do: :erlang.nif_error(:not_loaded) + def init(_python_dl_path, _python_home_path, _sys_paths), do: err!() + def terminate(), do: err!() + def janitor_decref(_ptr), do: err!() + def none_new(), do: err!() + def false_new(), do: err!() + def true_new(), do: err!() + def long_from_int64(_integer), do: err!() + def long_from_string(_string, _base), do: err!() + def float_new(_float), do: err!() + def bytes_from_binary(_binary), do: err!() + def unicode_from_string(_string), do: err!() + def unicode_to_string(_object), do: err!() + def dict_new(), do: err!() + def dict_set_item(_object, _key, _value), do: err!() + def tuple_new(_size), do: err!() + def tuple_set_item(_object, _index, _value), do: err!() + def list_new(_size), do: err!() + def list_set_item(_object, _index, _value), do: err!() + def set_new(), do: err!() + def set_add(_object, _key), do: err!() + def object_repr(_object), do: err!() + def format_exception(_error), do: err!() + def decode_once(_object), do: err!() + def eval(_code, _code_md5, _globals, _stdout_device, _stderr_device), do: err!() - def eval(_code, _code_md5, _globals, _stdout_device, _stderr_device), - do: :erlang.nif_error(:not_loaded) + defp err!(), do: :erlang.nif_error(:not_loaded) end diff --git a/mix.exs b/mix.exs index b5b17cf..25d5605 100644 --- a/mix.exs +++ b/mix.exs @@ -21,6 +21,7 @@ defmodule Pythonx.MixProject do compilers: [:elixir_make] ++ Mix.compilers(), docs: docs(), package: package(), + make_env: fn -> %{"FINE_INCLUDE_DIR" => Fine.include_dir()} end, # Precompilation make_precompiler: {:nif, CCPrecompiler}, make_precompiler_url: "#{@github_url}/releases/download/v#{@version}/@{artefact_filename}", @@ -38,6 +39,7 @@ defmodule Pythonx.MixProject do defp deps do [ + {:fine, github: "elixir-nx/fine", runtime: false}, {:elixir_make, "~> 0.9", runtime: false}, {:cc_precompiler, "~> 0.1", runtime: false}, {:ex_doc, "~> 0.36", only: :dev, runtime: false} @@ -46,7 +48,9 @@ defmodule Pythonx.MixProject do defp docs() do [ - main: "Pythonx" + main: "Pythonx", + source_url: @github_url, + source_ref: "v#{@version}" ] end diff --git a/mix.lock b/mix.lock index ecd3a7e..f9ef0a9 100644 --- a/mix.lock +++ b/mix.lock @@ -3,6 +3,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, + "fine": {:git, "https://github.com/elixir-nx/fine.git", "f97fb6f9cb0b0e0081e64da5933526d939916b62", []}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},