簡體   English   中英

如何在運行時在Elixir或Erlang中動態創建和加載模塊?

[英]How do you create and load modules dynamically at runtime in Elixir, or Erlang?

基本場景是這樣的:我需要從數據庫中加載文本,然后將該文本轉換為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”也存儲在數據庫中,這就是名稱,但是有什么方法可以使用字符串作為您要調用的模塊的名稱來調用模塊?

還有:

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\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

因此,總而言之:

  1. 當有進程調用模塊時,可以使用Code.eval重新定義模塊嗎?

  2. 是否可以使用Code.eval使其作用域綁定到調用Code.eval的過程的函數?

  3. 如果您了解我正在嘗試做的事情,那么您能建議一個更好的方法嗎?

此外,如果有更好的論壇可以提出這個問題,請隨時告訴我。 如果有我應該閱讀的文檔或相關示例,請隨時向我指出。 我並不是想讓您去做所有的工作,只是我自己無法弄清楚。

我正在專門學習Elixir,以具有動態評估代碼的能力,但是現在我對Elixir的知識很少,我剛剛開始學習,而且我的erlang也很生疏。

非常感謝!

正如您所描述的,最終有許多不同的方法可以歸結為兩個不同的類別: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

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

這有幫助嗎?

您是否檢查過: 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> dynamic_compile:load_from_string(String).
{module,add}
3> add:add(2,5).
7
4>
另外,請查看此問題及其答案

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM