View Source Collection Datatypes
Returning array-like datatypes
For array-like datatypes, we saw in Using Nifs how they can take both lists or binaries as inputs. However, when returning statically-typed data from zig, a choice needs to be made as to whether to return lists or binaries.
Arrays
Arrays are the simplest array-like datatype to return.
~Z"""
pub fn return_array(input: f32) [3]f32 {
var result: [3]f32 = undefined;
// set each item in the array:
for (&result, 0..) |*item, index| {
item.* = input + @as(f32, @floatFromInt(index));
}
return result;
}
"""
test "returning an array" do
assert [47.0, 48.0, 49.0] == return_array(47.0)
end
Slices
Slices can also be returned. Note that in many cases, returning a slice might need an allocation strategy
~Z"""
pub fn return_slice(input: []f32) []f32 {
// set each item in the array:
for (input, 0..) |*item, index| {
item.* = item.* + @as(f32, @floatFromInt(index));
}
return input;
}
"""
test "returning a slice" do
assert [47.0, 48.0, 49.0] == return_slice([47.0, 47.0, 47.0])
end
u8 array-likes output as binary
u8 array-like datatypes are marshalled into binary by default instead of list.
~Z"""
pub fn return_u8_array() [3]u8 {
const result: [3]u8 = .{97, 98, 99};
return result;
}
"""
test "u8 datatypes are returned as binary" do
assert "abc" == return_u8_array()
end
Selecting output type
It's also possible to return these collections as binaries, however, in order to do so you will have
to marshal manually. For datatypes that are more than 1 byte, be aware that the endianness of the
resulting data is native
.
~Z"""
const beam = @import("beam");
pub fn return_slice_binary(input: []f32) beam.term {
// set each item in the array:
for (input, 0..) |*item, index| {
item.* = item.* + @as(f32, @floatFromInt(index));
}
return beam.make(input, .{.as = .binary});
}
"""
test "returning a slice as binary" do
assert <<47.0 :: float-size(32)-native,
48.0 :: float-size(32)-native,
49.0 :: float-size(32)-native>> == return_slice_binary([47.0, 47.0, 47.0])
end
Conversely, for u8 array-like datatypes, selecting .as = .list
will result in outputting a list.
You can also automatically marshal as binary by using nif options
Full list of qualified array-like return types
The central challenge of exporting array-like data to the BEAM is that length information may not be known. In the case of arrays and slices, length is either comptime or runtime known. For other data types, the scope of datatypes accepted must be limited:
- array of any type (
[_]T
) - single pointers to an array (
*[_]T
) - slices (
[]T
) - sentinel-terminated forms of the above (
[_:0]T
or[:0]T
) - sentinel-terminated many-item-pointer (
[*:0]T
) - cpointer to u8 (
[*c]u8
). This will assume the cpointer is null-terminated, if it can't be considered[*:0]u8
the behaviour is undefined. - cpointer to a pointer (
[*c]?*T
). This will assume cpointer is null-terminated, if it can't be considered[*:null]?*T
then the behaviour is undefined.
Passing and returning enums
Enums are collections of integer values that are given special identifier status in the zig programming language. At compile-time, it's possible to get reflection on the string representation of those identifiers. Zigler thus is enabled to use enums as a representation of atoms coming from or going to the BEAM.
Enums as atoms
Enums can be created by referring to them by the atom that corresponds to their value:
~Z"""
const EnumType = enum(u8) {
foo,
bar = 47
};
pub fn flip(input: EnumType) EnumType {
return switch (input) {
.foo => .bar,
.bar => .foo
};
}
"""
test "flipping enums" do
assert :bar = flip(:foo)
assert :foo = flip(:bar)
end
Enums as integers
Functions taking an integer type can also by passed the associated integer value in the place of the atom.
test "enums passed as integer" do
assert :foo = flip(47)
end
Enum literals
Enum literals can be converted to atoms using beam.make
.
~Z"""
pub fn make_literal() beam.term {
return beam.make(.some_new_literal, .{});
}
"""
test "enum literals" do
assert :some_new_literal = make_literal()
end
This is especially useful for emitting :ok
or :error
tuples.
error atom
error
is a reserved word in Zig, so to create error atom you must use builtin syntax; the error enum literal is represented.@"error"
.
Passing and returning structs
Structs are atom-keyed maps
Most structs are interpreted as atom-keyed maps. Consider the following code:
~Z"""
pub const Point2D = struct{ x: i32, y: i32 };
pub fn reflect(input: Point2D) Point2D {
return .{.x = input.y, .y = input.x};
}
"""
test "structs" do
assert %{x: 48, y: 47} == reflect(%{x: 47, y: 48})
end
Structs in parameters and returns
for a struct type to be used in parameters and returns, it must be exported as
pub
in the module interface
Anonymous structs can be returned
It's possible to return anonymous structs as well, using beam.make
.
~Z"""
pub fn anonymous_struct() beam.term {
return beam.make(.{.foo = .bar}, .{});
}
"""
test "anonymous structs" do
assert %{foo: :bar} == anonymous_struct()
end
Zig tuples are structs.
Tuples in zig are structs (with hidden integer-valued keys). Zigler takes advantage of this and
allows you to construct BEAM tuples using zig tuples, when passed to beam.make
.
~Z"""
pub fn tuple() beam.term {
return beam.make(.{.ok, 47}, .{});
}
"""
test "tuples" do
assert {:ok, 47} == tuple()
end
Packed or Extern structs.
Packed or Extern structs can be passed as binaries.
~Z"""
pub const Packed = packed struct {x: u4, y: u4};
pub const Extern = extern struct {x: u16, y: u16};
pub fn diff_packed(value: Packed) u8 {
return value.x - value.y;
}
pub fn diff_extern(value: Extern) u16 {
return value.x - value.y;
}
"""
test "packed and extern structs as struct" do
assert 2 = diff_packed(%{x: 7, y: 5})
assert 5 = diff_extern(%{x: 47, y: 42})
end
test "packed and extern structs as binary" do
assert 2 = diff_packed(<<0x57>>)
assert 5 = diff_extern(<<47::unsigned-size(16)-native, 42::unsigned-size(16)-native>>)
end
Packed And Extern Endianness
Be careful about the endianness of packed and extern structs!
Pointers to structs.
Pointers to structs can also be used to marshal data in and out. This is enabled under the assumption that you might want to use the struct in a mutable fashion.
~Z"""
pub fn swap_pointer(value: *Point2D) *Point2D {
const temp = value.x;
value.x = value.y;
value.y = temp;
return value;
}
"""
test "pointer to structs" do
assert %{x: 47, y: 50} == swap_pointer(%{x: 50, y: 47})
end
Nested collections
The process of marshalling parameters and returns also works with nested array-like and struct data.
Arraylike of arraylike
~Z"""
pub fn array_of_array_sum(a_of_a: [][]u64) u64 {
var sum: u64 = 0;
for (a_of_a) |inner_array| {
for (inner_array) |value| {
sum += value;
}
}
return sum;
}
"""
test "array of array" do
assert 21 = array_of_array_sum([[1, 2, 3], [4], [5, 6]])
end
Structs of structs
~Z"""
pub const Arrow = struct {
head: Point2D,
tail: Point2D
};
pub fn reflect_reverse_arrow(arrow: Arrow) Arrow {
return .{
.head = reflect(arrow.tail),
.tail = reflect(arrow.head)
};
}
"""
test "structs of structs" do
assert %{
head: %{x: 1, y: 2},
tail: %{x: 3, y: 4}
} == reflect_reverse_arrow(%{
head: %{x: 4, y: 3},
tail: %{x: 2, y: 1}
})
end
Deep Argument Errors
Argument errors for deeply nested structs will help you understand where your arguments failed to serialize:
# note: skipped because map key order is nondeterministic before 1.15
@tag [skip: Version.compare(System.version(), "1.15.0") == :lt]
test "argument errors" do
assert_raise ArgumentError, """
errors were found at the given arguments:
* 1st argument:
expected: map | keyword (for `Arrow`)
got: `%{head: %{x: 4, y: 3}, tail: %{x: 2, y: 1.0}}`
in field `:tail`:
| expected: map | keyword (for `Point2D`)
| got: `%{x: 2, y: 1.0}`
| in field `:y`:
| | expected: integer (for `i32`)
| | got: `1.0`
""", fn ->
reflect_reverse_arrow(%{
head: %{x: 4, y: 3},
tail: %{x: 2, y: 1.0}
})
end
end
Arraylike of structs
~Z"""
pub fn sum_points(points: []Point2D) Point2D {
var result: Point2D = .{.x = 0, .y = 0};
for (points) |point| {
result.x += point.x;
result.y += point.y;
}
return result;
}
"""
test "array of struct" do
assert %{x: 9, y: 12} = sum_points([%{x: 1, y: 2}, %{x: 3, y: 4}, %{x: 5, y: 6}])
end
Structs of arraylikes
~Z"""
pub const PointSOA = struct{
x: []u16,
y: []u16
};
pub fn sum_point_soa(points: PointSOA) Point2D {
var result: Point2D = .{.x = 0, .y = 0};
for (points.x) |x| {
result.x += x;
}
for (points.y) |y| {
result.y += y;
}
return result;
}
"""
test "struct of array" do
assert %{x: 9, y: 12} = sum_point_soa(%{x: [1, 3, 5], y: [2, 4, 6]})
end
Interaction with allocators
if you directly return a datatype that was allocated, it won't be properly cleaned up. However, it can be properly cleaned up by manually deferring its cleanup after calling
beam.make
.Cleanup routines in nif options will be introduced in a future release, which will enable protection from these sorts of leaks.
For more on allocators, see allocators
~Z"""
pub fn leaks() !*Point2D {
var point = try beam.allocator.create(Point2D);
point.x = 47;
point.y = 50;
return point;
}
pub fn no_leak() !beam.term {
var point = try beam.allocator.create(Point2D);
defer beam.allocator.destroy(point);
point.x = 47;
point.y = 50;
return beam.make(point, .{});
}
"""
test "both leaky and non-leaky struct returns work" do
assert %{x: 47, y: 50} == leaks()
assert %{x: 47, y: 50} == no_leak()
end