-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
321 additions
and
231 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <erl_nif.h> | ||
#include <stdint.h> | ||
#include <stdio.h> | ||
#include <unifex/payload.h> | ||
#include <unifex/unifex.h> | ||
|
||
#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). |
Oops, something went wrong.