# 海外开发者实践分享：用 MoonBit 开发 SQLC 插件（其二）

**URL:** <https://taolun.moonbitlang.com/t/topic/1374>\
**Category:** Moonbit（月兔平台）\
**Created:** [2025 年12 月 18 日 10:29 UTC](https://taolun.moonbitlang.com/t/topic/1374 "2025-12-18T10:29:06Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![moonbitlang](https://taolun.moonbitlang.com/user_avatar/taolun.moonbitlang.com/moonbitlang/32/78_2.png) [@moonbitlang](https://taolun.moonbitlang.com/u/moonbitlang)\
发布日期： [2025 年12 月 18 日 10:29 UTC](https://taolun.moonbitlang.com/t/topic/1374/1 "2025-12-18T10:29:07Z")

</div>

**本文是第 2 篇。**

第一篇的链接：[MoonBitで作るsqlc plugin。moonbit protocを使ったり、wasmに触れてみたり。その1](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\]

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

幸运的是，MoonBit 已经存在用于处理 WASI preview 1 的包，这里直接使用它\[3\]。

```Shell
$ moon add peter-jerry-ye/wasi

```

* * *

## 为什么选择 WASI preview 1

你可能会想：

为什么不使用已经稳定的 preview 2\[4\] 呢？

实际上，MoonBit 本身已经具备了用于 preview 2 的工具。\[5\]

我最初也是基于 preview 2 来实现的，但最终无法将生成的 wasm 作为 sqlc 插件执行，只好放弃。

原因在于：sqlc 使用的是 **wazero** 这个 wasm 运行时，而 **wazero 尚未支持 WASI preview 2** 。

sqlc-dev/sqlc/go.mod\[6\]  
Lines 25 to 25 in main

```Plain
github.com/tetratelabs/wazero v1.10.1

```

究竟 wazero 什么时候会支持 WASI preview 2\[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\]

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

只要完成了输入输出的处理代码，就可以构建 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！！！**

* * *

1. [try\_moonbit\_sqlc\_plugin/cmd/main/moon.pkg.json at 5b9bc1eeda0be29919239c9248cfd680a2f5f01b · ryota0624/try\_moonbit\_sqlc\_plugin · GitHub](https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/5b9bc1eeda0be29919239c9248cfd680a2f5f01b/cmd/main/moon.pkg.json#L1-L14) 

2. [https://wasi.dev/](https://wasi.dev/) 

3. [peter-jerry-ye/wasi - mooncakes.io](http://mooncakes.io/docs/peter-jerry-ye/wasi) 

4. [Bytecode Alliance — WASI 0.2 Launched](https://bytecodealliance.org/articles/WASI-0.2) 

5. [wit-bindgen/crates/moonbit at main · bytecodealliance/wit-bindgen · GitHub](https://github.com/bytecodealliance/wit-bindgen/tree/main/crates/moonbit) 

6. [sqlc/go.mod at main · sqlc-dev/sqlc · GitHub](https://github.com/sqlc-dev/sqlc/blob/main/go.mod#L25) 

7. [WASI Preview 2 support · Issue #2289 · wazero/wazero · GitHub](https://github.com/wazero/wazero/issues/2289) 

8. [try\_moonbit\_sqlc\_plugin/cmd/wasm/moon.pkg.json at main · ryota0624/try\_moonbit\_sqlc\_plugin · GitHub](https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/cmd/wasm/moon.pkg.json) 

9. [Package Configuration — MoonBit nightly documentation](https://docs.moonbitlang.com/en/latest/toolchain/moon/package.html#wasm-backend-link-options)
