ハイサイ!オースティン やいびーん。
概要
Cargoのフィーチャーフラグ機能を使って、asyncとsync(非同期と同期)の両方に対応したRustライブラリーを作成する方法を紹介します。
この紹介で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
にします。
query_sync = []
query_async = []
配列には、このフィーチャーが必要とする他のクレートのライブラリーと、さらにこのクレートの他のフィーチャーを入れます。同クレートのフィーチャーに依存するフィーチャーもあります。
フィーチャーの依存クレートを追加する
今回は、query_sync
にはmysql
、query_async
にはmysql_async
のクレートにそれぞれに依存しますので、--optional
フラグで追加します。optional
の設定を有効にすると、そのクレートを必要とするフィーチャーを有効にしない限り、クレートに含まれないようにできます。これによってコンパイル時間を大幅に減らせます。
cargo add --optional mysql_async mysql
これでmysql
とmysql_async
はCargo.toml
に追加されますが、まだフィーチャーとの紐付けがない状態です。
[dependencies]
...
mysql = { version = "24.0.0", optional = true }
mysql_async = { version = "0.32.2", optional = true }
それぞれのフィーチャーとの依存関係を示すためには、[features]
の配列にクレート名を追加します。
[features]
query_sync = ["mysql"]
query_async = ["mysql_async"]
これでquery_sync
が有効の時には、mysql
、query_async
が有効の時にはmysql_async
が使われるように指定することができます。
cfgでフィーチャー別のメソッドを実装する
上記のmysql
とmysql_async
を使って別々の実装を追加していきたいのですが、それを可能にするのはcfg
のマクロです。
まず最初にquery_async
のWpQuery
のメソッドを実装します。
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
版のメソッドを実装します。
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
}
}
これらの定義は実際には隣同士に置かれていても大丈夫です。
#[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(...))
というマクロを使うと、フィーチャーのどれかが有効になっていれば、使われるように指定できます。
#[cfg(any(feature = "query_sync", feature = "query_async"))]
use query_builder::QueryBuilder;
query_sync
もquery_async
もQueryBuilder
を参照しているので、上記のようにuse
すれば、コンパイラーに文句を言われずに済みます。
まとめ
以上、Rustのライブラリでasyncとsyncのメソッドを同様なAPIとして実装する方法を紹介しました!
macro_rules!
を活用して、全くのコピペーにならないように工夫できたらいいですね。