diff --git a/README.md b/README.md index 503095c7..4466b462 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ defmodule MyApp.Mixfile do defp deps() do [ - {:unifex, "~> 0.2.0"} # add unifex to deps + {:unifex, "~> 0.3.0"} # add unifex to deps ] end end @@ -39,7 +39,7 @@ end ## Usage - For detailed usage description see [Creating Unifex NIF](https://hexdocs.pm/unifex/creating_unifex_nif.html) guide. + For detailed usage description see [Creating Unifex Natives](https://hexdocs.pm/unifex/creating_unifex_natives.html) guide. ## See also diff --git a/mix.exs b/mix.exs index 0fe905af..022b7961 100644 --- a/mix.exs +++ b/mix.exs @@ -42,7 +42,7 @@ defmodule Unifex.MixProject do defp docs do [ main: "readme", - extras: ["README.md", "pages/creating_unifex_nif.md"], + extras: ["README.md", "pages/creating_unifex_natives.md"], source_ref: "v#{@version}", nest_modules_by_prefix: [ Unifex.CodeGenerators, diff --git a/pages/creating_unifex_natives.md b/pages/creating_unifex_natives.md new file mode 100644 index 00000000..0eed4252 --- /dev/null +++ b/pages/creating_unifex_natives.md @@ -0,0 +1,318 @@ +# Creating Unifex Natives + +## Introduction +In this section we present how to create Unifex Natives. +We will show it by creating `NIF` but writing `CNodes` is analogous. + +## Preparation + +In order to start working on NIF, you need to prepare a few things: + +1. First, you have to add unifex to your dependencies as well as unifex and bundlex compilers. + Please refer to [Instalation](https://hexdocs.pm/unifex/readme.html#instalation) section to see how to do it. +2. After successful installation we should take a look at [Bundlex](https://github.com/membraneframework/bundlex). + Unifex uses bundlex to compile the native code. + You can think of bundlex as a tool that generates build scripts responsible for including proper libs, + compiling your native code and linking it with mentioned libs. + To make it work, create the `bundlex.exs` file in the project's root directory with the following content: + ```elixir + defmodule Example.BundlexProject do + use Bundlex.Project + + def project() do + [ + natives: natives(Bundlex.platform()) + ] + end + + def natives(_platform) do + [ + example: [ + deps: [unifex: :unifex], + src_base: "example", + sources: ["_generated/nif/example.c", "example.c"], + interface: :nif + ] + ] + end + end + ``` + This defines a native called `example` that will be implemented in two `.c` files and compiled as `NIF` which + is indicated by `interface` keyword. + In case of `CNode` we would just pass here `:cnode`. + Source files have to be located in `c_src/example` directory. + More details on how to use bundlex can be found in its [documentation](https://hexdocs.pm/bundlex). + +## Native code + +Let's start by creating a `c_src/example` directory, and the files that will be needed: + +```bash +mkdir -p c_src/example +cd c_src/example +touch example.c +touch example.h +touch example.spec.exs +``` + +You may wonder where is the `_generated/nif/example.c`. Well, as the name suggests, it will be generated based on `example.spec.exs`! + +Here are the contents of `example.spec.exs`: + +```elixir +module Example + +interface NIF + +callback :load + +state_type "MyState" + +spec init() :: {:ok :: label, was_handle_load_called :: int, state} + +spec foo(target :: pid, list_in :: [int], state) :: + {:ok :: label, list_out :: [int], answer :: int} | {:error :: label, reason :: atom} + +sends {:example_msg :: label, num :: int} +``` + +Note that here we also specified an interface. +It is not necessary because if we didn't do it Unifex would take it from `bundlex.exs`. +However, this is a good practice that makes code clearer and is also a little faster than fetching info from `bundlex.exs.` +It is also worth mentioning that we can specify multiple interfaces for a single `*.spec.exs` file. + +Above `example.spec.exs` will result in generating the following header files: + +```c +// _generated/nif/example.h` +#pragma once + +#include "../../example.h" +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define UNIFEX_MODULE "Elixir.Example" + +/* + * Functions that manage lib and state lifecycle + * Functions with 'unifex_' prefix are generated automatically, + * the user have to implement rest of them. + */ + +typedef MyState UnifexState; + +/** + * Allocates the state struct. Have to be paired with 'unifex_release_state' + * call + */ +UnifexState *unifex_alloc_state(UnifexEnv *env); + +/** + * Removes a reference to the state object. + * The state is destructed when the last reference is removed. + * Each call to 'unifex_release_state' must correspond to a previous + * call to 'unifex_alloc_state' or 'unifex_keep_state'. + */ +void unifex_release_state(UnifexEnv *env, UnifexState *state); + +/** + * Increases reference count of state object. + * Each call has to be balanced by 'unifex_release_state' call + */ +void unifex_keep_state(UnifexEnv *env, UnifexState *state); + +/** + * Callback called when the state struct is destroyed. It should + * be responsible for releasing any resources kept inside state. + */ +void handle_destroy_state(UnifexEnv *env, UnifexState *state); + +/* + * Declaration of native functions for module Elixir.Example. + * The implementation have to be provided by the user. + */ + +UNIFEX_TERM init(UnifexEnv *env); +UNIFEX_TERM foo(UnifexEnv *env, UnifexPid target, int *list_in, + unsigned int list_in_length, UnifexState *state); + +/* + * Callbacks for nif lifecycle hooks. + * Have to be implemented by user. + */ + +int handle_load(UnifexEnv *env, void **priv_data); + +/* + * Functions that create the defined output from Nif. + * They are automatically generated and don't need to be implemented. + */ + +UNIFEX_TERM init_result_ok(UnifexEnv *env, int was_handle_load_called, + UnifexState *state); +UNIFEX_TERM foo_result_ok(UnifexEnv *env, const int *list_out, + unsigned int list_out_length, int answer); +UNIFEX_TERM foo_result_error(UnifexEnv *env, const char *reason); + +/* + * Functions that send the defined messages from Nif. + * They are automatically generated and don't need to be implemented. + */ + +int send_example_msg(UnifexEnv *env, UnifexPid pid, int flags, int num); + +#ifdef __cplusplus +} +#endif +``` + +and + +```c +// _generated/example.h +#pragma once + +#ifdef BUNDLEX_NIF +#include "nif/example.h" +#endif +``` + +The first one is our main header file that contains function definitions and include statements. +The second one is something we call `tie header`. +As we mentioned before you can specify multiple interfaces for a single `*.spec.exs` file. +Generated files are located under suitable directories. +In our case it is `nif` directory inside `_generated` directory. +However, if we defined interface as e.g. `[NIF, CNode]` instead of `NIF` it would result in a following project structure: +``` +_generated/ +-- nif/ +-- -- example.h +-- -- example.c +-- -- example.cpp +-- cnode/ +-- -- example.h +-- -- example.c +-- -- example.cpp +-- example.h (our `tie header`) +``` +We can see that both `nif/` and `cnode/` has their own `example.h` files that differs in content. +Therefor we generate one more header file that acts like a connector and will include proper main header file. +Please refer to [test_projects](https://github.com/membraneframework/unifex/tree/master/test_projects/unified) +to see how it works. + +More information on how `.spec.exs` files should be created can be found in docs for +`Unifex.Specs.DSL` module. + +Along with the header, `_generated/nif/example.c` file will be created, providing definitions for some functions +you see in the header. + +Next step is to create struct that will be used as state for created nif and include generated header inside `example.h`. +Name of this struct has to be explicitly indicated in our `example.spec.exs` file using `state_type` macro. + +```c +#pragma once + +typedef struct MyState MyState; + +#include "_generated/example.h" + +struct MyState { + int a; +}; +``` + +As you can see we include here `_generated/example.h` file which in turn will include our main `_generated/nif/example.h`. + +Finally, let's provide required implementations in `example.c`: + +```c +#include "example.h" + +int example_was_handle_load_called = 0; + +int handle_load(UnifexEnv *env, void **priv_data) { + UNIFEX_UNUSED(env); + UNIFEX_UNUSED(priv_data); + example_was_handle_load_called = 1; + return 0; +} + +UNIFEX_TERM init(UnifexEnv *env) { + MyState *state = unifex_alloc_state(env); + state->a = 42; + UNIFEX_TERM res = init_result_ok(env, example_was_handle_load_called, state); + unifex_release_state(env, state); + return res; +} + +UNIFEX_TERM foo(UnifexEnv *env, UnifexPid pid, int *list, + unsigned int list_length, MyState *state) { + int res = send_example_msg(env, pid, 0, state->a); + if (!res) { + return foo_result_error(env, "send_failed"); + } + return foo_result_ok(env, list, list_length, state->a); +} + +void handle_destroy_state(UnifexEnv *env, MyState *state) { + UNIFEX_UNUSED(env); + state->a = 0; +} +``` + +Now the project should successfully compile. Run `mix deps.get && mix compile` to make sure everything is fine. + +## Elixir module + +All you have to do in order to access natively implemented functions is to create a module with the name as defined +in `example.spec.exs` and to use `Unifex.Loader` there: + +```elixir +defmodule Example do + use Unifex.Loader +end +``` + +And that's it! You can now run `iex -S mix` and check it out yourself: + +```elixir +iex(1)> {:ok, 1, state} = Example.init() +{:ok, 1, #Reference<0.3742807326.2498363393.21451>} +iex(3)> Example.foo(self(), [1, 2, 3], state) +{:ok, [1, 2, 3], 42} +iex(4)> flush() +{:example_msg, 42} +:ok +``` + +In case of `CNodes`, module with `Unifex.Loader` is unnecessary. We would just do: +```elixir +iex(1)> require Unifex.CNode +Unifex.CNode +iex(2)> {:ok, cnode} = Unifex.CNode.start_link(:example) +{:ok, + %Unifex.CNode{ + bundlex_cnode: %Bundlex.CNode{ + node: :"bundlex_cnode_0_5ce994a5-6a52-4702-9eb1-e7802dd4a05a@localhost", + server: #PID<0.863.0> + }, + node: :"bundlex_cnode_0_5ce994a5-6a52-4702-9eb1-e7802dd4a05a@localhost", + server: #PID<0.863.0> + }} +iex(bundlex_app_bundlex_app_...@localhost)3> :ok = Unifex.CNode.call(cnode, :init) +:ok +iex(bundlex_app_...@localhost)4> {:ok, 42, <<2, 2, 3>>} = Unifex.CNode.call(cnode, :foo, [self(), <<1, 2, 3>>]) +{:ok, 42, <<2, 2, 3>>} +iex(bundlex_app_...@localhost)5> flush +{:example_msg, 42} +:ok +``` + +You can find complete projects [here](https://github.com/membraneframework/unifex/tree/master/test_projects). diff --git a/pages/creating_unifex_nif.md b/pages/creating_unifex_nif.md deleted file mode 100644 index ce6633b2..00000000 --- a/pages/creating_unifex_nif.md +++ /dev/null @@ -1,228 +0,0 @@ -# Creating Unifex Nif - -## Preparation - -In order to start working on NIF, you need to prepare a few things: - -1. Add [Unifex](https://github.com/membraneframework/unifex) to deps in `mix.exs`: - ```elixir - defp deps do - [ - {:unifex, "~> 0.1"} - ] - end - ``` -2. And compilers to the project definition: - ```elixir - def project do - [ - compilers: [:unifex, :bundlex] ++ Mix.compilers(), - (..) - ] - end - ``` -3. Unifex uses [Bundlex](https://github.com/membraneframework/bundlex) to compile the native code. - To make it work, create the `bundlex.exs` file in the project's root directory with the following content: - ```elixir - defmodule Example.BundlexProject do - use Bundlex.Project - - def project() do - [ - nifs: nifs(Bundlex.platform()) - ] - end - - def nifs(_platform) do - [ - example: [ - deps: [unifex: :unifex], - src_base: "example", - sources: ["_generated/example.c", "example.c"] - ] - ] - end - end - ``` - This defines a nif called `example` that will be implemented in two `.c` files. - [Bundlex](https://github.com/membraneframework/bundlex) expects these files to be located in `c_src/example` directory. - More details on how to use it can be found in [its documentation](https://hexdocs.pm/bundlex). - -## Native code - -Let's start by creating a `c_src/example` directory and the files that will be needed: - -```bash -mkdir -p c_src/example -cd c_src/example -touch example.c -touch example.h -touch example.spec.exs -``` - -You may wonder where is the `_generated/example.c`. Well, as the name suggests, it will be generated based on `example.spec.exs`! - -Here are the contents of `example.spec.exs`: - -```elixir -module Example.Native - -callback :load - -spec init() :: {:ok :: label, state} - -spec foo(target :: pid, state) :: {:ok :: label, answer :: int} | {:error :: label, reason :: atom} - -sends {:example_msg :: label, num :: int} -``` - -This will result in generating the following header: - -```c -#pragma once - -#include -#include -#include -#include -#include "../example.h" - -/* - * Declaration of native functions for module Elixir.Example.Native. - * The implementation have to be provided by the user. - */ - -UNIFEX_TERM init(UnifexEnv* env); -UNIFEX_TERM foo(UnifexEnv* env, UnifexPid target, UnifexNifState* state); - -/* - * Functions that manage lib and state lifecycle - * Functions with 'unifex_' prefix are generated automatically, - * the user have to implement rest of them. - */ - -#define UNIFEX_MODULE "Elixir.Example.Native" - -/** - * Allocates the state struct. Have to be paired with 'unifex_release_state' call - */ -UnifexNifState* unifex_alloc_state(UnifexEnv* env); - -/** - * Releases state stuct allocated via 'unifex_alloc_state'. - * State struct should be considered invalid after this call. - */ -void unifex_release_state(UnifexEnv* env, UnifexNifState* state); - -/** - * Callback called when the state struct is destroyed. It should - * be responsible for releasing any resources kept inside state. - */ -void handle_destroy_state(UnifexEnv* env, UnifexNifState* state); - -/* - * Callbacks for nif lifecycle hooks. - * Have to be implemented by user. - */ - -int handle_load(UnifexEnv * env, void ** priv_data); - -/* - * Functions that create the defined output from Nif. - * They are automatically generated and don't need to be implemented. - */ - -UNIFEX_TERM init_result_ok(UnifexEnv* env, UnifexNifState* state); -UNIFEX_TERM foo_result_ok(UnifexEnv* env, int answer); -UNIFEX_TERM foo_result_error(UnifexEnv* env, char* reason); - -/* - * Functions that send the defined messages from Nif. - * They are automatically generated and don't need to be implemented. - */ - -int send_example_msg(UnifexEnv* env, UnifexPid pid, int flags, int num); -``` - -More information on how `.spec.exs` files should be created can be found in docs for -`Unifex.Specs.DSL` module. - -Along with the header, `_generated/example.c` file will be created, providing definitions for some of the functions -you see in the header. - -Next step is to create struct that will be used as state for created nif and include generated header inside `example.h`. -Since there is no name collision, `typdef` can be used to create an alias for `UnifexNifState` and refer to it as `State`. - -```c -#pragma once - -typedef struct MyState UnifexNifState; - -struct MyState { - int a; -}; - -typedef UnifexNifState State; - -#include "_generated/example.h" -``` - -Finally, let's provide required implementations in `example.c`: - -```c -#include "example.h" - -int handle_load(UnifexEnv * env, void ** priv_data) { - UNIFEX_UNUSED(env); - UNIFEX_UNUSED(priv_data); - printf("Hello from the native side!\r\n"); - return 0; -} - -UNIFEX_TERM init(UnifexEnv* env) { - State * state = unifex_alloc_state(env); - state->a = 42; - UNIFEX_TERM res = init_result_ok(env, state); - unifex_release_state(env, state); - return res; -} - -UNIFEX_TERM foo(UnifexEnv* env, UnifexPid pid, State* state) { - int res = send_example_msg(env, pid, 0, state->a); - if (!res) { - return foo_result_error(env, "send_failed"); - } - return foo_result_ok(env, state->a); -} - -void handle_destroy_state(UnifexEnv* env, State* state) { - UNIFEX_UNUSED(env); - state->a = 0; -} -``` - -Now the project should sucessfully compile. Run `mix deps.get && mix compile` to make sure everything is fine. - -## Elixir module - -All you have to do in order to access natively implemented functions is to create a module with the name as defined in `example.spec.exs` and to use `Unifex.Loader` there: - -```elixir -defmodule Example.Native do - use Unifex.Loader -end -``` - -And that's it! You can now run `iex -S mix` and check it out yourself: - -```elixir -iex(1)> alias Example.Native -iex(2)> {:ok, state} = Native.init() -Hello from the native side! -{:ok, #Reference<0.3961161465.633208834.69562>} -iex(3)> Native.foo(self(), state) -{:ok, 42} -iex(4)> flush() -{:example_msg, 42} -:ok -```