defprotocol Zig.Type do alias Zig.Type.Array alias Zig.Type.Bool alias Zig.Type.Cpointer alias Zig.Type.Error alias Zig.Type.Float alias Zig.Type.Integer alias Zig.Type.Manypointer alias Zig.Type.Optional alias Zig.Type.Slice alias Zig.Type.Struct alias Zig.Type.Resource @type t :: Bool.t() | Enum.t() | Float.t() | Integer.t() | Struct.t() | :env | :pid | :port | :term @spec marshals_param?(t) :: boolean @doc "beam-side type conversions that might be necessary to get an elixir parameter into a zig parameter" def marshals_param?(type) @spec marshal_param(t, Macro.t(), non_neg_integer, :elixir | :erlang) :: Macro.t() def marshal_param(type, variable, index, platform) @spec marshals_return?(t) :: boolean @doc "beam-side type conversions that might be necessary to get a zig return into an elixir return" def marshals_return?(type) @spec marshal_return(t, Macro.t(), :elixir | :erlang) :: Macro.t() def marshal_return(type, variable, platform) @doc "catch prongs to correctly perform error handling, atom is a reference to function in `Zig.ErrorProng`" @spec error_prongs(t, :argument | :return) :: [{atom, [atom]}] def error_prongs(type, context) @doc "generates make clauses in zig" @spec get_result(t, keyword) :: String.t() def get_result(type, opts) @spec needs_make?(t) :: boolean def needs_make?(type) @spec missing_size?(t) :: boolean def missing_size?(type) @spec to_call(t) :: String.t() def to_call(type) @spec return_allowed?(t) :: boolean def return_allowed?(type) @typep spec_context :: :param | :return @spec spec(t, spec_context, keyword) :: Macro.t() def spec(type, context, opts) import Protocol, only: [] import Kernel defmacro sigil_t({:<<>>, _, [string]}, _) do string |> parse |> Macro.escape() end def parse(string) do case string do "u" <> _ -> Integer.parse(string) "i" <> _ -> Integer.parse(string) "f" <> _ -> Float.parse(string) "c_uint" <> _ -> Integer.parse(string) "[]" <> rest -> Slice.of(parse(rest)) "[:0]" <> rest -> Slice.of(parse(rest), has_sentinel?: true) "[*]" <> rest -> Manypointer.of(parse(rest)) "[*:0]" <> rest -> Manypointer.of(parse(rest), has_sentinel?: true) "[*c]" <> rest -> Cpointer.of(parse(rest)) "?" <> rest -> Optional.of(parse(rest)) "[" <> maybe_array -> case Elixir.Integer.parse(maybe_array) do {count, "]" <> rest} -> Array.of(parse(rest), count) {count, ":0]" <> rest} -> Array.of(parse(rest), count, has_sentinel?: true) _ -> raise "unknown type #{string}" end "?*.cimport" <> rest -> if String.ends_with?(rest, "struct_enif_environment_t") do Env else unknown = rest |> String.split(".") |> List.last() raise "unknown type #{unknown}" end end end @pointer_types ~w(array struct) def from_json(json, module) do case json do nil -> # only allow during documentation sema passes if module do raise CompileError, description: "zigler encountered anytype" else :anytype end %{"type" => "unusable:" <> typename} -> # only allow during documentation sema passes if module do raise CompileError, description: "zigler encountered the unusable type #{typename}" else String.to_atom(typename) end %{"type" => "bool"} -> Bool.from_json(json) %{"type" => "void"} -> :void %{"type" => "integer"} -> Integer.from_json(json) %{"type" => "enum"} -> Zig.Type.Enum.from_json(json, module) %{"type" => "float"} -> Float.from_json(json) %{ "type" => "struct", "fields" => [%{"name" => "__payload"}, %{"name" => "__should_release"}] } -> Resource.from_json(json, module) %{"type" => "struct"} -> Struct.from_json(json, module) %{"type" => "array"} -> Array.from_json(json, module) %{"type" => "slice"} -> Slice.from_json(json, module) %{"type" => "pointer", "child" => child = %{"type" => type}} when type in @pointer_types -> child |> __MODULE__.from_json(module) |> Map.replace!(:mutable, true) %{"type" => "pointer", "child" => %{"type" => "unusable:anyopaque"}} -> :anyopaque_pointer %{"type" => "manypointer"} -> Manypointer.from_json(json, module) %{"type" => "cpointer"} -> Cpointer.from_json(json, module) %{"type" => "optional"} -> Optional.from_json(json, module) %{"type" => "error"} -> Error.from_json(json, module) %{"type" => "env"} -> :env %{"type" => "erl_nif_term"} -> :erl_nif_term %{"type" => "struct", "name" => "beam.term"} -> :term %{"type" => "pid"} -> :pid %{"type" => "port"} -> :port %{"type" => "term"} -> :term %{"type" => "e.ErlNifBinary"} -> :erl_nif_binary %{"type" => "e.ErlNifEvent"} -> :erl_nif_event %{"type" => "pointer", "child" => %{"type" => "e.ErlNifBinary"}} -> :erl_nif_binary_pointer %{"type" => "pointer", "child" => %{"type" => "builtin.StackTrace"}} -> :stacktrace end end # convenienece function def spec(atom) when is_atom(atom) do quote context: Elixir do unquote(atom)() end end defmacro __using__(opts) do module = __CALLER__.module inspect? = Keyword.get(opts, :inspect?, false) quote bind_quoted: [inspect?: inspect?, module: module] do import Kernel, except: [to_string: 1] def marshals_param?(_), do: false def marshals_return?(_), do: false def marshal_param(_, _, _, _), do: raise("unreachable") def marshal_return(_, _, _), do: raise("unreachable") def error_prongs(_, :argument), do: [:argument_error_prong] def error_prongs(_, :return), do: [] def get_result(type, opts) do return_type = Keyword.fetch!(opts, :type) "break :get_result beam.make(env, result, .{.output_type = .#{return_type}}).v;" end def needs_make?(_), do: true def missing_size?(_), do: false defoverridable get_result: 2, marshals_param?: 1, marshal_param: 4, marshals_return?: 1, marshal_return: 3, error_prongs: 2, needs_make?: 1, missing_size?: 1 defimpl String.Chars do defdelegate to_string(type), to: module end if inspect? do defimpl Inspect do defdelegate inspect(type, opts), to: module end else defimpl Inspect do defdelegate inspect(type, opts), to: Inspect.Any end end defimpl Zig.Type do defdelegate marshals_param?(type), to: module defdelegate marshal_param(type, variable, index, platform), to: module defdelegate marshals_return?(type), to: module defdelegate marshal_return(type, variable, platform), to: module defdelegate error_prongs(type, context), to: module defdelegate to_call(type), to: module defdelegate return_allowed?(type), to: module defdelegate get_result(type, opts), to: module defdelegate needs_make?(type), to: module defdelegate missing_size?(type), to: module defdelegate spec(type, context, opts), to: module end end end end defimpl Zig.Type, for: Atom do def marshals_param?(_), do: false def marshals_return?(_), do: false def marshal_param(_, _, _, _), do: raise("unreachable") def marshal_return(_, _, _), do: raise("unreachable") def error_prongs(type, :argument), do: List.wrap(if type in ~w(pid port)a, do: :argument_error_prong) def error_prongs(_, _), do: [] def to_call(:erl_nif_term), do: "e.ErlNifTerm" def to_call(:term), do: "beam.term" def to_call(:pid), do: "beam.pid" def to_call(:port), do: "beam.port" def to_call(:void), do: "void" def to_call(type), do: to_string(type) def return_allowed?(type), do: type in ~w(term erl_nif_term pid void)a def get_result(:erl_nif_term, _), do: "break :get_result result;" def get_result(:pid, _), do: "break :get_result beam.make(env, result, .{}).v;" def get_result(:term, _), do: "break :get_result result.v;" def get_result(:void, opts) do # TODO: move this out of void, and make it general? return_type = Keyword.get(opts, :type, :default) case {opts[:arg], opts[:length]} do {nil, _} -> "break :get_result beam.make(env, result, .{}).v;" {arg, nil} -> """ _ = result; break :get_result beam.make(env, payload[#{arg}], .{.output_type = .#{return_type}}).v; """ {arg, {:arg, length_arg}} -> """ _ = result; break :get_result beam.make(env, payload[#{arg}][0..@intCast(payload[#{length_arg}])], .{.output_type = .#{return_type}}).v; """ {arg, length} when is_integer(length) -> """ _ = result; break :get_result beam.make(env, payload[#{arg}][0..#{length})], .{.output_type = .#{return_type}}).v; """ end end def spec(:void, :return, _), do: :ok def spec(:pid, _, _), do: Zig.Type.spec(:pid) def spec(term, _, _) when term in ~w(term erl_nif_term)a, do: Zig.Type.spec(:term) def needs_make?(_), do: false def missing_size?(_), do: false end