View Source Zig (zigler v0.11.0)

Inline NIF support for Zig

Motivation

Zig is a general-purpose programming language designed for robustness, optimality, and maintainability.

The programming philosophy of Zig matches up nicely with the programming philosophy of the BEAM VM and in particular its emphasis on simplicity and structure should very appealing to the practitioners of Elixir.

The following features make Zig extremely amenable to inline language support in a BEAM language:

  • simplicity. Zig's syntax is definable in a simple YACC document and Zig takes a stance against making its featureset more complex (though it may evolve somewhat en route to 1.0)
  • Composability. Zig is unopinionated about how to go about memory allocations. Its allocator interface is very easily able to be backed by the BEAM's, which means that you have access to generic memory allocation strategies through its composable allocator scheme.
  • C integration. It's very easy to design C-interop between Zig and C. Zigler has been designed to make it easier to use Zigler to build C libraries than to use C directly see Easy C.

Guides

Please consult the following guides for detailed topics:

Zig version support

although the large-scale archictecture of zigler is settled, zigler features may break backwards compatibility until zig reaches 1.0

Nerves Support

Nerves is supported out of the box, and Zigler will be able to seamlessly detect the cross-compilation information (os, architecture, runtime) and build correctly for that target.

Basic usage

In the BEAM, you can define a NIF by consulting the following document and implementing the appropriate shared object/DLL callbacks. However, Zigler will take care of all of this for you.

Simply use Zig in your module, providing the app atom in the property list.

Then, use the sigil_Z/2 macro and write zig code. To present a function as a nif in your module, simply export it from your code namespace by making it a pub function in your zig code.

Example

defmodule MyModule do
  use Zig, otp_app: :my_app

  ~Z"""
  pub fn my_func(val: i64) i64 {
      return val + 1;
  }
  """

end

Zig will automatically fill out the appropriate NIF C template, compile the shared object, and bind it into the module pre-compilation. In the case of the example, there will be a MyModule.my_func/1 function call found in the module.

Zig will also make sure that your statically-typed Zig data are guarded when you marshal it from the dynamically-typed BEAM world. However, you may only pass in and return certain types. As an escape hatch, you may use the beam.term type which is a wrapped ERL_NIF_TERM type. See erl_nif.

Environment

For many functions, you'll need to import the beam package and create a function that takes a beam.env as its first argument. This will enable you to directly access or create wrapped beam term data. The equivalent of the above code will be:

Example

defmodule MyModule do
  use Zig, otp_app: :my_app

  ~Z"""
  const beam = @import("beam");

  pub fn my_func(env: beam.env, val_term: beam.term) !beam.term {
      const val = try beam.get(i64, env, val_term, .{});
      return beam.make(env, val + 1, .{});
  }
  """
end

For more details on get and make functions see the beam documentation.

Manual Term marshalling

If you don't use automatic marshalling, Zigler will not be able to provide the following conveniences:

  • argument error details. The zig code will raise a generic BEAM ArgumentError but it won't have specific details about what the expected type was and which argument was in error.

  • dialyzer type information for your function. You will have to supply that type information in your nif configuration.

Functions missing from beam

The beam module doesn't comprehensively contain all nif functions. For functions that correspond to erl_nif.h you can import the erl_nif package, which has the erl_nif C API

const erl_nif = @import("erl_nif");

Importing external files

If you need to write zig code outside of the module, just place it in the same directory as your module.

Example

~Z"""
const extra_code = @import("extra_code.zig");

pub fn use_extra_code(val: i64) i64 {
  return extra_code.extra_fn(val);
}

pub const forwarded_function = extra_code.forwarded_function;
"""

If you would like to include a custom c header file, create an subdirectory of your module's directory and add it as an available include directory, as shown here (in this case the subdirectory is called include). The Zig build system will add the include path(s) in the analysis and compilation pipelines.

defmodule MyModule
  use Zig,
    otp_app: :my_app,
    include_dir: "include"

  ~Z"""
  const c = @cImport({
    @cInclude("my_c_header.h");
  });
  ...
  """
end

If the c header defines extern functions, it's your responsibility to make sure those externed functions are available by compiling other c files or using an external library.

External Libraries

If you need to bind static (*.a) or dynamic (*.so) libraries into your module, you may link them with the :libs argument.

Note that zig statically binds shared libraries into the assets it creates. This simplifies deployment for you.

Example (explicit library path)

defmodule Blas do
  use Zig,
    otp_app: :my_app,
    link_lib: "path/to/libblas.a"

  ~Z"""
  const blas = @cImport({
    @cInclude("cblas.h");
  ...
  """
end

You can also link system libraries. This relies on zig build's ability to locate system libraries. Note that you will need to follow your system's library convention, for example in the case of linux, that means removing the "lib" prefix and the ".so" extension.

Example (system libraries)

defmodule Blas do
  use Zig,
    otp_app: :my_app,
    link_lib: {:system, "blas"}

  ~Z"""
  const blas = @cImport({
    @cInclude("cblas.h");
  ...
  """
end

Compiling C/C++ files

You can direct zigler to compile C or C++ files that are in your directory tree. Currently, you must explicitly pick each file, in the future, there may be support for directories (and selecting compile options) based on customizeable rules.

To do this, fill the "sources" option with a list of files (represented as strings), or a file/options pair (represented as a tuple).

defmodule UsesCOrCpp do
  use Zig,
    otp_app: :my_app,
    link_libcpp: true,  # note: optional for c-only code
    include_dir: ["include"],
    c_src: [
      "some_c_source.c",
      {"some_cpp_source.cpp", ["-std=c++17"]},
      {"directory_of_files/*", ["-std=c99"]},
    ]

  ~Z"""
  ...
  """
end

Easy C

In some cases, you may have a C project that ships with a library and a header file that you would like to mount as NIF functions in your module. In this case, you can use the easy_c option to automate the work of stitching your library into the module. Note that in this case, you must declare all of the function that you would like to import. Here is an example of importing three functions from the blas example as above.

For details of what the nif options mean, see: Zig.EasyC

defmodule BlasWithEasyC do
  use Zig,
    otp_app: :my_app,
    easy_c: "cblas.h",
    link_lib: {:system, "blas"},
    nifs: [
      :cblas_dasum,
      cblas_daxpy: [return: [4, length: {:arg, 0}]],
      daxpy_bin: [alias: :cblas_daxpy, return: [4, :binary, length: {:arg, 0}]]
    ]
end

Compilation debug

If something should go wrong, Zigler will translate the Zig compiler error into an Elixir compiler error, and let you know which line in the ~Z block it came from.

Documentation

Use the builtin zig /// docstring to write your documentation. If it's in front of the nif declaration, it will wind up in the correct place in your elixir documentation.

Note that the //! docstring is not supported. Use @moduledoc instead.

Bring your own version of Zig

If you would like to use your system's local zig command, specify this in your use Zig statement options.

use Zig, otp_app: :my_app, local_zig: true

This will use System.find_executable/1 to obtain the zig command. If you want to specify a specific zig path, use the following:

use Zig, otp_app: :my_app, zig_path: "path/to/zig/command"

Summary

Functions

retrieves the zig code from any given module that was compiled with zigler

outputs a String name for the module.

declares a string block to be included in the module's .zig source file.

like sigil_Z/2, but lets you interpolate values from the outside elixir context using string interpolation (the #{value} form)

default version of zig supported by this version of zigler.

Functions

retrieves the zig code from any given module that was compiled with zigler

Link to this function

nif_name(module, use_suffixes \\ true)

View Source

outputs a String name for the module.

note that for filesystem use, you must supply the extension. For internal (BEAM) use, the filesystem extension will be inferred. Therefore we provide two versions of this function.

Link to this macro

sigil_Z(arg, list)

View Source (macro)

declares a string block to be included in the module's .zig source file.

Link to this macro

sigil_z(code, list)

View Source (macro)

like sigil_Z/2, but lets you interpolate values from the outside elixir context using string interpolation (the #{value} form)

default version of zig supported by this version of zigler.

API warning

this API may change in the future.