dwyl / dwyl/learn-zig

Raw Ziggy NIF

Open
#16 1 comment 2 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
4
Forks
1
PR merge metrics
No merged PRs in 30d

Description

# 1) An example of a raw Zig NIF

Shamelessly copied from

Here we show how to write a "native" Zig based NIF that exposes 2 functions, "add_two_ints" and "multiply_three_doubles". One can guess that you add to integers and multiply 3 floats.

You can compare this to what `Zigler` proposes; `Zigler` does a lot for you, but here you can compile the way you want, and bring in easily libraries.
The question of "long-running" NIFs is not tackled here.

The Zig code below is compiled into a NIF (you need to have Zig installed) and can be used from Elixir.

```elixir
defmodule ZigInEx.Math do
@on_load :load_nif
@nif_out "./zig-out/lib/math"

def load_nif do
IO.puts("NIF loaded? ")

@nif_out |> String.to_charlist() |> :erlang.load_nif(0)
end

def add_two_ints(_, _) do
:erlang.nif_error(:nif_not_loaded)
end

def multiply_three_doubles(_, _, _) do
:erlang.nif_error(:nif_not_loaded)
end
end
```

Use it:

```
ZigInEx.multiply_three_doubles(2.0, 2.0, 2.0)
# 8.0
```

A first "noisy" but more straightforward approach below. It uses a "beam.zig" module to link Erlang Term and Zig types.

"noisy" math.zig

```zig
pub fn add_two_ints(
env: beam.Env,
argc: c_int,
argv: [*c]const beam.Term,
) callconv(.C) beam.Term {
assert(argc == 2);

// Convert to a slice to leverage Zig out of bound checks
const argv_slice = @as([*]const beam.Term, @ptrCast(argv))[0..@intCast(argc)];

const a = beam.get_u32(env, argv_slice[0]) catch {
return beam.raise_badarg(env);
};

const b = beam.get_u32(env, argv_slice[1]) catch {
return beam.raise_badarg(env);
};

const result = a + b;

return beam.make_u32(env, result);
}

pub fn multiply_three_doubles(
env: beam.Env,
argc: c_int,
argv: [*c]const beam.Term,
) callconv(.C) beam.Term {
assert(argc == 3);

const argv_slice = @as([*]const beam.Term, @ptrCast(argv))[0..@intCast(argc)];

const a = beam.get_f64(env, argv_slice[0]) catch {
return beam.raise_badarg(env);
};

const b = beam.get_f64(env, argv_slice[1]) catch {
return beam.raise_badarg(env);
};

const c = beam.get_f64(env, argv_slice[2]) catch {
return beam.raise_badarg(env);
};

const result = a * b * c;

return beam.make_f64(env, result);
}
```

A perhaps more intimidating approach where part of the code is outsourced. You need to enhance the "beam.zig" module. The later is an interface with [Erlang's NIF](https://www.erlang.org/doc/system/nif.html). It exposes a function `beam.make_nif_wrapper` that lets you write an "almost" standard Zig function, modulo the usage of types exposed in "beam.zig" and adding the context "env".

If your Zig functions uses other types than those two, you need to tailor the "beam.zig" module, the `get_arg_from_term` and add setters/getter for the type.

Two modules:

- the "math module",
- and the "beam.zig" module.

```zig
// math.zig
const std = @import("std");
const beam = @import("beam.zig");

pub const add_two_ints = beam.make_nif_wrapper(add_two_ints_impl);

fn add_two_ints_impl(env: beam.Env, a: u32, b: u32) beam.Term {
return beam.make_u32(env, a + b);
}

pub const multiply_three_doubles = beam.make_nif_wrapper(multiply_three_doubles_impl);

fn multiply_three_doubles_impl(env: beam.Env, a: f64, b: f64, c: f64) beam.Term {
return beam.make_f64(env, a * b * c);
}

// NIF initialization boilerplate below ----------------------------------
export var __exported_nifs__ = [_]beam.Func{
beam.Func{
.name = "add_two_ints",
.arity = 2,
.fptr = add_two_ints,
.flags = 0,
},
beam.Func{
.name = "multiply_three_doubles",
.arity = 3,
.fptr = multiply_three_doubles,
.flags = 0,
},
};

const entry = beam.Entry{
.major = 1,
.minor = 1,
.name = "Elixir.ZigInEx.Math",
.num_of_funcs = __exported_nifs__.len,
.funcs = &(__exported_nifs__[0]),
.load = null,
.reload = null,
.upgrade = null,
.unload = null,
.vm_variant = "beam.vanilla",
.options = 1,
.sizeof_ErlNifResourceTypeInit = @sizeOf(beam.e.ErlNifResourceTypeInit),
.min_erts = "erts-15.1.2",
};

export fn nif_init() *const beam.Entry {
return &entry;
}
```

You define the getter and setters for `u32` and `f64` types exposed by ERL_NIF. You also define the wrapper and how to link Zig types as used in the arguments to NIFs types.

## The beautiful "beam.zig" file

[A post on some comptime functions](https://www.openmymind.net/Basic-MetaProgramming-in-Zig/#hasMethod)

```zig
// beam.zig
const std = @import("std");

pub const e = @cImport(@cInclude("erl_nif.h"));

pub const Env = ?*e.ErlNifEnv;
pub const Term = e.ERL_NIF_TERM;
pub const Func = e.ErlNifFunc;
pub const Entry = e.ErlNifEntry;

// calling a Zig function from C, so we ned to use the .C calling convention
// |f| f, <- 0.13.0
.@"fn" => |f| f,
else => @compileError("Only functions can be wrapped"),
};

const params = function_info.params;
// Env is not counted in argc, so subtract one
const expected_argc = params.len - 1;

return struct {
pub fn wrapper(
env: Env,
argc: c_int,
argv: [*c]const Term,
) callconv(.C) Term {
if (argc != expected_argc) @panic("NIF called with the wrong number of arguments");

const argv_slice = @as([*]const Term, @ptrCast(argv))[0..@intCast(argc)];

// This creates a tuple with the right dimensions and types to store
// the arguments of the passed function type
var args: std.meta.ArgsTuple(Function) = undefined;
// Populate the args
inline for (&args, 0..) |*arg, arg_idx| {
if (arg_idx == 0) {
// The first argument is the environment
arg.* = env;
continue;
}

// There is an offset between args and argv since argv doesn't
// contain the env
const argv_idx = arg_idx - 1;
// For all the other arguments, extract them based on their type
const ArgType = @TypeOf(arg.*);
arg.* = get_arg_from_term(ArgType, env, argv_slice[argv_idx]) catch
return raise_badarg(env);
}

return @call(.auto, fun, args);
}
}.wrapper;
}

pub fn get_arg_from_term(comptime T: type, env: Env, term: Term) !T {
// These are what we currently need, the need to add new types to the switch will be caught
// by the compileError below
return switch (T) {
u32 => try get_u32(env, term),
f64 => try get_f64(env, term),
else => @compileError("Type " ++ @typeName(T) ++ " is not handled by get_arg_from_term"),
};
}

pub fn get_u32(env: Env, term: Term) !u32 {
var result: c_uint = undefined;
if (e.enif_get_uint(env, term, &result) == 0) {
return error.ArgumentError;
}
return @intCast(result);
}

pub fn get_f64(env: Env, term: Term) !f64 {
var result: f64 = undefined;
if (e.enif_get_double(env, term, &result) == 0) {
return error.ArgumentError;
}
return result;
}

pub fn make_u32(env: Env, value: u32) Term {
return e.enif_make_uint(env, @intCast(value));
}

pub fn make_f64(env: Env, value: f64) Term {
return e.enif_make_double(env, value);
}

pub fn raise_badarg(env: Env) Term {
return e.enif_make_badarg(env);
}
```

## The beautiful "build.zig" file

Run `zig build` and check the folder "zig-out/bin".

```zig
// build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

// Get ERTS_INCLUDE_DIR from the env populated by :build_dot_zig
const erts_include_dir = std.process.getEnvVarOwned(
b.allocator,
"ERTS_INCLUDE_DIR",
) catch blk: {
// Fallback to extracting it from the erlang shell so it's possible to
// execute zig build manually
const argv = [_][]const u8{
"erl",
"-eval",
"io:format(\"~s\", [lists:concat([code:root_dir(), \"/erts-\", erlang:system_info(version), \"/include\"])])",
"-s",
"init",
"stop",
"-noshell",
};

break :blk b.run(&argv);
};

const lib = b.addSharedLibrary(.{
.name = "math",
.root_source_file = b.path("./src/math.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});

lib.entry = .disabled;
lib.addSystemIncludePath(.{ .cwd_relative = erts_include_dir });
// This is needed to avoid errors on MacOS when loading the NIF
lib.linker_allow_shlib_undefined = true;

// Do this so `lib` doesn't get prepended to the lib name, and `.so` is used
// as suffix also on MacOS, since it's required by the NIF loading mechanism.
// See https://github.com/ziglang/zig/issues/2231
// Windows still needs the .dll suffix
const lib_name = if (target.result.os.tag == .windows) "math.dll" else "math.so";
const nif_so_install = b.addInstallFileWithDir(lib.getEmittedBin(), .lib, lib_name);
nif_so_install.step.dependOn(&lib.step);

b.getInstallStep().dependOn(&nif_so_install.step);
}
```

# 2) Doc on NIFs

- The NIF library must be explicitly loaded by Erlang code in the same module.
- All NIFs of a module must have an Erlang implementation as well.

A one page introduction in the documentation:

[Screenshot 2024-12-12 at 12 26 28](https://www.erlang.org/doc/system/nif.html)

The `erl_nif` documentation

[Screenshot 2024-12-12 at 12 32 06](https://www.erlang.org/doc/apps/erts/erl_nif#resource_objects)

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.