海外开发者实践分享:用 MoonBit 开发 SQLC 插件(其二)

本文是第 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!!!


  1. https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/5b9bc1eeda0be29919239c9248cfd680a2f5f01b/cmd/main/moon.pkg.json#L1-L14 ↩︎

  2. https://wasi.dev/ ↩︎

  3. http://mooncakes.io/docs/peter-jerry-ye/wasi ↩︎

  4. https://bytecodealliance.org/articles/WASI-0.2 ↩︎

  5. https://github.com/bytecodealliance/wit-bindgen/tree/main/crates/moonbit ↩︎

  6. https://github.com/sqlc-dev/sqlc/blob/main/go.mod#L25 ↩︎

  7. https://github.com/wazero/wazero/issues/2289 ↩︎

  8. https://github.com/ryota0624/try_moonbit_sqlc_plugin/blob/main/cmd/wasm/moon.pkg.json ↩︎

  9. https://docs.moonbitlang.com/en/latest/toolchain/moon/package.html#wasm-backend-link-options ↩︎

相关推荐
scan7242 小时前
python mcp see
开发语言·数据库·python
前端李易安2 小时前
ERROR in ./node_modules/vue-router/dist/vue-router.mjs 被报错折磨半天?真相竟是……
前端·javascript·vue.js
monkey_slh2 小时前
JS逆向实战——最新某东cfe滑块
开发语言·前端·javascript
Coder_Boy_2 小时前
前端和后端软件系统联调经典问题汇总(二)
开发语言·数据库·python
禅思院2 小时前
在win10上配置 Rust以及修改默认位置问题
开发语言·前端·后端·rust·cargo·mingw64·cargo安装位置
乾元2 小时前
把 SLA / SLO 放到网络可观测的核心:从指标到证据链的工程化路径
运维·开发语言·网络·人工智能·网络协议·架构
222you2 小时前
Java的Stream流
java·开发语言
管理大亨2 小时前
智慧农业ELK落地方案:数据驱动精准农业
大数据·redis·python·elk·elasticsearch
kevinzeng2 小时前
Redis的IO多路复用
java·redis