在 Elixir 或 Erlang 中,如何在运行时动态创建和加载模块?

Posted

技术标签:

【中文标题】在 Elixir 或 Erlang 中,如何在运行时动态创建和加载模块?【英文标题】:How do you create and load modules dynamically at runtime in Elixir, or Erlang? 【发布时间】:2012-10-24 18:22:02 【问题描述】:

基本场景是这样的:我需要从数据库加载文本,然后将该文本转换为 Elixir 模块(或 Erlang 模块),然后对其进行调用。该文本实际上与模块文件相同。所以这是热代码加载的一种形式。我想编译“文件”,然后加载生成的模块,然后调用它。稍后我会卸载它。唯一的区别是代码存在于数据库中而不是磁盘上的文件中。 (并且在我编写将加载它的代码时它不存在。)

我知道 Erlang 支持热代码加载,但似乎专注于在磁盘上编译文件然后加载梁。我希望将其作为一个更动态的过程来执行,我不会替换正在运行的代码,而是加载代码,然后运行它,然后卸载它。

Elixir 中有多种工具可以在运行时评估代码。我正在尝试弄清楚如何使用它们,并且文档有点稀疏。

Code.compile_string(string, "nofile")

“返回一个元组列表,其中第一个元素是模块名称,第二个是它的二进制”。所以,现在我有了模块名称和它们的二进制文件,但我不知道如何将二进制文件加载到运行时并调用它们。我该怎么做? (代码库中没有我能看到的函数。)

然后我可能可以使用 Erlang 函数:

:code.load_binary(Module, Filename, Binary)  ->
           module, Module | error, What

好的,所以这会返回一个带有原子“模块”的元组,然后是模块。如果从数据库加载的字符串定义了一个名为“Paris”的模块,那么我将如何在我的代码中执行

paris.handler([parameters])

因为我事先不知道会有一个叫paris的模块?我可以知道,通过将字符串“paris”也存储在数据库中,这是名称,但是有没有办法调用模块,使用字符串作为您正在调用的模块的名称?

还有:

eval(string, binding // [], opts // [])

评估字符串的内容。这个字符串可以是模块的完整定义吗?看来不是。我希望能够编写这个正在评估的代码,它具有多个相互调用的函数——例如一个完整的小程序,有一个预定义的入口点(可以是一个主入口,例如“DynamicModule.handle([parameter, list])”

然后是 EEx 模块,它有:

compile_string(source, options // [])

这对于制作模板非常有用。但最终它似乎只适用于存在字符串并且您已将 Elixir 代码嵌入其中的用例。它在选项的上下文中评估字符串并生成一个字符串。我正在寻求将字符串编译成一个或多个我可以调用的函数。 (如果我只能制作一个很好的功能,那么该功能可以进行模式匹配或切换到做其他需要的事情......)

我知道这是非常规的,但我这样做是有原因的,而且他们是好的。我正在寻找有关如何执行此操作的建议,但不需要被告知“不要那样做”。看起来应该是可能的,Erlang 支持热代码加载,并且 Elixir 非常动态,但我只是不知道语法或正确的功能。我会密切关注这个问题。提前致谢!


根据第一个答案进行编辑:

感谢您的回答,这是一个很好的进展。正如 Yuri 所展示的,eval 可以定义一个模块,正如 José 指出的那样,我可以将代码 eval 用于带有绑定的一小部分代码。

正在评估的代码(无论是否转换为模块)将相当复杂。它的开发最好将其分解为函数并调用这些函数。

为了提供帮助,让我提供一些背景信息。假设我正在构建一个 Web 框架。从数据库加载的代码是特定 URI 的处理程序。所以,当一个 HTTP 调用进来时,我可能会加载 example.com/blog/ 这个页面的代码,这个页面可能会涉及到几个不同的东西,比如 cmets、最近的帖子列表等等。

由于很多人同时访问该页面,我正在生成一个进程来处理每个页面视图。因此,对于不同的请求,可以多次同时评估此代码。

模块解决方案允许将代码分解为页面不同部分的功能(例如:帖子列表、cmets 等)并且我会在启动时加载模块一次,并让许多进程产生那个调用。该模块是全局的,对吗?

如果已经定义了一个模块会怎样? EG:当模块发生变化时,已经有进程调用了该模块。

在 iex 中,我可以重新定义已经定义的模块:

iex(20)> Code.eval "defmodule A do\ndef a do\n5\nend\nend"
nofile:1: redefining module A

如果我在运行时将模块重新定义到当前调用该模块的所有进程,会发生什么情况?另外,这种重新定义是否可以在 iex 之外正常运行?

假设重新定义模块会有问题,并且全局模块可能会遇到命名空间冲突的问题,我研究了使用 eval 来定义函数。

如果我只能让数据库中的代码定义函数,那么这些函数在任何进程的范围内,并且我们没有全局冲突的可能性。

但是,这似乎不起作用:

iex(31)> q = "f = function do
...(31)> x, y when x > 0 -> x+y
...(31)> x, y -> x* y
...(31)> end"
"f = function do\nx, y when x > 0 -> x+y\nx, y -> x* y\nend"
iex(32)> Code.eval q
#Fun<erl_eval.12.82930912>,[f: #Fun<erl_eval.12.82930912>]
iex(33)> f
** (UndefinedFunctionError) undefined function: IEx.Helpers.f/0
    IEx.Helpers.f()
    erl_eval.erl:572: :erl_eval.do_apply/6
    src/elixir.erl:110: :elixir.eval_forms/3
    /Users/jose/Work/github/elixir/lib/iex/lib/iex/loop.ex:18: IEx.Loop.do_loop/1

iex(33)> f.(1,3)
** (UndefinedFunctionError) undefined function: IEx.Helpers.f/0
    IEx.Helpers.f()
    erl_eval.erl:572: :erl_eval.do_apply/6
    erl_eval.erl:355: :erl_eval.expr/5
    src/elixir.erl:110: :elixir.eval_forms/3
    /Users/jose/Work/github/elixir/lib/iex/lib/iex/loop.ex:18: IEx.Loop.do_loop/1

我也试过了:

    iex(17)> y = Code.eval "fn(a,b) -> a + b end"
#Fun<erl_eval.12.82930912>,[]
iex(18)> y.(1,2)
** (BadFunctionError) bad function: #Fun<erl_eval.12.82930912>,[]
    erl_eval.erl:559: :erl_eval.do_apply/5
    src/elixir.erl:110: :elixir.eval_forms/3
    /Users/jose/Work/github/elixir/lib/iex/lib/iex/loop.ex:18: IEx.Loop.do_loop/1

所以,总结一下:

    当有进程调用模块时,是否可以使用 Code.eval 重新定义模块?

    是否可以使用 Code.eval 来创建范围绑定到调用 Code.eval 的进程的函数?

    如果你明白我想要做什么,你能建议一个更好的方法吗?

另外,如果有更好的论坛我应该问这个问题,请随时告诉我。如果有我应该阅读的文档或相关示例,请随时向我指出。我不是想让你做所有的工作,我只是自己无法弄清楚。

我正在学习 Elixir 专门用于动态评估代码的能力,但我的 Elixir 知识现在很少——我刚刚开始——而且我的 erlang 也生疏了。

非常感谢!

【问题讨论】:

【参考方案1】:

正如您所描述的,您可以采用许多不同的方法,最终它们归结为两个不同的类别:1) 代码编译和 2) 代码评估。您上面描述的示例需要编译,它将定义一个模块,然后您必须调用它。但是,正如您所发现的,它需要定义模块名称,并在数据库更改时可能会清除和丢弃这些模块。另外,请注意,定义模块可能会耗尽原子表,因为为每个模块创建一个原子。如果您需要编译最多十几个模块,我只会使用这种方法。

(一个小说明,Code.compile_string已经定义了模块,所以你不需要调用load_binary)。

也许更简单的方法是调用Code.eval 传递数据库中的代码,从而进行代码评估。如果您只想评估一些自定义规则,它可以正常工作。 Code.eval 接受绑定,这将允许您将参数传递给代码。假设您在数据库中存储了“a + b”,其中ab 是参数,您可以将其计算为:

Code.eval "a + b", [a: 1, b: 1]

根据问题的编辑进行编辑

如果您正在评估代码,请记住 Code.eval 返回评估代码和新绑定的结果,因此您上面的示例最好写成:

q = "f = function do
x, y when x > 0 -> x+y
x, y -> x* y
end"

 _, binding  = Code.eval q
binding[:f].(1, 2)

但是,鉴于您的用例,我不会考虑使用 eval 但我确实会编译一个模块。由于信息来自数据库,您可以使用此事实为您生成每条记录的唯一模块。例如,您可以假设开发人员会将模块的内容添加到数据库中,例如:

def foo, do: 1
def bar, do: 1

从数据库中获取此信息后,您可以使用以下命令对其进行编译:

record   = get_record_from_database
id       = record.id
contents = Code.string_to_quoted!(record.body)
module   = Module.concat(FromDB, "Data#record.id")
Module.create module, contents, Macro.Env.location(__ENV__)
module

其中记录是您从数据库返回的任何内容。假设记录 ID 是 1,它将定义一个模块 FromDB.Data1,然后您将能够调用 foobar

关于代码重载,defmoduleModule.create 都使用:code.load_binary 来加载模块。这意味着如果您更新模块,旧版本仍然会保留,直到加载另一个新版本。

您还应该添加的一件事是缓存过期,因此您不需要针对每个请求编译模块。如果您在每次更改记录内容时递增的数据库中有一个 lock_version,则可以做到这一点。最终代码类似于:

record  = get_record_from_database
module  = Module.concat(FromDB, "Data#record.id")
compile = :code.is_loaded(module) == false or
            record.lock_version > module.lock_version

if compile do
  id       = record.id
  contents = Code.string_to_quoted!(record.body)
  contents = quote do
    def lock_version, do: unquote(record.lock_version)
    unquote(contents)
  end
  Module.create module, contents, Macro.Env.location(__ENV__)
end

module

【讨论】:

这将在代码上强加一个预定的形式,这样它就被分解成小字符串来评估。我已经编辑了我的问题以进一步解释,但如果我可以在代码中定义函数,然后在字符串内部以及从外部调用它们,那可能是理想的。但是当我尝试它时它不起作用。我希望 STRING 能够定义函数 A、B 和 C,例如,外部代码定义 D、E 和 F。从发送到 Code.eval 和外部的字符串内部,能够调用A、B、C、D、E 和 F。EG:范围内的所有功能。这可能吗? 我刚刚回答了你的新问题。 只剩下一点混乱。查看您的代码,最后一行只是“模块”,在此示例中实际上是“FromDB.Data1”。当您像这样仅在一行上有“模块”时,是否相当于调用 FromDB.Data1.main() 或某些默认入口点?或者换一种说法,你的代码创建了一个模块,其名称基于数据库信息,这很好,我如何从它之外的代码中调用它?我可以写“module.renderRequest(a, b, c)”然后调用从数据库加载的模块中定义的renderRequest函数吗? 是的,我们返回module,在这种情况下将是FromDB.Data1(不是二进制文件,而是代表模块的原子)。这意味着我们可以在其中调用函数。您需要做的就是确保数据库中至少有一个名为main(或您喜欢的任何东西)的函数可用,然后将其调用为:module.main(a, b, c) Elixir 1.10.2 中似乎不再存在 Code.eval。相反,您应该使用Code.eval_string【参考方案2】:

Code.eval 可用于定义模块:

iex(1)> Code.eval "defmodule A do\ndef a do\n1\nend\nend" 
:module,A,<<70,79,82,49,0,0,2,168,66,69,65,77,65,116,111,109,0,0,0,98,0,0,0,11,8,69,108,105,120,105,114,45,65,8,95,95,105,110,102,111,95,95,4,100,111,99,115,9,102,117,...>>,:a,0,[]
iex(2)> A.a
1

这有帮助吗?

【讨论】:

这帮助很大,并促成了一个富有成效的探索之夜。我对您的答案投了赞成票,但不幸的是,有些混蛋投了反对票。我很遗憾我只能投一票。由于我的问题有点冗长和含糊,我根据您的回答和我的实验结果对其进行了扩展:您可以重新定义模块,这很好,但我不确定后果。而且我似乎无法定义简单的函数(这可能具有调用 Code.eval 的过程的范围。)【参考方案3】:

您检查了吗:Dynamic Compile Library by Jacob Vorreuter。见下面的例子

1> String = "-module(add).\n -export([add/2]). \n add(A,B) -> A + B. \n". "-module(add).\n -export([add/2]).\n add(A,B) -> A + B.\n" 2> 动态编译:加载从字符串(字符串)。 模块,添加 3> 添加:添加(2,5)。 7 4> 另外,看看这个question 和它的answer

【讨论】:

那个库很有帮助,它表明我需要做的事情可以在 erlang 中完成,如果我决定不想在 elixir 中编程,这将非常有用。然而,我对模块的关注是它们的全局性。考虑您提到的示例 - 假设服务器正在收到两个请求来完成这项工作。也许它会检查模块是否已经加载并且不会动态加载它两次......但是当它完成时,它怎么知道卸载它是安全的?没有使用该模块的其他进程?我认为流程范围是理想的。 我认为在这种情况下,您会尝试获取模块信息以查看它是否已经加载,然后您的应用程序会采取相应的行动。

以上是关于在 Elixir 或 Erlang 中,如何在运行时动态创建和加载模块?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 erlang 或 elixir 中接收可能值列表

如何在服务器上正确安装 Erlang、Elixir 和 mix?

Elixir/Erlang 并发状态访问

混合 Elixir 和 Erlang?

将本地 Elixir/Erlang 连接到 Docker 容器内正在运行的应用程序

E1.获取Elixir/Erlang版本信息