Current section
Files
Jump to
Current section
Files
lib/zig.ex
defmodule Zig do
@moduledoc """
Inline NIF support for [Zig](https://ziglang.org)
### 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](#module-easy-c).
### Guides
Please consult the following guides for detailed topics:
- [Using Nifs](01-nifs.html)
- [Collection datatypes](02-collections.html)
- [Allocator strategies](03-allocators.html)
- [Nif options](04-nif_options.html)
- [Resources](05-resources.html)
- [C integration](06-c_integration.html)
- [Concurrency strategies](07-concurrency.html)
- [Global module options](08-module_options.html)
- [Raw calling](09-raw_nifs.html)
- [Module callbacks](10-callbacks.html)
> ### Zig version support {: .warning }
>
> 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 NIFs
In the BEAM, you can define a NIF by consulting the following [document](
https://erlang.org/doc/man/erl_nif.html) 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 otp_app name as an option.
Then, use the `sigil_Z/2` macro and write inline 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
```elixir
defmodule BasicModule do
use Zig, otp_app: :zigler
~Z\"""
pub fn add_one(number: i64) i64 {
return number + 1;
}
\"""
end
test "basic module with nif" do
assert 48 = BasicModule.add_one(47)
end
```
> #### otp_app setting {: .info }
>
> You should replace `:zigler` in the following example with the name of
> your own app. If no such app exists (e.g. you are using livebook or
> are in the terminal or escript), you can use `:zigler` as a fallback.
Zigler will *automatically* fill out the appropriate NIF C template, compile
the shared object, and bind it into the module pre-compilation. In the above
example, there will be a `BasiceModule.add_one/1` function call created.
Zigler 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`](beam.html#term) type which is a wrapped
[`ERL_NIF_TERM`](https://www.erlang.org/doc/man/erl_nif.html#ERL_NIF_TERM) type.
See [`erl_nif`](https://www.erlang.org/doc/man/erl_nif.html).
```elixir
test "argument error when types are mismatched" do
assert_raise ArgumentError, fn -> BasicModule.add_one("not a number") end
end
```
### I don't want to use inline Zig
```zig
\\\\ .noinline.zig
pub fn add_one(number: i64) i64 {
return number + 1;
}
```
```elixir
defmodule NoInline do
use Zig, otp_app: :zigler, zig_code_path: ".noinline.zig"
end
test "non-inline zig" do
assert 48 = NoInline.add_one(47)
end
```
### Advanced usage: Unsupported erl_nif functions
the `beam` import does not comprehensively provide support for all functions
in `erl_nif.h`. If you need access to a function in `erl_nif.h` that isn't
provided by zigler, you would do it in the following fashion:
- import `erl_nif` into your zig code, typically under the `e` namespace.
- retrieve `beam.context.env` and use that as your ErlNifEnv pointer.
- use `beam.term` for function return types, which is a struct with a single
field, `v`, of type `ERL_NIF_TERM`.
#### Example
```elixir
defmodule WithErlNif do
use Zig, otp_app: :zigler
~Z\"""
const e = @import("erl_nif");
const beam = @import("beam");
pub fn add_one(number: u64) beam.term {
return .{.v = e.enif_make_uint64(beam.context.env, number + 1)};
}
\"""
end
test "raw erl_nif_function" do
assert 48 = WithErlNif.add_one(47)
end
```
> #### beam.context.env is a threadlocal {: .warning}
>
> `beam.context.env` is a threadlocal variable, and is not available when
> calling functions using `raw` mode. See [Raw](9-raw-mode.html) mode calling
> for more information.
### Advanced usage: Manual marshalling
If you need to marshal your own data, you may use the `beam.get` and
`beam.make` functions to marshal data to and from the BEAM world.
#### Example
```elixir
defmodule ManualMarshalling do
use Zig, otp_app: :zigler, nifs: [add_one: [spec: false]]
@spec add_one(integer) :: integer
~Z\"""
const beam = @import("beam");
pub fn add_one(val: beam.term) !beam.term {
const number = try beam.get(i64, val, .{});
return beam.make(number + 1, .{});
}
\"""
end
test "manual marshalling" do
assert 48 = ManualMarshalling.add_one(47)
end
```
For more details on [`get`](beam.html#get) and [`make`](beam.html#make)
functions see the [`beam`](beam.html) documentation.
> #### Manual Term marshalling {: .warning }
>
> 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 outside `~Z` block, as shown
> in the example.
### Importing external files
If you need to write zig code outside of the module, just place it in
the same directory as your module.
You may either call imported functions from the external file, or
forward a function from the external file, either strategy will
work correctly.
#### Example
```zig
\\\\ .extra_code.zig
pub fn add_one(number: u64) u64 {
return number + 1;
}
```
```elixir
defmodule ExternalImport do
use Zig, otp_app: :zigler
~Z\"""
const extra_code = @import(".extra_code.zig");
pub fn add_one(number: u64) u64 {
return extra_code.add_one(number);
}
pub const forwarded_add_one = extra_code.add_one;
\"""
end
test "external imports by calling" do
assert 48 = ExternalImport.add_one(47)
end
test "external imports by forwarding" do
assert 48 = ExternalImport.forwarded_add_one(47)
end
```
### Advanced Usage: Custom source location
By default, Zigler places generated source code in the same directory
as the module that uses Zigler, however, you may specify a different
directory:
```elixir
defmodule CustomSourceLocation do
use Zig, otp_app: :zigler, dir: "test/.custom_location"
~Z\"""
pub fn add_one(number: u64) u64 {
return number + 1;
}
\"""
end
test "custom_location is built" do
assert File.dir?("test/custom_location")
assert File.exists?("test/.custom_location/.Elixir.CustomSourceLocation.zig")
end
```
### Advanced usage: change staging directory location
By default, zigler stages files in `/tmp/{modulename}` directory. In some cases
this will cause user collisions and permissions errors when trying to build modules
on multitenant systems. If you need to change the staging directory, set the
`ZIGLER_STAGING_ROOT` environment variable to the desired directory. The
recommended staging directory is `~/.cache/zigler`. NB: In the future, this
may become the default staging directory.
"""
@spec __using__(keyword) :: Macro.t()
defmacro __using__(opts) do
module = __CALLER__.module
if module in :erlang.loaded() do
:code.purge(module)
end
unless Keyword.has_key?(opts, :otp_app) do
raise CompileError,
file: __CALLER__.file,
description:
"(module #{inspect(module)}) you must supply an `otp_app` option to `use Zig`"
end
opts =
opts
|> Keyword.put(:language, Elixir)
|> Zig.Module.new(__CALLER__)
Module.register_attribute(module, :zig_code_parts, accumulate: true)
Module.register_attribute(module, :zig_code, persist: true)
code =
quote do
@zigler_opts unquote(Macro.escape(opts))
import Zig, only: [sigil_Z: 2, sigil_z: 2]
@on_load :__load_nifs__
@before_compile Zig.Compiler
end
Zig.Macro.inspect(code, opts)
end
@doc """
declares a string block to be included in the module's .zig source file.
"""
defmacro sigil_Z({:<<>>, meta, [zig_code]}, []) do
quoted_code(zig_code, meta, __CALLER__)
end
@doc """
like `sigil_Z/2`, but lets you interpolate values from the outside
elixir context using string interpolation (the `\#{value}` form)
"""
defmacro sigil_z(code = {:<<>>, _, _}, []) do
quoted_code(code, [line: __CALLER__.line], __CALLER__)
end
defp quoted_code(zig_code, meta, caller) do
opts = Module.get_attribute(caller.module, :zigler_opts)
if opts[:easy_c] do
raise CompileError,
description: "you can't use ~Z in easy_c nifs",
line: caller.line,
file: caller.file
end
line = meta[:line]
module = caller.module
file = Path.relative_to_cwd(caller.file)
quote bind_quoted: [module: module, zig_code: zig_code, file: file, line: line] do
@zig_code_parts "// ref #{file}:#{line}\n"
@zig_code_parts zig_code
:nothing
end
end
@doc """
retrieves the zig code from any given module that was compiled with zigler
"""
def code(module) do
[code] = Keyword.fetch!(module.__info__(:attributes), :zig_code)
code
end
@extension (case :os.type() do
{:unix, :linux} -> ".so"
{:unix, :freebsd} -> ".so"
{:unix, :darwin} -> ".dylib"
{_, :nt} -> ".dll"
end)
@doc """
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.
"""
def nif_name(module, use_suffixes \\ true) do
if use_suffixes do
"lib#{module.module}#{@extension}"
else
"lib#{module.module}"
end
end
@version Zigler.MixProject.zig_version()
@doc """
default version of zig supported by this version of zigler.
> ### API warning {: .warning }
>
> this API may change in the future.
"""
def version, do: @version
end
# check that the otp_version is 24 or greater.
otp_version =
:otp_release
|> :erlang.system_info()
|> List.to_integer()
if otp_version < 24 do
raise CompileError, description: "zigler requires OTP 24 or greater"
end