使用Web Assembly在Elixir中运行Go代码

使用Web Assembly在Elixir中运行Go代码

💡 原文英文,约1100词,阅读约需4分钟。
📝

内容提要

最近我在一个Elixir项目上工作,想要使用一个用Go编写的开源包。我搜索了不同的方法来与Elixir中的Go代码进行交互。我知道使用NIF可以很容易地与Elixir中的Rust代码进行交互,但是我没有找到类似的方法来与Go进行交互。大部分的研究都指向了使用Ports,我差点放弃,直到意识到WASI(WebAssembly System Interface)也是一个选择。我一直听说过Web Assembly,现在正好是尝试的好时机。我还发现了Wasmex,它似乎是在Elixir中运行WASI二进制文件的好选择。编译Go代码为WASI、在Elixir主机中运行WASI二进制文件、将数据传递给WASI二进制文件、从WASI二进制文件中获取数据,这些都是我需要弄清楚的事情。最后,我成功地实现了这个解决方案。这个方法对于大多数基本用例来说是一个完美的解决方案。

🔎

延伸解读

为什么选择 WASI 而非 Ports 或 NIF

作者最初考虑用 Ports 与 Go 交互,但最终转向 WASI。与 NIF 不同,WASI 不需要将 Go 代码编译为共享库并直接链接到 Elixir 运行时,而是将 Go 编译为独立的 WASM 二进制,在 Wasmex 提供的沙箱中运行。这种方式避免了 NIF 可能导致的崩溃风险,同时比 Ports 更轻量,因为不需要启动外部操作系统进程。对于不想深入底层 FFI 的开发者,WASI 提供了一条更简单的路径。

数据传递的局限与变通

WASI 规范目前不支持字符串作为函数参数,因此无法像整数那样直接通过函数签名传递字符串。作者尝试共享内存失败后,改用 stdin/stdout 管道:Elixir 端通过 Wasmex.Pipe 写入 stdin,Go 端用 io.ReadAll(os.Stdin) 读取;输出则通过 stdout 管道回传。这种基于管道的方案虽然绕过了限制,但需要手动管理管道读写和 seek 位置,对于复杂数据结构可能不够高效。

适用场景与潜在风险

作者认为该方法适合大多数基本用例,尤其是当 Elixir 生态缺少所需库而 Go 有现成实现时。但文章也指出,随着 Go 代码复杂度增加,可能存在未知的坑;作者未对内存使用进行基准测试,因此性能敏感场景需谨慎。如果 WASI 方案遇到瓶颈,可以回退到基于 Ports 的解决方案。总体而言,WASI 为 Elixir 调用 Go 提供了一种可行的补充手段。

❓

Q&A

如何在Elixir中调用Go代码?

可以使用WASI(WebAssembly System Interface)将Go代码编译为WASM,然后在Elixir中使用Wasmex库运行WASI二进制文件并调用导出的函数。

如何将Go代码编译为WASI?

使用tinygo编译器,命令为:tinygo build -o main.wasm -scheduler=none --no-debug -target wasi main.go。需要在Go代码中使用//export注释导出函数,并包含一个空的main函数。

在Elixir中如何运行WASI二进制文件?

首先安装Wasmex作为mix依赖,然后读取WASM文件,使用Wasmex.start_link启动实例,最后通过Wasmex.call_function调用导出的函数。

如何向WASI二进制文件传递数据?

可以通过stdin管道传递数据。使用Wasmex.Pipe.new创建管道,在WasiOptions中设置stdin,然后写入数据并seek到开头,Go端通过io.ReadAll(os.Stdin)读取。

如何从WASI二进制文件获取输出数据?

使用stdout管道。在WasiOptions中设置stdout管道,调用函数后,从管道中读取数据即可获取Go代码的输出。

WASI方法有哪些限制?

WASI规范目前不支持字符串输入,因此不能直接通过函数参数传递字符串。此外,对于复杂的Go代码可能存在其他限制,且未进行内存使用基准测试。

🏷️

标签

➡️

继续阅读