Cnodex
Helper module to simplify working with a C-Node. It is also allows you to start and monitor an external C-Node process within a supervision tree.
In my opinion C-Nodes are the best option if you need to call into native code. The calling overhead is small, and on par with the calling overhead of remote node communication. And you get monitoring abilities through
Node.monitor(:node@host.example). You can scale your C-Nodes onto multiple machines.So instead of exposing your application to the risks that come with NIFs, you can enclose them in an external OS-process. And even better than port drivers, you gain all of the benefits including scalability.
Available as a hex-package:
{:cnodex, "~> 0.1.0"}Mount a C-Node inside your supervision tree
Mount Cnodex as a worker, and reference it by name:
children = [
worker(Cnodex, [%{exec_path: "priv/example_client"}], name: :ExampleClient)
]
Supervisor.init(children, strategy: :one_for_one)Later call into your C-node through Cnodex:
{:ok, reply} = Cnodex.call(:ExampleClient, {:ping, "hello world"})Start and call into a C-Node
{:ok, pid} = Cnodex.start_link(%{exec_path: "priv/example_client"})
{:ok, reply} = Cnodex.call(pid, {:ping, "hello world"})Implementation
Your supervisor spawns a gen_server worker process that is using a node monitor on the C-Node.
You can either provide a running C-Node to connect to, but you must provide an executable with startup arguments that implement a suitable C-Node.
A suitable C-Node prints a ready_line to stdout when it is ready to accept messages. This mechanism signals readyness to the gen_server that will wait for such a ready_line on the stdout of the C-Node it just started.
The C-Node startup and readyness handling is synchronous. After a configurable spawn_inactive_timeout the gen_server crashes the init procedure, and again supervision can take care of the failued startup.
When you shutdown the Cnodex gen_server, it will issue a SIGTERM to the OS-pid of the C-Node, to ensure you have no lingering processes.
The safest option to call into the C-Node is going through the Cnodex.call/2 or Cnodex.call/3 function.
It will, using a configurable timeout, await a response from the C-Node within the calling process.
In case the C-Node is unavailable, you will receive a proper error, like calling an unavailable gen_server.
If you don't want your process inbox to be hijacked for the C-Node response, use a Task in combination with Cnodex.call/2.
In case the C-Node becomes unavailable, the gen_server will be notified due to the node monitor. The gen_server exits too, so that supervision logic can restart the gen_server and it will start a new C-Node.
Helper modules for testing
Cnodex.Distributed can be used to programatically start and stop a distributed Elixir environment.
This is especially helpful, since you can only use C-Nodes if you have epmd and :net_kernel running.
So in test cases, you can simply use Cnodex.Distributed.up() to start epmd and :net_kernel.
Writing a C-Node
This repository provides you with some good starting points for writing a project or package that encloses a C-Node.
Checkout the following files:
# A proper Makefile is a great base for building C
├── Makefile
│
├── bench
│ │
│ # How to benchmark a C-Node
│ └── vm_vs_cnode_bench.exs
│
# Directory with C source files
├── c_src
│ │
│ # An example C-Node client that connects back to your node
│ └── example_client.c
│
# A mix file that defines a custom task for handling Makefile builds
├── mix.exs
│
# The priv directory should contain your C build artifacts
├── priv
│ │
│ # This is the example C-Node client that gets build
│ └── example_client
│
└── test
│
# An example test case on how to test C-Nodes and C-Node communication
└── cnodex_test.exs
In particular example_client.c contains a nice boilerplate for writing your own C-Node client that
connects back to your node. It is fully annotated, so have a look to find out what is going on there.
The general idea behind this is, that you start up a C-program, and in the startup arguments you provide the connection information to your Elixir node. If everything goes well, your C-program prints a shared message to STDOUT. The Elixir side that started the C-program will read all the STDOUT lines written by the C-program. If the correct shared message appears, it assumes the C-program is now ready to accept messages. And it will establish a node monitor on the C-program, so that if the connection goes down, the Elixir side is notified of the loss.
It is best to write the C-program in a way that is exits cleanly as soon as it looses the connection or something goes wrong. That aligns the C-program with the idea that it can be controlled by a supervisor.
Performance
The repository includes a benchmark comparing local, remote and cnode calling performance:
## VmVsCnodeBench
[20:16:07] 1/4: cnode
[20:16:09] 2/4: cnode direct
[20:16:12] 3/4: local
[20:16:14] 4/4: remote
Finished in 11.49 seconds
## VmVsCnodeBench
benchmark nam iterations average time
local 1000000 1.90 µs/op
cnode direct 50000 37.30 µs/op
cnode 50000 41.43 µs/op
remote 50000 48.64 µs/opExecuted on a MacBook Pro 2,5GHz.
Documentation
The docs can be found at https://hexdocs.pm/cnodex.