Skip to content(if available)orjump to list(if available)

Zigler: Zig NIFs in Elixir

Zigler: Zig NIFs in Elixir

62 comments

·October 24, 2024

ihumanable

For anyone mystified about what a NIF is that doesn't want to go read the docs.

The BEAM VM (which is the thing that runs erlang / elixir / gleam / etc) has 3 flavors of functions.

- BIFs - Built-in functions, these are written in C and ship with the VM

- NIFs - Natively implemented functions, these are written in any language that can speak the NIF ABI that BEAM exposes and allows you to provide a function that looks like a built-in function but that you build yourself.

- User - User functions are written in the language that's running on BEAM, so if you write a function in erlang or elixir, that's a user function.

NIFs allow you to drop down into a lower level language and extend the VM. Originally most NIFs were written in C, but now a lot more languages have built out nice facilities for writing NIFs. Rust has Rustler and Zig now has Zigler, although people have been writing zig nifs for a while without zigler and I'm sure people wrote rust nifs without rustler.

hinkley

It’s important to note that while Erlang has protections against user code crashing an Erlang process and recovering, a faulty NIF can take down the entire virtual machine.

kristoff_it

There's a series of things that a NIF must do to be a good citizen. Not crashing is a big one, but also not starving the VM by never yielding (in case the NIF is long-running) is important, plus a few secondary things like using the BEAM allocator so that tooling that monitors memory consumption can see resources consumed by the NIF.

The creator of Zigler has a talk from ElixirConf 2021 on how he made Zig NIFs behave nicely:

https://www.youtube.com/watch?v=lDfjdGva3NE

alberth

Hence why Rustler is of so much interest since it provides more protections against this happening.

Discord is a big Erlang + Rustler user.

johnisgood

What kind of protections as opposed to Zigler?

depr

Are they really? Their projects don't look so active

null

[deleted]

drawnwren

Is any of this code open source? As an outsider, I'm kind of at a loss for why anyone wants this or what you kids are doing over there and how offended I should be by it.

jlkjfuwnjalfw

Don't be like me and do a 20ms page fault in a NIF

cooljacob204

Do nifs have the equal process time stuff that regular elixir processes have? Where the BEAM will move the scheduler into another process if it's taking too long?

Forgive me if I'm mixing up my terminology it's been a bit since I have poked at Elixir.

throwawaymaths

You can write nifs that way but it seems like a pain in the ass

https://www.erlang.org/doc/apps/erts/erl_nif#enif_schedule_n...

After all, many of the BIFs have been replaced internally by NIFs

And there's this, which would scare me:

https://erlang.org/documentation/doc-15.0-rc3/erts-15.0/doc/...

rubyn00bie

Nope, at least not by default or like one would expect from pure Erlang (when it comes to preempting). Been a while since I dug into this admittedly but I write Elixir daily for work (and have for about ten years now). They don’t do the record keeping necessary for the BEAM to interrupt. You need to make sure the “dirty scheduler” is enabled or you can end up blocking other processes on the same scheduler.

Here’s a link I found talking about using the dirty scheduler with Rust(ler): https://bgmarx.com/2018/08/15/using-dirty-schedulers-with-ru...

bmitc

It's also important to point out ports, because as you mention, NIFs are a way to integrate external code. But as someone else points out, NIFs can crash the entire BEAM VM. Ports are a safer way to integrate external code because they are just another BEAM process that talks to an external program. If that program crashes, then the port process crashes just like any other BEAM process but it won't crash the entire BEAM VM.

gioazzi

And then there are port drivers which are the worst of both worlds! Can crash the BEAM and need much more ceremony than NIF to set up but they’re pretty nice to do in Zig[1] as well

[1]: https://github.com/borgoat/er_zig_driver

bmitc

That's true. Haha!

There's another option and that's setting up an Erlang node in the other language. The Erlang term format is relatively straightforward. But I'm honestly not sure of the benefit of a node versus just using a port.

abrookewood

Why would anyone use a NIF instead of a Port then?

toast0

NIFs are great for things that really feel like a relatively quick function call.

If you've got some mathematical/crypto function, chances are you don't want that to go through a command queue to an external port, because that's too much overhead. If it's a many round crypto function like bcrypt or something, you do need to be a bit careful doing it as a NIF because of runtime. But you wouldn't want to put a sha256 through an external program and have to pass all that data to it, etc.

Something that you might actually want queueing for and is likely to have potential for memory unsafety like say transcoding with ffmpeg, would be a good fit as an external Port rather than a NIF or a linked in Port driver.

Cyph0n

IPC/shared memory overhead?

null

[deleted]

Dowwie

Understand NIF risks: they can crash your entire Elixir Application, beyond their immediate supervision tree, because they operate in the same memory space as the BEAM itself.

NIF responsibly. :)

harrisi

Zig is also used in an excellent way by burrito[0]. I've also used zig for compiling NIFs written in C/C++/Objective-C, since `zig cc` makes cross-compiling much nicer.

I wish zig got more use and attention in the Erlang ecosystem, but rustler seems more popular.

lionkor

Completely lacking a description that made it clear, but basically, from what I can tell, this lets you embed Zig code inside Elixir code

derefr

Does anyone actually enjoy using these systems that encourage you to embed programming-language X code in programming-language Y heredocs?

I always find actually doing that — and then maintaining the results over time — to be quite painful: you don't get syntax highlighting inside the string; you can no longer search your worktree reliably using extension-based filtering; etc.

I personally find the workflow much more sane if/when you just have a separate file (e.g. `foo.zig`) for the guest-language code, and then your host-language code references it.

toast0

I've done some assembly in C, and for big functions, yeah, I want it in its own file, but smaller things often make sense to embed. I'm not sure if I'd like my nif code embedded into my erl files (assuming this works for Erlang as well), but it could conceivably make the nasty bit of boilerplate around ERL_NIF_INIT in the NIF (which I have to do in C anyway) and exit(nif_library_not_loaded) in the erl go away, which would be nice.

It's certainly possible to get syntax highlighting on the embedded code, but you'll need to work with your syntax highlighter; it certainly helps if you're not the only person using it.

But then again, I worked without syntax highlighting for years, so I'm happy when it works, but when it doesn't, I'm ok with that too.

devjab

I’m not too familiar with Elixir, but I definitely prefer building libraries in Zig and then consuming them in Python, TS, whatever over embedding them inside another language directly.

That being said, you can get IDE language support for embedded code if you use eMacs or vim (and probably other editors as well). As I mentioned I still vastly prefer separating it personally, especially if you don’t necessarily expect your Python or Typescript programmers to be knowledgeable about Zig (or C).

harrisi

Syntax highlighting here can work correctly, actually.

Also, I'm not sure why it's not better documented in Zigler, but you can also write the code in a separate file just fine.

h0l0cube

Links for anyone curious.

> Syntax highlighting here can work correctly, actually.

Highlighting shown here in the 2021 ElixirConf talk posted elsewhere in the comments:

https://youtu.be/lDfjdGva3NE?t=2064

> I'm not sure why it's not better documented in Zigler

Here's the docs for it (though buried in the 'advanced' section)

https://hexdocs.pm/zigler/Zig.html#module-importing-external...

travisgriggs

> Does anyone actually enjoy using these systems that encourage you to embed programming-language X code in programming-language Y heredocs?

Isn’t that essentially any web application?

systems

I initially agree

But, if all you do is write elixir wrappers around the zig function, to completely hide the foreign language functions, keeping both the wrapper and implementation in the same file, even if two different languages doesn't seem horrible, but again, keeping them in two file doesn't seem like a huge difference too

I think its really a matter of taste, both options viable

kuon

I use zig a lot in elixir nif, for things like audio and video processing, it works great. But I do not use zigler as I prefer the code to live in their own codebases. But zigler is really nice and it provides an easy way to do computational heavy tasks in elixir.

kansi

> I use zig a lot in elixir nif, for things like audio and video processing

Sounds interesting, is it open source? I am interested in seeing how the code layout looks like when mixing Zig and Elixir

kuon

I don't have open source code base to share but here it how it looks like:

        // the_nif.zig

        fn init_imp(
            env: ?*erl.ErlNifEnv,
            argc: c_int,
            argv: [*c]const erl.ERL_NIF_TERM,
        ) !erl.ERL_NIF_TERM {
            if (argc != 0) {
                return error.BadArg;
            }

            return try helpers.make("Hello world");
        }

        export fn media_tools_init(
            env: ?*erl.ErlNifEnv,
            argc: c_int,
            argv: [*c]const erl.ERL_NIF_TERM,
        ) erl.ERL_NIF_TERM {
            return init_imp(env, argc, argv) catch |err|
                return helpers.make_error(env, err);
        }


        var funcs = [_]erl.ErlNifFunc{ erl.ErlNifFunc{
            .name = "init",
            .arity = 1,
            .fptr = media_tools_init,
            .flags = erl.ERL_NIF_DIRTY_JOB_CPU_BOUND, 
        } };

        var entry = erl.ErlNifEntry{
            .major = erl.ERL_NIF_MAJOR_VERSION,
            .minor = erl.ERL_NIF_MINOR_VERSION,
            .name = "Elixir.MediaTools.Stream",
            .num_of_funcs = funcs.len,
            .funcs = &funcs,
            .load = load,
            .reload = null,
            .upgrade = null,
            .unload = null,
            .vm_variant = "beam.vanilla",
            .options = 0,
            .sizeof_ErlNifResourceTypeInit = @sizeOf(erl.ErlNifResourceTypeInit),
            .min_erts = "erts-10.4",
        };

        export fn nif_init() *erl.ErlNifEntry {
            return &entry;
        }

        # the_exlixir_file.ex

        assert "Hello world" == MediaTools.Stream.init()

The "helpers" library is used to convert types to and from erlang, I plan on open sourcing it but it is not ready now. In the above example, the code is explicit but "entry" can be created with an helper comptime function. erl is simply the erl_nif.h header converted by zig translate-c.

I wrote a piece back in 2022, but things evolved a lot since then: https://www.kuon.ch/post/2022-11-26-zig-nif/

filmor

This won't work on Windows as the BEAM uses a slightly different NIF initialisation method there.

kansi

Thanks for sharing the post, it was intriguing. The detailed comments mentioned in `main.zig` and `build.zig` towards the end helped a lot.

nine_k

(Yo dawg, we put a niche language into a niche language so that...)

I wonder if the Zig code can be not written inline, as an option. With anything larger than a few lines, I'd want syntax highlighting, LSP support, navigation, etc. It's easier to achieve with one language per file.

harrisi

Yes, you can put Zig code in a separate file.

psychoslave

Great! But, what is a nifs, please? :'D

sangnoir

It's Elixir's[1] equivalent of a Foreign Function Interface.

1. More accurately, NIFs sre BEAM's take on FFI functions, and Elixir is a BEAM language.

G4BB3R

Are sigils (~) restricted to one char? To me seems ~Zig would be more clear and short enough.

Miner49er

Erlang sigils are not, they can be any length, limited to characters allowed in atoms.

Elixir sigils also allow multiple characters in the name, but chars after the first must be upper case, according to the docs.

So for Elixir, it would have to be something like ~zIG

throwawaymaths

According to the docs, must be all upper case:

> Custom sigils may be either a single lowercase character, or an uppercase character followed by more uppercase characters and digits.

https://hexdocs.pm/elixir/sigils.html

Miner49er

Ah yeah, you're right.

Muromec

Wait, erlang has sigils?

com

Yeah, there’s a bit of a developer experience push going on in erlang world, which is great!