defmodule Kinda do alias Kinda.CodeGen.{Function, Type, Resource, NIF} require Logger @moduledoc """ Documentation for `Kinda`. """ defp gen_kind_name_from_module_name(%Type{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, %NIF{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 @doc """ Generate Zig code from a header and build a Zig project to produce a NIF library """ def gen( root_module, wrapper, project_dir, opts \\ [include_paths: %{}, library_paths: %{}, type_gen: nil, nif_gen: nil] ) do project_dir = Path.join(File.cwd!(), project_dir) source_dir = Path.join(project_dir, "src") project_dir = Path.join(project_dir, Atom.to_string(Mix.env())) project_source_dir = Path.join(project_dir, "src") Logger.debug("[Kinda] generating Zig code for wrapper: #{wrapper}") include_paths = Keyword.get(opts, :include_paths, %{}) library_paths = Keyword.get(opts, :library_paths, %{}) type_gen = Keyword.get(opts, :type_gen) || (&Type.default/2) nif_gen = Keyword.get(opts, :nif_gen) || (&NIF.from_function/1) cache_root = Path.join([Mix.Project.build_path(), "mlir-zig-build", "zig-cache"]) if not is_map(include_paths) do raise "include_paths must be a map so that we could generate variables for build.zig. Got: #{inspect(include_paths)}" end if not is_map(library_paths) do raise "library_paths must be a map so that we could generate variables for build.zig. Got: #{inspect(library_paths)}" end include_path_args = for {_, path} <- include_paths do ["-I", path] end |> List.flatten() out = with {out, 0} <- System.cmd( "zig", ["translate-c", wrapper, "--cache-dir", cache_root] ++ include_path_args ) do out else {_error, _} -> raise "fail to run zig translate-c" end functions = String.split(out, "\n") |> Enum.filter(fn x -> String.contains?(x, "mlir") || String.contains?(x, "beaver") end) |> Enum.filter(fn x -> String.contains?(x, "pub extern fn") end) # collecting functions with zig translate prints = for f <- functions do "pub extern fn " <> name = f name = name |> String.split("(") |> Enum.at(0) """ { @setEvalBranchQuota(10000); const func_type = @typeInfo(@TypeOf(c.#{name})); comptime var i = 0; if (func_type.Fn.calling_convention == .C) { print("func: #{name}\\n", .{}); inline while (i < func_type.Fn.args.len) : (i += 1) { const arg_type = func_type.Fn.args[i]; if (arg_type.arg_type) |t| { print("arg: {s}\\n", .{ @typeName(t) }); } else { unreachable; } } print("ret: {}\\n", .{ func_type.Fn.return_type }); } } """ end source = """ const expect = @import("std").testing.expect; const print = @import("std").debug.print; const c = @cImport({ @cDefine("_NO_CRT_STDIO_INLINE", "1"); @cInclude("#{wrapper}"); }); pub fn main() !void { #{Enum.join(prints, "\n")} } """ File.mkdir("tmp") dst = "tmp/print_arity.zig" File.write!(dst, source) out = with {out, 0} <- System.cmd("zig", ["run", dst, "--cache-dir", cache_root] ++ include_path_args, stderr_to_stdout: true ) do File.write!("#{dst}.out.txt", out) out else {error, ret_code} -> Logger.error("[Zig] #{error}") raise "fail to run reflection, ret_code: #{ret_code}" end functions = out |> String.trim() |> String.split("\n") |> Enum.reduce([], fn "func: " <> func, acc -> [%Function{name: func} | acc] "arg: " <> arg, [%Function{args: args} = f | tail] -> [%{f | args: args ++ [Function.process_type(arg)]} | tail] "ret: " <> ret, [%Function{ret: nil} = f | tail] -> [%{f | ret: Function.process_type(ret)} | tail] end) File.write!("#{dst}.functions.ex", inspect(functions, pretty: true, limit: :infinity)) types = Enum.map(functions, fn f -> [f.ret, f.args] end) |> List.flatten() extra = primitive_types() |> Enum.map(&Type.ptr_type_name/1) types = Enum.sort(types ++ extra) |> Enum.uniq() resource_kinds = types |> Enum.reject(fn x -> String.starts_with?(x, "[*c]") end) |> Enum.reject(fn x -> x in ["void"] end) |> Enum.map(fn x -> # TODO: support skip {:ok, t} = type_gen.(root_module, x) t |> gen_kind_name_from_module_name 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.inc.zig source source = for %Function{name: name, args: args, ret: ret} <- functions do proxy_arg_uses = for {_arg, i} <- Enum.with_index(args) do "arg#{i}" end arg_vars = for {arg, i} <- Enum.with_index(args) do """ var arg#{i}: #{Resource.resource_type_struct(arg, resource_kind_map)}.T = #{Resource.resource_type_resource_kind(arg, resource_kind_map)}.fetch(env, args[#{i}]) catch return beam.make_error_binary(env, "fail to fetch resource for argument ##{i}, expected: " ++ @typeName(#{Resource.resource_type_struct(arg, resource_kind_map)}.T)); """ end body = if ret == "void" do """ #{Enum.join(arg_vars, "")} c.#{name}(#{Enum.join(proxy_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(proxy_arg_uses, ", ")})) catch return beam.make_error_binary(env, "fail to make resource for: " ++ @typeName(#{Resource.resource_type_struct(ret, resource_kind_map)}.T)); """ end """ fn #{name}(env: beam.env, _: c_int, #{if length(args) == 0, do: "_", else: "args"}: [*c] const beam.term) callconv(.C) beam.term { #{body} } """ end |> Enum.join("\n") resource_kinds_str = resource_kinds |> Enum.map(&Type.gen_resource_kind/1) |> 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, nif_gen) |> Enum.map(&gen_nif_name_from_module_name(root_module, &1)) |> Enum.concat(List.flatten(Enum.map(resource_kinds, &NIF.from_resource_kind/1))) source = """ #{source} pub fn open_generated_resource_types(env: beam.env) void { #{resource_kinds_str_open_str} // TODO: reverse the alias here kinda.aliasKind(kinda.Internal.USize, USize); kinda.aliasKind(kinda.Internal.OpaquePtr, OpaquePtr); kinda.aliasKind(kinda.Internal.OpaqueArray, OpaqueArray); } pub export const generated_nifs = .{ #{nifs |> Enum.map(&Kinda.CodeGen.NIF.gen/1) |> Enum.join(" ")} } ++ #{Enum.map(resource_kinds, fn %{kind_name: kind_name} -> "#{kind_name}.nifs" end) |> Enum.join(" ++ \n")}; """ source = """ pub const c = @cImport({ @cDefine("_NO_CRT_STDIO_INLINE", "1"); @cInclude("#{wrapper}"); }); const beam = @import("beam.zig"); const kinda = @import("kinda.zig"); const e = @import("erl_nif.zig"); pub const root_module = "Elixir.Beaver.MLIR.CAPI"; """ <> source dst = Path.join(project_source_dir, "mlir.imp.zig") File.mkdir_p(project_source_dir) Logger.debug("[Kinda] writing source import to: #{dst}") File.write!(dst, source) # generate build.inc.zig source build_source = for {name, path} <- include_paths do """ pub const #{name} = "#{path}"; """ end |> Enum.join() build_source = for {name, path} <- library_paths do """ pub const #{name} = "#{path}"; """ end |> Enum.join() |> Kernel.<>(build_source) # TODO: make this a arg dest_dir = "#{Path.join(Mix.Project.build_path(), "native-install")}" erts_include = Path.join([ List.to_string(:code.root_dir()), "erts-#{:erlang.system_info(:version)}", "include" ]) build_source = build_source <> """ pub const cache_root = "#{cache_root}"; pub const erts_include = "#{erts_include}"; """ dst = Path.join(project_dir, "build.imp.zig") Logger.debug("[Kinda] writing build import to: #{dst}") File.write!(dst, build_source) if Mix.env() in [:test, :dev] do with {_, 0} <- System.cmd("zig", ["fmt", "."], cd: project_dir) do :ok else {_error, _} -> Logger.warn("fail to run zig fmt") end end sym_src_dir = Path.join(project_dir, "src") File.mkdir_p(sym_src_dir) for zig_source <- Path.wildcard(Path.join(source_dir, "*.zig")) do zig_source_link = Path.join(sym_src_dir, Path.basename(zig_source)) Logger.debug("[Kinda] sym linking source #{zig_source} => #{zig_source_link}") if File.exists?(zig_source_link) do File.rm(zig_source_link) end File.ln_s(zig_source, zig_source_link) end Logger.debug("[Kinda] building Zig project in: #{project_dir}") with {_, 0} <- System.cmd("zig", ["build", "--prefix", dest_dir], cd: project_dir) do Logger.debug("[Kinda] Zig library installed to: #{dest_dir}") :ok else {_error, ret_code} -> raise "fail to run zig compiler, ret_code: #{ret_code}" end %{ nifs: nifs, resource_kinds: resource_kinds, dest_dir: dest_dir, zig_t_module_map: zig_t_module_map } end defp is_array(%Type{zig_t: type}) do is_array(type) end defp is_array("[*c]const " <> _type) do true end defp is_array(type) when is_binary(type) do false end defp is_ptr("[*c]" <> _type) do true end defp is_ptr(type) when is_binary(type) do false end def unwrap_ref(%{ref: ref}) do ref end def unwrap_ref(arguments) when is_list(arguments) do Enum.map(arguments, &unwrap_ref/1) end def unwrap_ref(term) do term end def primitive_types do ~w{ bool c_int c_uint f32 f64 i16 i32 i64 i8 isize u16 u32 u64 u8 usize } end def module_name(zig_t, forward_module, zig_t_module_map) do if is_array(zig_t) do forward_module |> Module.concat("Array") else if is_ptr(zig_t) do forward_module |> Module.concat("Ptr") else zig_t_module_map |> Map.fetch!(zig_t) end end end end