defmodule Kinda.CodeGen.Wrapper do require Logger @moduledoc false defstruct types: [], functions: [], root_module: nil alias Kinda.CodeGen.{KindDecl, Resource, NIFDecl} def new(root_module) do %__MODULE__{ root_module: root_module } end defp dump_ast?() do System.get_env("KINDA_DUMP_AST") == "1" end defp put_types(%__MODULE__{} = w, zig_ast) when is_list(zig_ast) do functions = for {:fn, %Zig.Parser.FnOptions{extern: true, inline: inline}, _parts} = f <- zig_ast, inline != true do f end func_types = Enum.reduce(functions, [], fn {:fn, _opts, parts}, acc -> params = for {_name, _opts, t} <- parts[:params] || [] do t end return_type = parts[:type] [return_type | params] ++ acc end) primitive_ptr_types = KindDecl.primitive_types() |> Enum.map(&KindDecl.ptr_type_name/1) primitive_array_types = KindDecl.primitive_types() |> Enum.map(&KindDecl.array_type_name/1) primitive_types = KindDecl.primitive_types() ++ primitive_ptr_types ++ primitive_array_types types = (func_types ++ primitive_types) |> Enum.uniq() %__MODULE__{w | functions: functions, types: types} end # if kind_name is absent, generate it from last part of module_name defp gen_kind_name_from_module_name(%KindDecl{module_name: module_name, kind_name: nil} = t) do %{t | kind_name: Module.split(module_name) |> List.last()} end defp gen_kind_name_from_module_name(t), do: t defp gen_nif_name_from_module_name( module_name, %NIFDecl{wrapper_name: wrapper_name, nif_name: nil} = nif ) do %{nif | nif_name: Module.concat(module_name, wrapper_name)} end defp gen_nif_name_from_module_name(_module_name, f), do: f defp collect_types(zig_ast) when is_list(zig_ast) do functions = for {:fn, %Zig.Parser.FnOptions{extern: true, inline: inline}, _parts} = f <- zig_ast, inline != true do f end Enum.reduce(functions, MapSet.new(), fn {:fn, _opts, parts}, acc -> params = for {_name, _opts, t} <- parts[:params] || [] do t end return_type = parts[:type] for t <- [return_type | params], t not in [:void] do with {:ok, t} <- Kinda.ZigAST.extract_item_type(t) do t else _ -> t end end |> MapSet.new() |> MapSet.union(acc) end) end defp fmt_zig_project(project_dir) do Logger.debug("[Kinda] formatting zig project: #{project_dir}") if Mix.env() in [:test, :dev] do with {_, 0} <- System.cmd("zig", ["fmt", "."], cd: project_dir) do :ok else {_error, _} -> Logger.warning("fail to run zig fmt") end end end defp run_zig(args, opts \\ []) do Logger.debug("[Kinda] zig #{Enum.join(args, " ")}") System.cmd( "zig", args, opts ) end defp print_library_debug_info(dest_dir) do if System.get_env("KINDA_PRINT_LINKAGES") do for p <- dest_dir |> Path.join("**") |> Path.wildcard() do Logger.debug("[Kinda] [installed] #{p}") if Path.extname(p) in [".so"] do case :os.type() do {:unix, :darwin} -> {out, 0} = System.cmd("otool", ["-L", p]) Logger.debug("[Kinda] #{out}") {out, 0} = System.cmd("otool", ["-l", p]) String.split(out, "\n") |> Enum.filter(&String.contains?(String.downcase(&1), "rpath")) |> Enum.join("\n") |> Logger.debug() _ -> {out, 0} = System.cmd("ldd", [p]) Logger.debug("[Kinda] #{out}") {out, 0} = System.cmd("readelf", ["-d", p]) String.split(out, "\n") |> Enum.take(20) |> Enum.join("\n") |> Logger.debug() end end end end end # Generate Zig code from a header and build a Zig project to produce a NIF library def gen_and_build_zig(root_module, opts) do wrapper = Keyword.fetch!(opts, :wrapper) lib_name = Keyword.fetch!(opts, :lib_name) dest_dir = Keyword.fetch!(opts, :dest_dir) project_dir = Keyword.fetch!(opts, :zig_proj) |> Path.expand() source_dir = Keyword.get(opts, :zig_src, Path.join(project_dir, "src")) |> Path.expand() Logger.debug("[Kinda] generating Zig code for wrapper: #{wrapper}") translate_args = Keyword.get(opts, :translate_args, []) build_file = Keyword.get(opts, :build_file) build_args = Keyword.get(opts, :build_args, []) version = Keyword.fetch!(opts, :version) cache_root = Path.join([Mix.Project.app_path(), "zig_cache"]) code_gen_module = Keyword.fetch!(opts, :code_gen_module) translate_out = with {out, 0} <- run_zig(["translate-c", wrapper, "--cache-dir", cache_root] ++ translate_args) do out else {_error, _} -> raise "fail to run zig translate-c for wrapper: #{wrapper}" end File.mkdir("tmp") translate_out_filename = "#{lib_name}.translate.out.zig" File.write!("tmp/#{translate_out_filename}.zig", translate_out) zig_ast = Zig.Parser.parse(translate_out).code task_dump_ast = Task.async(fn -> if dump_ast?() do File.write!( "tmp/#{translate_out_filename}.ex", zig_ast |> inspect(pretty: true, limit: :infinity) ) end end) Logger.debug("[Kinda] generating Elixir code for wrapper: #{wrapper}") # collecting functions with zig translate used_types = collect_types(zig_ast) type_constants = for {:const, %Zig.Parser.ConstOptions{ pub: true, comptime: false }, {const_name, nil, v}} <- zig_ast do {const_name, v} end |> Map.new() w = new(root_module) |> put_types(zig_ast) functions = w.functions types = w.types resource_kinds = types |> Enum.reject(fn {:cptr, _, _} -> true _ -> false end) |> Enum.reject(fn x -> x in [:void] end) |> Enum.map(fn x -> with {:ok, t} <- code_gen_module.type_gen(root_module, x) do gen_kind_name_from_module_name(t) else :skip -> nil end end) resource_kind_map = resource_kinds |> Enum.map(fn %{zig_t: zig_t, kind_name: kind_name} -> {zig_t, kind_name} end) |> Map.new() zig_t_module_map = resource_kinds |> Enum.map(fn %{zig_t: zig_t, module_name: module_name} -> {zig_t, module_name} end) |> Map.new() # generate wrapper.imp.zig source source = for {:fn, _, parts} <- functions do [name: name, params: params, type: ret] = parts param_types = for {{name, _, t}, i} <- Enum.with_index(params) do case name do :_ -> {"arg\##{i}", t} _ -> {name, t} end end for {arg_name, t} <- [{"return", ret} | param_types], t not in [:void] do t = with {:ok, t} <- Kinda.ZigAST.extract_item_type(t) do t else _ -> t end if t not in used_types do raise "type #{inspect(t)} is not used in wrapper: #{wrapper}" end if not (Map.has_key?(type_constants, t) or Map.has_key?(zig_t_module_map, t) or KindDecl.is_primitive_type?(t) or KindDecl.is_opaque_ptr?(t)) do Logger.error( "in function: #{name}, #{arg_name}'s type is not resolved, consider adding a typedef in the wrapper header. Zig ast:\n #{inspect(t, pretty: true)}" ) raise "type not resolved" end end error_prefix_when_calling = "when calling C function #{name}" {arg_vars, arg_uses} = for {{arg_name, _t_opts, t}, i} <- Enum.with_index(params) do arg_var_name = case arg_name do :_ -> "arg_#{i}" :args -> "arg_#{i}" _ -> if Kinda.ZigAST.is_keyword?(arg_name) do "#{arg_name}_" else arg_name end end resource_type_struct = Resource.resource_type_struct(t, resource_kind_map) arg_var = """ var #{arg_var_name}: #{resource_type_struct}.T = #{Resource.resource_type_resource_kind(t, resource_kind_map)}.fetch(env, args[#{i}]) catch return beam.make_error_binary(env, "#{error_prefix_when_calling}, fail to fetch resource for #{arg_var_name}, expected: " ++ @typeName(#{resource_type_struct}.T)); """ {arg_var, arg_var_name} end |> Enum.reduce({[], []}, fn {arg_var, arg_use}, {arg_vars, arg_uses} -> {arg_vars ++ [arg_var], arg_uses ++ [arg_use]} end) body = if ret == :void do """ #{Enum.join(arg_vars, "")} c.#{name}(#{Enum.join(arg_uses, ", ")}); return beam.make_ok(env); """ else """ #{Enum.join(arg_vars, "")} return #{Resource.resource_type_resource_kind(ret, resource_kind_map)}.make(env, c.#{name}(#{Enum.join(arg_uses, ", ")})) catch return beam.make_error_binary(env, "#{error_prefix_when_calling}, fail to make resource for: " ++ @typeName(#{Resource.resource_type_struct(ret, resource_kind_map)}.T)); """ end len_params = case params do [:...] -> 0 _ -> length(params) end """ fn #{name}(env: beam.env, _: c_int, #{if len_params == 0, do: "_", else: "args"}: [*c] const beam.term) callconv(.C) beam.term { #{body} } """ end |> Enum.join("\n") resource_kinds_str = resource_kinds |> Enum.map(fn k -> KindDecl.gen_resource_kind(k) end) |> Enum.join() resource_kinds_str_open_str = resource_kinds |> Enum.map(&Resource.resource_open/1) |> Enum.join() source = resource_kinds_str <> source nifs = Enum.map(functions, fn x -> code_gen_module.nif_gen(x) end) |> Enum.map(&gen_nif_name_from_module_name(root_module, &1)) |> Enum.concat(List.flatten(Enum.map(resource_kinds, &NIFDecl.from_resource_kind/1))) # TODO: reverse the alias here source = """ #{source} pub fn open_generated_resource_types(env: beam.env) void { #{resource_kinds_str_open_str} } pub const generated_nifs = .{ #{nifs |> Enum.map(&Kinda.CodeGen.NIFDecl.gen/1) |> Enum.join(" ")} } #{if length(resource_kinds) > 0, do: "++", else: ""} #{Enum.map(resource_kinds, fn %{kind_name: kind_name} -> "#{kind_name}.nifs" end) |> Enum.join(" ++ \n")}; """ source = """ pub const c = @import("prelude.zig"); const beam = @import("beam"); const kinda = @import("kinda"); const e = @import("erl_nif"); pub const root_module = "#{root_module}"; """ <> source dst = Path.join(source_dir, "#{lib_name}.imp.zig") Logger.debug("[Kinda] writing source import to: #{dst}") File.write!(dst, source) erts_include = Path.join([ List.to_string(:code.root_dir()), "erts-#{:erlang.system_info(:version)}" ]) {:ok, target} = RustlerPrecompiled.target() lib_name = "#{lib_name}-v#{version}-#{target}" # zig will add the 'lib' prefix to the library name prefixed_lib_name = "lib#{lib_name}" fmt_zig_project(project_dir) Logger.debug("[Kinda] building Zig project in: #{project_dir}") build_file_args = if build_file do ["--build-file", Path.expand(build_file)] else [] end with {_, 0} <- run_zig( ["build", "--prefix", dest_dir, "-freference-trace", "--cache-dir", cache_root] ++ build_file_args ++ build_args ++ ["--search-prefix", erts_include, "-DKINDA_LIB_NAME=#{lib_name}"], cd: project_dir, stderr_to_stdout: true, env: [{"KINDA_LIB_NAME", lib_name}] ) do Logger.debug("[Kinda] Zig library installed to: #{dest_dir}") :ok else {error, ret_code} -> Logger.error(error) raise "fail to run zig compiler, ret_code: #{ret_code}" end print_library_debug_info(dest_dir) meta = %Kinda.Prebuilt.Meta{ nifs: nifs, resource_kinds: resource_kinds, zig_t_module_map: zig_t_module_map } File.write!( Path.join(dest_dir, "kinda-meta-#{prefixed_lib_name}.ex"), inspect(meta, pretty: true, limit: :infinity) ) if dump_ast?() do Task.await(task_dump_ast, :infinity) end {meta, %{dest_dir: dest_dir, lib_name: prefixed_lib_name}} end end