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:

  1. 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.

  2. 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 the example.c file. We'll also need example.spec.exs file, that Unifex needs to generate boilerplate code for compiling the native as NIF and CNode. Both files should be located in the c_src/example folder. Setting Unifex 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.