defmodule Zig.Nif do @moduledoc """ 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. """ # nif gets an access behaviour so that it can be easily used in EEx # files. @behaviour Access @enforce_keys [:export, :concurrency, :type] @impl true defdelegate fetch(function, key), to: Map defstruct @enforce_keys ++ ~w[name raw args return leak_check alias doc spec]a alias Zig.Nif.DirtyCpu alias Zig.Nif.DirtyIo alias Zig.Nif.Synchronous alias Zig.Nif.Threaded alias Zig.Nif.Yielding alias Zig.Type alias Zig.Type.Error alias Zig.Type.Function @type t :: %__MODULE__{ name: atom, export: boolean, concurrency: Synchronous | Threaded | Yielding | DirtyCpu | DirtyIo, type: Function.t(), raw: nil | :beam | :erl_nif | :c, args: [keyword], return: keyword, leak_check: boolean(), alias: nil | atom, doc: nil | String.t(), spec: Macro.t() } defmodule Concurrency do @moduledoc """ behaviour module which describes the interface for "plugins" which generate concurrency-specific code. """ alias Zig.Nif @callback render_elixir(Nif.t()) :: Macro.t() @callback render_erlang(Nif.t()) :: term @callback render_zig(Nif.t()) :: iodata @type concurrency :: :synchronous | :dirty_cpu | :dirty_io @type table_entry :: {name :: atom, arity :: non_neg_integer, function_pointer :: atom, bootstrap :: concurrency} @doc """ returns "table_entry" tuples which are then used to generate the nif table. if a nif function needs multiple parts, for example, for concurrency management, then multiple entries should be returned. """ @callback table_entries(Nif.t()) :: [table_entry] @callback resources(Nif.t()) :: [{:root, atom}] end @concurrency_modules %{ :synchronous => Synchronous, :threaded => Threaded, :yielding => Yielding, :dirty_cpu => DirtyCpu, :dirty_io => DirtyIo } @doc """ based on nif options for this function keyword at (opts :: nifs :: function_name) """ def new(name, opts) do %__MODULE__{ name: name, export: Keyword.fetch!(opts, :export), concurrency: Map.get(@concurrency_modules, Keyword.fetch!(opts, :concurrency)), type: opts[:type], raw: extract_raw(opts[:raw], opts[:type]), args: opts[:args], return: opts[:return], leak_check: opts[:leak_check], alias: opts[:alias], doc: opts[:doc], spec: Keyword.get(opts, :spec, :auto) } end defp extract_raw(raw_opt, %{return: return}) do case {raw_opt, return} do {nil, _} -> nil {{:c, arity}, _} when is_integer(arity) -> :c {arity, :term} when is_integer(arity) -> :beam {arity, :erl_nif_term} when is_integer(arity) -> :erl_nif end end def render_elixir(%{concurrency: concurrency} = nif) do doc = if nif_doc = nif.doc do quote do @doc unquote(nif_doc) end end typespec = case nif.spec do false -> quote do end [{:->, _, [params, res]}] -> quote do @spec unquote(nif.name)(unquote_splicing(params)) :: unquote(res) end :auto -> quote do @spec unquote(spec(nif)) end end functions = concurrency.render_elixir(nif) quote context: Elixir do unquote(doc) unquote(typespec) unquote(functions) end end def render_erlang(nif, _opts \\ []) do # TODO: typespec in erlang. 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.concurrency.table_entries(nif) |> Enum.map(fn {function, arity, fptr, concurrency} -> flags = Map.fetch!(@flags, concurrency) ~s(.{.name="#{function}", .arity=#{arity}, .fptr=#{fptr}, .flags=#{flags}}) end) end def indexed_parameters([:env | rest]) do indexed_parameters(rest) end def indexed_parameters(params_list) do Enum.with_index(params_list) end def indexed_args([:env | rest]) do case indexed_args(rest) do "" -> "env" argstrs -> "env, #{argstrs}" end end def indexed_args(params_list) do params_list |> Enum.with_index() |> Enum.map_join(", ", fn {_, index} -> "arg#{index}" end) end def maybe_catch(%Error{}) do """ catch |err| { return beam.raise_with_error_return(env, err, @errorReturnTrace()).v; } """ end def maybe_catch(_), do: nil def validate_return!(function, file, line) do unless Type.return_allowed?(function.return) do raise CompileError, description: "functions returning #{function.return} are not allowed", file: Path.relative_to_cwd(file), line: line end end def spec(nif) do if nif.raw do spec_raw(nif) else spec_coded(nif) end end defp spec_raw(%{type: type}) do params = List.duplicate( quote do term() end, type.arity ) quote context: Elixir do unquote(type.name)(unquote_splicing(params)) :: term() end end defp spec_coded(%{type: type, return: return_opts}) do trimmed = case type.params do [:env | list] -> list list -> list end param_types = Enum.map(trimmed, &Type.spec(&1, :param, [])) # TODO: check for easy_c return = if arg = return_opts[:arg] do type.params |> Enum.at(arg) |> Type.spec(:return, return_opts) else Type.spec(type.return, :return, return_opts) end quote context: Elixir do unquote(type.name)(unquote_splicing(param_types)) :: unquote(return) end end ############################################################################# ## OPTIONS NORMALIZATION # nifs are either atom() or {atom(), keyword()}. This turns all atom nifs # into {atom(), []}, so that downstream they are easier to deal with. @doc false def default_options, do: [ concurrency: :synchronous, args: nil, return: [type: :default], export: true ] @doc false def normalize_options!(function_name, common_options) when is_atom(function_name) do {function_name, common_options} end def normalize_options!({function_name, opts}, common_options) do updated_opts = default_options() |> Keyword.merge(common_options) |> Keyword.merge(Enum.map(opts, &normalize_option!/1)) {function_name, updated_opts} end @nif_option_types %{ concurrency: "one of `:synchronous`, `:dirty_cpu`, `:dirty_io`, `:threaded`, `:yielding`", leak_check: "boolean", alias: "atom", export: "boolean" } @atom_options %{ leak_check: {:leak_check, true}, def: {:export, true}, defp: {:export, false}, dirty_cpu: {:concurrency, :dirty_cpu}, dirty_io: {:concurrency, :dirty_io}, synchronous: {:concurrency, :synchronous}, threaded: {:concurrency, :threaded}, yielding: {:concurrency, :yielding} } defp normalize_option!(atom) when is_map_key(@atom_options, atom), do: Map.fetch!(@atom_options, atom) defp normalize_option!(:defp), do: {} defp normalize_option!({:leak_check, should_check} = option) when is_boolean(should_check), do: option defp normalize_option!({:alias, function} = option) when is_atom(function), do: option defp normalize_option!({:export, should_export} = option) when is_boolean(should_export), do: option defp normalize_option!({:raw, integer} = option) when is_integer(integer), do: option defp normalize_option!({:raw, {:c, integer}} = option) when is_integer(integer), do: option @return_types [:list, :binary, :default] @return_option_types %{ raw: "integer or `{:c, integer}`", arg: "integer", type: "one of `:list`, `:binary`, `:default`", noclean: "boolean", length: "`integer` or `{:arg, integer}`" } @as_is_options ~w[args spec docs]a defp normalize_option!({:return, return_opts}) do updated_return = return_opts |> List.wrap() |> Enum.map(fn integer when is_integer(integer) -> {:arg, integer} type when type in @return_types -> {:type, type} :noclean -> {:noclean, true} option = {:raw, integer} when is_integer(integer) -> option option = {:raw, {:c, integer}} when is_integer(integer) -> option option = {:arg, integer} when is_integer(integer) -> option option = {:type, type} when type in @return_types -> option option = {:noclean, boolean} when is_boolean(boolean) -> option option = {:length, integer} when is_integer(integer) -> option option = {:length, {:arg, integer}} when is_integer(integer) -> option {ret_opt, _} when is_map_key(@return_option_types, ret_opt) -> raise CompileError, description: "return option `:#{ret_opt}` must be #{Map.fetch!(@return_option_types, ret_opt)}" other -> raise CompileError, description: "unrecognized return option `#{inspect(other)}`" end) |> Keyword.put_new(:type, :default) {:return, updated_return} end defp normalize_option!({tag, _} = as_is) when tag in @as_is_options, do: as_is defp normalize_option!({opt, value}) when is_map_key(@nif_option_types, opt) do raise CompileError, description: "nif option :#{opt} must be #{Map.fetch!(@nif_option_types, opt)}, got: #{inspect(value)}" end defp normalize_option!(other) do raise CompileError, description: "unrecognized nif option `#{inspect(other)}`" 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