├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .formatter.exs ├── .gitignore ├── Makefile ├── Makefile.win ├── README.md ├── c_src │ └── example.cpp ├── lib │ └── example.ex ├── mix.exs ├── mix.lock └── test │ ├── example_test.exs │ └── test_helper.exs ├── include └── fine.hpp ├── lib └── fine.ex ├── mix.exs ├── mix.lock └── test ├── .gitignore ├── Makefile ├── Makefile.win ├── c_src └── finest.cpp ├── lib └── finest │ ├── error.ex │ ├── nif.ex │ └── point.ex ├── mix.exs ├── mix.lock └── test ├── finest_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: 4 | ["{mix,.formatter}.exs", "{config,lib}/**/*.{ex,exs}"] ++ 5 | ["test/{mix,.formatter}.exs", "test/{config,lib,test}/**/*.{ex,exs}"] 6 | ] 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | - "v*.*" 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-24.04 11 | env: 12 | MIX_ENV: test 13 | name: Lint 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: erlef/setup-beam@v1 17 | with: 18 | otp-version: "27.2" 19 | elixir-version: "1.18.2" 20 | - run: mix deps.get --check-locked 21 | - run: mix deps.compile 22 | - run: mix format --check-formatted 23 | - run: mix deps.unlock --check-unused 24 | - run: mix compile --warnings-as-errors 25 | 26 | test: 27 | runs-on: ${{ matrix.os.id }} 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | os: 32 | - { name: "Linux", id: "ubuntu-24.04" } 33 | - { name: "Windows", id: "windows-2022" } 34 | - { name: "macOS", id: "macos-14" } 35 | pair: 36 | - { elixir: "1.15.8", otp: "25.3" } 37 | - { elixir: "1.18.2", otp: "27.2" } 38 | env: 39 | MIX_ENV: test 40 | name: Test (${{ matrix.os.name }}, ${{ matrix.pair.elixir }}, ${{ matrix.pair.otp }}) 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: erlef/setup-beam@v1 44 | with: 45 | otp-version: ${{ matrix.pair.otp }} 46 | elixir-version: ${{ matrix.pair.elixir }} 47 | if: ${{ !startsWith(matrix.os.id, 'macos') }} 48 | - run: | 49 | curl -fsSO https://elixir-lang.org/install.sh 50 | sh install.sh elixir@${{ matrix.pair.elixir }} otp@${{ matrix.pair.otp }} 51 | otp_bin=($HOME/.elixir-install/installs/otp/*/bin) 52 | elixir_bin=($HOME/.elixir-install/installs/elixir/*/bin) 53 | echo "$otp_bin" >> "$GITHUB_PATH" 54 | echo "$elixir_bin" >> "$GITHUB_PATH" 55 | if: ${{ startsWith(matrix.os.id, 'macos') }} 56 | - uses: ilammy/msvc-dev-cmd@v1 57 | if: ${{ startsWith(matrix.os.id, 'windows') }} 58 | - run: mix deps.get 59 | working-directory: test 60 | - run: mix test 61 | working-directory: test 62 | - name: Test example/ 63 | run: | 64 | mix deps.get --check-locked 65 | mix format --check-formatted 66 | mix deps.unlock --check-unused 67 | mix test --warnings-as-errors 68 | working-directory: example 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | fine-*.tar 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | ## [v0.1.0](https://github.com/elixir-nx/fine/tree/v0.1.0) (2025-02-19) 8 | 9 | Initial release. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fine 2 | 3 | [![Docs](https://img.shields.io/badge/hex.pm-docs-8e7ce6.svg)](https://hexdocs.pm/fine) 4 | [![Actions Status](https://github.com/elixir-nx/fine/workflows/CI/badge.svg)](https://github.com/elixir-nx/fine/actions) 5 | 6 | 7 | 8 | Fine is a C++ library enabling more ergonomic NIFs, tailored to Elixir. 9 | 10 | Erlang provides C API for implementing native functions 11 | ([`erl_nif`](https://www.erlang.org/doc/apps/erts/erl_nif.html)). 12 | Fine is not a replacement of the C API, instead it is designed as a 13 | complementary API, enhancing the developer experience when implementing 14 | NIFs in C++. 15 | 16 | ## Features 17 | 18 | - Automatic encoding/decoding of NIF arguments and return value, 19 | inferred from function signatures. 20 | 21 | - Smart pointer enabling safe management of resource objects. 22 | 23 | - Registering NIFs and resource types via simple annotations. 24 | 25 | - Support for encoding/decoding Elixir structs based on compile time 26 | metadata. 27 | 28 | - Propagating C++ exceptions as Elixir exceptions, with support for 29 | raising custom Elixir exceptions. 30 | 31 | - Creating all static atoms at load time. 32 | 33 | ## Motivation 34 | 35 | Some projects make extensive use of NIFs, where using the C API results 36 | in a lot of boilerplate code and a set of ad-hoc helper functions that 37 | get copied from project to project. The main idea behind Fine is to 38 | reduce the friction of getting from Elixir to C++ and vice versa, so 39 | that developers can focus on writing the actual native code. 40 | 41 | ## Requirements 42 | 43 | Currently Fine requires C++17. The supported compilers include GCC, 44 | Clang and MSVC. 45 | 46 | ## Installation 47 | 48 | Add `Fine` as a dependency in your `mix.exs`: 49 | 50 | ```elixir 51 | def deps do 52 | [ 53 | {:fine, "~> 0.1.0", runtime: false} 54 | ] 55 | end 56 | ``` 57 | 58 | Modify your makefiles to look for Fine header files, similarly to the 59 | ERTS ones. Also make sure to use at least C++17. 60 | 61 | ```shell 62 | # GCC/Clang (Makefile) 63 | CPPFLAGS += -I$(FINE_INCLUDE_DIR) 64 | CPPFLAGS += -std=c++17 65 | 66 | # MSVC (Makefile.win) 67 | CPPFLAGS=$(CPPFLAGS) /I"$(FINE_INCLUDE_DIR)" 68 | CPPFLAGS=$(CPPFLAGS) /std:c++17 69 | ``` 70 | 71 | When using `elixir_make`, set `FINE_INCLUDE_DIR` like this: 72 | 73 | ```elixir 74 | def project do 75 | [ 76 | ..., 77 | make_env: fn -> %{"FINE_INCLUDE_DIR" => Fine.include_dir()} end 78 | ] 79 | end 80 | ``` 81 | 82 | Otherwise, you can inline the dir to `deps/fine/include`. 83 | 84 | > #### Symbol visibility {: .info} 85 | > 86 | > When using GCC and Clang it is recommended to compile with 87 | > `-fvisibility=hidden`. This flag hides symbols in your NIF shared 88 | > library, which prevents from symbol clashes with other NIF libraries. 89 | > This is required when multiple NIF libraries use Fine, otherwise 90 | > loading the libraries fails. 91 | > 92 | > ```shell 93 | > # GCC/Clang (Makefile) 94 | > CPPFLAGS += -fvisibility=hidden 95 | > ``` 96 | 97 | ## Usage 98 | 99 | A minimal NIF adding two numbers can be implemented like so: 100 | 101 | ```c++ 102 | #include 103 | 104 | int64_t add(ErlNifEnv *env, int64_t x, int64_t y) { 105 | return x + y; 106 | } 107 | 108 | FINE_NIF(add, 0); 109 | 110 | FINE_INIT("Elixir.MyLib.NIF"); 111 | ``` 112 | 113 | See [`example/`](https://github.com/elixir-nx/fine/tree/main/example) project. 114 | 115 | ## Encoding/Decoding 116 | 117 | Terms are automatically encoded and decoded at the NIF boundary based 118 | on the function signature. In some cases, you may also want to invoke 119 | encode/decode directly: 120 | 121 | ```c++ 122 | // Encode 123 | auto message = std::string("hello world"); 124 | auto term = fine::encode(env, message); 125 | 126 | // Decode 127 | auto message = fine::decode(env, term); 128 | ``` 129 | 130 | Fine provides implementations for the following types: 131 | 132 | | Type | Encoder | Decoder | 133 | | ------------------------------------ | ------- | ------- | 134 | | `fine::Term` | x | x | 135 | | `int64_t` | x | x | 136 | | `uint64_t` | x | x | 137 | | `double` | x | x | 138 | | `bool` | x | x | 139 | | `ErlNifPid` | x | x | 140 | | `ErlNifBinary` | x | x | 141 | | `std::string` | x | x | 142 | | `fine::Atom` | x | x | 143 | | `std::nullopt_t` | x | | 144 | | `std::optional` | x | x | 145 | | `std::variant` | x | x | 146 | | `std::tuple` | x | x | 147 | | `std::vector` | x | x | 148 | | `std::map` | x | x | 149 | | `fine::ResourcePtr` | x | x | 150 | | `T` with [struct metadata](#structs) | x | x | 151 | | `fine::Ok` | x | | 152 | | `fine::Error` | x | | 153 | 154 | > #### ERL_NIF_TERM {: .warning} 155 | > 156 | > In some cases, you may want to define a NIF that accepts or returns 157 | > a term and effectively skip the encoding/decoding. However, the NIF 158 | > C API defines `ERL_NIF_TERM` as an alias for an integer type, which 159 | > may introduce an ambiguity for encoding/decoding. For this reason 160 | > Fine provides a wrapper type `fine::Term` and it should be used in 161 | > the NIF signature in those cases. `fine::Term` defines implicit 162 | > conversion to and from `ERL_NIF_TERM`, so it can be used with all 163 | > `enif_*` functions with no changes. 164 | 165 | > #### Binaries {: .info} 166 | > 167 | > `std::string` is just a sequence of `char`s and therefore it makes 168 | > for a good counterpart for Elixir binaries, regardless if we are 169 | > talking about UTF-8 encoded strings or arbitrary binaries. 170 | > 171 | > However, when dealing with large binaries, it is preferable for the 172 | > NIF to accept `ErlNifBinary` as arguments and deal with the raw data 173 | > explicitly, which is zero-copy. That said, keep in mind that `ErlNifBinary` 174 | > is read-only and only valid during the NIF call lifetime. 175 | > 176 | > Similarly, when returning large binaries, prefer creating the term 177 | > with `enif_make_new_binary` and returning `fine::Term`, as shown below. 178 | > 179 | > ```c++ 180 | > fine::Term read_data(ErlNifEnv *env) { 181 | > const char *buffer = ...; 182 | > uint64_t size = ...; 183 | > 184 | > ERL_NIF_TERM binary_term; 185 | > auto binary_data = enif_make_new_binary(env, size, &binary_term); 186 | > memcpy(binary_data, buffer, size); 187 | > 188 | > return binary_term; 189 | > } 190 | > ``` 191 | > 192 | > You can also return `ErlNifBinary` allocated with `enif_alloc_binary`, 193 | > but keep in mind that returning the binary converts it to term, which 194 | > in turn transfers the ownership, so you should not use that `ErlNifBinary` 195 | > after the NIF finishes. 196 | 197 | You can extend encoding/decoding to work on custom types by defining 198 | the following specializations: 199 | 200 | ```c++ 201 | // Note that the specialization must be defined in the `fine` namespace. 202 | namespace fine { 203 | template <> struct Decoder { 204 | static MyType decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 205 | // ... 206 | } 207 | }; 208 | 209 | template <> struct Encoder { 210 | static ERL_NIF_TERM encode(ErlNifEnv *env, const MyType &value) { 211 | // ... 212 | } 213 | }; 214 | } 215 | ``` 216 | 217 | ## Resource objects 218 | 219 | Resource objects is a mechanism for passing pointers to C++ data 220 | structures to and from NIFs, and around your Elixir code. On the Elixir 221 | side those pointer surface as reference terms (`#Reference<...>`). 222 | 223 | Fine provides a construction function `fine::make_resource(...)`, 224 | similar to `std::make_unique` and `std::make_shared` available in the 225 | C++ standard library. This function creates a new object of the type 226 | `T`, invoking its constructor with the given arguments and it returns 227 | a smart pointer of type `fine::ResourcePtr`. The pointer is 228 | automatically decoded and encoded as a reference term. It can also be 229 | passed around C++ code, automatically managing the reference count 230 | (similarly to `std::shared_ptr`). 231 | 232 | You need to indicate that a given class can be used as a resource type 233 | via the `FINE_RESOURCE` macro. 234 | 235 | ```c++ 236 | #include 237 | 238 | class Generator { 239 | public: 240 | Generator(uint64_t seed) { /* ... */ } 241 | int64_t random_integer() { /* ... */ } 242 | // ... 243 | }; 244 | 245 | FINE_RESOURCE(Generator); 246 | 247 | fine::ResourcePtr create_generator(ErlNifEnv *env, uint64_t seed) { 248 | return fine::make_resource(seed); 249 | } 250 | 251 | FINE_NIF(create_generator, 0); 252 | 253 | int64_t random_integer(ErlNifEnv *env, fine::ResourcePtr generator) { 254 | return generator->random_integer(); 255 | } 256 | 257 | FINE_NIF(random_integer, 0); 258 | 259 | FINE_INIT("Elixir.MyLib.NIF"); 260 | ``` 261 | 262 | Once neither Elixir nor C++ holds a reference to the resource object, 263 | it gets destroyed. By default only the `T` type destructor is called. 264 | However, in some cases you may want to interact with NIF APIs as part 265 | of the destructor. In that case, you can implement a `destructor` 266 | callback on `T`, which receives the relevant `ErlNifEnv`: 267 | 268 | ```c++ 269 | class Generator { 270 | // ... 271 | 272 | void destructor(ErlNifEnv *env) { 273 | // Example: send a message to some process using env 274 | } 275 | }; 276 | ``` 277 | 278 | If defined, the `destructor` callback is called first, and then the 279 | `T` destructor is called as usual. 280 | 281 | Oftentimes NIFs deal with classes from third-party packages, in which 282 | case, you may not control how the objects are created and you cannot 283 | add callbacks such as `destructor` to the implementation. If you run 284 | into any of these limitations, you can define your own wrapper class, 285 | holding an object of the third-party class and implementing the desired 286 | construction/destruction on top. 287 | 288 | You can use `fine::make_resource_binary(env, resource, data, size)` 289 | to create a binary term with memory managed by the resource. 290 | 291 | ## Structs 292 | 293 | Elixir structs can be passed to and from NIFs. To do that, you need to 294 | define a corresponding C++ class that includes metadata fields used 295 | for automatic encoding and decoding. The metadata consists of: 296 | 297 | - `module` - the Elixir struct name as an atom reference 298 | 299 | - `fields` - a mapping between Elixir struct and C++ class fields 300 | 301 | - `is_exception` (optional) - when defined as true, indicates the 302 | Elixir struct is an exception 303 | 304 | For example, given an Elixir struct `%MyLib.Point{x: integer, y: integer}`, 305 | you could operate on it in the NIF, like this: 306 | 307 | ```c++ 308 | #include 309 | 310 | namespace atoms { 311 | auto ElixirMyLibPoint = fine::Atom("Elixir.MyLib.Point"); 312 | auto x = fine::Atom("x"); 313 | auto y = fine::Atom("y"); 314 | } 315 | 316 | struct ExPoint { 317 | int64_t x; 318 | int64_t y; 319 | 320 | static constexpr auto module = &atoms::ElixirMyLibPoint; 321 | 322 | static constexpr auto fields() { 323 | return std::make_tuple(std::make_tuple(&ExPoint::x, &atoms::x), 324 | std::make_tuple(&ExPoint::y, &atoms::y)); 325 | } 326 | }; 327 | 328 | ExPoint point_reflection(ErlNifEnv *env, ExPoint point) { 329 | return ExPoint{-point.x, -point.y}; 330 | } 331 | 332 | FINE_NIF(point_reflection, 0); 333 | 334 | FINE_INIT("Elixir.MyLib.NIF"); 335 | ``` 336 | 337 | Structs can be particularly convenient when using NIF resource objects. 338 | When working with resources, it is common to have an Elixir struct 339 | corresponding to the resource. In the previous `Generator` example, 340 | you may define an Elixir struct such as `%MyLib.Generator{resource: reference}`. 341 | Instead of passing and returning the reference from the NIF, you can 342 | pass and return the struct itself: 343 | 344 | ```c++ 345 | #include 346 | 347 | class Generator { 348 | public: 349 | Generator(uint64_t seed) { /* ... */ } 350 | int64_t random_integer() { /* ... */ } 351 | // ... 352 | }; 353 | 354 | namespace atoms { 355 | auto ElixirMyLibGenerator = fine::Atom("Elixir.MyLib.Generator"); 356 | auto resource = fine::Atom("resource"); 357 | } 358 | 359 | struct ExGenerator { 360 | fine::ResourcePtr resource; 361 | 362 | static constexpr auto module = &atoms::ElixirMyLibPoint; 363 | 364 | static constexpr auto fields() { 365 | return std::make_tuple( 366 | std::make_tuple(&ExGenerator::resource, &atoms::resource), 367 | ); 368 | } 369 | }; 370 | 371 | ExGenerator create_generator(ErlNifEnv *env, uint64_t seed) { 372 | return ExGenerator{fine::make_resource(seed)}; 373 | } 374 | 375 | FINE_NIF(create_generator, 0); 376 | 377 | int64_t random_integer(ErlNifEnv *env, ExGenerator ex_generator) { 378 | return ex_generator.resource->random_integer(); 379 | } 380 | 381 | FINE_NIF(random_integer, 0); 382 | 383 | FINE_INIT("Elixir.MyLib.NIF"); 384 | ``` 385 | 386 | ## Exceptions 387 | 388 | All C++ exceptions thrown within the NIF are caught and raised as 389 | Elixir exceptions. 390 | 391 | ```c++ 392 | throw std::runtime_error("something went wrong"); 393 | // ** (RuntimeError) something went wrong 394 | 395 | throw std::invalid_argument("expected x, got y"); 396 | // ** (ArgumentError) expected x, got y 397 | 398 | throw OtherError(...); 399 | // ** (RuntimeError) unknown exception thrown within NIF 400 | ``` 401 | 402 | Additionally, you can use `fine::raise(env, value)` to raise exception, 403 | where `value` is encoded into a term and used as the exception. This 404 | is not particularly useful with regular types, however it can be used 405 | to raise custom Elixir exceptions. Consider the following exception: 406 | 407 | ```elixir 408 | defmodule MyLib.MyError do 409 | defexception [:data] 410 | 411 | @impl true 412 | def message(error) do 413 | "got error with data #{error.data}" 414 | end 415 | end 416 | ``` 417 | 418 | First, we need to implement the corresponding C++ class: 419 | 420 | ```c++ 421 | namespace atoms { 422 | auto ElixirMyLibMyError = fine::Atom("Elixir.MyLib.MyError"); 423 | auto data = fine::Atom("data"); 424 | } 425 | 426 | struct ExMyError { 427 | int64_t data; 428 | 429 | static constexpr auto module = &atoms::ElixirMyLibMyError; 430 | 431 | static constexpr auto fields() { 432 | return std::make_tuple( 433 | std::make_tuple(&ExMyError::data, &atoms::data)); 434 | } 435 | 436 | static constexpr auto is_exception = true; 437 | }; 438 | ``` 439 | 440 | Then, we can raise it anywhere in a NIF: 441 | 442 | ```c++ 443 | fine::raise(env, ExMyError{42}) 444 | // ** (MyLib.MyError) got error with data 42 445 | ``` 446 | 447 | ## Atoms 448 | 449 | It is preferable to define atoms as static variables, this way the 450 | corresponding terms are created once, at NIF load time. 451 | 452 | ```c++ 453 | namespace atoms { 454 | auto hello_world = fine::Atom("hello_world"); 455 | } 456 | ``` 457 | 458 | ## Result types 459 | 460 | When it comes to NIFs, errors often indicate unexpected failures and 461 | raising an exception makes sense, however you may also want to handle 462 | certain errors gracefully by returning `:ok`/`:error` tuples, similarly 463 | to usual Elixir functions. Fine provides `Ok` and `Error` 464 | types for this purpose. 465 | 466 | ```c++ 467 | fine::Ok<>() 468 | // :ok 469 | 470 | fine::Ok(1) 471 | // {:ok, 1} 472 | 473 | fine::Error<>() 474 | // :error 475 | 476 | fine::Error("something went wrong") 477 | // {:error, "something went wrong"} 478 | ``` 479 | 480 | You can use `std::variant` to express a union of possible result types 481 | a NIF may return: 482 | 483 | ```c++ 484 | std::variant, fine::Error> find_meaning(ErlNifEnv *env) { 485 | if (...) { 486 | return fine::Error("something went wrong"); 487 | } 488 | 489 | return fine::Ok(42); 490 | } 491 | ``` 492 | 493 | Note that if you use a particular union frequently, it may be convenient 494 | to define a type alias with `using`/`typedef` to keep signatures brief. 495 | 496 | 497 | 498 | ## Prior work 499 | 500 | Some of the ideas have been previously explored by Serge Aleynikov (@saleyn) 501 | and Daniel Goertzen (@goertzenator) ([source](https://github.com/saleyn/nifpp)). 502 | 503 | ## License 504 | 505 | ```text 506 | Copyright (c) 2025 Dashbit 507 | 508 | Licensed under the Apache License, Version 2.0 (the "License"); 509 | you may not use this file except in compliance with the License. 510 | You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 511 | 512 | Unless required by applicable law or agreed to in writing, software 513 | distributed under the License is distributed on an "AS IS" BASIS, 514 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 515 | See the License for the specific language governing permissions and 516 | limitations under the License. 517 | ``` 518 | -------------------------------------------------------------------------------- /example/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | example-*.tar 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | PRIV_DIR := $(MIX_APP_PATH)/priv 2 | NIF_PATH := $(PRIV_DIR)/libexample.so 3 | C_SRC := $(shell pwd)/c_src 4 | 5 | CPPFLAGS := -shared -fPIC -fvisibility=hidden -std=c++17 -Wall -Wextra 6 | CPPFLAGS += -I$(ERTS_INCLUDE_DIR) -I$(FINE_INCLUDE_DIR) 7 | 8 | ifdef DEBUG 9 | CPPFLAGS += -g 10 | else 11 | CPPFLAGS += -O3 12 | endif 13 | 14 | ifndef TARGET_ABI 15 | TARGET_ABI := $(shell uname -s | tr '[:upper:]' '[:lower:]') 16 | endif 17 | 18 | ifeq ($(TARGET_ABI),darwin) 19 | CPPFLAGS += -undefined dynamic_lookup -flat_namespace 20 | endif 21 | 22 | SOURCES := $(wildcard $(C_SRC)/*.cpp) 23 | 24 | all: $(NIF_PATH) 25 | @ echo > /dev/null # Dummy command to avoid the default output "Nothing to be done" 26 | 27 | $(NIF_PATH): $(SOURCES) 28 | @ mkdir -p $(PRIV_DIR) 29 | $(CXX) $(CPPFLAGS) $(SOURCES) -o $(NIF_PATH) 30 | -------------------------------------------------------------------------------- /example/Makefile.win: -------------------------------------------------------------------------------- 1 | PRIV_DIR=$(MIX_APP_PATH)\priv 2 | NIF_PATH=$(PRIV_DIR)\libexample.dll 3 | C_SRC=$(MAKEDIR)\c_src 4 | 5 | CPPFLAGS=/LD /std:c++17 /W4 /O2 /EHsc 6 | CPPFLAGS=$(CPPFLAGS) /I"$(ERTS_INCLUDE_DIR)" /I"$(FINE_INCLUDE_DIR)" 7 | 8 | SOURCES=$(C_SRC)\*.cpp 9 | 10 | all: $(NIF_PATH) 11 | 12 | $(NIF_PATH): $(SOURCES) 13 | @ if not exist "$(PRIV_DIR)" mkdir "$(PRIV_DIR)" 14 | cl $(CPPFLAGS) $(SOURCES) /Fe"$(NIF_PATH)" 15 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | Example project using Fine to implement NIFs. 4 | 5 | To run tests, execute: 6 | 7 | ```shell 8 | $ mix deps.get 9 | $ mix test 10 | ``` 11 | -------------------------------------------------------------------------------- /example/c_src/example.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int64_t add(ErlNifEnv *env, int64_t x, int64_t y) { 4 | return x + y; 5 | } 6 | 7 | FINE_NIF(add, 0); 8 | FINE_INIT("Elixir.Example"); 9 | -------------------------------------------------------------------------------- /example/lib/example.ex: -------------------------------------------------------------------------------- 1 | defmodule Example do 2 | @on_load :__on_load__ 3 | 4 | def __on_load__ do 5 | path = :filename.join(:code.priv_dir(:example), ~c"libexample") 6 | 7 | case :erlang.load_nif(path, 0) do 8 | :ok -> :ok 9 | {:error, reason} -> raise "failed to load NIF library, reason: #{inspect(reason)}" 10 | end 11 | end 12 | 13 | @doc """ 14 | Adds two numbers using NIF. 15 | 16 | ## Examples 17 | 18 | iex> Example.add(1, 2) 19 | 3 20 | """ 21 | def add(_x, _y) do 22 | :erlang.nif_error("nif not loaded") 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /example/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Example.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :example, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | compilers: [:elixir_make] ++ Mix.compilers(), 10 | make_env: fn -> %{"FINE_INCLUDE_DIR" => Fine.include_dir()} end, 11 | deps: deps() 12 | ] 13 | end 14 | 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:elixir_make, "~> 0.9.0"}, 24 | {:fine, path: ".."} 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /example/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 3 | } 4 | -------------------------------------------------------------------------------- /example/test/example_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExampleTest do 2 | use ExUnit.Case, async: true 3 | doctest Example 4 | end 5 | -------------------------------------------------------------------------------- /example/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /include/fine.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FINE_HPP 2 | #define FINE_HPP 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #if ERL_NIF_MAJOR_VERSION > 2 || \ 19 | (ERL_NIF_MAJOR_VERSION == 2 && ERL_NIF_MINOR_VERSION >= 17) 20 | #define FINE_ERL_NIF_CHAR_ENCODING ERL_NIF_UTF8 21 | #else 22 | #define FINE_ERL_NIF_CHAR_ENCODING ERL_NIF_LATIN1 23 | #endif 24 | 25 | namespace fine { 26 | 27 | // Forward declarations 28 | 29 | template T decode(ErlNifEnv *env, const ERL_NIF_TERM &term); 30 | template ERL_NIF_TERM encode(ErlNifEnv *env, const T &value); 31 | 32 | template struct Decoder; 33 | template struct Encoder; 34 | 35 | namespace __private__ { 36 | std::vector &get_erl_nif_funcs(); 37 | int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info); 38 | } // namespace __private__ 39 | 40 | // Definitions 41 | 42 | namespace __private__ { 43 | inline ERL_NIF_TERM make_atom(ErlNifEnv *env, const char *msg) { 44 | ERL_NIF_TERM atom; 45 | if (enif_make_existing_atom(env, msg, &atom, FINE_ERL_NIF_CHAR_ENCODING)) { 46 | return atom; 47 | } else { 48 | return enif_make_atom(env, msg); 49 | } 50 | } 51 | } // namespace __private__ 52 | 53 | // A representation of an atom term. 54 | class Atom { 55 | public: 56 | Atom(std::string name) : name(name), term(std::nullopt) { 57 | if (!Atom::initialized) { 58 | Atom::atoms.push_back(this); 59 | } 60 | } 61 | 62 | std::string to_string() const { return this->name; } 63 | 64 | bool operator==(const Atom &other) const { return this->name == other.name; } 65 | 66 | bool operator==(const char *other) const { return this->name == other; } 67 | 68 | bool operator<(const Atom &other) const { return this->name < other.name; } 69 | 70 | private: 71 | static void init_atoms(ErlNifEnv *env) { 72 | for (auto atom : Atom::atoms) { 73 | atom->term = fine::__private__::make_atom(env, atom->name.c_str()); 74 | } 75 | 76 | Atom::atoms.clear(); 77 | Atom::initialized = true; 78 | } 79 | 80 | friend struct Encoder; 81 | 82 | friend int __private__::load(ErlNifEnv *env, void **priv_data, 83 | ERL_NIF_TERM load_info); 84 | 85 | // We accumulate all globally defined atom objects and create the 86 | // terms upfront as part of init (called from the NIF load callback). 87 | inline static std::vector atoms = {}; 88 | inline static bool initialized = false; 89 | 90 | std::string name; 91 | std::optional term; 92 | }; 93 | 94 | namespace __private__::atoms { 95 | inline auto ok = Atom("ok"); 96 | inline auto error = Atom("error"); 97 | inline auto nil = Atom("nil"); 98 | inline auto true_ = Atom("true"); 99 | inline auto false_ = Atom("false"); 100 | inline auto __struct__ = Atom("__struct__"); 101 | inline auto __exception__ = Atom("__exception__"); 102 | inline auto message = Atom("message"); 103 | inline auto ElixirArgumentError = Atom("Elixir.ArgumentError"); 104 | inline auto ElixirRuntimeError = Atom("Elixir.RuntimeError"); 105 | } // namespace __private__::atoms 106 | 107 | // Represents any term. 108 | // 109 | // This type should be used instead of ERL_NIF_TERM in the NIF signature 110 | // and encode/decode APIs. 111 | class Term { 112 | // ERL_NIF_TERM is typedef-ed as an integer type. At the moment of 113 | // writing it is unsigned long int. This means that we cannot define 114 | // separate Decoder and Decoder, 115 | // (which could potentially match uint64_t). The same applies to 116 | // Encoder. For this reason we need a wrapper object for terms, so 117 | // they can be unambiguously distinguished. We define implicit 118 | // bidirectional conversion between Term and ERL_NIF_TERM, so that 119 | // Term is effectively just a typing tag for decoder and encoder 120 | // (and the nif signature). 121 | 122 | public: 123 | Term(const ERL_NIF_TERM &term) : term(term) {} 124 | 125 | operator ERL_NIF_TERM() const { return this->term; } 126 | 127 | private: 128 | ERL_NIF_TERM term; 129 | }; 130 | 131 | // Represents a `:ok` tagged tuple, useful as a NIF result. 132 | template class Ok { 133 | public: 134 | Ok(const Args &...items) : items(items...) {} 135 | 136 | private: 137 | friend struct Encoder>; 138 | 139 | std::tuple items; 140 | }; 141 | 142 | // Represents a `:error` tagged tuple, useful as a NIF result. 143 | template class Error { 144 | public: 145 | Error(const Args &...items) : items(items...) {} 146 | 147 | private: 148 | friend struct Encoder>; 149 | 150 | std::tuple items; 151 | }; 152 | 153 | namespace __private__ { 154 | template struct ResourceWrapper { 155 | T resource; 156 | bool initialized; 157 | 158 | static void dtor(ErlNifEnv *env, void *ptr) { 159 | auto resource_wrapper = reinterpret_cast *>(ptr); 160 | 161 | if (resource_wrapper->initialized) { 162 | if constexpr (has_destructor::value) { 163 | resource_wrapper->resource.destructor(env); 164 | } 165 | resource_wrapper->resource.~T(); 166 | } 167 | } 168 | 169 | template 170 | struct has_destructor : std::false_type {}; 171 | 172 | template 173 | struct has_destructor< 174 | U, 175 | typename std::enable_if().destructor(std::declval())), 177 | void>::value>::type> : std::true_type {}; 178 | }; 179 | } // namespace __private__ 180 | 181 | // A smart pointer that retains ownership of a resource object. 182 | template class ResourcePtr { 183 | // For more context see [1] and [2]. 184 | // 185 | // [1]: https://stackoverflow.com/a/3279550 186 | // [2]: https://stackoverflow.com/a/5695855 187 | 188 | public: 189 | // Make default constructor public, so that classes with ResourcePtr 190 | // field can also have default constructor. 191 | ResourcePtr() : ptr(nullptr) {} 192 | 193 | ResourcePtr(const ResourcePtr &other) : ptr(other.ptr) { 194 | if (this->ptr != nullptr) { 195 | enif_keep_resource(reinterpret_cast(this->ptr)); 196 | } 197 | } 198 | 199 | ResourcePtr(ResourcePtr &&other) : ResourcePtr() { swap(other, *this); } 200 | 201 | ~ResourcePtr() { 202 | if (this->ptr != nullptr) { 203 | enif_release_resource(reinterpret_cast(this->ptr)); 204 | } 205 | } 206 | 207 | ResourcePtr &operator=(ResourcePtr other) { 208 | swap(*this, other); 209 | return *this; 210 | } 211 | 212 | T &operator*() const { return this->ptr->resource; } 213 | 214 | T *operator->() const { return &this->ptr->resource; } 215 | 216 | T *get() const { return &this->ptr->resource; } 217 | 218 | friend void swap(ResourcePtr &left, ResourcePtr &right) { 219 | using std::swap; 220 | swap(left.ptr, right.ptr); 221 | } 222 | 223 | private: 224 | // This constructor assumes the pointer is already accounted for in 225 | // the resource reference count. Since it is private, we guarantee 226 | // this in all the callers. 227 | ResourcePtr(__private__::ResourceWrapper *ptr) : ptr(ptr) {} 228 | 229 | // Friend functions that use the resource_type static member or the 230 | // private constructor. 231 | 232 | template 233 | friend ResourcePtr make_resource(Args &&...args); 234 | 235 | friend class Registration; 236 | 237 | friend struct Decoder>; 238 | 239 | inline static ErlNifResourceType *resource_type = nullptr; 240 | 241 | __private__::ResourceWrapper *ptr; 242 | }; 243 | 244 | // Allocates a new resource object, invoking its constructor with the 245 | // given arguments. 246 | template 247 | ResourcePtr make_resource(Args &&...args) { 248 | auto type = ResourcePtr::resource_type; 249 | 250 | if (type == nullptr) { 251 | throw std::runtime_error( 252 | "calling make_resource with unexpected type. Make sure" 253 | " to register your resource type with the FINE_RESOURCE macro"); 254 | } 255 | 256 | void *allocation_ptr = 257 | enif_alloc_resource(type, sizeof(__private__::ResourceWrapper)); 258 | 259 | auto resource_wrapper = 260 | reinterpret_cast<__private__::ResourceWrapper *>(allocation_ptr); 261 | 262 | // We create ResourcePtr right away, to make sure the resource is 263 | // properly released in case the constructor below throws 264 | auto resource = ResourcePtr(resource_wrapper); 265 | 266 | // We use a wrapper struct with an extra field to track if the 267 | // resource has actually been initialized. This way if the constructor 268 | // below throws, we can skip the destructor calls in the Erlang dtor 269 | resource_wrapper->initialized = false; 270 | 271 | // Invoke the constructor with prefect forwarding to initialize the 272 | // object at the VM-allocated memory 273 | new (&resource_wrapper->resource) T(std::forward(args)...); 274 | 275 | resource_wrapper->initialized = true; 276 | 277 | return resource; 278 | } 279 | 280 | // Creates a binary term pointing to the given buffer. 281 | // 282 | // The buffer is managed by the resource object and should be deallocated 283 | // once the resource is destroyed. 284 | template 285 | Term make_resource_binary(ErlNifEnv *env, ResourcePtr resource, 286 | const char *data, size_t size) { 287 | return enif_make_resource_binary( 288 | env, reinterpret_cast(resource.get()), data, size); 289 | } 290 | 291 | // Decodes the given Erlang term as a value of the specified type. 292 | // 293 | // The given type must have a specialized Decoder implementation. 294 | template T decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 295 | return Decoder::decode(env, term); 296 | } 297 | 298 | // Encodes the given value as a Erlang term. 299 | // 300 | // The value type must have a specialized Encoder implementation. 301 | template ERL_NIF_TERM encode(ErlNifEnv *env, const T &value) { 302 | return Encoder::encode(env, value); 303 | } 304 | 305 | // We want decode to return the value, and since the argument types 306 | // are always the same, we need template specialization, so that the 307 | // caller can explicitly specify the desired type. However, in order 308 | // to implement decode for a type such as std::vector we need 309 | // partial specialization, and that is not supported for functions. 310 | // To solve this, we specialize a struct instead and have the decode 311 | // logic in a static member function. 312 | // 313 | // In case of encode, the argument type differs, so we could use 314 | // function overloading. That said, we pick struct specialization as 315 | // well for consistency with decode. This approach also prevents from 316 | // implicit argument conversion, which is arguably good in this case, 317 | // as it makes the encoding explicit. 318 | 319 | template struct Decoder {}; 320 | 321 | template struct Encoder {}; 322 | 323 | template <> struct Decoder { 324 | static Term decode(ErlNifEnv *, const ERL_NIF_TERM &term) { 325 | return Term(term); 326 | } 327 | }; 328 | 329 | template <> struct Decoder { 330 | static int64_t decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 331 | int64_t integer; 332 | if (!enif_get_int64(env, term, 333 | reinterpret_cast(&integer))) { 334 | throw std::invalid_argument("decode failed, expected an integer"); 335 | } 336 | return integer; 337 | } 338 | }; 339 | 340 | template <> struct Decoder { 341 | static uint64_t decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 342 | uint64_t integer; 343 | if (!enif_get_uint64(env, term, 344 | reinterpret_cast(&integer))) { 345 | throw std::invalid_argument( 346 | "decode failed, expected an unsigned integer"); 347 | } 348 | return integer; 349 | } 350 | }; 351 | 352 | template <> struct Decoder { 353 | static double decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 354 | double number; 355 | if (!enif_get_double(env, term, &number)) { 356 | throw std::invalid_argument("decode failed, expected a float"); 357 | } 358 | return number; 359 | } 360 | }; 361 | 362 | template <> struct Decoder { 363 | static bool decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 364 | char atom_string[6]; 365 | auto length = enif_get_atom(env, term, atom_string, 6, ERL_NIF_LATIN1); 366 | 367 | if (length == 5 && strcmp(atom_string, "true") == 0) { 368 | return true; 369 | } 370 | 371 | if (length == 6 && strcmp(atom_string, "false") == 0) { 372 | return false; 373 | } 374 | 375 | throw std::invalid_argument("decode failed, expected a boolean"); 376 | } 377 | }; 378 | 379 | template <> struct Decoder { 380 | static ErlNifPid decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 381 | ErlNifPid pid; 382 | if (!enif_get_local_pid(env, term, &pid)) { 383 | throw std::invalid_argument("decode failed, expected a local pid"); 384 | } 385 | return pid; 386 | } 387 | }; 388 | 389 | template <> struct Decoder { 390 | static ErlNifBinary decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 391 | ErlNifBinary binary; 392 | if (!enif_inspect_binary(env, term, &binary)) { 393 | throw std::invalid_argument("decode failed, expected a binary"); 394 | } 395 | return binary; 396 | } 397 | }; 398 | 399 | template <> struct Decoder { 400 | static std::string decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 401 | auto binary = fine::decode(env, term); 402 | return std::string( 403 | {reinterpret_cast(binary.data), binary.size}); 404 | } 405 | }; 406 | 407 | template <> struct Decoder { 408 | static Atom decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 409 | unsigned int length; 410 | if (!enif_get_atom_length(env, term, &length, FINE_ERL_NIF_CHAR_ENCODING)) { 411 | throw std::invalid_argument("decode failed, expected an atom"); 412 | } 413 | 414 | auto buffer = std::make_unique(length + 1); 415 | 416 | // Note that enif_get_atom writes the NULL byte at the end 417 | if (!enif_get_atom(env, term, buffer.get(), length + 1, 418 | FINE_ERL_NIF_CHAR_ENCODING)) { 419 | throw std::invalid_argument("decode failed, expected an atom"); 420 | } 421 | 422 | return Atom(std::string(buffer.get(), length)); 423 | } 424 | }; 425 | 426 | template struct Decoder> { 427 | static std::optional decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 428 | char atom_string[4]; 429 | if (enif_get_atom(env, term, atom_string, 4, ERL_NIF_LATIN1) == 4) { 430 | if (strcmp(atom_string, "nil") == 0) { 431 | return std::nullopt; 432 | } 433 | } 434 | 435 | return fine::decode(env, term); 436 | } 437 | }; 438 | 439 | template struct Decoder> { 440 | static std::variant decode(ErlNifEnv *env, 441 | const ERL_NIF_TERM &term) { 442 | return do_decode(env, term); 443 | } 444 | 445 | private: 446 | template 447 | static std::variant do_decode(ErlNifEnv *env, 448 | const ERL_NIF_TERM &term) { 449 | try { 450 | return fine::decode(env, term); 451 | } catch (const std::invalid_argument &) { 452 | if constexpr (sizeof...(Rest) > 0) { 453 | return do_decode(env, term); 454 | } else { 455 | throw std::invalid_argument( 456 | "decode failed, none of the variant types could be decoded"); 457 | } 458 | } 459 | } 460 | }; 461 | 462 | template struct Decoder> { 463 | static std::tuple decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 464 | constexpr auto expected_size = sizeof...(Args); 465 | 466 | int size; 467 | const ERL_NIF_TERM *terms; 468 | if (!enif_get_tuple(env, term, &size, &terms)) { 469 | throw std::invalid_argument("decode failed, expected a tuple"); 470 | } 471 | 472 | if (size != expected_size) { 473 | throw std::invalid_argument("decode failed, expected tuple to have " + 474 | std::to_string(expected_size) + 475 | " elements, but had " + std::to_string(size)); 476 | } 477 | 478 | return do_decode(env, terms, std::make_index_sequence()); 479 | } 480 | 481 | private: 482 | template 483 | static std::tuple do_decode(ErlNifEnv *env, 484 | const ERL_NIF_TERM *terms, 485 | std::index_sequence) { 486 | return std::make_tuple(fine::decode(env, terms[Indices])...); 487 | } 488 | }; 489 | 490 | template struct Decoder> { 491 | static std::vector decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 492 | unsigned int length; 493 | 494 | if (!enif_get_list_length(env, term, &length)) { 495 | throw std::invalid_argument("decode failed, expected a list"); 496 | } 497 | 498 | std::vector vector; 499 | vector.reserve(length); 500 | 501 | auto list = term; 502 | 503 | ERL_NIF_TERM head, tail; 504 | while (enif_get_list_cell(env, list, &head, &tail)) { 505 | auto elem = fine::decode(env, head); 506 | vector.push_back(elem); 507 | list = tail; 508 | } 509 | 510 | return vector; 511 | } 512 | }; 513 | 514 | template struct Decoder> { 515 | static std::map decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 516 | auto map = std::map(); 517 | 518 | ERL_NIF_TERM key, value; 519 | ErlNifMapIterator iter; 520 | if (!enif_map_iterator_create(env, term, &iter, 521 | ERL_NIF_MAP_ITERATOR_FIRST)) { 522 | throw std::invalid_argument("decode failed, expected a map"); 523 | } 524 | 525 | // Define RAII cleanup for the iterator 526 | auto cleanup = IterCleanup{env, iter}; 527 | 528 | while (enif_map_iterator_get_pair(env, &iter, &key, &value)) { 529 | map[fine::decode(env, key)] = fine::decode(env, value); 530 | enif_map_iterator_next(env, &iter); 531 | } 532 | 533 | return map; 534 | } 535 | 536 | private: 537 | struct IterCleanup { 538 | ErlNifEnv *env; 539 | ErlNifMapIterator iter; 540 | 541 | ~IterCleanup() { enif_map_iterator_destroy(env, &iter); } 542 | }; 543 | }; 544 | 545 | template struct Decoder> { 546 | static ResourcePtr decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 547 | void *ptr; 548 | auto type = ResourcePtr::resource_type; 549 | 550 | if (!enif_get_resource(env, term, type, &ptr)) { 551 | throw std::invalid_argument( 552 | "decode failed, expected a resource reference"); 553 | } 554 | 555 | enif_keep_resource(ptr); 556 | 557 | return ResourcePtr( 558 | reinterpret_cast<__private__::ResourceWrapper *>(ptr)); 559 | } 560 | }; 561 | 562 | template 563 | struct Decoder> { 564 | static T decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { 565 | ERL_NIF_TERM struct_value; 566 | if (!enif_get_map_value(env, term, 567 | encode(env, __private__::atoms::__struct__), 568 | &struct_value)) { 569 | throw std::invalid_argument("decode failed, expected a struct"); 570 | } 571 | 572 | // Make sure __struct__ matches 573 | const auto &struct_atom = *T::module; 574 | if (enif_compare(struct_value, encode(env, struct_atom)) != 0) { 575 | throw std::invalid_argument("decode failed, expected a " + 576 | struct_atom.to_string() + " struct"); 577 | } 578 | 579 | T ex_struct; 580 | 581 | constexpr auto fields = T::fields(); 582 | 583 | std::apply( 584 | [&](auto... field) { 585 | (set_field(env, term, ex_struct, std::get<0>(field), 586 | std::get<1>(field)), 587 | ...); 588 | }, 589 | fields); 590 | 591 | return ex_struct; 592 | } 593 | 594 | private: 595 | template 596 | static void set_field(ErlNifEnv *env, ERL_NIF_TERM term, T &ex_struct, 597 | U T::*field_ptr, const Atom *atom) { 598 | ERL_NIF_TERM value; 599 | if (!enif_get_map_value(env, term, encode(env, *atom), &value)) { 600 | throw std::invalid_argument( 601 | "decode failed, expected the struct to have " + atom->to_string() + 602 | " field"); 603 | } 604 | 605 | ex_struct.*(field_ptr) = fine::decode(env, value); 606 | } 607 | }; 608 | 609 | template <> struct Encoder { 610 | static ERL_NIF_TERM encode(ErlNifEnv *, const Term &term) { return term; } 611 | }; 612 | 613 | template <> struct Encoder { 614 | static ERL_NIF_TERM encode(ErlNifEnv *env, const int64_t &integer) { 615 | return enif_make_int64(env, integer); 616 | } 617 | }; 618 | 619 | template <> struct Encoder { 620 | static ERL_NIF_TERM encode(ErlNifEnv *env, const uint64_t &integer) { 621 | return enif_make_uint64(env, integer); 622 | } 623 | }; 624 | 625 | template <> struct Encoder { 626 | static ERL_NIF_TERM encode(ErlNifEnv *env, const double &number) { 627 | return enif_make_double(env, number); 628 | } 629 | }; 630 | 631 | template <> struct Encoder { 632 | static ERL_NIF_TERM encode(ErlNifEnv *env, const bool &boolean) { 633 | return fine::encode(env, boolean ? __private__::atoms::true_ 634 | : __private__::atoms::false_); 635 | } 636 | }; 637 | 638 | // enif_make_pid is a macro that does a cast (const ERL_NIF_TERM) 639 | // and GCC complains that the cast is ignored, so we ignore this 640 | // specific warning explicitly here. 641 | #ifdef __GNUC__ 642 | #pragma GCC diagnostic push 643 | #pragma GCC diagnostic ignored "-Wignored-qualifiers" 644 | #endif 645 | 646 | template <> struct Encoder { 647 | static ERL_NIF_TERM encode(ErlNifEnv *env, const ErlNifPid &pid) { 648 | return enif_make_pid(env, &pid); 649 | } 650 | }; 651 | 652 | #ifdef __GNUC__ 653 | #pragma GCC diagnostic pop 654 | #endif 655 | 656 | template <> struct Encoder { 657 | static ERL_NIF_TERM encode(ErlNifEnv *env, const ErlNifBinary &binary) { 658 | return enif_make_binary(env, const_cast(&binary)); 659 | } 660 | }; 661 | 662 | template <> struct Encoder { 663 | static ERL_NIF_TERM encode(ErlNifEnv *env, const std::string &string) { 664 | ERL_NIF_TERM term; 665 | auto data = enif_make_new_binary(env, string.length(), &term); 666 | if (data == nullptr) { 667 | throw std::runtime_error("encode failed, failed to allocate new binary"); 668 | } 669 | memcpy(data, string.data(), string.length()); 670 | return term; 671 | } 672 | }; 673 | 674 | template <> struct Encoder { 675 | static ERL_NIF_TERM encode(ErlNifEnv *env, const Atom &atom) { 676 | if (atom.term) { 677 | return atom.term.value(); 678 | } else { 679 | return fine::__private__::make_atom(env, atom.name.c_str()); 680 | } 681 | } 682 | }; 683 | 684 | template <> struct Encoder { 685 | static ERL_NIF_TERM encode(ErlNifEnv *env, const std::nullopt_t &) { 686 | return fine::encode(env, __private__::atoms::nil); 687 | } 688 | }; 689 | 690 | template struct Encoder> { 691 | static ERL_NIF_TERM encode(ErlNifEnv *env, const std::optional &optional) { 692 | if (optional) { 693 | return fine::encode(env, optional.value()); 694 | } else { 695 | return fine::encode(env, __private__::atoms::nil); 696 | } 697 | } 698 | }; 699 | 700 | template struct Encoder> { 701 | static ERL_NIF_TERM encode(ErlNifEnv *env, 702 | const std::variant &variant) { 703 | return do_encode(env, variant); 704 | } 705 | 706 | private: 707 | template 708 | static ERL_NIF_TERM do_encode(ErlNifEnv *env, 709 | const std::variant &variant) { 710 | if (auto value = std::get_if(&variant)) { 711 | return fine::encode(env, *value); 712 | } 713 | 714 | if constexpr (sizeof...(Rest) > 0) { 715 | return do_encode(env, variant); 716 | } else { 717 | throw std::runtime_error("unreachable"); 718 | } 719 | } 720 | }; 721 | 722 | template struct Encoder> { 723 | static ERL_NIF_TERM encode(ErlNifEnv *env, const std::tuple &tuple) { 724 | return do_encode(env, tuple, std::make_index_sequence()); 725 | } 726 | 727 | private: 728 | template 729 | static ERL_NIF_TERM do_encode(ErlNifEnv *env, 730 | const std::tuple &tuple, 731 | std::index_sequence) { 732 | constexpr auto size = sizeof...(Args); 733 | return enif_make_tuple(env, size, 734 | fine::encode(env, std::get(tuple))...); 735 | } 736 | }; 737 | 738 | template struct Encoder> { 739 | static ERL_NIF_TERM encode(ErlNifEnv *env, const std::vector &vector) { 740 | auto terms = std::vector(); 741 | terms.reserve(vector.size()); 742 | 743 | for (const auto &item : vector) { 744 | terms.push_back(fine::encode(env, item)); 745 | } 746 | 747 | return enif_make_list_from_array(env, terms.data(), 748 | static_cast(terms.size())); 749 | } 750 | }; 751 | 752 | template struct Encoder> { 753 | static ERL_NIF_TERM encode(ErlNifEnv *env, const std::map &map) { 754 | auto keys = std::vector(); 755 | auto values = std::vector(); 756 | 757 | for (const auto &[key, value] : map) { 758 | keys.push_back(fine::encode(env, key)); 759 | values.push_back(fine::encode(env, value)); 760 | } 761 | 762 | ERL_NIF_TERM map_term; 763 | if (!enif_make_map_from_arrays(env, keys.data(), values.data(), keys.size(), 764 | &map_term)) { 765 | throw std::runtime_error("encode failed, failed to make a map"); 766 | } 767 | 768 | return map_term; 769 | } 770 | }; 771 | 772 | template struct Encoder> { 773 | static ERL_NIF_TERM encode(ErlNifEnv *env, const ResourcePtr &resource) { 774 | return enif_make_resource(env, reinterpret_cast(resource.get())); 775 | } 776 | }; 777 | 778 | template 779 | struct Encoder> { 780 | static ERL_NIF_TERM encode(ErlNifEnv *env, const T &ex_struct) { 781 | const auto &struct_atom = *T::module; 782 | constexpr auto fields = T::fields(); 783 | constexpr auto is_exception = get_is_exception(); 784 | 785 | constexpr auto num_fields = std::tuple_size::value; 786 | constexpr auto num_extra_fields = is_exception ? 2 : 1; 787 | 788 | ERL_NIF_TERM keys[num_extra_fields + num_fields]; 789 | ERL_NIF_TERM values[num_extra_fields + num_fields]; 790 | 791 | keys[0] = fine::encode(env, __private__::atoms::__struct__); 792 | values[0] = fine::encode(env, struct_atom); 793 | 794 | if constexpr (is_exception) { 795 | keys[1] = fine::encode(env, __private__::atoms::__exception__); 796 | values[1] = fine::encode(env, __private__::atoms::true_); 797 | } 798 | 799 | put_key_values(env, ex_struct, keys + num_extra_fields, 800 | values + num_extra_fields, 801 | std::make_index_sequence()); 802 | 803 | ERL_NIF_TERM map; 804 | if (!enif_make_map_from_arrays(env, keys, values, 805 | num_extra_fields + num_fields, &map)) { 806 | throw std::runtime_error("encode failed, failed to make a map"); 807 | } 808 | 809 | return map; 810 | } 811 | 812 | private: 813 | template 814 | static void put_key_values(ErlNifEnv *env, const T &ex_struct, 815 | ERL_NIF_TERM keys[], ERL_NIF_TERM values[], 816 | std::index_sequence) { 817 | constexpr auto fields = T::fields(); 818 | 819 | std::apply( 820 | [&](auto... field) { 821 | ((keys[Indices] = fine::encode(env, *std::get<1>(field)), 822 | values[Indices] = 823 | fine::encode(env, ex_struct.*(std::get<0>(field)))), 824 | ...); 825 | }, 826 | fields); 827 | } 828 | 829 | static constexpr bool get_is_exception() { 830 | if constexpr (has_is_exception::value) { 831 | return T::is_exception; 832 | } else { 833 | return false; 834 | } 835 | } 836 | 837 | template 838 | struct has_is_exception : std::false_type {}; 839 | 840 | template 841 | struct has_is_exception> 842 | : std::true_type {}; 843 | }; 844 | 845 | template struct Encoder> { 846 | static ERL_NIF_TERM encode(ErlNifEnv *env, const Ok &ok) { 847 | auto tag = __private__::atoms::ok; 848 | 849 | if constexpr (sizeof...(Args) > 0) { 850 | return fine::encode(env, std::tuple_cat(std::tuple(tag), ok.items)); 851 | } else { 852 | return fine::encode(env, tag); 853 | } 854 | } 855 | }; 856 | 857 | template struct Encoder> { 858 | static ERL_NIF_TERM encode(ErlNifEnv *env, const Error &error) { 859 | auto tag = __private__::atoms::error; 860 | 861 | if constexpr (sizeof...(Args) > 0) { 862 | return fine::encode(env, std::tuple_cat(std::tuple(tag), error.items)); 863 | } else { 864 | return fine::encode(env, tag); 865 | } 866 | } 867 | }; 868 | 869 | namespace __private__ { 870 | class ExceptionError : public std::exception { 871 | public: 872 | ERL_NIF_TERM reason; 873 | 874 | ExceptionError(ERL_NIF_TERM reason) : reason(reason) {} 875 | const char *what() const noexcept { return "erlang exception raised"; } 876 | }; 877 | } // namespace __private__ 878 | 879 | // Raises an Elixir exception with the given value as reason. 880 | template void raise(ErlNifEnv *env, const T &value) { 881 | auto term = encode(env, value); 882 | throw __private__::ExceptionError(term); 883 | } 884 | 885 | // Mechanism for accumulating information via static object definitions. 886 | class Registration { 887 | public: 888 | template 889 | static Registration register_resource(const char *name) { 890 | Registration::resources.push_back({&fine::ResourcePtr::resource_type, 891 | name, 892 | __private__::ResourceWrapper::dtor}); 893 | return {}; 894 | } 895 | 896 | static Registration register_nif(ErlNifFunc erl_nif_func) { 897 | Registration::erl_nif_funcs.push_back(erl_nif_func); 898 | return {}; 899 | } 900 | 901 | private: 902 | static bool init_resources(ErlNifEnv *env) { 903 | for (const auto &[resource_type_ptr, name, dtor] : 904 | Registration::resources) { 905 | auto flags = ERL_NIF_RT_CREATE; 906 | auto type = enif_open_resource_type(env, NULL, name, dtor, flags, NULL); 907 | 908 | if (type) { 909 | *resource_type_ptr = type; 910 | } else { 911 | return false; 912 | } 913 | } 914 | 915 | Registration::resources.clear(); 916 | 917 | return true; 918 | } 919 | 920 | friend std::vector &__private__::get_erl_nif_funcs(); 921 | 922 | friend int __private__::load(ErlNifEnv *env, void **priv_data, 923 | ERL_NIF_TERM load_info); 924 | 925 | inline static std::vector> 927 | resources = {}; 928 | 929 | inline static std::vector erl_nif_funcs = {}; 930 | }; 931 | 932 | // NIF definitions 933 | 934 | namespace __private__ { 935 | inline ERL_NIF_TERM raise_error_with_message(ErlNifEnv *env, Atom module, 936 | std::string message) { 937 | ERL_NIF_TERM keys[3] = {fine::encode(env, __private__::atoms::__struct__), 938 | fine::encode(env, __private__::atoms::__exception__), 939 | fine::encode(env, __private__::atoms::message)}; 940 | ERL_NIF_TERM values[3] = { 941 | fine::encode(env, module), 942 | fine::encode(env, __private__::atoms::true_), 943 | fine::encode(env, message), 944 | }; 945 | 946 | ERL_NIF_TERM map; 947 | if (!enif_make_map_from_arrays(env, keys, values, 3, &map)) { 948 | return enif_raise_exception(env, encode(env, message)); 949 | } 950 | 951 | return enif_raise_exception(env, map); 952 | } 953 | 954 | template 955 | ERL_NIF_TERM nif_impl(ErlNifEnv *env, const ERL_NIF_TERM argv[], 956 | Return (*fun)(ErlNifEnv *, Args...), 957 | std::index_sequence) { 958 | try { 959 | auto result = fun(env, decode(env, argv[Indices])...); 960 | return encode(env, result); 961 | } catch (const ExceptionError &error) { 962 | return enif_raise_exception(env, error.reason); 963 | } catch (const std::invalid_argument &error) { 964 | return raise_error_with_message( 965 | env, __private__::atoms::ElixirArgumentError, error.what()); 966 | } catch (const std::runtime_error &error) { 967 | return raise_error_with_message(env, __private__::atoms::ElixirRuntimeError, 968 | error.what()); 969 | } catch (...) { 970 | return raise_error_with_message(env, __private__::atoms::ElixirRuntimeError, 971 | "unknown exception thrown within NIF"); 972 | } 973 | } 974 | } // namespace __private__ 975 | 976 | template 977 | ERL_NIF_TERM nif(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[], 978 | Return (*fun)(ErlNifEnv *, Args...)) { 979 | const auto num_args = sizeof...(Args); 980 | 981 | if (num_args != argc) { 982 | return enif_raise_exception( 983 | env, encode(env, std::string("wrong number of arguments"))); 984 | } 985 | 986 | return __private__::nif_impl(env, argv, fun, 987 | std::make_index_sequence()); 988 | } 989 | 990 | template 991 | constexpr unsigned int nif_arity(Ret (*)(Args...)) { 992 | return sizeof...(Args) - 1; 993 | } 994 | 995 | namespace __private__ { 996 | inline std::vector &get_erl_nif_funcs() { 997 | return Registration::erl_nif_funcs; 998 | } 999 | 1000 | inline int load(ErlNifEnv *env, void **, ERL_NIF_TERM) { 1001 | Atom::init_atoms(env); 1002 | 1003 | if (!Registration::init_resources(env)) { 1004 | return -1; 1005 | } 1006 | 1007 | return 0; 1008 | } 1009 | } // namespace __private__ 1010 | 1011 | // Macros 1012 | 1013 | #define FINE_NIF(name, flags) \ 1014 | static ERL_NIF_TERM name##_nif(ErlNifEnv *env, int argc, \ 1015 | const ERL_NIF_TERM argv[]) { \ 1016 | return fine::nif(env, argc, argv, name); \ 1017 | } \ 1018 | auto __nif_registration_##name = fine::Registration::register_nif( \ 1019 | {#name, fine::nif_arity(name), name##_nif, flags}); \ 1020 | static_assert(true, "require a semicolon after the macro") 1021 | 1022 | // Note that we use static, in case FINE_REASOURCE is used in another 1023 | // translation unit on the same line. 1024 | 1025 | #define FINE_RESOURCE(class_name) \ 1026 | static auto __FINE_CONCAT__(__resource_registration_, __LINE__) = \ 1027 | fine::Registration::register_resource(#class_name); \ 1028 | static_assert(true, "require a semicolon after the macro") 1029 | 1030 | // An extra level of indirection is necessary to make sure __LINE__ 1031 | // is expanded before concatenation. 1032 | #define __FINE_CONCAT__(a, b) __FINE_CONCAT_IMPL__(a, b) 1033 | #define __FINE_CONCAT_IMPL__(a, b) a##b 1034 | 1035 | // This is a modified version of ERL_NIF_INIT that points to the 1036 | // registered NIF functions and also sets the load callback. 1037 | 1038 | #define FINE_INIT(name) \ 1039 | ERL_NIF_INIT_PROLOGUE \ 1040 | ERL_NIF_INIT_GLOB \ 1041 | ERL_NIF_INIT_DECL(NAME); \ 1042 | ERL_NIF_INIT_DECL(NAME) { \ 1043 | auto &nif_funcs = fine::__private__::get_erl_nif_funcs(); \ 1044 | auto num_funcs = static_cast(nif_funcs.size()); \ 1045 | auto funcs = nif_funcs.data(); \ 1046 | auto load = fine::__private__::load; \ 1047 | static ErlNifEntry entry = {ERL_NIF_MAJOR_VERSION, \ 1048 | ERL_NIF_MINOR_VERSION, \ 1049 | name, \ 1050 | num_funcs, \ 1051 | funcs, \ 1052 | load, \ 1053 | NULL, \ 1054 | NULL, \ 1055 | NULL, \ 1056 | ERL_NIF_VM_VARIANT, \ 1057 | 1, \ 1058 | sizeof(ErlNifResourceTypeInit), \ 1059 | ERL_NIF_MIN_ERTS_VERSION}; \ 1060 | ERL_NIF_INIT_BODY; \ 1061 | return &entry; \ 1062 | } \ 1063 | ERL_NIF_INIT_EPILOGUE \ 1064 | static_assert(true, "require a semicolon after the macro") 1065 | 1066 | } // namespace fine 1067 | 1068 | #endif 1069 | -------------------------------------------------------------------------------- /lib/fine.ex: -------------------------------------------------------------------------------- 1 | defmodule Fine do 2 | @external_resource "README.md" 3 | 4 | [_, readme_docs, _] = 5 | "README.md" 6 | |> File.read!() 7 | |> String.split("") 8 | 9 | @moduledoc readme_docs 10 | 11 | @include_dir Path.expand("include") 12 | 13 | @doc """ 14 | Returns the directory with Fine header files. 15 | """ 16 | @spec include_dir() :: String.t() 17 | def include_dir(), do: @include_dir 18 | end 19 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Fine.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.1.0" 5 | @description "C++ library enabling more ergonomic NIFs, tailored to Elixir" 6 | @github_url "https://github.com/elixir-nx/fine" 7 | 8 | def project do 9 | [ 10 | app: :fine, 11 | version: @version, 12 | elixir: "~> 1.15", 13 | name: "Fine", 14 | description: @description, 15 | start_permanent: Mix.env() == :prod, 16 | deps: deps(), 17 | aliases: aliases(), 18 | docs: docs(), 19 | package: package() 20 | ] 21 | end 22 | 23 | def application do 24 | [] 25 | end 26 | 27 | defp deps do 28 | [ 29 | {:ex_doc, "~> 0.37", only: :dev, runtime: false}, 30 | {:makeup_syntect, "~> 0.1", only: :dev, runtime: false} 31 | ] 32 | end 33 | 34 | defp aliases() do 35 | [test: fn _ -> Mix.shell().error("To run tests, go to the test directory") end] 36 | end 37 | 38 | defp docs() do 39 | [ 40 | main: "Fine", 41 | source_url: @github_url, 42 | source_ref: "v#{@version}" 43 | ] 44 | end 45 | 46 | defp package do 47 | [ 48 | licenses: ["Apache-2.0"], 49 | links: %{"GitHub" => @github_url}, 50 | files: ~w(include lib mix.exs README.md LICENSE CHANGELOG.md) 51 | ] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, 4 | "ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [: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", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"}, 5 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 6 | "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"}, 7 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 8 | "makeup_syntect": {:hex, :makeup_syntect, "0.1.3", "ae2c3437f479ea50d08d794acaf02a2f3a8c338dd1f757f6b237c42eb27fcde1", [:mix], [{:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: false]}, {:rustler, "~> 0.36.1", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8.2", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "a27bd3bd8f7b87465d110295a33ed1022202bea78701bd2bbeadfb45d690cdbf"}, 9 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 10 | "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, 11 | } 12 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | finest-*.tar 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | PRIV_DIR := $(MIX_APP_PATH)/priv 2 | NIF_PATH := $(PRIV_DIR)/libfinest.so 3 | C_SRC := $(shell pwd)/c_src 4 | 5 | CPPFLAGS := -shared -fPIC -fvisibility=hidden -std=c++17 -Wall -Wextra 6 | CPPFLAGS += -I$(ERTS_INCLUDE_DIR) -I$(FINE_INCLUDE_DIR) 7 | # We want to eliminate all warnings, so the end user doesn't see any. 8 | CPPFLAGS += -Werror 9 | 10 | ifdef DEBUG 11 | CPPFLAGS += -g 12 | else 13 | CPPFLAGS += -O3 14 | endif 15 | 16 | ifndef TARGET_ABI 17 | TARGET_ABI := $(shell uname -s | tr '[:upper:]' '[:lower:]') 18 | endif 19 | 20 | ifeq ($(TARGET_ABI),darwin) 21 | CPPFLAGS += -undefined dynamic_lookup -flat_namespace 22 | endif 23 | 24 | SOURCES := $(wildcard $(C_SRC)/*.cpp) 25 | # We add dependency on Fine header file, since it can change as we test. 26 | FINE_HEADERS := $(wildcard $(FINE_INCLUDE_DIR)/*.hpp) 27 | 28 | all: $(NIF_PATH) 29 | @ echo > /dev/null # Dummy command to avoid the default output "Nothing to be done" 30 | 31 | $(NIF_PATH): $(SOURCES) $(FINE_HEADERS) 32 | @ mkdir -p $(PRIV_DIR) 33 | $(CXX) $(CPPFLAGS) $(SOURCES) -o $(NIF_PATH) 34 | -------------------------------------------------------------------------------- /test/Makefile.win: -------------------------------------------------------------------------------- 1 | PRIV_DIR=$(MIX_APP_PATH)\priv 2 | NIF_PATH=$(PRIV_DIR)\libfinest.dll 3 | C_SRC=$(MAKEDIR)\c_src 4 | 5 | CPPFLAGS=/LD /std:c++17 /W4 /O2 /EHsc 6 | CPPFLAGS=$(CPPFLAGS) /I"$(ERTS_INCLUDE_DIR)" /I"$(FINE_INCLUDE_DIR)" 7 | # We want to eliminate all warnings, so the end user doesn't see any. 8 | CPPFLAGS=$(CPPFLAGS) /WX 9 | 10 | SOURCES=$(C_SRC)\*.cpp 11 | # We add dependency on Fine header file, since it can change as we test. 12 | FINE_HEADERS=$(FINE_INCLUDE_DIR)\*.hpp 13 | 14 | all: $(NIF_PATH) 15 | 16 | $(NIF_PATH): $(SOURCES) $(FINE_HEADERS) 17 | @ if not exist "$(PRIV_DIR)" mkdir "$(PRIV_DIR)" 18 | cl $(CPPFLAGS) $(SOURCES) /Fe"$(NIF_PATH)" 19 | -------------------------------------------------------------------------------- /test/c_src/finest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace finest { 10 | 11 | namespace atoms { 12 | auto ElixirFinestError = fine::Atom("Elixir.Finest.Error"); 13 | auto ElixirFinestPoint = fine::Atom("Elixir.Finest.Point"); 14 | auto data = fine::Atom("data"); 15 | auto destructor_with_env = fine::Atom("destructor_with_env"); 16 | auto destructor_default = fine::Atom("destructor_default"); 17 | auto x = fine::Atom("x"); 18 | auto y = fine::Atom("y"); 19 | } // namespace atoms 20 | 21 | struct TestResource { 22 | ErlNifPid pid; 23 | 24 | TestResource(ErlNifPid pid) : pid(pid) {} 25 | 26 | void destructor(ErlNifEnv *env) { 27 | auto msg_env = enif_alloc_env(); 28 | auto msg = fine::encode(msg_env, atoms::destructor_with_env); 29 | enif_send(env, &this->pid, msg_env, msg); 30 | enif_free_env(msg_env); 31 | } 32 | 33 | ~TestResource() { 34 | auto target_pid = this->pid; 35 | 36 | // We don't have access to env, so we spawn another thread and 37 | // pass NULL as the env. In usual cases messages should be sent 38 | // as part of the custom destructor, as we do above, but here we 39 | // want to test that both of them are called. 40 | auto thread = std::thread([target_pid] { 41 | auto msg_env = enif_alloc_env(); 42 | auto msg = fine::encode(msg_env, atoms::destructor_default); 43 | enif_send(NULL, &target_pid, msg_env, msg); 44 | enif_free_env(msg_env); 45 | }); 46 | 47 | thread.detach(); 48 | } 49 | }; 50 | FINE_RESOURCE(TestResource); 51 | 52 | struct ExPoint { 53 | int64_t x; 54 | int64_t y; 55 | 56 | static constexpr auto module = &atoms::ElixirFinestPoint; 57 | 58 | static constexpr auto fields() { 59 | return std::make_tuple(std::make_tuple(&ExPoint::x, &atoms::x), 60 | std::make_tuple(&ExPoint::y, &atoms::y)); 61 | } 62 | }; 63 | 64 | struct ExError { 65 | int64_t data; 66 | 67 | static constexpr auto module = &atoms::ElixirFinestError; 68 | 69 | static constexpr auto fields() { 70 | return std::make_tuple(std::make_tuple(&ExError::data, &atoms::data)); 71 | } 72 | 73 | static constexpr auto is_exception = true; 74 | }; 75 | 76 | int64_t add(ErlNifEnv *, int64_t x, int64_t y) { return x + y; } 77 | FINE_NIF(add, 0); 78 | 79 | fine::Term codec_term(ErlNifEnv *, fine::Term term) { return term; } 80 | FINE_NIF(codec_term, 0); 81 | 82 | int64_t codec_int64(ErlNifEnv *, int64_t term) { return term; } 83 | FINE_NIF(codec_int64, 0); 84 | 85 | uint64_t codec_uint64(ErlNifEnv *, uint64_t term) { return term; } 86 | FINE_NIF(codec_uint64, 0); 87 | 88 | double codec_double(ErlNifEnv *, double term) { return term; } 89 | FINE_NIF(codec_double, 0); 90 | 91 | bool codec_bool(ErlNifEnv *, bool term) { return term; } 92 | FINE_NIF(codec_bool, 0); 93 | 94 | ErlNifPid codec_pid(ErlNifEnv *, ErlNifPid term) { return term; } 95 | FINE_NIF(codec_pid, 0); 96 | 97 | ErlNifBinary codec_binary(ErlNifEnv *, ErlNifBinary term) { 98 | ErlNifBinary copy; 99 | enif_alloc_binary(term.size, ©); 100 | std::memcpy(copy.data, term.data, term.size); 101 | return copy; 102 | } 103 | FINE_NIF(codec_binary, 0); 104 | 105 | std::string codec_string(ErlNifEnv *, std::string term) { return term; } 106 | FINE_NIF(codec_string, 0); 107 | 108 | fine::Atom codec_atom(ErlNifEnv *, fine::Atom term) { return term; } 109 | FINE_NIF(codec_atom, 0); 110 | 111 | std::nullopt_t codec_nullopt(ErlNifEnv *) { return std::nullopt; } 112 | FINE_NIF(codec_nullopt, 0); 113 | 114 | std::optional codec_optional_int64(ErlNifEnv *, 115 | std::optional term) { 116 | return term; 117 | } 118 | FINE_NIF(codec_optional_int64, 0); 119 | 120 | std::variant 121 | codec_variant_int64_or_string(ErlNifEnv *, 122 | std::variant term) { 123 | return term; 124 | } 125 | FINE_NIF(codec_variant_int64_or_string, 0); 126 | 127 | std::tuple 128 | codec_tuple_int64_and_string(ErlNifEnv *, 129 | std::tuple term) { 130 | return term; 131 | } 132 | FINE_NIF(codec_tuple_int64_and_string, 0); 133 | 134 | std::vector codec_vector_int64(ErlNifEnv *, 135 | std::vector term) { 136 | return term; 137 | } 138 | FINE_NIF(codec_vector_int64, 0); 139 | 140 | std::map 141 | codec_map_atom_int64(ErlNifEnv *, std::map term) { 142 | return term; 143 | } 144 | FINE_NIF(codec_map_atom_int64, 0); 145 | 146 | fine::ResourcePtr 147 | codec_resource(ErlNifEnv *, fine::ResourcePtr term) { 148 | return term; 149 | } 150 | FINE_NIF(codec_resource, 0); 151 | 152 | ExPoint codec_struct(ErlNifEnv *, ExPoint term) { return term; } 153 | FINE_NIF(codec_struct, 0); 154 | 155 | ExError codec_struct_exception(ErlNifEnv *, ExError term) { return term; } 156 | FINE_NIF(codec_struct_exception, 0); 157 | 158 | fine::Ok<> codec_ok_empty(ErlNifEnv *) { return fine::Ok(); } 159 | FINE_NIF(codec_ok_empty, 0); 160 | 161 | fine::Ok codec_ok_int64(ErlNifEnv *, int64_t term) { 162 | return fine::Ok(term); 163 | } 164 | FINE_NIF(codec_ok_int64, 0); 165 | 166 | fine::Error<> codec_error_empty(ErlNifEnv *) { return fine::Error(); } 167 | FINE_NIF(codec_error_empty, 0); 168 | 169 | fine::Error codec_error_string(ErlNifEnv *, std::string term) { 170 | return fine::Error(term); 171 | } 172 | FINE_NIF(codec_error_string, 0); 173 | 174 | fine::ResourcePtr resource_create(ErlNifEnv *, ErlNifPid pid) { 175 | return fine::make_resource(pid); 176 | } 177 | FINE_NIF(resource_create, 0); 178 | 179 | ErlNifPid resource_get(ErlNifEnv *, fine::ResourcePtr resource) { 180 | return resource->pid; 181 | } 182 | FINE_NIF(resource_get, 0); 183 | 184 | int64_t throw_runtime_error(ErlNifEnv *) { 185 | throw std::runtime_error("runtime error reason"); 186 | } 187 | FINE_NIF(throw_runtime_error, 0); 188 | 189 | int64_t throw_invalid_argument(ErlNifEnv *) { 190 | throw std::invalid_argument("invalid argument reason"); 191 | } 192 | FINE_NIF(throw_invalid_argument, 0); 193 | 194 | int64_t throw_other_exception(ErlNifEnv *) { throw std::exception(); } 195 | FINE_NIF(throw_other_exception, 0); 196 | 197 | int64_t raise_elixir_exception(ErlNifEnv *env) { 198 | fine::raise(env, ExError{10}); 199 | 200 | // MSVC detects that raise throws and treats return as unreachable 201 | #if !defined(_WIN32) 202 | return 0; 203 | #endif 204 | } 205 | FINE_NIF(raise_elixir_exception, 0); 206 | 207 | int64_t raise_erlang_error(ErlNifEnv *env) { 208 | fine::raise(env, fine::Atom("oops")); 209 | 210 | // MSVC detects that raise throws and treats return as unreachable 211 | #if !defined(_WIN32) 212 | return 0; 213 | #endif 214 | } 215 | FINE_NIF(raise_erlang_error, 0); 216 | 217 | } // namespace finest 218 | 219 | FINE_INIT("Elixir.Finest.NIF"); 220 | -------------------------------------------------------------------------------- /test/lib/finest/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Finest.Error do 2 | defexception [:data] 3 | 4 | @impl true 5 | def message(error) do 6 | "got error with data #{error.data}" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/lib/finest/nif.ex: -------------------------------------------------------------------------------- 1 | defmodule Finest.NIF do 2 | @moduledoc false 3 | 4 | @on_load :__on_load__ 5 | 6 | def __on_load__ do 7 | path = :filename.join(:code.priv_dir(:finest), ~c"libfinest") 8 | 9 | case :erlang.load_nif(path, 0) do 10 | :ok -> :ok 11 | {:error, reason} -> raise "failed to load NIF library, reason: #{inspect(reason)}" 12 | end 13 | end 14 | 15 | def add(_x, _y), do: err!() 16 | 17 | def codec_term(_term), do: err!() 18 | def codec_int64(_term), do: err!() 19 | def codec_uint64(_term), do: err!() 20 | def codec_double(_term), do: err!() 21 | def codec_bool(_term), do: err!() 22 | def codec_pid(_term), do: err!() 23 | def codec_binary(_term), do: err!() 24 | def codec_string(_term), do: err!() 25 | def codec_atom(_term), do: err!() 26 | def codec_nullopt(), do: err!() 27 | def codec_optional_int64(_term), do: err!() 28 | def codec_variant_int64_or_string(_term), do: err!() 29 | def codec_tuple_int64_and_string(_term), do: err!() 30 | def codec_vector_int64(_term), do: err!() 31 | def codec_map_atom_int64(_term), do: err!() 32 | def codec_resource(_term), do: err!() 33 | def codec_struct(_term), do: err!() 34 | def codec_struct_exception(_term), do: err!() 35 | def codec_ok_empty(), do: err!() 36 | def codec_ok_int64(_term), do: err!() 37 | def codec_error_empty(), do: err!() 38 | def codec_error_string(_term), do: err!() 39 | 40 | def resource_create(_pid), do: err!() 41 | def resource_get(_resource), do: err!() 42 | 43 | def throw_runtime_error(), do: err!() 44 | def throw_invalid_argument(), do: err!() 45 | def throw_other_exception(), do: err!() 46 | def raise_elixir_exception(), do: err!() 47 | def raise_erlang_error(), do: err!() 48 | 49 | defp err!(), do: :erlang.nif_error(:not_loaded) 50 | end 51 | -------------------------------------------------------------------------------- /test/lib/finest/point.ex: -------------------------------------------------------------------------------- 1 | defmodule Finest.Point do 2 | defstruct [:x, :y] 3 | end 4 | -------------------------------------------------------------------------------- /test/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Finest.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :finest, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | compilers: [:elixir_make] ++ Mix.compilers(), 11 | deps: deps(), 12 | make_env: fn -> %{"FINE_INCLUDE_DIR" => Fine.include_dir()} end 13 | ] 14 | end 15 | 16 | def application do 17 | [] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:fine, path: "..", runtime: false}, 23 | {:elixir_make, "~> 0.9", runtime: false} 24 | ] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 3 | } 4 | -------------------------------------------------------------------------------- /test/test/finest_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FinestTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Finest.NIF 5 | 6 | test "add" do 7 | assert NIF.add(1, 2) == 3 8 | end 9 | 10 | describe "codec" do 11 | test "term" do 12 | assert NIF.codec_term(10) == 10 13 | assert NIF.codec_term("hello world") == "hello world" 14 | assert NIF.codec_term([1, 2, 3]) == [1, 2, 3] 15 | end 16 | 17 | test "int64" do 18 | assert NIF.codec_int64(10) == 10 19 | assert NIF.codec_int64(-10) == -10 20 | 21 | assert_raise ArgumentError, "decode failed, expected an integer", fn -> 22 | NIF.codec_int64(10.0) 23 | end 24 | end 25 | 26 | test "uint64" do 27 | assert NIF.codec_uint64(10) 28 | 29 | assert_raise ArgumentError, "decode failed, expected an unsigned integer", fn -> 30 | NIF.codec_uint64(-10) 31 | end 32 | end 33 | 34 | test "double" do 35 | assert NIF.codec_double(10.0) == 10.0 36 | assert NIF.codec_double(-10.0) == -10.0 37 | 38 | assert_raise ArgumentError, "decode failed, expected a float", fn -> 39 | NIF.codec_double(1) 40 | end 41 | end 42 | 43 | test "bool" do 44 | assert NIF.codec_bool(true) == true 45 | assert NIF.codec_bool(false) == false 46 | 47 | assert_raise ArgumentError, "decode failed, expected a boolean", fn -> 48 | NIF.codec_bool(1) 49 | end 50 | end 51 | 52 | test "pid" do 53 | assert NIF.codec_pid(self()) == self() 54 | 55 | assert_raise ArgumentError, "decode failed, expected a local pid", fn -> 56 | NIF.codec_pid(1) 57 | end 58 | end 59 | 60 | test "binary" do 61 | assert NIF.codec_binary("hello world") == "hello world" 62 | assert NIF.codec_binary(<<0, 1, 2>>) == <<0, 1, 2>> 63 | assert NIF.codec_binary(<<>>) == <<>> 64 | 65 | assert_raise ArgumentError, "decode failed, expected a binary", fn -> 66 | NIF.codec_binary(1) 67 | end 68 | end 69 | 70 | test "string" do 71 | assert NIF.codec_string("hello world") == "hello world" 72 | assert NIF.codec_string(<<0, 1, 2>>) == <<0, 1, 2>> 73 | assert NIF.codec_string(<<>>) == <<>> 74 | 75 | assert_raise ArgumentError, "decode failed, expected a binary", fn -> 76 | NIF.codec_string(1) 77 | end 78 | end 79 | 80 | test "atom" do 81 | assert NIF.codec_atom(:hello) == :hello 82 | 83 | # NIF APIs support UTF8 atoms since OTP 26 84 | if System.otp_release() >= "26" do 85 | assert NIF.codec_atom(:"🦊 in a 📦") == :"🦊 in a 📦" 86 | end 87 | 88 | assert_raise ArgumentError, "decode failed, expected an atom", fn -> 89 | NIF.codec_atom(1) 90 | end 91 | end 92 | 93 | test "nullopt" do 94 | assert NIF.codec_nullopt() == nil 95 | end 96 | 97 | test "optional" do 98 | assert NIF.codec_optional_int64(10) == 10 99 | assert NIF.codec_optional_int64(nil) == nil 100 | 101 | assert_raise ArgumentError, "decode failed, expected an integer", fn -> 102 | NIF.codec_optional_int64(10.0) 103 | end 104 | end 105 | 106 | test "variant" do 107 | assert NIF.codec_variant_int64_or_string(10) == 10 108 | assert NIF.codec_variant_int64_or_string("hello world") == "hello world" 109 | 110 | assert_raise ArgumentError, 111 | "decode failed, none of the variant types could be decoded", 112 | fn -> 113 | NIF.codec_variant_int64_or_string(10.0) 114 | end 115 | end 116 | 117 | test "tuple" do 118 | assert NIF.codec_tuple_int64_and_string({10, "hello world"}) == {10, "hello world"} 119 | 120 | assert_raise ArgumentError, "decode failed, expected a tuple", fn -> 121 | NIF.codec_tuple_int64_and_string(10) 122 | end 123 | 124 | assert_raise ArgumentError, 125 | "decode failed, expected tuple to have 2 elements, but had 0", 126 | fn -> 127 | NIF.codec_tuple_int64_and_string({}) 128 | end 129 | 130 | assert_raise ArgumentError, "decode failed, expected a binary", fn -> 131 | NIF.codec_tuple_int64_and_string({10, 10}) 132 | end 133 | end 134 | 135 | test "vector" do 136 | assert NIF.codec_vector_int64([1, 2, 3]) == [1, 2, 3] 137 | 138 | assert_raise ArgumentError, "decode failed, expected a list", fn -> 139 | NIF.codec_vector_int64(10) 140 | end 141 | 142 | assert_raise ArgumentError, "decode failed, expected an integer", fn -> 143 | NIF.codec_vector_int64([10.0]) 144 | end 145 | end 146 | 147 | test "map" do 148 | assert NIF.codec_map_atom_int64(%{hello: 1, world: 2}) == %{hello: 1, world: 2} 149 | 150 | assert_raise ArgumentError, "decode failed, expected a map", fn -> 151 | NIF.codec_map_atom_int64(10) 152 | end 153 | 154 | assert_raise ArgumentError, "decode failed, expected an atom", fn -> 155 | NIF.codec_map_atom_int64(%{"hello" => 1}) 156 | end 157 | 158 | assert_raise ArgumentError, "decode failed, expected an integer", fn -> 159 | NIF.codec_map_atom_int64(%{hello: 1.0}) 160 | end 161 | end 162 | 163 | test "resource" do 164 | resource = NIF.resource_create(self()) 165 | assert is_reference(resource) 166 | 167 | assert NIF.codec_resource(resource) == resource 168 | 169 | assert_raise ArgumentError, "decode failed, expected a resource reference", fn -> 170 | NIF.codec_resource(10) 171 | end 172 | end 173 | 174 | test "struct" do 175 | struct = %Finest.Point{x: 1, y: 2} 176 | assert NIF.codec_struct(struct) == struct 177 | 178 | assert_raise ArgumentError, "decode failed, expected a struct", fn -> 179 | NIF.codec_struct(10) 180 | end 181 | 182 | assert_raise ArgumentError, "decode failed, expected a struct", fn -> 183 | NIF.codec_struct(%{}) 184 | end 185 | 186 | assert_raise ArgumentError, "decode failed, expected a Elixir.Finest.Point struct", fn -> 187 | NIF.codec_struct(~D"2000-01-01") 188 | end 189 | end 190 | 191 | test "exception struct" do 192 | struct = %Finest.Error{data: 1} 193 | assert NIF.codec_struct_exception(struct) == struct 194 | assert is_exception(NIF.codec_struct_exception(struct)) 195 | 196 | assert_raise ArgumentError, "decode failed, expected a struct", fn -> 197 | NIF.codec_struct_exception(10) 198 | end 199 | end 200 | 201 | test "ok tagged tuple" do 202 | assert NIF.codec_ok_empty() == :ok 203 | assert NIF.codec_ok_int64(10) == {:ok, 10} 204 | end 205 | 206 | test "error tagged tuple" do 207 | assert NIF.codec_error_empty() == :error 208 | assert NIF.codec_error_string("this is the reason") == {:error, "this is the reason"} 209 | end 210 | end 211 | 212 | describe "resource" do 213 | test "survives across NIF calls" do 214 | resource = NIF.resource_create(self()) 215 | assert NIF.resource_get(resource) == self() 216 | end 217 | 218 | test "calls destructors when garbage collected" do 219 | NIF.resource_create(self()) 220 | :erlang.garbage_collect(self()) 221 | 222 | assert_receive :destructor_with_env 223 | assert_receive :destructor_default 224 | end 225 | end 226 | 227 | describe "exceptions" do 228 | test "standard exceptions" do 229 | assert_raise RuntimeError, "runtime error reason", fn -> 230 | NIF.throw_runtime_error() 231 | end 232 | 233 | assert_raise ArgumentError, "invalid argument reason", fn -> 234 | NIF.throw_invalid_argument() 235 | end 236 | 237 | assert_raise RuntimeError, "unknown exception thrown within NIF", fn -> 238 | NIF.throw_other_exception() 239 | end 240 | end 241 | 242 | test "raising an elixir exception" do 243 | assert_raise Finest.Error, "got error with data 10", fn -> 244 | NIF.raise_elixir_exception() 245 | end 246 | end 247 | 248 | test "raising any term" do 249 | assert_raise ErlangError, "Erlang error: :oops", fn -> 250 | NIF.raise_erlang_error() 251 | end 252 | end 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /test/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------