{"id":27938782,"url":"https://github.com/elixir-nx/fine","last_synced_at":"2025-05-07T08:57:46.424Z","repository":{"id":278347028,"uuid":"935330494","full_name":"elixir-nx/fine","owner":"elixir-nx","description":"C++ library enabling more ergonomic NIFs, tailored to Elixir","archived":false,"fork":false,"pushed_at":"2025-03-24T04:33:08.000Z","size":43,"stargazers_count":84,"open_issues_count":0,"forks_count":3,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-05-07T08:57:37.608Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elixir-nx.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-19T09:18:04.000Z","updated_at":"2025-05-07T05:34:15.000Z","dependencies_parsed_at":null,"dependency_job_id":"d5da41c2-8cd7-45a0-8ec1-3495f87ef3ae","html_url":"https://github.com/elixir-nx/fine","commit_stats":null,"previous_names":["elixir-nx/fine"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-nx%2Ffine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-nx%2Ffine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-nx%2Ffine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-nx%2Ffine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elixir-nx","download_url":"https://codeload.github.com/elixir-nx/fine/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252847507,"owners_count":21813450,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-05-07T08:57:45.667Z","updated_at":"2025-05-07T08:57:46.415Z","avatar_url":"https://github.com/elixir-nx.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fine\n\n[![Docs](https://img.shields.io/badge/hex.pm-docs-8e7ce6.svg)](https://hexdocs.pm/fine)\n[![Actions Status](https://github.com/elixir-nx/fine/workflows/CI/badge.svg)](https://github.com/elixir-nx/fine/actions)\n\n\u003c!-- Docs --\u003e\n\nFine is a C++ library enabling more ergonomic NIFs, tailored to Elixir.\n\nErlang provides C API for implementing native functions\n([`erl_nif`](https://www.erlang.org/doc/apps/erts/erl_nif.html)).\nFine is not a replacement of the C API, instead it is designed as a\ncomplementary API, enhancing the developer experience when implementing\nNIFs in C++.\n\n## Features\n\n- Automatic encoding/decoding of NIF arguments and return value,\n  inferred from function signatures.\n\n- Smart pointer enabling safe management of resource objects.\n\n- Registering NIFs and resource types via simple annotations.\n\n- Support for encoding/decoding Elixir structs based on compile time\n  metadata.\n\n- Propagating C++ exceptions as Elixir exceptions, with support for\n  raising custom Elixir exceptions.\n\n- Creating all static atoms at load time.\n\n## Motivation\n\nSome projects make extensive use of NIFs, where using the C API results\nin a lot of boilerplate code and a set of ad-hoc helper functions that\nget copied from project to project. The main idea behind Fine is to\nreduce the friction of getting from Elixir to C++ and vice versa, so\nthat developers can focus on writing the actual native code.\n\n## Requirements\n\nCurrently Fine requires C++17. The supported compilers include GCC,\nClang and MSVC.\n\n## Installation\n\nAdd `Fine` as a dependency in your `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:fine, \"~\u003e 0.1.0\", runtime: false}\n  ]\nend\n```\n\nModify your makefiles to look for Fine header files, similarly to the\nERTS ones. Also make sure to use at least C++17.\n\n```shell\n# GCC/Clang (Makefile)\nCPPFLAGS += -I$(FINE_INCLUDE_DIR)\nCPPFLAGS += -std=c++17\n\n# MSVC (Makefile.win)\nCPPFLAGS=$(CPPFLAGS) /I\"$(FINE_INCLUDE_DIR)\"\nCPPFLAGS=$(CPPFLAGS) /std:c++17\n```\n\nWhen using `elixir_make`, set `FINE_INCLUDE_DIR` like this:\n\n```elixir\ndef project do\n  [\n    ...,\n    make_env: fn -\u003e %{\"FINE_INCLUDE_DIR\" =\u003e Fine.include_dir()} end\n  ]\nend\n```\n\nOtherwise, you can inline the dir to `deps/fine/include`.\n\n\u003e #### Symbol visibility {: .info}\n\u003e\n\u003e When using GCC and Clang it is recommended to compile with\n\u003e `-fvisibility=hidden`. This flag hides symbols in your NIF shared\n\u003e library, which prevents from symbol clashes with other NIF libraries.\n\u003e This is required when multiple NIF libraries use Fine, otherwise\n\u003e loading the libraries fails.\n\u003e\n\u003e ```shell\n\u003e # GCC/Clang (Makefile)\n\u003e CPPFLAGS += -fvisibility=hidden\n\u003e ```\n\n## Usage\n\nA minimal NIF adding two numbers can be implemented like so:\n\n```c++\n#include \u003cfine.hpp\u003e\n\nint64_t add(ErlNifEnv *env, int64_t x, int64_t y) {\n  return x + y;\n}\n\nFINE_NIF(add, 0);\n\nFINE_INIT(\"Elixir.MyLib.NIF\");\n```\n\nSee [`example/`](https://github.com/elixir-nx/fine/tree/main/example) project.\n\n## Encoding/Decoding\n\nTerms are automatically encoded and decoded at the NIF boundary based\non the function signature. In some cases, you may also want to invoke\nencode/decode directly:\n\n```c++\n// Encode\nauto message = std::string(\"hello world\");\nauto term = fine::encode(env, message);\n\n// Decode\nauto message = fine::decode\u003cstd::string\u003e(env, term);\n```\n\nFine provides implementations for the following types:\n\n| Type                                 | Encoder | Decoder |\n| ------------------------------------ | ------- | ------- |\n| `fine::Term`                         | x       | x       |\n| `int64_t`                            | x       | x       |\n| `uint64_t`                           | x       | x       |\n| `double`                             | x       | x       |\n| `bool`                               | x       | x       |\n| `ErlNifPid`                          | x       | x       |\n| `ErlNifBinary`                       | x       | x       |\n| `std::string`                        | x       | x       |\n| `fine::Atom`                         | x       | x       |\n| `std::nullopt_t`                     | x       |         |\n| `std::optional\u003cT\u003e`                   | x       | x       |\n| `std::variant\u003cArgs...\u003e`              | x       | x       |\n| `std::tuple\u003cArgs...\u003e`                | x       | x       |\n| `std::vector\u003cT\u003e`                     | x       | x       |\n| `std::map\u003cK, V\u003e`                     | x       | x       |\n| `fine::ResourcePtr\u003cT\u003e`               | x       | x       |\n| `T` with [struct metadata](#structs) | x       | x       |\n| `fine::Ok\u003cArgs...\u003e`                  | x       |         |\n| `fine::Error\u003cArgs...\u003e`               | x       |         |\n\n\u003e #### ERL_NIF_TERM {: .warning}\n\u003e\n\u003e In some cases, you may want to define a NIF that accepts or returns\n\u003e a term and effectively skip the encoding/decoding. However, the NIF\n\u003e C API defines `ERL_NIF_TERM` as an alias for an integer type, which\n\u003e may introduce an ambiguity for encoding/decoding. For this reason\n\u003e Fine provides a wrapper type `fine::Term` and it should be used in\n\u003e the NIF signature in those cases. `fine::Term` defines implicit\n\u003e conversion to and from `ERL_NIF_TERM`, so it can be used with all\n\u003e `enif_*` functions with no changes.\n\n\u003e #### Binaries {: .info}\n\u003e\n\u003e `std::string` is just a sequence of `char`s and therefore it makes\n\u003e for a good counterpart for Elixir binaries, regardless if we are\n\u003e talking about UTF-8 encoded strings or arbitrary binaries.\n\u003e\n\u003e However, when dealing with large binaries, it is preferable for the\n\u003e NIF to accept `ErlNifBinary` as arguments and deal with the raw data\n\u003e explicitly, which is zero-copy. That said, keep in mind that `ErlNifBinary`\n\u003e is read-only and only valid during the NIF call lifetime.\n\u003e\n\u003e Similarly, when returning large binaries, prefer creating the term\n\u003e with `enif_make_new_binary` and returning `fine::Term`, as shown below.\n\u003e\n\u003e ```c++\n\u003e fine::Term read_data(ErlNifEnv *env) {\n\u003e   const char *buffer = ...;\n\u003e   uint64_t size = ...;\n\u003e\n\u003e   ERL_NIF_TERM binary_term;\n\u003e   auto binary_data = enif_make_new_binary(env, size, \u0026binary_term);\n\u003e   memcpy(binary_data, buffer, size);\n\u003e\n\u003e   return binary_term;\n\u003e }\n\u003e ```\n\u003e\n\u003e You can also return `ErlNifBinary` allocated with `enif_alloc_binary`,\n\u003e but keep in mind that returning the binary converts it to term, which\n\u003e in turn transfers the ownership, so you should not use that `ErlNifBinary`\n\u003e after the NIF finishes.\n\nYou can extend encoding/decoding to work on custom types by defining\nthe following specializations:\n\n```c++\n// Note that the specialization must be defined in the `fine` namespace.\nnamespace fine {\n  template \u003c\u003e struct Decoder\u003cMyType\u003e {\n    static MyType decode(ErlNifEnv *env, const ERL_NIF_TERM \u0026term) {\n      // ...\n    }\n  };\n\n  template \u003c\u003e struct Encoder\u003cMyType\u003e {\n    static ERL_NIF_TERM encode(ErlNifEnv *env, const MyType \u0026value) {\n      // ...\n    }\n  };\n}\n```\n\n## Resource objects\n\nResource objects is a mechanism for passing pointers to C++ data\nstructures to and from NIFs, and around your Elixir code. On the Elixir\nside those pointer surface as reference terms (`#Reference\u003c...\u003e`).\n\nFine provides a construction function `fine::make_resource\u003cT\u003e(...)`,\nsimilar to `std::make_unique` and `std::make_shared` available in the\nC++ standard library. This function creates a new object of the type\n`T`, invoking its constructor with the given arguments and it returns\na smart pointer of type `fine::ResourcePtr\u003cT\u003e`. The pointer is\nautomatically decoded and encoded as a reference term. It can also be\npassed around C++ code, automatically managing the reference count\n(similarly to `std::shared_ptr`).\n\nYou need to indicate that a given class can be used as a resource type\nvia the `FINE_RESOURCE` macro.\n\n```c++\n#include \u003cfine.hpp\u003e\n\nclass Generator {\npublic:\n  Generator(uint64_t seed) { /* ... */ }\n  int64_t random_integer() { /* ... */ }\n  // ...\n};\n\nFINE_RESOURCE(Generator);\n\nfine::ResourcePtr\u003cGenerator\u003e create_generator(ErlNifEnv *env, uint64_t seed) {\n  return fine::make_resource\u003cGenerator\u003e(seed);\n}\n\nFINE_NIF(create_generator, 0);\n\nint64_t random_integer(ErlNifEnv *env, fine::ResourcePtr\u003cGenerator\u003e generator) {\n  return generator-\u003erandom_integer();\n}\n\nFINE_NIF(random_integer, 0);\n\nFINE_INIT(\"Elixir.MyLib.NIF\");\n```\n\nOnce neither Elixir nor C++ holds a reference to the resource object,\nit gets destroyed. By default only the `T` type destructor is called.\nHowever, in some cases you may want to interact with NIF APIs as part\nof the destructor. In that case, you can implement a `destructor`\ncallback on `T`, which receives the relevant `ErlNifEnv`:\n\n```c++\nclass Generator {\n  // ...\n\n  void destructor(ErlNifEnv *env) {\n    // Example: send a message to some process using env\n  }\n};\n```\n\nIf defined, the `destructor` callback is called first, and then the\n`T` destructor is called as usual.\n\nOftentimes NIFs deal with classes from third-party packages, in which\ncase, you may not control how the objects are created and you cannot\nadd callbacks such as `destructor` to the implementation. If you run\ninto any of these limitations, you can define your own wrapper class,\nholding an object of the third-party class and implementing the desired\nconstruction/destruction on top.\n\nYou can use `fine::make_resource_binary(env, resource, data, size)`\nto create a binary term with memory managed by the resource.\n\n## Structs\n\nElixir structs can be passed to and from NIFs. To do that, you need to\ndefine a corresponding C++ class that includes metadata fields used\nfor automatic encoding and decoding. The metadata consists of:\n\n- `module` - the Elixir struct name as an atom reference\n\n- `fields` - a mapping between Elixir struct and C++ class fields\n\n- `is_exception` (optional) - when defined as true, indicates the\n  Elixir struct is an exception\n\nFor example, given an Elixir struct `%MyLib.Point{x: integer, y: integer}`,\nyou could operate on it in the NIF, like this:\n\n```c++\n#include \u003cfine.hpp\u003e\n\nnamespace atoms {\n  auto ElixirMyLibPoint = fine::Atom(\"Elixir.MyLib.Point\");\n  auto x = fine::Atom(\"x\");\n  auto y = fine::Atom(\"y\");\n}\n\nstruct ExPoint {\n  int64_t x;\n  int64_t y;\n\n  static constexpr auto module = \u0026atoms::ElixirMyLibPoint;\n\n  static constexpr auto fields() {\n    return std::make_tuple(std::make_tuple(\u0026ExPoint::x, \u0026atoms::x),\n                           std::make_tuple(\u0026ExPoint::y, \u0026atoms::y));\n  }\n};\n\nExPoint point_reflection(ErlNifEnv *env, ExPoint point) {\n  return ExPoint{-point.x, -point.y};\n}\n\nFINE_NIF(point_reflection, 0);\n\nFINE_INIT(\"Elixir.MyLib.NIF\");\n```\n\nStructs can be particularly convenient when using NIF resource objects.\nWhen working with resources, it is common to have an Elixir struct\ncorresponding to the resource. In the previous `Generator` example,\nyou may define an Elixir struct such as `%MyLib.Generator{resource: reference}`.\nInstead of passing and returning the reference from the NIF, you can\npass and return the struct itself:\n\n```c++\n#include \u003cfine.hpp\u003e\n\nclass Generator {\npublic:\n  Generator(uint64_t seed) { /* ... */ }\n  int64_t random_integer() { /* ... */ }\n  // ...\n};\n\nnamespace atoms {\n  auto ElixirMyLibGenerator = fine::Atom(\"Elixir.MyLib.Generator\");\n  auto resource = fine::Atom(\"resource\");\n}\n\nstruct ExGenerator {\n  fine::ResourcePtr\u003cGenerator\u003e resource;\n\n  static constexpr auto module = \u0026atoms::ElixirMyLibPoint;\n\n  static constexpr auto fields() {\n    return std::make_tuple(\n      std::make_tuple(\u0026ExGenerator::resource, \u0026atoms::resource),\n    );\n  }\n};\n\nExGenerator create_generator(ErlNifEnv *env, uint64_t seed) {\n  return ExGenerator{fine::make_resource\u003cGenerator\u003e(seed)};\n}\n\nFINE_NIF(create_generator, 0);\n\nint64_t random_integer(ErlNifEnv *env, ExGenerator ex_generator) {\n  return ex_generator.resource-\u003erandom_integer();\n}\n\nFINE_NIF(random_integer, 0);\n\nFINE_INIT(\"Elixir.MyLib.NIF\");\n```\n\n## Exceptions\n\nAll C++ exceptions thrown within the NIF are caught and raised as\nElixir exceptions.\n\n```c++\nthrow std::runtime_error(\"something went wrong\");\n// ** (RuntimeError) something went wrong\n\nthrow std::invalid_argument(\"expected x, got y\");\n// ** (ArgumentError) expected x, got y\n\nthrow OtherError(...);\n// ** (RuntimeError) unknown exception thrown within NIF\n```\n\nAdditionally, you can use `fine::raise(env, value)` to raise exception,\nwhere `value` is encoded into a term and used as the exception. This\nis not particularly useful with regular types, however it can be used\nto raise custom Elixir exceptions. Consider the following exception:\n\n```elixir\ndefmodule MyLib.MyError do\n  defexception [:data]\n\n  @impl true\n  def message(error) do\n    \"got error with data #{error.data}\"\n  end\nend\n```\n\nFirst, we need to implement the corresponding C++ class:\n\n```c++\nnamespace atoms {\n  auto ElixirMyLibMyError = fine::Atom(\"Elixir.MyLib.MyError\");\n  auto data = fine::Atom(\"data\");\n}\n\nstruct ExMyError {\n  int64_t data;\n\n  static constexpr auto module = \u0026atoms::ElixirMyLibMyError;\n\n  static constexpr auto fields() {\n    return std::make_tuple(\n        std::make_tuple(\u0026ExMyError::data, \u0026atoms::data));\n  }\n\n  static constexpr auto is_exception = true;\n};\n```\n\nThen, we can raise it anywhere in a NIF:\n\n```c++\nfine::raise(env, ExMyError{42})\n// ** (MyLib.MyError) got error with data 42\n```\n\n## Atoms\n\nIt is preferable to define atoms as static variables, this way the\ncorresponding terms are created once, at NIF load time.\n\n```c++\nnamespace atoms {\n  auto hello_world = fine::Atom(\"hello_world\");\n}\n```\n\n## Result types\n\nWhen it comes to NIFs, errors often indicate unexpected failures and\nraising an exception makes sense, however you may also want to handle\ncertain errors gracefully by returning `:ok`/`:error` tuples, similarly\nto usual Elixir functions. Fine provides `Ok\u003cArgs...\u003e` and `Error\u003cArgs...\u003e`\ntypes for this purpose.\n\n```c++\nfine::Ok\u003c\u003e()\n// :ok\n\nfine::Ok\u003cint64_t\u003e(1)\n// {:ok, 1}\n\nfine::Error\u003c\u003e()\n// :error\n\nfine::Error\u003cstd::string\u003e(\"something went wrong\")\n// {:error, \"something went wrong\"}\n```\n\nYou can use `std::variant` to express a union of possible result types\na NIF may return:\n\n```c++\nstd::variant\u003cfine::Ok\u003cint64_t\u003e, fine::Error\u003cstd::string\u003e\u003e find_meaning(ErlNifEnv *env) {\n  if (...) {\n    return fine::Error\u003cstd::string\u003e(\"something went wrong\");\n  }\n\n  return fine::Ok\u003cint64_t\u003e(42);\n}\n```\n\nNote that if you use a particular union frequently, it may be convenient\nto define a type alias with `using`/`typedef` to keep signatures brief.\n\n\u003c!-- Docs --\u003e\n\n## Prior work\n\nSome of the ideas have been previously explored by Serge Aleynikov (@saleyn)\nand Daniel Goertzen (@goertzenator) ([source](https://github.com/saleyn/nifpp)).\n\n## License\n\n```text\nCopyright (c) 2025 Dashbit\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-nx%2Ffine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felixir-nx%2Ffine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-nx%2Ffine/lists"}