近日,日本开发者 4245ryomt 在 Zenn 上发布了一系列围绕 MoonBit 与 WebAssembly 插件实践 的技术文章,分享了他使用 MoonBit 编写 sqlc Wasm 插件 的完整尝试过程。
文章通过可运行代码,介绍了插件请求处理、文件生成以及 Wasm 执行等关键环节,展示了 MoonBit 在工具链与 Wasm 场景下的实际开发体验,适合作为相关方向的实践参考。

📄 原文链接:https://zenn.dev/4245ryomt/articles/9680434dc60c0c
↓以下为系列第一篇的原文翻译
本文内容
本文介绍了使用 MoonBit 来制作 sqlc 插件的过程。
本文并不会完成一个能够在 MoonBit 中"很好地执行 SQL "的库。
在这个过程中,会用到 在 MoonBit 中使用 protoc,以及 执行由 MoonBit 构建的 wasm。
写着写着发现内容比预期要长,因此拆分成多篇文章。
第 1 篇 :将 sqlc 插件的输入用 MoonBit 进行解析。
第 2 篇 :将 MoonBit 输出的 wasm **作为 sqlc 的插件来使用。
本文中使用的 moon 命令版本如下:
shell
$ moon version
> moon 0.1.20251202 (1a59800 2025-12-02)
首先,使用 moon new 命令创建一个项目。
shell
$ moon new try_moonbit_sqlc_plugin_dev
$ cd try_moonbit_sqlc_plugin_dev
什么是 sqlc 插件
sqlc 是一个以 SQL 作为输入,并生成能够 "很好地" 执行这些 SQL 的 源代码 的工具。[1](#1)
这里所说的"很好地",指的是:
在静态类型语言中,执行 SQL 所需的参数,以及执行后得到的结果,都被指定类型。[2](#2)
sqlc 的插件本身,就是实现这种"很好地输出"的逻辑。
插件的输入和输出由 protobuf 定义,可以通过标准输入和标准输出,与任意编程语言编写的程序进行通信。
插件有两种执行方式:
一种是作为普通命令,通过标准输入/输出运行的进程类型;
另一种是以 wasm 的形式执行。
MoonBit 的一大优点就是可以输出 wasm,因此这次就尝试用 MoonBit 来实现。
用于定义插件输入输出的 protobuf 文件位于 sqlc 仓库中(Lines 1 to 132 in main)。[3](#3)
将其作为子模块添加进来会比较方便。
ProtoBuf
$ git submodule add git@github.com:sqlc-dev/sqlc.git sqlc
MoonBit 与 protoc
这里就不展开介绍 Protocol Buffers[4](#4) 是什么了。
从 proto 文件中定义的 schema 出发,用于在各种语言中处理 protobuf 格式消息的工具已经有很多了。
令人惊喜的是,MoonBit 已经存在用于处理 protobuf 消息的工具 。[5](#5)
不过,这次要使用的 proto 文件[6](#6)------codegen.proto,会触发 protoc-gen-mbt 中的一个缺陷。
只要修改 proto 文件本身即可规避这个问题,所以这里直接进行修改。
Shell
$ mkdir proto
$ cp -r sqlc/protos/plugin/codegen.proto proto/codegen.proto
将 params 字段名修改为与 json_name 一致的 parameters。
json
message Query {
string text = 1 [json_name = "text"];
string name = 2 [json_name = "name"];
string cmd = 3 [json_name = "cmd"];
repeated Column columns = 4 [json_name = "columns"];
repeated Parameter params = 5 [json_name = "parameters"];
repeated string comments = 6 [json_name = "comments"];
string filename = 7 [json_name = "filename"];
Identifier insert_into_table = 8 [json_name = "insert_into_table"];
}
protoc-gen-mbt 是作为 protoc 命令的插件实现的。
因此只要有 protoc 命令即可,不过我个人平时更习惯使用 buf,所以这里选择使用 buf。[7](#7)
从 buf 调用 protoc-gen-mbt
首先,从源码构建 protoc-gen-mbt。
shell
$ git clone git@github.com:moonbitlang/protoc-gen-mbt.git tmp
$ cd tmp
$ moon build -C cli
$ cp cli/target/native/release/build/protoc-gen-mbt.exe ../protoc-gen-mbt.exe
$ cd ..
$ rm -rf tmp
接下来,在两个 buf 的配置文件中指定 proto 文件的位置,以及刚刚构建好的 protoc-gen-mbt 可执行文件。
buf.yaml
yaml
version: v2
modules:
- path: proto
buf.gen.yaml
yaml
version: v2
inputs:
- directory: proto
plugins:
- local: ["./protoc-gen-mbt.exe"]
out: .
opt:
# https://github.com/moonbitlang/protoc-gen-mbt?tab=readme-ov-file#arguments
- project_name=sqlc_plugin
- json=false
- async=false
- username=yourname
使用 buf generate 命令,根据 proto 文件生成 MoonBit 的源代码。
Shell
$ buf generate
生成的代码会作为一个 MoonBit 项目输出。
shell
$ tree sqlc_plugin
> sqlc_plugin
> ├── moon.mod.json
> └── src
> └── plugin
> ├── moon.pkg.json
> └── top.mbt
看看 sqlc 插件的输入内容
在制作 sqlc 插件之前,先来看一下它的输入到底是什么样的。
因此,这里先尝试将标准输入原样输出到标准错误输出。
首先,创建一个 shell 可执行文件。
sqlc 的插件在异常退出时,会把标准错误输出的内容记录到日志中。
相反,如果是正常退出,标准错误输出的内容是不会出现在日志里的,这一点需要注意。
dump_stdin_to_stderr.sh
shell
#!/bin/sh
cat - >&2
exit 1
shell
$ chmod +x dump_stdin_to_stderr.sh
为了使用 sqlc 进行代码生成,创建一些示例 SQL 文件。
shell
$ mkdir sqlite
$ touch sqlite/schema.sql
$ touch sqlite/query.sql
sqlite /schema.sql
yaml
CREATE TABLE authors (
id integer PRIMARY KEY AUTOINCREMENT,
name text NOT NULL,
bio text
);
sqlite /query.sql
yaml
/* name: get_author :one */
SELECT * FROM authors
WHERE id = ? LIMIT 1;
/* name: list_authors :many */
SELECT * FROM authors
ORDER BY name;
/* name: create_author :execresult */
INSERT INTO authors (
name, bio
) VALUES (
?, ?
);
/* name: delete_author :exec */
DELETE FROM authors
WHERE id = ?;
创建 sqlc 的配置文件,指定 SQL 文件和插件(shell 可执行文件)。
sqlc.yaml
yaml
version: '2'
plugins:
- name: dump_stdin_to_stderr
process:
cmd: ./dump_stdin_to_stderr.sh
sql:
- name: sqlite
schema: sqlite/schema.sql
queries: sqlite/query.sql
engine: sqlite
database:
uri: file:authors?mode=memory&cache=shared
codegen:
- out: generated
plugin: dump_stdin_to_stderr
执行 sqlc generate 后,dump_stdin_to_stderr.sh 输出的内容会原样显示出来。
shell
$ sqlc generate main ✭ ✱
> # package dump_stdin_to_stderr
> error generating code: process: error running command
> p
> 2sqlitesqlite/schema.sql"sqlite/query.sqlb>
> generateddump_stdin_to_stderr*
> ump_stdin_to_stderr.sh�main"�main�
> authors'
> id0���������R authorsb integer&
> name0���������R authorsbtext#
> bio0���������R authorsbtext�
虽然直接看并不能理解内容,但可以确认:
确实有 protobuf 格式的二进制数据输入了插件。
在 MoonBit 中处理 sqlc 插件的输入
终于到了这里,可以开始编写 MoonBit 代码了。
首先,将必要的模块添加到项目中。
shell
$ moon add moonbitlang/async
$ moon add moonbitlang/protobuf
通过 buf 生成的、定义了 sqlc 插件输入输出的模块,需要直接编辑 moon.mod.json 来进行引用 。[8](#8)
json
"deps": {
"ryota0624/sqlc_plugin": {
"version": "0.1.0",
"path": "./sqlc_plugin"
},
}
作为运行时入口点的包,其 moon.pkg.json 如下:
cmd /main/moon.pkg.json
yaml
{
"is-main": true,
"import": [
{
"path": "yourname/try_moonbit_sqlc_plugin_dev",
"alias": "lib"
},
"moonbitlang/async/stdio",
"moonbitlang/async",
"moonbitlang/async/io",
"yourname/sqlc_plugin/plugin",
"moonbitlang/protobuf"
]
}
由于 GenerateRequest 消息中包含了查询名称,因此将这些名称输出到标准错误。
最后,通过调用 panic 让进程异常退出。
cmd /main/main.mbt
rust
fn main {
@async.run_async_main(main_async)
panic()
}
async fn main_async() -> Unit {
let input = @stdio.stdin.read_all()
let request = @lib.parse_generate_request(input.binary())
for query in request.queries {
@stdio.stderr.write(query.name + "\n")
}
}
fn parse_generate_request(data : Bytes) -> @plugin.GenerateRequest raise {
@protobuf.BytesReader::from_bytes(data) |> @protobuf.Read::read
}
使用 moon build 并指定 native 作为目标进行构建后,会在
target/native/release/build/cmd/main/main.exe 生成可执行文件。
shell
$ moon build --target native
在 sqlc.yaml 中指定该可执行文件。
yaml
version: '2'
plugins:
- name: moonbit
process:
cmd: target/native/release/build/cmd/main/main.exe
sql:
- name: sqlite
schema: sqlite/schema.sql
queries: sqlite/query.sql
engine: sqlite
database:
uri: file:authors?mode=memory&cache=shared
codegen:
- out: generated
plugin: moonbit
执行 sqlc generate 后,就会输出查询名称。
yaml
& sqlc generate
> # package moonbit
> error generating code: process: error running command get_author
> list_authors
> create_author
> delete_author