@rbino Thanks for this code. "Beam.zig" is brilliant, nicely cleans the Zig code.
I would not have understood the project without your blog: https://rbino.com/posts/wrap-your-nif-with-zig/
For Zig > 0.14, just one change in "beam.zig": the field .Fn does not exist (0.13), but use instead .@"fn".
cf std.meta.ArgsTuple:
https://github.com/ziglang/zig/blob/c172877b81f4eff50cf214eb553c9df108fbd9eb/lib/std/meta.zig#L984
-
This is not an issue but since you have no discussion folder, I opened an issue because I can only compile the Zig code using zig build. When I run mix compile.build_dot_zig, it does not populate the "_build/.../priv/lib/" folder.
-
How do you deal with "long" running functions (> 1ms)?
That being said, I followed your example with two functions add_two_ints and multiply_three_doubles in 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);
}
I can compile with zig build and get "math.so" (on OSX).
Again, MIX_DEBUG=1 mix compile.build_dot_zig compiles (?) but does not populate the "_build/dev/../priv/lib/"" folder.
! One catch : in the "math.zig" file, the Beam.Entry struct field ".name" must be the FULL module name.
the "cleaned" math.zig thanks to "beam.zig"
const std = @import("std");
const beam = @import("beam.zig");
const assert = @import("std").debug.assert;
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 = 2,
.minor = 16,
.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-13.1.2",
};
export fn nif_init() *const beam.Entry {
return &entry;
}
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
// <https://zig.guide/working-with-c/calling-conventions/
const Nif = *const fn (Env, argc: c_int, argv: [*c]const Term) callconv(.C) Term;
pub fn make_nif_wrapper(comptime fun: anytype) Nif {
const Function = @TypeOf(fun);
const function_info = switch (@typeInfo(Function)) {
// .Fn => |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);
}
First "noisy" implementation of the functions"
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);
}
Thanks.
@rbino Thanks for this code. "Beam.zig" is brilliant, nicely cleans the Zig code.
I would not have understood the project without your blog: https://rbino.com/posts/wrap-your-nif-with-zig/
For Zig > 0.14, just one change in "beam.zig": the field
.Fndoes not exist (0.13), but use instead.@"fn".cf
std.meta.ArgsTuple:https://github.com/ziglang/zig/blob/c172877b81f4eff50cf214eb553c9df108fbd9eb/lib/std/meta.zig#L984
This is not an issue but since you have no discussion folder, I opened an issue because I can only compile the Zig code using
zig build. When I runmix compile.build_dot_zig, it does not populate the "_build/.../priv/lib/" folder.How do you deal with "long" running functions (> 1ms)?
That being said, I followed your example with two functions
add_two_intsandmultiply_three_doublesin Zig.build.zig
I can compile with
zig buildand get "math.so" (on OSX).Again,
MIX_DEBUG=1 mix compile.build_dot_zigcompiles (?) but does not populate the "_build/dev/../priv/lib/"" folder.! One catch : in the "math.zig" file, the
Beam.Entrystruct field ".name" must be the FULL module name.the "cleaned" math.zig thanks to "beam.zig"
beam.zig
First "noisy" implementation of the functions"
Thanks.