Popcorn:在 WASM 中运行 Elixir
Getting started API Limitations Under the hood
Popcorn:在 WASM 中运行 Elixir
Popcorn 是一个允许在 Web 浏览器中执行 Elixir 代码的库。
编译后的 Elixir 代码在客户端的 AtomVM 运行时环境中执行。 Popcorn 提供了 Elixir 和 JavaScript 之间交互的 API,处理序列化和通信,并确保浏览器的响应性。
我们准备了三个使用 Popcorn 的在线示例,快来看看吧! 您可以在 "API" 部分找到 Popcorn API,并在 "Under the hood" 部分了解其工作原理。
Popcorn 实践
一个简单的 Elixir REPL,可以在 WASM 中动态编译代码。
Elixir docs 带有交互式代码片段的 “Getting started” 指南。
Game of life,将每个单元格表示为一个进程。
Getting started
Note
这个库还在开发中。 API 不稳定,有些东西不能正常工作。 您可以在 "Limitations" 部分阅读更多信息。
Popcorn 通过发送消息和直接从 Elixir 执行 JS 代码来连接你的 JS 和 Elixir 代码。 为此,你需要设置 JS 和 Elixir WASM 入口点。
在你的 mix.exs
中添加 Popcorn 作为依赖项 {:popcorn, "~> 0.1"}
并运行 mix deps.get
。 之后,设置 JS 和 Elixir WASM 入口点。
JS
首先,生成一个目录,用于存放 Popcorn JS 库、WASM 和生成的应用程序 bundle。 为此,请运行:
$ mix popcorn.build_runtime --target wasm --out-dir static/wasm
接下来,在你的主 html 中,你需要包含该库和设置与 Elixir 通信通道的代码。 将这些脚本添加到 HTML 中 body
元素的末尾。
HTML snippet
# static/index.html
<script type="module" src="wasm/popcorn.js" defer></script>
<script type="module" defer>
import { Popcorn } from "./wasm/popcorn.js";
const popcorn = await Popcorn.init({
onStdout: console.log,
onStderr: console.error,
});
</script>
WASM Entrypoint
WASM 入口点是任何具有 start/0
函数且永不退出的 Elixir 模块。 如果你正在使用 supervision tree,你可以这样编写它:
Entrypoint snippet
# lib/app/application.ex
defmodule App.Application do
use Application
alias Popcorn.Wasm
@receiver_name :main
# entrypoint
def start do
{:ok, _pid} = start(:normal, [])
Wasm.send_elixir_ready(default_receiver: @receiver_name)
Process.sleep(:infinity)
end
@impl true
def start(_type, _args) do
# Create default receiver process and register it under `@receiver_name`
# ...
end
end
在我们完成 Elixir 的初始化(设置 supervision trees 等)之后,我们通过调用 Wasm.send_elixir_ready/1
通知 JS 端。 为了方便起见,我们还传递了默认接收进程的名称。 如果未指定其他进程名称,JS 将向其发送消息。
我们需要在配置中设置入口点名称:
Config snippet
# config/config.ex
config :popcorn, start_module: App.Application
此时,你的应用程序已准备好在 JS 和 Elixir 之间交换消息。 接下来,我们将实现一个 Elixir GenServer,它将处理 JS 消息并与 DOM 交互。
Elixir 接收进程
这是一个将接收来自 JS 的消息的进程。 有关如何接收发送到 JS 的消息以及如何调用 JS 代码的详细信息,请参见 "API" 部分。
API
JS
主组件是 Popcorn
类,它管理 WASM 模块并将消息发送给它。
要创建实例,请使用 Popcorn.init(options)
静态方法。 选项:
onStdout ((text: string) => void)
– 接收来自标准输出的任何文本的函数。 默认为 no-op 函数。onStderr ((text: string) => void)
– 接收来自标准错误的任何文本的函数。 默认为 no-op 函数。container (DOMElement)
– 应该挂载 iframe 的 DOM 元素。 在 "Under the hood" 部分阅读更多内容。 默认为document.body
。bundlePath (string)
– 编译后的 Elixir code bundle 的路径。 默认为static/wasm/app.avm
。heartbeatTimeoutMs (number)
– 为 iframe 设置的发送 heartbeat 消息的时间限制。 在 "Under the hood" 部分阅读更多内容。 默认为 15 秒。debug (boolean)
– 用于启用内部日志以调试库的选项。 默认为 false。
用于从 JS 与 Elixir 交互的方法:
async call(args, options)
– 接受 JS 中的可序列化值,将消息发送到注册的 Elixir 进程,并等待 Elixir 代码完成 promise。 选项:process (string)
– 将接收消息的进程的名称。 默认为在Wasm.send_elixir_ready/1
调用中设置的进程名称。timeoutMs (number)
– 为 Elixir 设置的完成 promise 的时间限制。 超过此时间后,promise 会自动被拒绝。 默认为 5 秒。
cast(args, options)
– 接受 JS 中的可序列化值,并将消息发送到注册的 Elixir 进程。 选项:process (string)
– 将接收消息的进程的名称。 默认为在Wasm.send_elixir_ready/1
调用中设置的进程名称。
要销毁实例,请使用 popcorn.deinit()
方法。
Elixir
主组件是 Popcorn.Wasm
模块,它处理与 JS 的通信。
send_elixir_ready(opts)
– 通知 JS Elixir 已完成初始化的函数。 Opts:default_receiver (string or atom)
– 设置 JS calls 和 casts 的默认接收器。 可选。
is_wasm_message(raw_message)
– 如果参数是从 JS 收到的原始消息,则返回 true 的 guard。handle_message!(raw_message, handler)
– 解析从 JS 收到的原始消息,并将其分派给handler
。
对于 :wasm_call
,handler
应返回 {promise_status, promise_value, result}
元组,其中:
promise_status
是:resolve
或:reject
,promise_value
是 JS 应该接收到的任何可序列化值作为响应,result
是传递回调用者的任何值。
Popcorn 使用它来解析 JS promise,从而完成调用。
对于 :wasm_cast
消息,它应该只返回 result
。
run_js(js_function, opts)
– 在 iframe 上下文中执行 JS 函数,并返回包含 JS 对象引用(RemoteObject
struct)的 map。
JS 函数接受一个对象并返回任何值。 该对象包含:
* `bindings` – 包含从 Elixir 传递的可序列化值的对象,位于 `bindings` 选项中。
* `window` – 绑定到主浏览器上下文的 JS `window`。 用于 DOM 操作。
从 JS 函数返回的值将以 RemoteObject
的形式返回给 Elixir。 如果返回的值是可序列化的,则可以使用下面描述的 return
选项在 Elixir 中检索它。 Opts:
* `bindings` – 将传递给 JS 函数的可序列化 Elixir 值的 map。 默认为 `%{}`。
* `return (list)` – 如果 `:value` 包含在列表中,则 `run_js/2` 还将在返回的 map 中包含可序列化的 JS 值。 默认为 `[:ref]`。
register_event_listener(event_name, opts)
– 注册event_name
事件的事件侦听器(例如"click"
)。 Opts:selector (string)
– 监听器将附加到的 DOM 元素的 selector。target (atom or string)
– 将接收事件的进程的名称。event_keys (list)
– 包含事件对象 atom 名称的列表。 指定的键将包含在消息中。
unregister_event_listener(ref)
– 注销由ref
引用的事件监听器。parse_message!(raw_message)
– 解析 JS 消息的底层函数。resolve(term, promise)
– 一个底层函数,用可序列化的term
解析 JSpromise
。resolve(term, promise)
– 一个底层函数,用可序列化的term
拒绝 JSpromise
。
Limitations
我们依赖 AtomVM 来运行编译后的 beams。 它是为微控制器设计的运行时,它不包含完整的 OTP。 最值得注意的是,OTP 标准库中缺少一些本地实现的功能 (NIF)。 我们提供补丁,在 Erlang 中重新实现一些,并致力于将重要的 NIF 直接添加到 AtomVM。 然而,某些模块(例如 :timer
、完整的 :ets
select - 核心 Elixir 代码依赖于它们)暂时无法正常工作。
除了标准库的部分内容之外,AtomVM 对大整数和 bitstring 的支持也不好。 目前正在努力支持这两者。
Popcorn 提供了一组与 JS 配合使用的函数。 并非所有值都可以发送到 JS 或 Elixir。 处理这些值基于传递对它们的不透明引用。
API 尚未稳定,但我们主要希望保持 JS 的当前形式,并稍微改善 Elixir 部分的开发者体验。
Under the hood
Overall architecture
要在 Web 上运行 Elixir,你需要将 Erlang/Elixir 运行时编译为 WASM 并加载编译后的 Elixir 字节码。 我们使用 AtomVM 运行时。 它通过 Emscripten 编译并在 iframe 中加载,以将主窗口上下文与崩溃和冻结隔离。 然后,运行时使用 .avm
扩展名加载用户的代码 bundle。 该 bundle 是一个包含连接的 .beam
文件的文件。
此流程指导架构 - 主窗口创建一个 iframe 并通过 postMessage()
与其通信。 iframe 中的脚本加载 WASM 模块和代码 bundle。 WASM 模块在多个 webworker 上初始化运行时。 主窗口设置超时,如果 call()
花费的时间太长,或者如果 iframe 未及时响应(很可能崩溃或陷入长时间的计算),则会触发超时。
初始化 WASM 模块时,iframe 中的脚本还会等待来自 Elixir 的消息。 这确保了在我们能够处理它们之前,我们不能向 Elixir 发送消息。
Patching
为了使用 Elixir 和 Erlang 标准库,我们使用自定义 patching 机制。 它从已知版本的 Erlang 和 Elixir 中获取 .beam
,并可选择使用我们的更改对其进行 patching。 这允许覆盖行为(解决 AtomVM 中缺少的功能)并将诸如 :emscripten
之类的模块添加到标准库中。 此机制当前未向最终用户公开。
Elixir 和 JS 通信
JS calls 和 casts 是 AtomVM 中 WASM 平台的扩展。 两者都允许将带有字符串或数字数据的消息发送到命名进程。 call()
另外创建一个 promise,Elixir 代码需要 resolve 才能完成请求。
Popcorn 基于此机制构建,以允许发送任何结构化数据。 我们使用 JSON 作为序列化策略。
对于 Elixir 与 JS 的通信,我们使用 Emscripten API 在 iframe JS 上下文中进行 JS 调用。 worker 线程上的任何调度程序都可以将 JS 调用排队以在主浏览器线程上执行。 我们公开了一个函数,该函数将 JS 函数作为字符串并返回任何值。 该值被持久化在 JS 的全局 map 中,以唯一的键作为索引,该函数返回对该键的引用。 如果 Elixir 丢失了此引用,则该值将从 JS map 中删除。
如果从 JS 函数返回的值是可序列化的,则可以使用 return: :value
选项将该值发送回 Elixir。
About
Popcorn 由 Software Mansion 创建。
自 2012 年以来,Software Mansion 是一家软件机构,在构建 Web 和移动应用程序以及复杂的多媒体解决方案方面拥有丰富的经验。 我们是 Core React Native Contributors,也是直播和广播技术方面的专家。 我们可以帮助你构建你的下一个梦想产品 – Hire us。
Copyright 2025, Software Mansion
Licensed under the Apache License, Version 2.0.