如何在Elixir或Erlang中在运行时动态创建和加载模块?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在Elixir或Erlang中在运行时动态创建和加载模块?相关的知识,希望对你有一定的参考价值。
基本方案是:我需要从数据库加载文本,然后将该文本转换为Elixir模块(或Erlang模块),然后调用它。该文本实际上与模块文件相同。所以这是一种热代码加载形式。我想编译“文件”,然后加载生成的模块,然后调用它。后来我会把它卸下来。唯一的区别是代码存在于数据库中而不是磁盘上的文件中。 (当我正在编写将加载它的代码时,它不存在。)
我知道Erlang支持热代码加载,但似乎专注于编译磁盘上的文件然后加载梁。我希望这是一个更动态的过程,我不会替换正在运行的代码,而是加载代码,然后运行它,然后卸载它。
Elixir中有几个工具用于在运行时评估代码。我正在试图弄清楚如何使用它们,文档有点稀疏。
Code.compile_string(string, "nofile")
“返回元组列表,其中第一个元素是模块名称,第二个元素是其二进制元素”。所以,现在我有了模块名称和它们的二进制文件,但我不知道如何将二进制文件加载到运行时并调用它们。我该怎么办? (我可以看到代码库中没有这个功能。)
可能我可以使用Erlang函数:
:code.load_binary(Module, Filename, Binary) ->
{module, Module} | {error, What}
好的,所以这会返回一个带有原子“module”然后是Module的元组。如果从数据库加载的字符串定义了一个名为“Paris”的模块,那么我的代码将如何执行
paris.handler([parameters])
既然我事先不知道会有一个名为巴黎的模块?我可以知道,通过让字符串“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/的代码。此页面可能涉及几个不同的内容,例如注释,最近发布的列表等。
由于许多人同时访问该页面,我正在产生一个处理每个页面视图的过程。因此,对于不同的请求,有很多时候可以同时评估该代码。
模块解决方案允许人们将代码分解为页面不同部分的函数(例如:帖子列表,注释等)。我会在启动时加载模块一次,让许多进程产生该调用进去。该模块是全球性的,对吗?
如果已定义模块会发生什么? EG:模块更改时,有进程已调用该模块。
在iex中,我能够重新定义已经定义的模块:
iex(20)> Code.eval "defmodule A do
def a do
5
end
end"
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
x, y when x > 0 -> x+y
x, y -> x* y
end"
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)代码编译和2)代码评估。您上面描述的示例需要编译,它将定义一个模块,然后您必须调用它。但是,正如您所发现的那样,它需要定义模块名称,并在数据库更改时可能清除和丢弃这些模块。另请注意,定义模块可能会耗尽原子表,因为为每个模块创建了一个原子。如果你需要编译最多十几个模块,我只会使用这种方法。
(小小的说明,Code.compile_string
已经定义了模块,所以你不需要调用load_binary
)。
也许更简单的方法是调用Code.eval
从数据库传递代码,从而进行代码评估。如果你想要的是评估一些自定义规则,它工作正常。 Code.eval
接受绑定,这将允许您将参数传递给代码。假设您在数据库中存储了“a + b”,其中a
和b
是参数,您可以将其评估为:
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
然后你可以调用foo
和bar
。
关于代码重新加载,defmodule
和Module.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
Code.eval可用于定义模块:
iex(1)> Code.eval "defmodule A do
def a do
1
end
end"
{{: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
这有帮助吗?
你检查过:Dynamic Compile Library by Jacob Vorreuter
。见下面的例子
1> String = "-module(add). -export([add/2]). add(A,B) -> A + B. ". "-module(add). -export([add/2]). add(A,B) -> A + B. " 2> dynamic_compile:load_from_string(String). {module,add} 3> add:add(2,5). 7 4>Also, check out this question and its answer
以上是关于如何在Elixir或Erlang中在运行时动态创建和加载模块?的主要内容,如果未能解决你的问题,请参考以下文章
大神来了Elixir语言设计者José Valim:释放Erlang VM的能量