lib/prebuilt.ex

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