本文是第 2 篇。
第一篇的链接:https://zenn.dev/4245ryomt/articles/9680434dc60c0c
本文中使用的 moon 命令版本如下:
yaml
$ moon version
> moon 0.1.20251202 (1a59800 2025-12-02)
GenerateResponse 的输出
在第 1 篇中,已经完成了 用 MoonBit 处理 sqlc 插件输入 的部分。
在第 2 篇中,将从 由 MoonBit 作为 sqlc 插件向 sqlc 返回响应并生成文件 开始。
当输入能够成功解析之后,接下来就是输出。
只要将表示 GenerateResponse 消息的二进制数据输出到标准输出,就可以指示 sqlc 生成文件。
GenerateResponse 中可以包含多个 File,因此为每个查询分别生成文件也很容易。
**cmd/main/main.mbt **
rust
fn main {
@async.run_async_main(main_async)
}
async fn main_async() -> Unit {
let input = @stdio.stdin.read_all()
let request = @lib.parse_generate_request(input.binary())
let response = try? @lib.process_generate_request(request)
match response {
Err(err) => {
@stdio.stderr.write("Error: \{err}\n")
panic()
}
Ok(res) => {
let writer = @buffer.new()
@protobuf.Write::write(res, writer)
@stdio.stdout.write(writer.to_bytes())
}
}
}
/// --- @lib ---
fn process_generate_request(
generate_request : @plugin.GenerateRequest,
) -> @plugin.GenerateResponse raise {
let codes = "I am a file. contents wrote by MoonBit"
let file = @plugin.File::new(
"generated_queries.mbt",
@encoding/utf8.encode(codes),
)
@plugin.GenerateResponse::new([file])
}
ryota0624/try_moonbit_sqlc_plugin/cmd/main/moon.pkg.json[1](#1)
Lines 1 to 14 in 5b9bc1e
json
{
"is-main": true,
"import": [
{
"path": "username/try_moonbit_sqlc_plugin_dev",
"alias": "lib"
},
"moonbitlang/async/stdio",
"moonbitlang/async",
"moonbitlang/async/io",
"ryota0624/sqlc_plugin/plugin",
"moonbitlang/protobuf"
]
}
在编写好 MoonBit 代码后,使用 moon build 生成可执行文件,然后执行 sqlc generate。
如果一切正常,就可以看到由 MoonBit 定义的字符串内容被写入到生成的文件中。
shell
$ moon build
$ sqlc generate
$ cat generated/generated_queries.mbt
> I am a file. contents wrote by MoonBit
让用 MoonBit 编写的 sqlc 插件能够以 wasm 形式运行
尝试将构建目标改为 wasm。
shell
$ moon build --target wasm
> ...
> error: failed to run build for target Wasm
> Caused by:
> failed when building project
很遗憾,直接这样做是无法构建成功的。
在 cmd/main/main.mbt 中使用的 moonbitlang/async 等包,并没有针对 wasm 目标提供实现。
rust
.mooncakes/moonbitlang/async/src/internal/time/time.mbt:16:1 ]
│
16 │ pub extern "C" fn ms_since_epoch() -> Int64 = "moonbitlang_async_get_ms_since_epoch"
│ ──────────────────────────────────────────┬─────────────────────────────────────────
│ ╰─────────────────────────────────────────── extern "C" is unsupported in wasm backend.
因此,接下来要做的是:
让由 MoonBit 输出的 wasm 可以处理标准输入和 标准输出。
让 MoonBit 输出的 wasm 能够处理标准输入/输出
之前在 cmd/main 中创建的包是面向 native 实现的,因此新建一个 cmd/wasm:
shell
$ mkdir cmd/wasm
$ touch cmd/wasm.mbt
$ touch cmd/moon.pkg.json
在 wasm 中处理标准输入/输出,需要通过 WASI 来实现。
WASI 有 preview 1 和 preview 2 两个版本,这里使用 preview 1 [2](#2)。
幸运的是,MoonBit 已经存在用于处理 WASI preview 1 的包,这里直接使用它[3](#3)。
shell
$ moon add peter-jerry-ye/wasi
为什么选择 WASI preview 1
你可能会想:
为什么不使用已经稳定的 preview 2[4](#4) 呢?
实际上,MoonBit 本身已经具备了用于 preview 2 的工具。[5](#5)
我最初也是基于 preview 2 来实现的,但最终无法将生成的 wasm 作为 sqlc 插件执行,只好放弃。
原因在于:sqlc 使用的是 wazero 这个 wasm 运行时,而 wazero 尚未支持 WASI preview 2。
sqlc-dev/sqlc/go.mod[6](#6)
Lines 25 to 25 in main
plaintext
github.com/tetratelabs/wazero v1.10.1
究竟 wazero 什么时候会支持 WASI preview 2[7](#7) 呢......
像 wasmtime、jco 这样的运行时已经支持了,但其他运行时似乎还在努力推进中。
通过 WASI 作为 sqlc 插件处理标准输入 / 标准输出
在 peter-jerry-ye/wasi 中,并没有直接提供诸如"读取标准输入"、"写入标准输出"这样刚好对应的函数,
因此通过 文件描述符的读写 来实现与标准输入/输出的交互。
cmd / wasm /main.mbt
rust
fn main {
let result = try? {
let reader = @protobuf.BytesReader::from_bytes(
Bytes::from_array(read_file(@wasi.Fd(0))),
)
let request = @protobuf.Read::read(reader)
let response = @lib.process_generate_request(request)
let writer = @buffer.new()
@protobuf.Write::write(response, writer)
let contents : Array[BytesView] = [
Bytes::from_array(writer.to_bytes().to_array()),
]
@wasi.Fd(1).fd_write(contents)
}
match result {
Err(e) => {
log_error("Failed to process request: \{e.to_string()}")
@wasi.proc_exit(1)
}
Ok(_) => @wasi.proc_exit(0)
}
}
const BUFFER_LEN : UInt64 = 4096
fn read_file(fd : @wasi.Fd) -> Array[Byte] raise @wasi.Errno {
let result : Array[Byte] = []
while true {
let output = FixedArray::make(BUFFER_LEN.to_int(), b'\x00')
let read_output : Array[FixedArray[Byte]] = [output]
let size = try? fd.fd_read(read_output)
match size {
Err(err) => {
log_error(err.to_string())
raise err
}
Ok(size) =>
if size == 0 {
break
} else {
let read = FixedArray::make(size.0.reinterpret_as_int(), b'\x00')
output.blit_to(
read,
src_offset=0,
dst_offset=0,
len=size.0.reinterpret_as_int(),
)
result.append([..read])
}
}
}
result
}
fn log_error(err : String) -> Unit {
let contents : Array[BytesView] = [
@encoding/utf8.encode("Error: \{err.to_string()}\n"),
]
(try? @wasi.Fd(2).fd_write(contents)) |> ignore
}
在处理 wasm 时还需要指定 link 等内容,但这里先按"理解氛围"为主,不展开细节。
ryota0624/try_moonbit_sqlc_plugin/cmd/wasm/moon.pkg.json[8](#8)
Lines 1 to 17 in main
json
{
"is-main": true,
"import": [
{
"path": "username/try_moonbit_sqlc_plugin_dev",
"alias": "lib"
},
"ryota0624/sqlc_plugin/plugin",
"moonbitlang/protobuf",
"peter-jerry-ye/wasi"
],
"link": {
"wasm": {
"export-memory-name": "memory"
}
}
}
Package Configuration[9](#9)
只要完成了输入输出的处理代码,就可以构建 wasm 了。
由于现在存在两个针对不同目标的入口点,构建时需要指定对应的包。
shell
$ moon build cmd/wasm --target wasm
构建生成的 wasm 可以通过任意 wasm 运行时执行,于是随便把一些内容通过标准输入传给它。
由于 wasm 端会尝试解析 README,自然会失败并报错。
shell
$ cat README.md | wasmtime target/wasm/release/build/cmd/wasm/wasm.wasm
> Error: Failed to process request: UnknownWireType(7)
接下来,在 sqlc 中指定这个 wasm 文件。
指定 wasm 文件时,还需要给出其 checksum,因此先计算它。
shell
$ openssl sha256 ./target/wasm/release/build/cmd/wasm/wasm.wasm
> SHA2-256(./target/wasm/release/build/cmd/wasm/wasm.wasm)= ?
在 sqlc.yaml 中指定即可。
yaml
version: '2'
plugins:
- name: moonbit
wasm:
url: file://target/wasm/release/build/cmd/wasm/wasm.wasm
sha256: ?
sql:
- name: sqlite
schema: sqlite/schema.sql
queries: sqlite/query.sql
engine: sqlite
database:
uri: file:authors?mode=memory&cache=shared
codegen:
- out: generated_by_wasm
plugin: moonbit
执行 sqlc generate 后,确认输出的文件,可以看到内容与以 进程形式process 方式执行时完全一致。
shell
$ sqlc generate
$ cat generated_by_wasm/generated_queries.mbt
> I am a file. contents wrote by MoonBit
目标达成
成功将 由 MoonBit 输出的 wasm 作为 sqlc 插件执行,
也完成了「用 MoonBit 制作 sqlc 插件:使用 moonbit protoc,并尝试接触 wasm」这一目标。
MoonBit 已经提供了用于处理 protobuf、WASI 的库,让人切实感受到厉害。
如果将来出现一个通用的、可连接任意 RDBMS 的超酷数据库连接库,
那时我想真正实现一个 能够输出 MoonBit 源代码 的 sqlc 插件。
自己动手写一个数据库连接库也不错。
年轻的语言就是有很多值得探索的地方,真好!!
enjoy MoonBit!!!
-
https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/5b9bc1eeda0be29919239c9248cfd680a2f5f01b/cmd/main/moon.pkg.json#L1-L14 ↩︎
-
https://github.com/bytecodealliance/wit-bindgen/tree/main/crates/moonbit ↩︎
-
https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/cmd/wasm/moon.pkg.json ↩︎
-
https://docs.moonbitlang.com/en/latest/toolchain/moon/package.html#wasm-backend-link-options ↩︎