Current section
Files
Jump to
Current section
Files
lib/clixir.ex
defmodule Clixir do
@moduledoc """
Code to emit Elixir and C code from a single "clixir" (.cx)
file.
"""
defmacro __using__(_opts) do
quote do
import Clixir
Module.register_attribute(__MODULE__, :cfuns, accumulate: true)
@before_compile Clixir
end
end
defmacro def_c(clause, do: expression) do
{function_name, _, parameter_ast} = clause
parameter_list = Enum.map(parameter_ast, fn({p, _, _}) -> p end)
{_block, _, exprs} = expression
module = __CALLER__.module
location = {__CALLER__.file, __CALLER__.line}
c_code = make_c(module, function_name, parameter_list, exprs, location)
e_code = make_e(module, function_name, parameter_ast, exprs)
cfun_name = cfun_name(module, function_name)
quote do
@cfuns {unquote(cfun_name), unquote(c_code)}
unquote(e_code)
end
end
# TODO only do this when needed (compare timestamps,etc)
defmacro __before_compile__(env) do
require Logger
clixir_dir = Path.join(Mix.Project.build_path(), "clixir")
:ok = File.mkdir_p(clixir_dir)
# Write C file
target = Path.join(clixir_dir, Atom.to_string(env.module))
{:ok, target_file} = File.open(target <> ".c", [:write])
header_name = Module.get_attribute(env.module, :clixir_header)
if is_nil(header_name) do
Logger.warn("No @clixir_header specified in #{env.module}")
""
else
header_file = Path.join("c_src", header_name <> ".hx")
header = File.read!(header_file)
IO.write(target_file, "#line 1 \"#{header_file}\"\n")
IO.write(target_file, header)
end
IO.puts(target_file, "\n\n// END OF HEADER\n\n")
cfuns = Module.get_attribute(env.module, :cfuns)
Enum.map(cfuns, fn {_fun, {hdr, body}} ->
IO.puts(target_file, hdr)
IO.puts(target_file, body)
end)
File.close(target_file)
# Dump data for gperf
{:ok, target_file} = File.open(target <> ".gperf", [:write])
Enum.map(cfuns, fn {fun, _} -> IO.puts target_file, "#{fun}, _dispatch_#{fun}" end)
File.close(target_file)
end
if false do
def gen_perf do
# TODO Invoke this at the end of compiling everything
# TOOD Generate makefile.
gperf_file = tmpfile.() <> ".gperf"
{:ok, gperf_data} = File.open(gperf_file, [:write])
IO.write gperf_data, """
struct dispatch_entry {
char *name;
void (*dispatch_func)(const char *buf, unsigned short len, int *index);
};
%%
"""
Enum.map(cfuns, fn {fun, _} -> IO.puts gperf_data, "#{fun}, _dispatch_#{fun}" end)
File.close(gperf_data)
# Call gperf and append to generated code
{result, 0} = System.cmd("gperf", ["-t", gperf_file])
IO.puts(target_file, result)
File.rm(gperf_file)
# Emit dispatch function
IO.puts target_file, """
void _dispatch_command(const char *buf, unsigned short len, int *index) {
char atom[MAXATOMLEN];
struct dispatch_entry *dpe;
assert(ei_decode_atom(buf, index, atom) == 0);
dpe = in_word_set(atom, strlen(atom));
if (dpe != NULL) {
(dpe->dispatch_func)(buf, len, index);
} else {
fprintf(stderr, "Dispatch function not found for [%s]\\\n", atom);
}
}
"""
File.close(target_file)
end
end
# C code stuff starts here
def make_c(module, function_name, parameter_list, exprs, {file, line}) do
{:ok, iobuf} = StringIO.open("// Generated code for #{function_name} from #{Atom.to_string(module)}\n")
cdecls = cdecls(exprs)
non_decls = non_decls(exprs)
IO.write(iobuf, "#line #{line} \"#{file}\"\n")
start_c_fun(iobuf, module, function_name)
emit_c_local_vars(iobuf, cdecls)
emit_c_unmarshalling(iobuf, parameter_list, cdecls)
emit_c_body(iobuf, cdecls, non_decls)
end_c_fun(iobuf)
StringIO.contents(iobuf)
end
defp cdecls(exprs) do
# Return c declarations as %{name -> type} map
exprs
|> Enum.flat_map(fn
{:cdecl, _, [[{ctype, {cname, _, _}}]]} ->
[{cname, ctype}]
{:cdecl, _, [[{ctype, cnames}]]} ->
Enum.map(cnames, fn({cname, _, _}) -> {cname, ctype} end)
_ -> []
end)
|> Enum.filter(fn e -> !is_nil(e) end)
|> Map.new
end
defp non_decls(exprs) do
exprs
|> Enum.filter(fn maybe_decl -> elem(maybe_decl, 0) != :cdecl end)
end
defp start_c_fun(iobuf, module, function_name) do
IO.puts(iobuf, "static void _dispatch_#{cfun_name(module, function_name)}(const char *buf, unsigned short len, int *index) {")
end
defp emit_c_local_vars(iobuf, cdecls) do
cdecls
|> Enum.map(fn
({decl, :"char *"}) -> IO.puts(iobuf, " char #{decl}[BUF_SIZE];")
IO.puts(iobuf, " long #{decl}_len;")
({decl, type}) -> IO.puts(iobuf, " #{to_string type} #{decl};")
end)
end
defp emit_c_unmarshalling(iobuf, parameter_list, cdecls) do
parameter_list
|> Enum.map(fn(p) -> {p, cdecls[p]} end)
|> Enum.map(fn
# Fairly manual list, we can clean this up later when we have a better overview of regularities
{name, :double} ->
" assert(ei_decode_double(buf, index, &#{name}) == 0);"
{name, :long} ->
" assert(ei_decode_long(buf, index, &#{name}) == 0);"
{name, :"char *"} ->
" assert(ei_decode_binary(buf, index, #{name}, &#{name}_len) == 0);\n" <>
" #{name}[#{name}_len] = '\\0';"
{name, :erlang_pid} ->
" assert(ei_decode_pid(buf, index, &#{name}) == 0);"
{name, type} ->
if String.ends_with?(to_string(type), "*") do
" assert(ei_decode_longlong(buf, index, (long long *) &#{name}) == 0);"
else
raise "unknown type #{type} for variable #{name}, please fix macro"
end
end)
|> Enum.map(&(IO.puts(iobuf, &1)))
end
# Ok, the following couple of functions are currently horribly named. Also, this
# is not really clean - got incrementally built when working on Clixir's spec and
# first implementation.
# What really needs to happen is: TODO:
# a) transform Elixir AST into C AST
# b) emit C code for C AST
@indent " "
defp emit_c_body(iobuf, cdecls, exprs, indent \\ @indent)
defp emit_c_body(iobuf, cdecls, {:__block__, _, exprs}, indent) do
emit_c_body(iobuf, cdecls, exprs, indent)
end
defp emit_c_body(iobuf, cdecls, exprs, indent) when is_list(exprs) do
Enum.map(exprs, &(emit_c_body(iobuf, cdecls, &1, indent)))
end
defp emit_c_body(iobuf, cdecls, expr, indent) do
case expr do
{:=, _, [{left, _, _}, right]} ->
# Assignment
IO.write(iobuf, "#{indent}#{left} = ")
emit_c_body(iobuf, cdecls, [right], "")
{:if, _, if_stmt} ->
emit_c_if(iobuf, cdecls, if_stmt, indent)
{binary_op, _, args} when binary_op in [:+, :-, :/, :*] ->
# Note that the list above is incomplete. Add as needed.
[lhs, rhs] = args
|> Enum.map(&to_c_var/1)
IO.puts(iobuf, "#{indent}#{lhs} #{to_string binary_op} #{rhs};")
{var, _, nil} when is_atom(var) ->
# Variable reference, e.g. in assignment
IO.write(iobuf, "#{var};")
{funcall, _, args} ->
# Function call
cargs = args
|> Enum.map(&to_c_var/1)
|> Enum.join(", ")
IO.puts(iobuf, "#{indent}#{funcall}(#{cargs});")
# Return tuple. This is probably more hardcoded than we need. Better safe than sorry
# We _always_ return {pid, {return_tuple}}. We need multiple clauses as Elixir handles
# 2-tuples in a special way.
{{:pid, _, _}, {:{}, _, return_values}} ->
retvals = return_values
|> Enum.map(fn
{name, _, _} -> name
atom -> {:atom, atom}
end)
emit_marshal_return_values(iobuf, retvals, cdecls, indent)
{{:pid, _, _}, return_values} when is_tuple(return_values) ->
retvals = return_values
|> Tuple.to_list
|> Enum.map(fn
{name, _, _} -> name
atom -> {:atom, atom}
end)
emit_marshal_return_values(iobuf, retvals, cdecls, indent)
{{:pid, _, _}, return_value} ->
retval = case return_value do
{name, _, _} -> name
atom -> {:atom, atom}
end
emit_marshal_return_values(iobuf, [retval], cdecls, indent)
expr -> raise "unknown expr #{inspect expr}, please fix macro or defgfx declaration"
end
end
def to_c_var(expr) do
case expr do
{name, _, nil} -> to_string(name)
{name, _, context} when is_atom(context) -> "#{to_string(name)}"
{:&, _, [{name, _, nil}]} -> "&" <> to_string(name)
{:__aliases__, _, [name]} -> to_string(name)
number when is_integer(number) or is_float(number) -> to_string(number)
{oper, _, [lhs, rhs]} -> "#{to_c_var(lhs)} #{to_string(oper)} #{to_c_var(rhs)}"
constant_string when is_binary(constant_string) ->
"\"#{constant_string}\"" |> String.replace("\n", "\\n")
{{:., _, [{var, _, nil}, struct_elem]}, _, []} -> "#{var}.#{struct_elem}"
{funcall, _, args} ->
cargs = args
|> Enum.map(&to_c_var/1)
|> Enum.join(", ")
"#{funcall}(#{cargs})"
other_pattern -> raise "unknown C AST form #{inspect other_pattern}, please fix macro"
end
end
# For now, only single-operator if statements are handled.
defp emit_c_if(iobuf, cdecls, [conditional, [do: if_true_exprs]], indent) do
IO.puts(iobuf, "#{indent}if (#{to_c_var(conditional)}) {")
emit_c_body(iobuf, cdecls, if_true_exprs, indent <> @indent)
IO.puts(iobuf, "#{indent}}")
end
defp emit_c_if(iobuf, cdecls, [conditional, [do: if_true_exprs, else: if_false_exprs]], indent) do
IO.puts(iobuf, "#{indent}if (#{to_c_var(conditional)}) {")
emit_c_body(iobuf, cdecls, if_true_exprs, indent <> @indent)
IO.puts(iobuf, "#{indent}} else {")
emit_c_body(iobuf, cdecls, if_false_exprs, indent <> @indent)
IO.puts(iobuf, "#{indent}}")
end
defp emit_marshal_return_values(iobuf, retvals, cdecls, indent) do
IO.write(iobuf, """
#{indent}char response[BUF_SIZE];
#{indent}int response_index = 0;
#{indent}ei_encode_version(response, &response_index);
#{indent}ei_encode_tuple_header(response, &response_index, 2);
#{indent}ei_encode_pid(response, &response_index, &pid);
""")
if length(retvals) > 1 do
IO.puts(iobuf, "#{indent}ei_encode_tuple_header(response, &response_index, #{length(retvals)});")
end
retvals
|> Enum.map(fn(retval) ->
type = cdecls[retval]
case {retval, type} do
{{:atom, atom}, nil} when is_binary(atom) ->
IO.puts(iobuf, "#{indent}ei_encode_string(response, &response_index, \"#{atom}\");")
{{:atom, atom}, nil} when is_integer(atom) ->
IO.puts(iobuf, "#{indent}ei_encode_longlong(response, &response_index, #{atom});")
{{:atom, atom}, nil} when is_float(atom) ->
IO.puts(iobuf, "#{indent}ei_encode_double(response, &response_index, #{atom});")
{{:atom, atom}, nil} ->
IO.puts(iobuf, "#{indent}ei_encode_atom(response, &response_index, \"#{to_string atom}\");")
{name, :double} ->
IO.puts(iobuf, "#{indent}ei_encode_double(response, &response_index, #{name});")
{name, integer} when integer in [:int, :long] ->
IO.puts(iobuf, "#{indent}ei_encode_long(response, &response_index, #{name});")
{name, type} ->
if String.ends_with?(to_string(type), "*") do
IO.puts(iobuf, "#{indent}ei_encode_longlong(response, &response_index, (long long) #{name});")
else
raise("unknown type in return #{inspect name}: #{inspect type}, please fix macro")
end
end
end)
IO.puts(iobuf, "#{indent}write_response_bytes(response, response_index);")
end
defp end_c_fun(iobuf) do
IO.puts(iobuf, "}")
end
defp cfun_name(module, function_name) do
module_s = module
|> Atom.to_string()
|> String.replace(".", "_")
function_s = function_name
|> Atom.to_string()
Enum.join([module_s, function_s], "_")
end
# Elixir code stuff starts here
def make_e(module, function_name, parameter_list, _exprs) do
cfun_name = cfun_name(module, function_name) |> String.to_atom
quote do
def unquote(function_name)(unquote_splicing(parameter_list)) do
Clixir.Server.send_command(Clixir.Server, {unquote(cfun_name), unquote_splicing(parameter_list)})
end
end
end
end