Current section

Files

Jump to
zigler lib zig _module.ex
Raw

lib/zig/_module.ex

defmodule Zig.Module do
@moduledoc false
# abstraction representing multiple zig nif functions bound into a single
# module
alias Zig.C
alias Zig.Nif
alias Zig.Options
alias Zig.Resources
alias Zig.Type.Error
# you'll never see me ever import any other module.
import Zig.QuoteErl
# for easy access in EEx files
@behaviour Access
@impl true
defdelegate fetch(function, key), to: Map
@enforce_keys [:otp_app, :module, :file, :line]
defstruct @enforce_keys ++
[
# user options:
:precompiled,
:module_code_path,
:zig_code_path,
:version,
:easy_c,
:dir,
:c,
:nifs,
# generated by zigler
:parsed,
:manifest,
:manifest_module,
:sema,
# defaulted options
release_mode: {:env, :debug},
ignore: [],
packages: [],
resources: [],
callbacks: [],
# assembled from collected `opts` fields
default_nif_opts: [],
# generated by zigler
language: Elixir,
external_resources: [],
attributes: [],
# debug options: user-supplied, but unchecked.
dump: false,
dump_sema: false,
dump_build_zig: false
]
@type t :: %__MODULE__{
otp_app: atom,
module: module,
file: Path.t(),
line: pos_integer,
module_code_path: nil | Path.t(),
zig_code_path: nil | Path.t(),
manifest: nil | Manifest.t(),
manifest_module: nil | module,
sema: Sema.t(),
parsed: Parser.t(),
version: String.t(),
easy_c: nil | Path.t(),
dir: Path.t(),
c: C.t(),
nifs: {:auto, [Nif.t()]} | [Nif.t()],
parsed: Parser.t(),
manifest: nil | Manifest.t(),
manifest_module: nil | module,
sema: Sema.t(),
release_mode: Zig.release_mode(),
ignore: [atom],
packages: [Zig.package_spec()],
resources: [atom],
callbacks: [on_load: atom, on_upgrade: atom, on_unload: atom],
default_nif_opts: [cleanup: boolean, leak_check: boolean],
language: Elixir | :erlang,
external_resources: [Path.t()],
attributes: keyword,
dump: boolean,
dump_sema: boolean,
dump_build_zig: boolean | :stdout | :stderr | Path.t()
}
@defaultable_nif_opts ~w[cleanup leak_check]a
@affix %{"Elixir": "`use Zig`", erlang: "`zig_opts(...)`"}
# this function builds and verifies the module options and turns it into the struct.
def new(opts, caller) do
# make sure that the caller has declared otp_app here.
context =
case Keyword.fetch(opts, :otp_app) do
:error ->
lang = Keyword.fetch!(opts, :language)
raise CompileError,
description:
"(module #{inspect(caller.module)}) you must supply an `otp_app` option to #{@affix[lang]}",
file: caller.file,
line: caller.line
{:ok, otp_app} ->
Options.initialize_context(caller, otp_app)
end
default_nif_opts = Keyword.take(opts, @defaultable_nif_opts)
common_values = [
module: caller.module,
file: caller.file,
line: caller.line,
default_nif_opts: default_nif_opts
]
opts
|> obtain_version
|> Options.normalize_kw(:c, %C{}, Options.struct_normalizer(C), context)
|> Options.normalize_kw(
:callbacks,
[],
list_normalizer(&normalize_callback/2, "callback"),
context
)
|> Options.normalize_kw(
:packages,
[],
list_normalizer(&normalize_package/2, "package"),
context
)
|> Options.normalize_path(:dir, context)
|> Options.normalize_path(:module_code_path, context)
|> Options.normalize_path(:zig_code_path, context)
|> Options.normalize_path(:easy_c, context)
|> Options.normalize_kw(:ignore, [], &normalize_atom_or_atomlist/2, context)
|> Options.normalize_kw(:resources, [], &normalize_atom_or_atomlist/2, context)
|> Options.validate(:release_mode, &validate_release_modes/1, context)
|> Options.validate(:cleanup, :boolean, context)
|> Options.validate(:leak_check, :boolean, context)
|> Keyword.drop(@defaultable_nif_opts)
|> Keyword.merge(common_values)
|> normalize_nifs(context)
|> then(&struct!(__MODULE__, &1))
rescue
e in KeyError ->
context = Options.initialize_context(caller, nil)
lang = Keyword.fetch!(opts, :language)
Options.raise_with("#{@affix[lang]} was supplied the invalid option `#{e.key}`", context)
end
defp obtain_version(opts) do
otp_app = Keyword.fetch!(opts, :otp_app)
Keyword.put_new_lazy(opts, :version, fn ->
cond do
# try checking the mix project first (this is if the project is being compiled for the first time)
version = function_exported?(Mix.Project, :config, 0) and Mix.Project.config()[:version] ->
Version.parse!(version)
# try checking the application version (this is if we are hot-patching the so file)
tuple = Application.loaded_applications() |> List.keyfind(otp_app, 0) ->
tuple
|> elem(2)
|> to_string()
|> Version.parse!()
:else ->
Version.parse!("0.0.0")
end
end)
end
defp list_normalizer(callback, error_msg),
do: fn
list, context when is_list(list) ->
try do
Enum.map(list, &callback.(&1, context))
rescue
_ in FunctionClauseError ->
Options.raise_with(
"must be a list of #{error_msg} specifications",
list,
context
)
end
other, context ->
Options.raise_with(
"must be a list of #{error_msg} specifications",
other,
context
)
end
@callbacks ~w[on_load on_upgrade on_unload]a
defp normalize_callback(callback, _context) when callback in @callbacks,
do: {callback, callback}
defp normalize_callback({callback, fun}, _context) when callback in @callbacks and is_atom(fun),
do: {callback, fun}
defp normalize_callback({callback, invalid}, context) when callback in @callbacks,
do: Options.raise_with("must be an atom", invalid, Options.push_key(context, callback))
defp normalize_callback(other, context),
do:
Options.raise_with(
"must be a keyword list of callback specs",
other,
context
)
@dependencies_atom_error "must be a list of atoms representing dependencies"
@package_tuple_error "must be a tuple of the form `{path, [deps...]}`"
defp normalize_package({k, {path, deps}}, context) when is_atom(k) and is_list(deps) do
Enum.each(deps, fn dep ->
is_atom(dep) or
Options.raise_with(@dependencies_atom_error, dep, Options.push_key(context, k))
end)
try do
{k, {IO.iodata_to_binary(path), deps}}
rescue
_ ->
Options.raise_with(
@package_tuple_error,
{:tag, "path", path},
Options.push_key(context, k)
)
end
end
defp normalize_package({k, {_, malformed}}, context) when is_atom(k),
do: Options.raise_with(@dependencies_atom_error, malformed, Options.push_key(context, k))
defp normalize_package({k, malformed}, context) when is_atom(k),
do: Options.raise_with(@package_tuple_error, malformed, Options.push_key(context, k))
defp normalize_package(other, opts),
do:
Options.raise_with(
"must be a list of package specifications",
other,
opts
)
@atom_or_atomlist_error "must be an atom or a list of atoms"
defp normalize_atom_or_atomlist(atom, _context) when is_atom(atom), do: [atom]
defp normalize_atom_or_atomlist(list, context) when is_list(list) do
Enum.each(list, fn
item ->
is_atom(item) or Options.raise_with(@atom_or_atomlist_error, item, context)
end)
list
rescue
_ in FunctionClauseError -> Options.raise_with(@atom_or_atomlist_error, list, context)
end
defp normalize_atom_or_atomlist(other, context),
do:
Options.raise_with(
@atom_or_atomlist_error,
other,
context
)
defp normalize_nifs(opts, context) do
common_values = Keyword.take(opts, ~w[module file line module_code_path zig_code_path]a)
Options.normalize_kw(
opts,
:nifs,
{:auto, []},
&normalize_nifs(&1, common_values, &2),
context
)
end
defp normalize_nifs(nifs, common_values, context) when is_list(nifs) do
Enum.map(nifs, fn
{atom, opts} when is_atom(atom) and is_list(opts) ->
Nif.new({atom, common_values ++ opts}, context)
atom when is_atom(atom) ->
Nif.new({atom, common_values}, context)
other ->
Options.raise_with("", other, context)
end)
end
defp normalize_nifs({:auto, nifs}, common_values, context) do
{:auto, normalize_nifs(nifs, common_values, context)}
end
defp validate_release_modes(mode) when mode in ~w[debug safe fast small env]a, do: :ok
defp validate_release_modes({:env, mode}) when mode in ~w[debug safe fast small]a, do: :ok
defp validate_release_modes(other) do
{:error, "must be one of `:debug`, `:safe`, `:fast`, `:small`, `:env` or `{:env, mode}`",
other}
end
# CODE RENDERING
# zig file rendering
require EEx
nif = Path.join(__DIR__, "templates/module.zig.eex")
EEx.function_from_file(:def, :render_zig, nif, [:assigns])
on_load = Path.join(__DIR__, "templates/on_load.zig.eex")
EEx.function_from_file(:def, :render_on_load, on_load, [:assigns])
on_upgrade = Path.join(__DIR__, "templates/on_upgrade.zig.eex")
EEx.function_from_file(:def, :render_on_upgrade, on_upgrade, [:assigns])
on_unload = Path.join(__DIR__, "templates/on_unload.zig.eex")
EEx.function_from_file(:def, :render_on_unload, on_unload, [:assigns])
# internal helpers for rendering the c
defp table_entries(nifs) when is_list(nifs) do
nifs
|> Enum.flat_map(&Nif.table_entries/1)
|> Enum.join(",")
end
@index_of %{major: 0, minor: 1}
defp nif_version(at) do
:nif_version
|> :erlang.system_info()
|> List.to_string()
|> String.split(".")
|> Enum.at(@index_of[at])
end
# ast rendering
def render_elixir(module, zig_code) do
module_name = "#{module.module}"
on_load_code =
if {:__on_load__, 0} in Module.definitions_in(module.module) do
quote do
__on_load__()
end
else
0
end
external_resources =
Enum.map(module.external_resources, fn file ->
quote do
@external_resource unquote(file)
end
end)
load_nif_fn =
quote do
unquote_splicing(external_resources)
def __load_nifs__ do
# LOADS the nifs from :code.lib_dir() <> "ebin", which is
# a path that has files correctly moved in to release packages.
require Logger
unquote(module.otp_app)
|> :code.priv_dir()
|> Path.join("lib")
|> Path.join(unquote(module_name))
|> String.to_charlist()
|> :erlang.load_nif(unquote(on_load_code))
|> case do
:ok ->
Logger.debug("loaded module at #{unquote(module_name)}")
error = {:error, any} ->
Logger.error("loading module #{unquote(module_name)} #{inspect(any)}")
end
end
end
function_code = Enum.map(module.nifs, &Nif.render_elixir/1)
manifest_code =
if Enum.any?(module.nifs, &match?(%Error{}, &1.return.type)) do
quote do
require Zig.Manifest
Zig.Manifest.resolver(unquote(module.manifest), unquote(module.zig_code_path), :defp)
end
end
quote do
# these two attribs can be persisted for code inspection.
@zigler_module unquote(Macro.escape(module))
@zig_code unquote(zig_code)
unquote_splicing(function_code)
unquote(load_nif_fn)
unquote(manifest_code)
def _format_error(_, [{_, _, _, opts} | _rest] = _stacktrace) do
if formatted = opts[:zigler_error], do: formatted, else: %{}
end
end
end
def render_erlang(module, _zig_code) do
otp_app = module.otp_app
module_name = Atom.to_charlist(module.module)
init_function =
quote_erl(
"""
'__init__'() ->
erlang:load_nif(filename:join(code:priv_dir(unquote(otp_app)), unquote(module_id)), []).
""",
otp_app: otp_app,
module_id: ~C'lib/' ++ module_name
)
function_code = Enum.map(module.nifs, &Nif.render_erlang/1)
Enum.flat_map(function_code, & &1) ++ init_function
end
# Access behaviour guards
@impl true
def get_and_update(_, _, _), do: raise("you should not update a module")
@impl true
def pop(_, _), do: raise("you should not pop a module")
end