虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

DenoのForeign function interface(FFI)でMySQLに接続する

※この記事は予約投稿です。

f:id:toranoana-lab:20211201180819j:plain

こんにちは。虎の穴ラボのY.Fです。 この記事は、虎の穴ラボ Advent Calendar 2021- Qiita4日目の記事になります。

3日目は 【Tailwind CSS 3.0 alpha1〜2の注目の新機能をご紹介! 】です。ぜひ読んでみてください。

5日目はY.Iさんの【 anime.js + SVG を使用して画像をヌルヌル動かす!】です。お楽しみに!

本記事ではDeno1.13あたりから導入されたForeign function interface(以下FFI)機能を使ってMySQLに接続するまでに行ったことや、どのように実装してみたかを紹介していきたいと思います。

はじめに

Denoには1.13リリース前から Deno.openPlugin という、Rustのライブラリを呼び出せるAPIがありました。ただ、技術的な課題がいくつかあったとのことでこのAPIは一旦消されています。

その代わりに、Deno1.13で Deno.dlopen という形でFFI用のAPIが追加されました。

deno.com

さらにDeno1.15では、非同期でFFIの処理を実行できるnonblockingやbuffer引数なども追加されました。

deno.com

ただし、これらの機能はこの記事を書くのに使っている1.16.3の時点でも、 --unstable フラグが必要な実験的な機能となっています。

What's FFI?

ところで、皆さんFFIとはなにかご存知でしょうか?

FFIの正式名称は Foreign function interface となります。名前の通り解釈するならば、異なるプログラミング言語間で関数等を呼び出せる機能ということになります。

例えば、RustからC言語で作ったライブラリを呼び出す場合は以下のようになります。

(hello_world.c)

#include<stdio.h>

void hello_world(){
    printf("foo!!\n");
    fprintf(stdout, "HelloWorld!\n");
    fflush(stdout);
}

(main.rs)

extern "C" {
    fn hello_world();
}

fn main() {
    unsafe {hello_world();};
}

以下のコマンドで実行できます。(同じフォルダにある場合)

gcc -c hello_world.c
ar rcs libhello_world.a hello_world.o
rustc -L. -lhello_world ./src/main.rs
./main

このように、FFIを使えば異なる言語間でのやり取りが簡単にできるようになります。

代表的なライブラリ

DenoのFFIを使えばC言語の資産などが比較的容易に使えそうだなというのはご理解いただけたかと思います。

ところで、ライブラリと言われてどういったものを思い浮かべるでしょうか?

RubyでいうgemやNode.jsでいうnode_modulesなど色々あると思いますが、今回扱いたいのは以下のような、C言語またはそれに類する言語で作られた、いわゆる動的ライブラリや静的ライブラリといったものになります。(拡張子dll,so,dylibのようなもの)

本記事での目的は、libmysqlclient.soをDenoから呼び出せるようにすることになります。

やりたいこと

上記のように、本記事の目的はDenoでlibmysqlclient.soを呼び出してみるということになります。

ただ、DenoにはすでにMySQLクライアントライブラリが存在します。

https://deno.land/x/mysql@v2.10.1

ソースも見てみます。

 private async _connect() {
   // TODO: implement connect timeout
   const { hostname, port = 3306, socketPath, username = "", password } = this.config;
   log.info(`connecting ${this.remoteAddr}`);
   this.conn = !socketPath
     ? await Deno.connect({
       transport: "tcp",
       hostname,
       port,
     })
     : await Deno.connect({
       transport: "unix",
       path: socketPath,
     } as any);

// …

Deno.connect を使ってtcpまたはUnix Domain Socketでmysqlと接続していることがわかります。今回の目標はこれと同じ動作をlibmysqlclient.soをDenoから呼び出すことで実現する、ということになります。

DenoでのFFI

DenoでFFIするときの具体的なインターフェースなども確認しておきます。

deno.land

特別なことはなく、C言語やRustで作ったsoファイルなどを Deno.dlopen を使って呼び出すという流れになります。

ただし、引数や戻り値に使える型には制限があります。以下公式サイトからの抜粋になります。

FFI Type C Rust
i8 char / signed char i8
u8 unsigned char u8
i16 short int i16
u16 unsigned short int u16
i32 int / signed int i32
u32 unsigned int u32
i64 long long int i64
u64 unsigned long long int u64
usize size_t usize
f32 float f32
f64 double f64
void void ()
buffer[1] const uint8_t * *const u8

すなわち、今の段階ではC言語とやり取りする場合、この型のみを使ってやり取りする必要があるということになります。

※今回は触れませんが、Rustでライブラリを作る場合は、deno_bindgen を使うことで構造体などのやり取りもできるようです。

github.com

MySQLのインターフェース

DenoでFFIする方法はわかったので、libmysqlclient側で開放されているインターフェースを見てます。

dev.mysql.com

MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag)

この関数を使うことで、MySQLと接続ができそうです。

ただ、よく見てみると戻り値が MYSQL構造体 のポインタになっています。上記で紹介したように、DenoとC言語間で扱える型には制約があります。

他の言語ではどうやっているか

直接 mysql_real_conenct 関数を呼び出すのは難しそうだということがわかりました。では、他の言語ではどのように行っているか探ってみます。

ちょうどRubyのmysql2 gemがlibmysqlclient.soを呼び出しているようだったので見てみます。

github.com

実際にコネクション張っている部分を探してみます。

github.com

抜粋すると以下のような感じです。

module Mysql2
 class Client
   attr_reader :query_options, :read_timeout
  
   # 略

   def initialize(opts = {})
     # 略

     connect user, pass, host, port, database, socket, flags, conn_attrs
   end
   # 略
 end
end

connectメソッドでコネクション張っていそうですが、ソースを眺めていてもconnectメソッドの実態がどこにもありません。ではどこにあるかというと、これがC言語で書かれている部分になります。

Rubyには拡張ライブラリという、RubyとC言語をシームレスに結合できる仕組みがあります。

docs.ruby-lang.org

いくつか用意されている特殊な関数を使うことで、C言語側からRubyのクラスやモジュール、メソッドなどを定義することができます。例えば、以下のようなものがあります。

  • rb_define_module:新しいモジュール定義
  • rb_define_class_unser: あるクラスを継承したクラスをモジュール配下に定義
  • rb_define_method:クラスにメソッドを定義
  • rb_define_private_method: クラスにプライベートメソッドを定義

今回知りたいのはconnectメソッドがどこにあるか、なので rb_define_private_method または rb_define_method が使われているだろうとあたりを付けて探してみます。

github.com

rb_define_private_method(cMysql2Client, "connect", rb_mysql_connect, 8);

となっており、実態は rb_mysql_connect のようなのでそちらも見てみます。

github.com

rv = (VALUE) rb_thread_call_without_gvl(nogvl_connect, &args, RUBY_UBF_IO, 0);

となっており更に nogvl_connect で調べてみます。(rb_thread_call_without_gvlは割愛します。上記ドキュメント等を参照ください)

github.com

最終的な部分は以下のようになります。

static void *nogvl_connect(void *ptr) {
 struct nogvl_connect_args *args = ptr;
 MYSQL *client;


 client = mysql_real_connect(args->mysql, args->host,
                             args->user, args->passwd,
                             args->db, args->port, args->unix_socket,
                             args->client_flag);


 return (void *)(client ? Qtrue : Qfalse);
}

これでわかったことは、

  • Rubyでもmysql_real_connectを直接呼ぶようなことはなく、C言語でラップしている
  • MYSQL構造体についても直接やり取りしていることはなさそう

ということで、Denoからlibmysqlclient.soを呼び出す場合もC言語でラップすることにします。

実際の実装

まずは初期化に必要な mysql_init を実装します。

(mysql.c)

#include <mysql/mysql.h>

MYSQL *conn = NULL;

void init(void)
{
    conn = mysql_init(NULL);
}

MYSQL構造体をグローバルに置きます。このようにすることで、Deno側とMYSQL構造体をやり取りすることなく扱うことできます。

ではDeno側からこれを呼び出してみます。

(mod.ts)

function ffiSuffix(): string {
  switch (Deno.build.os) {
    case "windows":
      return "dll";
    case "darwin":
      return "dylib";
    case "linux":
      return "so";
  }
}

interface LibInterfaces extends Record<string, Deno.ForeignFunction> {
  init: Deno.ForeignFunction;
}

const libInterfaces: LibInterfaces = {
  init: { parameters: [], result: "void" }
};

const libName = `./ext/mysql/mysql.${ffiSuffix()}`;
const dylib = Deno.dlopen(libName, libInterfaces);
dylib.symbols.init();

Deno.dlopenの定義は以下のようになっています。

export function dlopen<S extends Record<string, ForeignFunction>>(
  filename: string | URL,
  symbols: S,
): DynamicLibrary<S>;

これを満たすようにLibInterface型を定義しています。

他、拡張子の指定などは公式ドキュメントをそのままなぞっています。戻り値も引数もない関数の連携なので特に難しいことはないのではないかと思います。

では、続いて実際にコネクションのOpen/Close処理を実装してみます。Openだけだとコネクション張りっぱなしで終了処理がなくなってしまうのでCloseも一緒に実装します。

(mysql.c)

#include <mysql/mysql.h>
#include<stdio.h>

MYSQL *conn = NULL;

void init(void)
{
    conn = mysql_init(NULL);
}

int connect(const char *host, const char *user, const char *passwd, const char *db_name, int port)
{
    if (!mysql_real_connect(conn, host, user, passwd, db_name, port, NULL, 0))
    {
        fprintf(stderr, "Failed to connect to database: Error: %s\n", mysql_error(conn));
        fflush(stderr);
        return 0;
    }
    return 1;
}

void close(void)
{
    mysql_close(conn);
}

close関数はinit関数とそんなに変わりありませんが、connect関数は一気に複雑になりました。

MySQLに接続するためのホスト名、ユーザー名、パスワード、データベース名、ポートはDeno側から受け渡す必要があります。 特に、C言語にはstring型のような文字列型はないので、文字列となる引数はchar型のポインタを受け渡す必要があります。includeしているmysql_real_connect関数も同様のインターフェースになっています。

では、Deno側もみてみます。

(mod.ts)

const C_TERMINAL_SYMBOL = "\0";
const MYSQL_HOST = "localhost";
const MYSQL_USER = "root";
const MYSQL_PASSWD = "";
const MYSQL_DB_NAME = "mysql";
const MYSQL_PORT = 3306;

function strToBytes(word: string): Uint8Array {
  return new TextEncoder().encode(`${word}${C_TERMINAL_SYMBOL}`);
}

function ffiSuffix(): string {
  switch (Deno.build.os) {
    case "windows":
      return "dll";
    case "darwin":
      return "dylib";
    case "linux":
      return "so";
  }
}

interface LibInterfaces extends Record<string, Deno.ForeignFunction> {
  init: Deno.ForeignFunction;
  connect: Deno.ForeignFunction;
  close: Deno.ForeignFunction;
}

const libInterfaces: LibInterfaces = {
  init: { parameters: [], result: "void" },
  connect: {
    parameters: ["buffer", "buffer", "buffer", "buffer", "i32"],
    result: "i32",
  },
  close: { parameters: [], result: "void" },
};

const host = strToBytes(MYSQL_HOST);
const user = strToBytes(MYSQL_USER);
const passwd = strToBytes(MYSQL_PASSWD);
const dbName = strToBytes(MYSQL_DB_NAME);

const libName = `./ext/mysql/mysql.${ffiSuffix()}`;
const dylib = Deno.dlopen(libName, libInterfaces);
dylib.symbols.init();
const res = dylib.symbols.connect(
  host,
  user,
  passwd,
  dbName,
  MYSQL_PORT,
);
if (res === 0) {
  this.close();
  throw new Error("コネクション確立に失敗しました。");
}
dylib.symbols.close();

ここでbuffer引数がでてきました。上記で紹介した通り、DenoのFFIでは直接文字列をやり取りすることはできません。一方でbufferという形で Uint8Array ならば受け渡すことができます。 JavaScriptでは TextEncoder を使うことで文字列から Uint8Array を生成することができるので、それを利用しました。

ただし、C言語側からすると単なるchar型のポインタになるので終端文字が必要になります。これがないと 127.0.0.1L のように文字列にゴミがくっついてきてしまいます。C言語を直接扱う場合は常識的なことではありますが、受け渡す側にこれが必要だということに思い当たらず、これで数時間ハマりました。

なお、終端文字である \0 はJavaScriptにおいてもNull文字とされています。

developer.mozilla.org

最後に、classなどにしておいたほうがライフサイクル管理などが楽だと思いますのでクラス化します。以下に最終的なDenoのソースを示します。

(mod.ts)

const C_TERMINAL_SYMBOL = "\0";
const MYSQL_HOST = "localhost";
const MYSQL_USER = "root";
const MYSQL_PASSWD = "";
const MYSQL_DB_NAME = "mysql";
const MYSQL_PORT = 3306;

function strToBytes(word: string): Uint8Array {
  return new TextEncoder().encode(`${word}${C_TERMINAL_SYMBOL}`);
}

function ffiSuffix(): string {
  switch (Deno.build.os) {
    case "windows":
      return "dll";
    case "darwin":
      return "dylib";
    case "linux":
      return "so";
  }
}

interface LibInterfaces extends Record<string, Deno.ForeignFunction> {
  init: Deno.ForeignFunction;
  connect: Deno.ForeignFunction;
  // deno-lint-ignore camelcase
  real_query: Deno.ForeignFunction;
  close: Deno.ForeignFunction;
}

const libInterfaces: LibInterfaces = {
  init: { parameters: [], result: "void" },
  connect: {
    parameters: ["buffer", "buffer", "buffer", "buffer", "i32"],
    result: "i32",
  },
  // deno-lint-ignore camelcase
  real_query: { parameters: ["buffer", "u64"], result: "i32" },
  close: { parameters: [], result: "void" },
};

class MySQL {
  #dylib: Deno.DynamicLibrary<LibInterfaces> | null = null;
  readonly host = strToBytes(MYSQL_HOST);
  readonly user = strToBytes(MYSQL_USER);
  readonly passwd = strToBytes(MYSQL_PASSWD);
  readonly dbName = strToBytes(MYSQL_DB_NAME);

  private isConn = false;

  constructor() {
    const libName = `./ext/mysql/mysql.${ffiSuffix()}`;
    this.#dylib = Deno.dlopen(libName, libInterfaces);

    this.#dylib.symbols.init();
    const res = this.#dylib.symbols.connect(
      this.host,
      this.user,
      this.passwd,
      this.dbName,
      MYSQL_PORT,
    );
    if (res === 0) {
      this.close();
      throw new Error("コネクション確立に失敗しました。");
    }
    this.isConn = true;
  }

  close() {
    if (!this.#dylib) {
      return;
    }

    if (this.isConn) {
      this.#dylib.symbols.close();
      this.#dylib.close();
      this.#dylib = null;
    }
  }
}

const client = new MySQL();
client.close();

これでコネクション張るという目的は達成できました。ただし、クエリの実行等はまだできません。クエリを実行するには以下のような課題があります。

  • クエリを実行するC APIはmysql_real_query→mysql_use_queryなどだが、戻り値がMYSQL_RES構造体などでこれもそのままはDeno側に持っていけない
    • MYSQL_RES構造体の中から結果文字列等を取り出してbufferでやり取りする必要があるが、戻り値でのbuffer型はまだ対応されていない
    • 複数の戻り値がある場合はループで取り出すなどやり方を考えないといけない

まとめ

  • Denoでlibmysqlclientを扱うためにRubyのmysql2 gemを参考にし、紹介しました。
  • Denoからlibmysqlclientを扱う事ができました。
  • とりあえず初期化処理からコネクション、コネクションクローズまでの流れは実装できました。
  • クエリの実行にはまだ若干の課題があります。

P.S.

採用情報

■募集職種
yumenosora.co.jp

カジュアル面談も随時開催中です

■お申し込みはこちら!
news.toranoana.jp

■ToraLab.fmスタートしました!

メンバーによるPodcastを配信中!
是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm

■Twitterもフォローしてくださいね!

ツイッターでも随時情報発信をしています
twitter.com