🈷️

MoonBitで作るsqlc plugin。moonbit protocを使ったり、wasmに触れてみたり。その2

に公開

この記事は https://qiita.com/advent-calendar/2025/moonbit の11日目です。

この記事では

MoonBitでsqlcのプラグインを作ってみたことを紹介します。
この記事ではMoonBitでいい感じにSQLが実行できるライブラリは完成しません。

過程でMoonBitでprotocを使ったり、MoonBitでビルドしたwasmを実行しています。

書き始めたら思いの外長いドキュメントになってしまったので分割します。
この記事はその1でsqlcのプラグインとしての入力をMoonBitで解釈するまでしてみます。
その2でMoonBitで出力したwasmをsqlcのプラグインとして使ってみます。

この記事はその2です。

その1
https://zenn.dev/4245ryomt/articles/9680434dc60c0c

使っていくmoonコマンドは以下のバージョンです。

$ moon version                                                                                                                                                                     ✘ 2
> moon 0.1.20251202 (1a59800 2025-12-02)

GenerateResponseの出力

その1ではMoonBitでsqlc pluginの入力をMoonBitで扱うところまでやってみました。
その2ではMoonBitからsqlc pluginとしてsqlcへ応答を返してファイル出力するところから始めてみます。

入力がパースできたところで次に出力ですがGenerateResponseのメッセージを表すバイナリを標準出力に流すことでsqlcへファイル生成を指示できます。
GenerateResponseへ含むFileは複数取れるのでクエリごとにファイルを出力するのも容易です。

cmd/main/main.mbt
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])
}

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

MoonBitのコードを書いたらmoon buildで実行バイナリを出力して、sqlc generateを実行します。
実行できればMoonBitから定義した文字が書き込まれたファイルが出力されます。

$ moon build
$ sqlc generate
$ cat generated/generated_queries.mbt
> I am a file. contents wrote by MoonBit

MoonBitでSQLをタイプセーフに実行するコードを生成するところは...

割愛します。というか作ってません。
きっとそのうち作ってそれも記事にするかもしれません。

MoonBitで作ったsqlc pluginをwasm実行できるようにする

ということでbuildのターゲットをwasmにします。

$ 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で出力するための実装をしていません。

.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.

https://docs.moonbitlang.com/en/latest/language/ffi.html#declare-foreign-function

そこで次ではMoonBitで出力したwasmで標準入力/出力が扱えるようにします。

MoonBitで出力したwasmで標準入力/出力が扱えるようにしてみる

cmd/mainに作っていたパッケージはnativeに向けて実装されているので、cmd/wasm

$ mkdir cmd/wasm
$ touch cmd/wasm.mbt
$ touch cmd/moon.pkg.json

wasmで標準入出力を扱うにはwasiを通じて利用する必要があります。preview 1, preview 2が存在しますが今回preview 1を利用します。

https://wasi.dev
ありがたいことにMoonBitにはすでにpreview1のwasiを扱うためのパッケージが存在します。
http://mooncakes.io/docs/peter-jerry-ye/wasi
それを使っていきます。

$ moon add peter-jerry-ye/wasi

why wasi preview 1

なぜpreview 2なるモノを使わないの?preview 2は安定版になっているので使っても良さそうだけど?と思うところでしょう。
https://bytecodealliance.org/articles/WASI-0.2

MoonBit自体にはpreview 2でやり取りをするための道具はすでにそろっています。
https://github.com/bytecodealliance/wit-bindgen/tree/main/crates/moonbit

実際自分も最初preview 2で実装していましたが出力したwasmファイルをsqlc pluginとして実行できなく断念しました。
というのもsqlcがwazeroというwasmランタイムを使っていて、wazeroではwasi preview 2への対応がまだという具合です。
https://github.com/sqlc-dev/sqlc/blob/main/go.mod#L25
https://github.com/wazero/wazero/issues/2289

はたしてwazeroではwasi preview 2への対応がされる日はくるのでしょうか...

wasmtime, jcoといったランタイムでは対応済みですが、他のランタイムでは今頑張っているような感じとかそうじゃないとか。

wasi経由でsqlc pluginとして標準入力・標準出力を扱う

「標準入力を読み取る。」「標準出力に書き込む。」といったドンピシャな関数等はpeter-jerry-ye/wasiで定義されていないのでファイルディスクリプタの読み書きで標準入出力とのやり取りを実現します。

cmd/wasm/main.mbt

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の指定等必要ですがここでは雰囲気で進めます。
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

入出力を扱うコードができあがればwasmへビルドできます。
それぞれターゲットが違うエントリーポイントが2つある状態なのでビルド実行時には対象のパッケージを指定します。

$ moon build cmd/wasm --target wasm

ビルドで出力できたwasmは適当なwasmランタイムから実行できるのでいい加減なモノを標準入力に流してみます。
wasm側でREADMEをパースしようとしても当然できないのでエラーで落ちますね。

$ cat README.md | wasmtime target/wasm/release/build/cmd/wasm/wasm.wasm
> Error: Failed to process request: UnknownWireType(7)

次にsqlcでwasmファイルを指定します。
wasmファイルを指定するときにはchecksumの指定も必要なので出力します。

$ openssl sha256 ./target/wasm/release/build/cmd/wasm/wasm.wasm
> SHA2-256(./target/wasm/release/build/cmd/wasm/wasm.wasm)= ?

sqlc.yamlで指定すればOK

sqlc.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として実行した際と同じ内容が書かれているのがわかります。

$ sqlc generate
$ cat generated_by_wasm/generated_queries.mbt
> I am a file. contents wrote by MoonBit

ゴール

MoonBitで出力したwasmをsqlc pluginとして実行できて、「MoonBitで作るsqlc plugin。moonbit protocを使ったり、wasmに触れてみたり。」がゴールしました。

MoonBitではすでにprotobuf、wasiを扱うためのライブラリが存在していて、「なんとなくすげー」が感じられました。

ユニバーサルでどんなRDBMSとも接続できるようなsuper coolなDB接続ライブラリが登場したら実際にMoonBitのソースコードを出力できるsqlc pluginを作ってみたいですね!
自分でDB接続ライブラリを作ってみるのもいいですね!
若い言語は色々開拓しがいがあっていいですね!!
enjoy MoonBit!!!

Discussion