Current section
Files
Jump to
Current section
Files
lib/zig/_nif.ex
defmodule Zig.Nif do
@moduledoc false
# module encapsulating all of the information required to correctly generate
# a nif function.
#
# Note that all information obtained from semantic analysis of the function is
# stashed in the `Zig.Nif.Function` module.
# This module gets an access behaviour so that it can be easily used in EEx files.
@behaviour Access
alias Zig.Nif.DirtyCpu
alias Zig.Nif.DirtyIo
alias Zig.Nif.Synchronous
alias Zig.Nif.Threaded
alias Zig.Nif.Yielding
alias Zig.Options
alias Zig.Parameter
alias Zig.Return
alias Zig.Type
alias Zig.Type.Error
alias Zig.Type.Function
@enforce_keys ~w[name module file line module_code_path zig_code_path cleanup]a
defstruct @enforce_keys ++
[
:allocator,
:impl,
:alias,
:arity,
# next three are determined internally as a part of parsing or sema.
:signature,
:raw,
:doc,
# user-unmerged options with defaults
# note that `cleanup` default is set programatically since `return` depends on it.
:return,
params: %{},
export: true,
concurrency: Synchronous,
spec: true,
leak_check: false
]
@typep unmerged :: %__MODULE__{
name: atom,
module: atom,
file: Path.t(),
line: pos_integer,
module_code_path: Path.t(),
zig_code_path: Path.t(),
cleanup: boolean,
# user-specified options
allocator: atom,
impl: nil | boolean | module,
alias: atom,
export: boolean,
concurrency: Zig.concurrency(),
arity: [arity | Range.t(arity, arity)],
params: %{optional(integer) => Parameter.unmerged()},
return: Return.unmerged(),
spec: boolean,
leak_check: boolean,
# the following are set after user-specified options.
signature: nil | Function.t(),
doc: nil | String.t()
}
@typep sema_raw :: %__MODULE__{
signature: Function.t(),
raw: :term | :erl_nif_term,
return: Return.sema()
}
@typep sema_typed :: %__MODULE__{
signature: Function.t(),
raw: nil,
params: %{optional(integer) => Parameter.sema()},
return: Return.sema()
}
@typep raw :: %__MODULE__{
name: atom,
module: module,
file: Path.t(),
line: pos_integer,
module_code_path: Path.t(),
zig_code_path: Path.t(),
cleanup: false,
# user-specified options
allocator: nil,
impl: nil | boolean | module,
alias: atom,
export: boolean,
concurrency: :synchronous,
arity: [arity | Range.t(arity, arity)],
return: Return.t(),
spec: boolean,
leak_check: false,
signature: Function.t(),
doc: nil | String.t()
}
@typep typed :: %__MODULE__{
name: atom,
module: module,
file: Path.t(),
line: pos_integer,
module_code_path: Path.t(),
zig_code_path: Path.t(),
cleanup: boolean,
# user-specified options
allocator: atom,
impl: nil | boolean | module,
alias: atom,
export: boolean,
concurrency: Zig.concurrency(),
params: %{optional(integer) => Parameter.t()},
return: Return.t(),
spec: boolean,
leak_check: boolean,
signature: Function.t(),
doc: nil | String.t()
}
@type t :: raw | typed
@impl true
defdelegate fetch(function, key), to: Map
@doc """
based on nif options for this function keyword at (opts :: nifs :: function_name)
"""
@spec new(Zig.nif_options(), Options.context()) :: unmerged
def new({name, opts}, context) do
# all options which can take atoms must be normalized first.
{opts, context} =
opts
|> Options.normalize(:cleanup, Options.boolean_normalizer(noclean: false), context)
|> pull_cleanup(context)
opts
|> Options.normalize(:leak_check, Options.boolean_normalizer(leak_check: true), context)
|> Options.normalize(:spec, Options.boolean_normalizer(nospec: false), context)
|> Options.normalize(:concurrency, &normalize_concurrency/2, context)
|> Options.scrub_non_keyword(context)
|> Keyword.put(:name, name)
|> Options.normalize_kw(:params, %{}, &normalize_params/2, context)
|> Options.normalize_kw(
:return,
Return.new(context),
Options.struct_normalizer(Return),
context
)
|> set_return_in_out(context)
|> Options.normalize_kw(:arity, &normalize_arity/2, context)
|> Options.validate(:impl, {:atom, "a module or `true`"}, context)
|> Options.validate(:alias, &validate_alias(&1, name), context)
|> Options.validate(:export, :boolean, context)
|> Options.validate(:allocator, :atom, context)
|> then(&struct!(__MODULE__, &1))
rescue
e in KeyError ->
Options.raise_with("was supplied the invalid option `#{e.key}`", context)
end
defp pull_cleanup(opts, context), do: pull_cleanup(opts, opts, context)
defp pull_cleanup([{:cleanup, value} | _], opts, context),
do: {opts, %{context | cleanup: value}}
defp pull_cleanup([_ | rest], opts, context), do: pull_cleanup(rest, opts, context)
defp pull_cleanup([], opts, context), do: {[{:cleanup, context.cleanup} | opts], context}
@concurrency_map %{
synchronous: Synchronous,
threaded: Threaded,
yielding: Yielding,
dirty_cpu: DirtyCpu,
dirty_io: DirtyIo
}
defp normalize_concurrency({atom}, _context), do: Map.fetch(@concurrency_map, atom)
defp normalize_concurrency(value, context) do
case Map.fetch(@concurrency_map, value) do
{:ok, module} ->
module
:error ->
valid_options =
@concurrency_map
|> Map.keys()
|> Enum.sort()
|> Options.list_of()
Options.raise_with("must be one of #{valid_options}", value, context)
end
end
@params_msg "must be a map with non-negative integer keys"
defp normalize_params(params, context) when is_map(params) do
Map.new(params, fn
{index, v} when is_integer(index) and index >= 0 ->
{index, Parameter.new(v, Options.push_key(context, index))}
{k, _} ->
Options.raise_with(@params_msg, k, context)
end)
end
defp normalize_params(other, context), do: Options.raise_with(@params_msg, other, context)
@arity_error "must be a non-negative integer, range or a list of those"
defp normalize_arity(arity, context) do
arity
|> List.wrap()
|> Enum.flat_map(fn
n when is_integer(n) and n >= 0 ->
[n]
a.._//1 = range when a >= 0 ->
Enum.to_list(range)
other ->
Options.raise_with(@arity_error, other, context)
end)
end
defp set_return_in_out(opts, context) do
opts
|> Keyword.fetch!(:params)
|> Enum.flat_map(fn {index, param} ->
List.wrap(if param.in_out, do: "arg#{index}")
end)
|> case do
[] ->
opts
[arg] ->
Keyword.update!(opts, :return, &%{&1 | in_out: arg})
[_ | _] ->
Options.raise_with(
"may only have one parameter with `:in_out`",
Options.push_key(context, :return)
)
end
end
defp validate_alias(name, name), do: {:error, "may not be the same as the nif name `#{name}`"}
defp validate_alias(alias_name, _name) when is_atom(alias_name), do: :ok
defp validate_alias(other, _name), do: {:error, "must be an atom", other}
# merging user-specified options with semantic analysis
@spec merge(sema_raw, unmerged) :: raw
@spec merge(sema_typed, unmerged) :: typed
def merge(sema_nif, spec_nif) do
verify_raw_concurrency(sema_nif, spec_nif)
verify_return_struct(sema_nif, spec_nif)
# these are the fields we are going to merge
merge_fields = ~w[name cleanup allocator impl alias export concurrency spec leak_check]a
# params and return needs to be deep-merged.
merge_fields
|> Enum.reduce(sema_nif, fn field, so_far ->
Map.replace!(so_far, field, Map.fetch!(spec_nif, field))
end)
|> merge_arity(spec_nif)
|> Map.update!(:return, &Return.merge(&1, spec_nif.return))
|> Map.update!(:params, &deepmerge_params(&1, spec_nif.params))
end
defp verify_raw_concurrency(%{raw: nil}, _), do: :ok
defp verify_raw_concurrency(_, %{concurrency: concurrency})
when concurrency in [Synchronous, DirtyCpu, DirtyIo],
do: :ok
defp verify_raw_concurrency(sema, spec) do
raise CompileError,
description:
"the raw function `#{sema.name}` may only be used with `:synchronous`, `:dirty_cpu` or `:dirty_io` concurrency",
file: spec.file,
line: spec.line
end
defp verify_return_struct(_, %{return: %{struct: nil}}), do: :ok
defp verify_return_struct(%{return: %{type: %Zig.Type.Struct{}}}, _), do: :ok
defp verify_return_struct(sema, spec) do
raise CompileError,
description:
"the `struct:` return option was specified for `#{sema.name}` but the return type is `#{Zig.Type.render_zig(sema.return.type)}`, not a struct",
file: spec.file,
line: spec.line
end
defp merge_arity(%{raw: nil} = sema_nif, %{arity: nil}),
do: %{sema_nif | arity: [sema_nif.signature.arity]}
defp merge_arity(%{raw: nil, arity: arity}, spec_nif) do
raise CompileError,
description:
"the non-raw function #{inspect(spec_nif.module)}.#{spec_nif.name}/#{arity} may not have an arity specified in the nif parameters",
file: spec_nif.file,
line: spec_nif.line
end
defp merge_arity(sema_nif, %{arity: arity}) when is_list(arity), do: %{sema_nif | arity: arity}
defp merge_arity(_, spec_nif) do
raise CompileError,
description:
"the raw function #{inspect(spec_nif.module)}.#{spec_nif.name}/? must have arities specified in zigler parameters",
file: spec_nif.file,
line: spec_nif.line
end
defp deepmerge_params(sema_params, spec_params) do
Enum.reduce(spec_params, sema_params, fn
{index, parameter}, so_far ->
Map.update!(so_far, index, &Parameter.merge(&1, parameter))
end)
end
defp symbol(nif) do
if nif.export, do: :def, else: :defp
end
# rendering functions
def render_elixir(%{concurrency: concurrency} = nif) do
doc =
if nif_doc = nif.doc do
quote do
@doc unquote(nif_doc)
end
end
override_arities =
Enum.flat_map(
nif.arity,
&List.wrap(if Module.defines?(nif.module, {nif.name, &1}, symbol(nif)), do: &1)
)
functions = concurrency.render_elixir(nif, override_arities)
specs = if nif.spec, do: render_elixir_spec(nif)
impl =
nif.impl
|> List.wrap()
|> Enum.map(
"e do
@impl unquote(&1)
end
)
quote context: Elixir do
unquote(specs)
unquote(doc)
unquote(impl)
unquote(functions)
end
end
def render_elixir_spec(%{raw: t, arity: arities} = nif) when not is_nil(t) do
Enum.map(arities, fn arity ->
param_spec = raw_param_spec(arity)
quote do
@spec unquote(nif.name)(unquote_splicing(param_spec)) :: term
end
end)
end
defp raw_param_spec(0), do: []
defp raw_param_spec(arity), do: Enum.map(1..arity, fn _ -> {:term, [], []} end)
def render_elixir_spec(nif) do
param_spec =
nif.params
|> Enum.sort()
|> Enum.map(fn {_, p} -> Type.render_elixir_spec(p.type, p) end)
return_spec =
case nif.return do
%{spec: nil} ->
Type.render_elixir_spec(nif.return.type, nif.return)
%{type: term, spec: spec} when term in ~w[term erl_nif_term]a ->
spec
_ ->
raise CompileError,
description:
"you may only specify return spec when the return type is `term` or `e.ErlNifTerm`"
end
quote context: Elixir do
@spec unquote(nif.name)(unquote_splicing(param_spec)) :: unquote(return_spec)
end
end
def render_erlang_spec(%{raw: t, arity: arities} = nif) when not is_nil(t) do
Enum.map(arities, fn arity ->
params = Enum.map_join(1..arity//1, ", ", fn _ -> "term()" end)
"-spec #{nif.name}(#{params}) -> term()."
end)
end
def render_erlang_spec(nif) do
params =
nif.params
|> Enum.sort()
|> Enum.map_join(", ", fn {_, p} -> Type.render_erlang_spec(p.type, p) end)
return_spec = Type.render_erlang_spec(nif.return.type, nif.return)
["-spec #{nif.name}(#{params}) -> #{return_spec}."]
end
def render_erlang(nif, _opts \\ []) do
function =
nif
|> nif.concurrency.render_erlang
|> List.wrap()
function
end
require EEx
def render_zig(%__MODULE__{} = nif) do
nif.concurrency.render_zig(nif)
end
@flags %{
synchronous: "0",
dirty_cpu: "e.ERL_NIF_DIRTY_JOB_CPU_BOUND",
dirty_io: "e.ERL_NIF_DIRTY_JOB_IO_BOUND"
}
def table_entries(nif) do
nif
|> nif.concurrency.table_entries()
|> Enum.map(fn
{function, arity, fptr, concurrency} ->
flags = Map.fetch!(@flags, concurrency)
~s(.{.name="#{function}", .arity=#{arity}, .fptr=#{fptr}, .flags=#{flags}})
end)
end
def maybe_catch(%Error{}) do
"""
catch |err| {
if (beam.thread_not_running(err)) return 0;
return beam.raise_with_error_return(err, @errorReturnTrace(), .{}).v;
}
"""
end
def maybe_catch(_), do: nil
def resources(nif), do: nif.concurrency.resources(nif)
@spec set_file_line(t(), module, Parser.t()) :: t()
def set_file_line(nif, manifest_module, parsed) do
expected_name = if name = nif.alias, do: name, else: nif.name
raw_line =
Enum.find_value(parsed.code, fn
%{name: name, location: {line, _}} -> if name == expected_name, do: line
end) || raise "raw line information not found for #{expected_name}"
{file, line} = manifest_module.__resolve(%{file_name: nif.zig_code_path, line: raw_line})
%{nif | file: file, line: line}
end
# COMMON TOOLS
# generates AST for parameters.
@spec elixir_parameters(arity, used :: boolean) :: [Macro.t()]
def elixir_parameters(0, _), do: []
def elixir_parameters(arity, true), do: Enum.map(1..arity, &{:"arg#{&1}", [], Elixir})
def elixir_parameters(arity, false), do: Enum.map(1..arity, &{:"_arg#{&1}", [], Elixir})
@spec erlang_parameters(arity, used :: boolean) :: [{:var, atom()}]
def erlang_parameters(0, _), do: []
def erlang_parameters(arity, true), do: Enum.map(1..arity, &{:var, :"X#{&1}"})
def erlang_parameters(arity, false), do: Enum.map(1..arity, &{:var, :"_X#{&1}"})
def style(nif) do
if nif.export, do: :def, else: :defp
end
def binding_error(name, arity) do
"nif for function #{name}/#{arity} not bound"
end
# Access behaviour guards
@impl true
def get_and_update(_, _, _), do: raise("you should not update a function")
@impl true
def pop(_, _), do: raise("you should not pop a function")
end