Rustでasyncとsyncに対応したCrateを作成する方法

ハイサイ！オースティン やいびーん。

# 概要

Cargoのフィーチャーフラグ機能を使って、asyncとsync（非同期と同期）の両方に対応したRustライブラリーを作成する方法を紹介します。

この紹介でwp_query_rsのソースコードを使います。

https://github.com/tronicboy1/wp_query_rs

# フィーチャーフラグ

Cargoの機能にはクレートの一部のコードを、指定しない限りコンパイルに含めないフィーチャー機能があります。これによって不必要なコードをクレートの使用者にコンパイルさせずに済むのでありがたい機能です。

例えば、同wp_query_rsには、php_ext_rsを使ったトレート実装が含まれていますが、php_ext_rsを使うと、Cargo.tomlに細かい設定が必要な上、コンパイルするパソコンにPHPをインストールしないといけないので、wp_query_rsの使い勝手が非常に悪くなってしまいます。

そこでそのphp_ext_rsを使うコードは、指定するまで含めないようにすると、使用者にはありがたいはずです。

## フィーチャーの設定方法

フィーチャーを追加するにはクレートの`Cargo.toml`に`[features]`の項目を記載した上、提供したいフィーチャーを追加します。

今回は、asyncとsyncのフィーチャーを追加します。asyncがRust言語の独占記号になっているので`query_async`と`query_sync`にします。

```toml:Cargo.toml
query_sync = []
query_async = []
```

配列には、このフィーチャーが必要とする他のクレートのライブラリーと、さらにこのクレートの他のフィーチャーを入れます。同クレートのフィーチャーに依存するフィーチャーもあります。

## フィーチャーの依存クレートを追加する

今回は、`query_sync`には`mysql`、`query_async`には`mysql_async`のクレートにそれぞれに依存しますので、`--optional`フラグで追加します。`optional`の設定を有効にすると、そのクレートを必要とするフィーチャーを有効にしない限り、クレートに含まれないようにできます。これによってコンパイル時間を大幅に減らせます。

```
cargo add --optional mysql_async mysql
```

これで`mysql`と`mysql_async`は`Cargo.toml`に追加されますが、まだフィーチャーとの紐付けがない状態です。

```toml:Cargo.toml
[dependencies]
...
mysql = { version = "24.0.0", optional = true }
mysql_async = { version = "0.32.2", optional = true }
```

それぞれのフィーチャーとの依存関係を示すためには、`[features]`の配列にクレート名を追加します。

```toml:Cargo.toml
[features]
query_sync = ["mysql"]
query_async = ["mysql_async"]
```

これで`query_sync`が有効の時には、`mysql`、`query_async`が有効の時には`mysql_async`が使われるように指定することができます。


# cfgでフィーチャー別のメソッドを実装する

上記の`mysql`と`mysql_async`を使って別々の実装を追加していきたいのですが、それを可能にするのは`cfg`のマクロです。

まず最初に`query_async`の`WpQuery`のメソッドを実装します。

```rust:src/lib.rs
impl WpQuery {
...
    #[cfg(feature = "query_sync")]
    pub fn with_connection<'a, T>(
        conn: &mut impl Queryable,
        params: T,
    ) -> Result<Self, mysql::Error>
    where
        T: Into<Params<'a>>,
    {
        let posts: Vec<WpPost> = Self::query(conn, params)?;

        Ok(Self { posts })
    }

    #[cfg(feature = "query_sync")]
    fn query<'a, T>(conn: &mut impl Queryable, params: T) -> Result<Vec<WpPost>, mysql::Error>
    where
        T: Into<Params<'a>>,
    {
        let query_builder::QueryAndValues(q, values) = QueryBuilder::new(params.into()).query();

        let stmt = conn.prep(q)?;

        conn.exec(stmt, values)
    }
}
```

次に、**同メソッド名**の`query_async`版のメソッドを実装します。

```rust:src/lib.rs
impl WpQuery {
... 
    #[cfg(feature = "query_async")]
    pub async fn with_connection<'a, T>(
        conn: &mut mysql_async::Conn,
        params: T,
    ) -> Result<Self, mysql_async::Error>
    where
        T: Into<Params<'a>>,
    {
        let posts = Self::query(conn, params).await?;

        Ok(Self { posts })
    }

    #[cfg(feature = "query_async")]
    async fn query<'a, T>(
        conn: &mut mysql_async::Conn,
        params: T,
    ) -> Result<Vec<WpPost>, mysql_async::Error>
    where
        T: Into<Params<'a>>,
    {
        let query_builder::QueryAndValues(q, values) = QueryBuilder::new(params.into()).query();

        let stmt = conn.prep(q).await?;

        conn.exec(stmt, values).await
    }
}
```

これらの定義は実際には隣同士に置かれていても大丈夫です。

```rust
    #[cfg(feature = "query_sync")]
    pub fn with_connection<'a, T>(
        conn: &mut impl Queryable,
        params: T,
    ) -> Result<Self, mysql::Error>
    where
        T: Into<Params<'a>>,
    {
        let posts: Vec<WpPost> = Self::query(conn, params)?;

        Ok(Self { posts })
    }
    #[cfg(feature = "query_async")]
    pub async fn with_connection<'a, T>(
        conn: &mut mysql_async::Conn,
        params: T,
    ) -> Result<Self, mysql_async::Error>
    where
        T: Into<Params<'a>>,
    {
        let posts = Self::query(conn, params).await?;

        Ok(Self { posts })
    }
```

こうすると、非同期のメソッドも、同期のメソッドも同じ名前とAPIで提供することができます！

# cfgのanyで共通するロジックを含める

上記のメソッドが共通して必要とするコードがあれば、`cfg(any(...))`というマクロを使うと、フィーチャーのどれかが有効になっていれば、使われるように指定できます。

```rust:src/lib.rs
#[cfg(any(feature = "query_sync", feature = "query_async"))]
use query_builder::QueryBuilder;
```

`query_sync`も`query_async`も`QueryBuilder`を参照しているので、上記のように`use`すれば、コンパイラーに文句を言われずに済みます。

# まとめ

以上、Rustのライブラリでasyncとsyncのメソッドを同様なAPIとして実装する方法を紹介しました！

`macro_rules!`を活用して、全くのコピペーにならないように工夫できたらいいですね。