View Source 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 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: true
Basic 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)
end
Note 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_t
andssize_t
in 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 atomstrue
orfalse
exclusively)
Floating point datatypes
Floating point datatypes can take the atoms
:infinity
,:neg_infinity
, and:NaN
.
Boolean datatypes
You must pass boolean datatypes
true
orfalse
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])
end
The 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>>)
end
This 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
end
For 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
package. 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)
end
Send
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}
end
Note 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)
end
Error 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;
}
"""
test "erroring" do
assert_raise ErlangError, "Erlang error: :oops", fn -> erroring() end
end
A few notes on the above code.
- to marshal a value out of a
beam.term
and 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.term
from a zig static type, usebeam.make
. This function does not fail. - for more information on the final options parameter of
get
andmake
, see their respective documentation. - zigler will translate the hoisted marshalling failures into detailed BEAM exceptions of type
ArgumentError
.