Using Nifs
Nifs are the entrypoint between your BEAM code and your C ABI code. Zigler provides semantics which are designed to make it easy to write safe
Basic operation
/// nif: my_nif/1
fn my_nif(input: i32) i32 {
return input + 1;
}
simply define a zig fn
with desired input parameters. Zigler will
generate a corresponding function in the surrounding Elixir module,
and a mismatched value will raise a FunctionClauseError
. The
following types are accepted as inputs:
bool
u8
u16
i32
u32
i64
u64
c_int
*c_uint
*c_long
*c_ulong
*isize
*usize
beam.term
beam.atom
beam.pid
*only use these types to interface with C code and libraries.
Slices of all of the above, except u8
, are also accepted (see below).
Example (with slices)
/// nif: sum/1
fn sum(input: []f32) f32 {
var total: f32 = 0.0;
for (input) | value | { total += value };
return total;
}
Binaries
Binaries are marshalled into u8
slices.
/// nif: double/1
fn capitalize(input: []u8) []u8 {
// note this is poorly bounds tested.
input[0] = input[0] - 32
return input;
}
Allocation
Use the beam.allocator
to
allocate memory when you need it.
Optional beam.env
term
In order to build terms to be consumed by the rest of the BEAM, you need to have access to the "environment" of the calling process. In order to be provided this, you may request it as the first parameter to your zig function. Play close attention to the arity of the nif declaration.
/// nif: add_3/1
fn add_3(env: beam.env, number: i32) beam.term {
return beam.make_i32(env, number + 3);
}
Dirty nifs
If you want to launch your nif as a dirty, use the dirty_cpu
or
dirty_io
attributes. Note that by default, your vm will only have
a limited number of dirty threads available and launching the nif
may fail if all of them are occupied.
/// nif: dirty_nif/1 dirty_cpu
fn dirty_nif(env: beam.env, input: u64) u64 {
...
// code that takes a long time
...
return my_result;
}
Future feature: long
An upcoming feature will be the ability to run a function inside of a sidecar (OS) process. There is a considerable amount of boilerplate required to safely wrap launching such a process, and Zigler will take care of that for you.
Example
/// nif: my_nif/1 long
fn my_nif(env: beam.env, input: beam.term) u64 {
...
// code that takes a long time
...
return my_result;
}
In the above example situation, the result my_result
will be returned
to the process which called it, which will block awaiting a completion
message, without consuming NIF timeslices except for entry into the
function (serialization of the input data).
Optional parent pid example
In some instances, you may want to monitor the parent pid from within the long-running NIF. In those situations, if you request the parent's pid as the second term you can for example, effectively monitor to silently quit out of loops in case the parent dies.
If you don't want the parent pid to block awaiting a response, use the
long:detached
attribute.
/// nif: my_nif/1 long:detached
fn my_nif(env: beam.env, parent: beam.pid, input: beam.term) void {
while (true) {
...
// code inside of a loop
...
if is_dead(env, parent) break;
}
}
This attribute may be combined with the managed
or dirty
attributes.
You should use dirty long
nifs if you think serialization of your BEAM
data structure will take a very long time.
Future feature: managed
and managed:arena
An upcoming feature will be the ability to use a 'managed' allocation
system. Within a managed nif, performing free
operations on allocated data
is optional, preventing the risk of memory leaks. All memory fetched from the
managed allocator will be automatically freed on function exit. It is
important to never pass these data into a resource
. If you don't want to
ever perform free operations you can use managed:arena
which will use an
arena allocation strategy, which can potentially offer better latency.
Example
/// nif: my_nif/1 managed
fn my_nif(env: beam.env, input: beam.term) beam.term {
beam.managed.alloc(u8, 100)
...
}
This attribute may be combined with the long
attribute.