defmodule Kinda.Prebuilt do
require Logger
alias Kinda.CodeGen.{NIFDecl, Wrapper}
defmacro __using__(opts) do
quote do
require Logger
opts = unquote(opts)
otp_app = Keyword.fetch!(opts, :otp_app)
opts =
Keyword.put_new(
opts,
:force_build,
Application.compile_env(:kinda, [:force_build, otp_app])
)
case RustlerPrecompiled.__using__(__MODULE__, opts) do
{:force_build, _only_rustler_opts} ->
contents = Kinda.Prebuilt.__using__(__MODULE__, opts)
Module.eval_quoted(__MODULE__, contents)
{:ok, config} ->
@on_load :load_rustler_precompiled
@rustler_precompiled_load_from config.load_from
@rustler_precompiled_load_data config.load_data
{otp_app, path} = @rustler_precompiled_load_from
load_path =
otp_app
|> Application.app_dir(path)
{meta, _binding} =
Path.dirname(load_path)
|> Path.join("kinda-meta-#{Path.basename(load_path)}.ex")
|> File.read!()
|> Code.eval_string()
contents = Kinda.Prebuilt.__using__(__MODULE__, Keyword.put(opts, :meta, meta))
Module.eval_quoted(__MODULE__, contents)
@doc false
def load_rustler_precompiled do
# Remove any old modules that may be loaded so we don't get
# {:error, {:upgrade, 'Upgrade not supported by this NIF library.'}}
:code.purge(__MODULE__)
{otp_app, path} = @rustler_precompiled_load_from
load_path =
otp_app
|> Application.app_dir(path)
|> to_charlist()
:erlang.load_nif(load_path, @rustler_precompiled_load_data)
end
{:error, precomp_error} when is_bitstring(precomp_error) ->
precomp_error
|> String.split("You can force the project to build from scratch with")
|> List.first()
|> String.trim()
|> Kernel.<>("""
You can force the project to build from scratch with:
config :kinda, :force_build, #{otp_app}: true
""")
|> raise
{:error, precomp_error} ->
raise precomp_error
end
end
end
defp nif_ast(kinds, nifs, forward_module, zig_t_module_map) do
# generate stubs for generated NIFs
Logger.debug("[Kinda] generating NIF wrappers, forward_module: #{inspect(forward_module)}")
extra_kind_nifs =
kinds
|> Enum.map(&NIFDecl.from_resource_kind/1)
|> List.flatten()
for nif <- nifs ++ extra_kind_nifs do
args_ast = Macro.generate_unique_arguments(nif.arity, __MODULE__)
%NIFDecl{wrapper_name: wrapper_name, nif_name: nif_name, ret: ret} = nif
wrapper_name =
if is_bitstring(wrapper_name) do
String.to_atom(wrapper_name)
else
wrapper_name
end
stub_ast =
quote do
@doc false
def unquote(nif_name)(unquote_splicing(args_ast)),
do:
raise(
"NIF for resource kind is not implemented, or failed to load NIF library. Function: :\"#{unquote(nif_name)}\"/#{unquote(nif.arity)}"
)
end
wrapper_ast =
if wrapper_name do
if ret == :void do
quote do
def unquote(wrapper_name)(unquote_splicing(args_ast)) do
refs = Kinda.unwrap_ref([unquote_splicing(args_ast)])
ref = apply(__MODULE__, unquote(nif_name), refs)
:ok = unquote(forward_module).check!(ref)
end
end
else
return_module = Kinda.module_name(ret, forward_module, zig_t_module_map)
quote do
def unquote(wrapper_name)(unquote_splicing(args_ast)) do
refs = Kinda.unwrap_ref([unquote_splicing(args_ast)])
ref = apply(__MODULE__, unquote(nif_name), refs)
struct!(unquote(return_module),
ref: unquote(forward_module).check!(ref)
)
end
end
end
end
[stub_ast, wrapper_ast]
end
|> List.flatten()
end
# generate resource modules
defp kind_ast(root_module, forward_module, resource_kinds) do
for %Kinda.CodeGen.KindDecl{
module_name: module_name,
zig_t: zig_t,
fields: fields
} <-
resource_kinds,
Atom.to_string(module_name)
|> String.starts_with?(Atom.to_string(root_module)) do
Logger.debug("[Kinda] building resource kind #{module_name}")
quote bind_quoted: [
root_module: root_module,
module_name: module_name,
zig_t: zig_t,
fields: fields,
forward_module: forward_module
] do
defmodule module_name do
@moduledoc """
#{zig_t}
"""
use Kinda.ResourceKind,
root_module: root_module,
fields: fields,
forward_module: forward_module
end
end
end
end
defp load_ast(dest_dir, lib_name) do
quote do
# setup NIF loading
@on_load :kinda_on_load
@dest_dir unquote(dest_dir)
def kinda_on_load do
require Logger
nif_path = Path.join(@dest_dir, "lib/#{unquote(lib_name)}")
dylib = "#{nif_path}.dylib"
so = "#{nif_path}.so"
if File.exists?(dylib) do
File.ln_s(dylib, so)
end
Logger.debug("[Kinda] loading NIF, path: #{nif_path}")
with :ok <- :erlang.load_nif(nif_path, 0) do
Logger.debug("[Kinda] NIF loaded, path: #{nif_path}")
:ok
else
{:error, {:load_failed, msg}} when is_list(msg) ->
Logger.error("[Kinda] NIF failed to load, path: #{nif_path}")
Logger.error("[Kinda] #{msg}")
:abort
error ->
Logger.error(
"[Kinda] NIF failed to load, path: #{nif_path}, error: #{inspect(error)}"
)
:abort
end
end
end
end
defp ast_from_meta(
root_module,
forward_module,
kinds,
%Kinda.Prebuilt.Meta{
nifs: nifs,
resource_kinds: resource_kinds,
zig_t_module_map: zig_t_module_map
}
) do
kind_ast(root_module, forward_module, resource_kinds) ++
nif_ast(kinds, nifs, forward_module, zig_t_module_map)
end
# A helper function to extract the logic from __using__ macro.
@doc false
def __using__(root_module, opts) do
code_gen_module = Keyword.fetch!(opts, :code_gen_module)
kinds = code_gen_module.kinds()
forward_module = Keyword.fetch!(opts, :forward_module)
if opts[:force_build] do
{meta, %{dest_dir: dest_dir, lib_name: lib_name}} =
Wrapper.gen_and_build_zig(root_module, opts)
ast_from_meta(root_module, forward_module, kinds, meta) ++ [load_ast(dest_dir, lib_name)]
else
meta = Keyword.fetch!(opts, :meta)
ast_from_meta(root_module, forward_module, kinds, meta)
end
end
end