View Source Creating Unifex Natives
Introduction
In this tutorial, you will learn how to use Unifex natives to write native code that can be compiled both as NIF and CNode.
Preparation
In order to start working, you need to prepare a few things:
First, you have to add unifex to your dependencies as well as unifex and bundlex compilers. Please refer to Installation section to see how to do it.
After successful installation we should take a look at 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:defmodule Example.BundlexProject do use Bundlex.Project def project() do [ natives: natives(Bundlex.platform()) ] end def natives(_platform) do [ example: [ sources: ["example.c"], interface: [:nif, :cnode], preprocessor: Unifex ] ] end end
This defines a native called
example
, that should be implemented in theexample.c
file. We'll also needexample.spec.exs
file, that Unifex needs to generate boilerplate code for compiling the native as NIF and CNode. Both files should be located in thec_src/example
folder. SettingUnifex
as a preprocessor lets it extend the configuration with the generated code. More details on how to use bundlex can be found in its documentation.
Native code
Let's start by creating a c_src/example
directory, and the files that will be needed:
mkdir -p c_src/example
cd c_src/example
touch example.c
touch example.h
touch example.spec.exs
Here are the contents of example.spec.exs
:
module Example
interface [NIF, CNode]
spec foo(num :: int) :: {:ok :: label, answer :: int}
Note that here we also specified an interface or even interfaces!
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.
Next step is to implement our example.h
.
Since our example is very simple our example.h
will be very simple too:
#include "_generated/example.h"
It just includes generated header file that contains some function definitions we use in our example.c
. The header will be generated by Unifex based on the example.spec.exs
file.
Now, let's provide required implementation in example.c
:
#include "example.h"
UNIFEX_TERM foo(UnifexEnv* env, int num) {
return foo_result_ok(env, num);
}
The provided foo
implementation returns the passed argument, by using foo_result_ok
. For every tuple, that might be returned from our Unifex function, there will be generated implementation of result function named [orginal_function_name]_result_[label]
, where label
is an atom with type label
in returned tuple. The type of the first argument of this function will be UnifexEnv*
, number and types of the rest of the arguments will depend on the number and types of rest of elements in the returned tuple
At this moment the project should successfully compile.
Run mix deps.get && mix compile
to make sure everything is fine.
In c_src/_generated/
directory there should appear files both for usage as NIF and CNode.
Types
With Unifex, you can use some of built-in Elixir types (see more in documentation), as well as your custom types. Currently, we support defining enums and structs. If you want to add structs or enums to the API of your native functions, you have to define them in the *.spec.exs
file. There is an example of this:
type my_enum :: :option_one | :option_two | :option_three | :option_four | :option_five
type my_struct :: %My.Struct{
id: int,
data: [int],
name: string
}
type nested_struct :: %Nested.Struct{
inner_struct: my_struct,
id: int
}
Then, we will generate two versions of native declarations of our custom types, each for C
and C++
. An example of version for C
is posted below
enum MyEnum_t {
MY_ENUM_OPTION_ONE,
MY_ENUM_OPTION_TWO,
MY_ENUM_OPTION_THREE,
MY_ENUM_OPTION_FOUR,
MY_ENUM_OPTION_FIVE
};
typedef enum MyEnum_t MyEnum;
struct my_struct_t {
int id;
int *data;
unsigned int data_length;
char *name;
};
typedef struct my_struct_t my_struct;
struct nested_struct_t {
my_struct inner_struct;
int id;
};
typedef struct nested_struct_t nested_struct;
The name of enum will be translated to PascalCase, specific options will be prefixed with enum name and translated to MACRO_CASE.
Remember, that in C
it is forbidden to make a cyclic chain of containment in the way, that it was made above.
Now, you can use your custom types, like basic ones, e.g.
spec example_function(in_enum :: my_enum, in_struct :: nested_struct) :: {:ok :: label, out_struct :: my_struct} | {:error :: label, reason :: atom}
UNIFEX_TERM example_function(UnifexEnv *env, MyEnum in_enum, nested_struct in_struct) {
if (in_enum == MY_ENUM_OPTION_ONE) {
return example_function_result_error(env, "failed");
}
return example_function_result_ok(env, in_struct.inner_struct);
}
If you want to pass enum to Unifex function on Elixir side, you have to use atom (this same, that was used in enum definition in *.spec.exs
file). By analogy, the result of Unifex function returning enum will be visible as an atom on the Elixir side. What is important, declaration of a specific struct in *.spec.exs
will not automatically make it available in your Elixir code - you are responsible for providing struct module on your own.
Running code
NIF
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:
defmodule Example do
use Unifex.Loader
end
And that's it! You can now run iex -S mix
and check it out yourself:
iex(1)> Example.foo(10)
{:ok, 10}
CNode
In case of CNodes
, module with Unifex.Loader
is unnecessary. We would just do:
iex(2)> require Unifex.CNode
Unifex.CNode
iex(3)> {:ok, cnode} = Unifex.CNode.start_link(:example)
{:ok,
%Unifex.CNode{
bundlex_cnode: %Bundlex.CNode{
node: :"bundlex_cnode_0_4eb957f2-fd47-47c2-b816-6e9c580e658a@michal",
server: #PID<0.259.0>
},
node: :"bundlex_cnode_0_4eb957f2-fd47-47c2-b816-6e9c580e658a@michal",
server: #PID<0.259.0>
}}
iex(bundlex_app_4eb957f2-...)4> Unifex.CNode.call(cnode, :foo, [10])
{:ok, 10}
More examples
You can find more complete projects here.
Also check out how we use Unifex in our repositories
and please refer to Unifex.Specs.DSL
module's documentation to see how to create more advanced *.spec.exs
files.