Skip to content

example-explanations on "build.zig" for a pur Zig example #13

@ndrean

Description

@ndrean

@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

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions