Using Nifs
View SourceNifs 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 NIF code with the safety, memory guarantees that Zig provides.
Preamble
Near the top of your module you should use the use Zig directive. This will activate Zigler to
seek zig code blocks and convert them to functions. You must provide the otp_app option, which
enables Zigler to find a directory to place compilation artifacts (such as libraries) so that they
can be shipped with releases. By default, this will be /priv/lib.
compilation artifacts
defaults for compilation artifacts may change in future versions of zigler.
Note that we can ship zig code in any module in-place, so they will live alongside other functions or even other macro alterations you make to the module. In this case, we'll build our code into a module that will be tested with ExUnit.
defmodule NifGuideTest do
use Zig, otp_app: :zigler
use ExUnit.Case, async: trueBasic function writing
Once zigler has been activated for a module, write ~Z code anywhere and this code will be
assembled into a zig file that will be compiled into the nif artifact.
Then write your desired zig function Zigler will also mount functions with the same name as the function in the body of the module.
Example: simple scalar values
~Z"""
pub fn add_one(input: i32) i32 {
return input + 1;
}
"""
test "add one" do
assert 48 == add_one(47)
endNote that Zigler will automatically marshal input and output values across the nif boundary. The following scalar types are accepted by zigler:
- signed integer (
i0..i65535), including non-power-of-two values - unsigned integer (
u0..u65535), including non-power-of-two values usize,isize, architecture-dependent size (roughlysize_tandssize_tin C)c_char,c_short,c_ushort,c_int,c_uint,c_ulong,c_longlong,c_ulonglong, which are architecture-dependent integer sizes mostly used for c interop.- floats
f16,f32, andf64. bool(use the atomstrueorfalseexclusively)
Floating point datatypes
Floating point datatypes can take the atoms :infinity, :neg_infinity, and :NaN.
Boolean datatypes
You must pass boolean datatypes true or false atoms.
Zigler can also marshal list parameters into array, and array-like datatypes:
Example: Array-like datatypes
~Z"""
pub fn sum(input: []f32) f32 {
var total: f32 = 0.0;
for (input) | value | { total += value; }
return total;
}
"""
test "sum" do
assert 6.0 == sum([1.0, 2.0, 3.0])
endThe following array-like datatypes are allowed for parameters:
- arrays
[3]T(for example). Note the length is compile-time known. - slices
[]T - pointers to arrays
*[3]T(for example). - multipointers
[*]T - sentinel-terminated versions of all of the above.
- cpointers
[*c]T
Example: Array-like datatypes as binaries
For all scalar child types, Array-like datatypes may be passed as binaries, thus the following code works with no alteration:
test "sum, with binary input" do
assert 6.0 == sum(<<1.0 :: float-size(32)-native, 2.0 :: float-size(32)-native, 3.0 :: float-size(32)-native>>)
endThis also results in a natural interface for treating BEAM binaries as u8 arrays or slices.
Example: Marshalling errors
Zigler will generate code that protects you from sending incompatible datatypes to the desired function:
test "marshalling error" do
assert_raise ArgumentError, """
errors were found at the given arguments:
* 1st argument:
expected: list(float | :infinity | :neg_infinity | :NaN) | <<_::_*32>> (for `[]f32`)
got: `:atom`
""", fn ->
sum(:atom)
end
endFor more on marshalling collection datatypes, see collections.
Marshalling types manually
You may also manually marshal types into and out of the beam by using the
beam.term datatype. To do so, you must first import the beam
module. The beam.term type is an opaque, wrapped datatype that ensures safe manipulation of terms
as a token in your zig code.
~Z"""
const beam = @import("beam");
pub fn manual_addone(value_term: beam.term) !beam.term {
const value = try beam.get(i32, value_term, .{});
return beam.make(value + 1, .{});
}
"""
test "manual marshalling" do
assert 48 == manual_addone(47)
endSend
An important mechanism for exporting values out of a NIF is to send it to a process ID using the
send function. Zigler provides an advanced beam.send function which will perform this operation
for you.
~Z"""
pub fn test_send(pid: beam.pid) !void {
try beam.send(pid, .{.ok, 47}, .{});
}
"""
test "sending" do
test_send(self())
assert_receive {:ok, 47}
endNote that the second argument of send and the options argument are equivalent to a beam.make
function call.
Optional values
You may use optional values as both input and output terms. Note that the empty optional value in
elixir is nil and the empty optional value in zig is null.
~Z"""
pub fn optional(number: ?u32) ?u32 {
if (number) | n | {
if (n == 42) return null;
return n;
} else {
return 47;
}
}
"""
test "optionals" do
assert is_nil(optional(42))
assert 47 = optional(nil)
assert 10 = optional(10)
endError Returns
You may use functions which return an error, in which case an ErlangException will be thrown with
the value being the atom representing the error.
~Z"""
const OopsError = error { oops };
pub fn erroring() !void {
return error.oops;
}
"""
@tag :erroring
test "erroring" do
assert_raise ErlangError, "Erlang error: :oops", fn -> erroring() end
endError Return Trace Availability
errorReturnTrace is enabled by default in Debug and ReleaseSafe builds, and disabled in ReleaseFast and ReleaseSmall builds. This differs from the zig default.
Note that the erlang compiler has a --no-debug-info parameter that might be set; if this flag is set, then errorReturnTrace will be disabled in ReleaseSafe builds.
To override this policy, set the :error_tracing option.
A few notes on the above code.
- to marshal a value out of a
beam.termand into a zig static type, usebeam.get. This is a failable function, and in this case we hoist the failure in the function return. - to marshal a value into a
beam.termfrom a zig static type, usebeam.make. This function does not fail. - for more information on the final options parameter of
getandmake, see their respective documentation. - zigler will translate the hoisted marshalling failures into detailed BEAM exceptions of type
ArgumentError.
Overriding the default fallback function (elixir-only)
If the module's .so file fails to load, you might want to override the default fallback function
with a pure elixir function. To do so, simply write the function body in elixir elsewhere in the
module. This will be overwritten with the NIF only if the NIF successfully loads. You might use
this, for example, to detect CPU features in on_load, and fallback to a 'slower' elixir code if
CPU features required for performance are not present.