初学 elixir 时就被它方便的文档编写方式所吸引,我们可以这样编写模块的文档和函数的文档:

defmodule M4 do
  @moduledoc """
  Module doc for M4.
  """

  @doc "function doc for f"
  def f do
  end
end

可以在 repl 里直接查看,也可以生成网页版的文档。

iex(2)> h M4

                                       M4                                       

Module doc for M4.

iex(3)> h M4.f

                                    def f()                                     

function doc for f

那么 elixir 究竟是如何从代码文件中获取到 doc 内容的呢。

Code.fetch_docs

标准库的 Code 模块里自带了很多用于处理源文件的函数,其中 Code.fetch_docs 可以直接获取一个模块里全部的 doc 内容:

iex(1)> Code.fetch_docs M4
{:docs_v1, 2, :elixir, "text/markdown", %{"en" => "Module doc for M4.\n"}, %{},
 [{{:function, :f, 0}, 6, ["f()"], %{"en" => "function doc for f"}, %{}}]}

让我们来看看它的内部实现:

首先,它调用了 :code.get_object_code/1 函数来获取模块的 beam 文件的内容和路径。

iex(5)> :code.get_object_code M4
{M4,
 <<70, 79, 82, 49, 0, 0, 4, 216, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 122,
   0, 0, 0, 14, 9, 69, 108, 105, 120, 105, 114, 46, 77, 52, 8, 95, 95, 105, 110,
   102, 111, 95, 95, 7, 99, 111, 109, 112, ...>>,
 '.../_build/dev/lib/compile/ebin/Elixir.M4.beam'}

然后使用 :beam_lib.chunks/2 从 beam 文件中获取到 chunkref 为 'Docs' 的内容。

:beam_lib.chunks bin, ['Docs']
{:ok,
 {M4,
  [
    {'Docs',
     <<131, 80, 0, 0, 0, 165, 120, 156, 203, 96, 79, 97, 96, 79, 201, 79, 46,
       142, 47, 51, 76, 100, 74, 97, 96, 75, 205, 201, 172, 200, 44, 202, 101,
       96, 96, 224, 45, 73, 173, 40, 209, 207, 77, ...>>}
  ]}}

最后把获取到的内容转换成 erlang term 就行了:

iex(9)> :erlang.binary_to_term bin    
{:docs_v1, 2, :elixir, "text/markdown", %{"en" => "Module doc for M4.\n"}, %{},
 [{{:function, :f, 0}, 6, ["f()"], %{"en" => "function doc for f"}, %{}}]}

doc 内容是如何被编译到 beam 文件的

我们知道了 elixir 是如何从 beam 文件中获取到 doc 内容的,但 doc 内容又是如何从源代码被编译进 beam 文件的呢?最重要的是,@doc 里的内容是如何与每个函数关联起来的呢?

首先,和其它模块属性一样,我们调用 @doc "abc" 的时候是使用 @/1 macro 来设置了模块属性 :doc 的内容。

然后,模块属性的值会被存储到一个编译时的ets里,我们可以这样看到这个编译时的ets的内容:

  @doc "function doc for g"
  {set, bag} = :elixir_module.data_tables(__MODULE__)
  IO.inspect(:ets.tab2list(set))
  IO.inspect(:ets.tab2list(bag))

  def g do
  end

模块属性 @doc@moduledoc 都是可覆盖的,也就是后面的定义会覆盖掉之前的值。

之后,elixir 编译器会调用 Module.compile_definition_attributes/6 这个内部函数,在定义新的函数时读取当前的 @doc 的值。

最后,生成好的函数签名(signature)会被存储到 data_tables 中,形如:

{{:function, :f, 0}, 6, [], "function doc for f", %{}},

Ljzn
399 声望102 粉丝

网络安全;函数式编程;数字货币;人工智能