Rustにおけるビットフラグ
背景
KDEのシーズン2022でKConfigのRustバインディングに取り組んでいたときに、QFlags
をRustでどう表現するかという問題に直面しました。
- 多くの
QFlags
は、複数のメンバーが同じ値を持つC++のenumとして定義されています。Rustのenumではこれは不可能です。 - ビット単位のORを使って複数のフラグを有効にすることができますが、Rustのenumではビット操作はできません。
この投稿では、私が考え出したさまざまな実装方法とそれぞれのトレードオフについてご紹介します。
C++のenum
私が実装しようとしていたenumは、KConfig::OpenFlags
でした。以下にそのenumを示します。
enum OpenFlag {
IncludeGlobals = 0x01, ///< kdeglobalsを設定オブジェクトに統合する。
CascadeConfig = 0x02, ///< システム全体の設定ファイルにカスケードする。
SimpleConfig = 0x00, ///< 単独の設定ファイル。
NoCascade = IncludeGlobals, ///< ユーザーのグローバル設定は含むが、システム設定は省く。
NoGlobals = CascadeConfig, ///< システム設定にカスケードするが、ユーザーのグローバル設定は省く。
FullConfig = IncludeGlobals | CascadeConfig, ///< グローバル設定を含み、システム設定にカスケードする完全な設定。
};
実装1:Rustのモジュールを使用する
この方法は、Rustのモジュールと定数を組み合わせています。サンプル実装は以下の通りです。
pub mod OpenFlags {
type E = u32;
const INCLUDE_GLOBALS: Self::E = 0x01;
const CASCADE_CONFIG: Self::E = 0x02;
const SIMPLE_CONFIG: Self::E = 0x00;
const NO_CASCASE: Self::E = Self::INCLUDE_GLOBALS;
const NO_GLOBALS: Self::E = Self::CASCADE_CONFIG;
const FULL_CONFIG: Self::E = Self::INCLUDE_GLOBALS | Self::CASCADE_CONFIG;
}
fn something(flag: OpenFlags::E) {}
利点
- 定数はコンパイル時に置き換えられるため、パフォーマンスへのコストはありません。
- 全ての値をRustのコメントを使って同じように文書化できます。
- 複数のフラグを有効にできます。
欠点
- enumではなく、定数の集まりです。
実装2:Implの中でconstを使用する
この方法は、問題のあるメンバーをimpl
内でconst
として定義します。サンプル実装は以下の通りです。
#[repr(C)]
pub enum OpenFlags {
IncludeGlobals = 0x01,
CascadeConfig = 0x02,
SimpleConfig = 0x00,
FullConfig = 0x01 | 0x02,
}
#[allow(non_upper_case_globals)]
impl OpenFlags {
const NoCascade: Self = Self::IncludeGlobals;
const NoGlobals: Self = Self::CascadeConfig;
}
fn something(flag: OpenFlags) {}
利点
- ほとんどがenumです。
欠点
- 文書化が一貫していません。定数はenumのバリアントとして表示されません。
- 複数のフラグを同時に有効にすることはできません。
実装3:C++に渡す際に標準のRust enumsを変換する
この方法は標準のRust enumsを使用しています。サンプル実装は以下の通りです。
pub enum OpenFlags {
IncludeGlobals,
CascadeConfig,
SimpleConfig,
NoCascade,
NoGlobals,
FullConfig
}
impl OpenFlags {
type E = u32;
const INCLUDE_GLOBALS: Self::E = 0x01;
const CASCADE_CONFIG: Self::E = 0x02;
const SIMPLE_CONFIG: Self::E = 0x00;
pub fn to_cpp(&self) -> Self::E {
match self {
Self::IncludeGlobals => Self::INCLUDE_GLOBALS,
Self::CascadeConfig => Self::CASCADE_CONFIG,
Self::SimpleConfig => Self::SIMPLE_CONFIG,
Self::NoCascade => Self::INCLUDE_GLOBALS,
Self::NoGlobals => Self::CASCADE_CONFIG,
Self::FullConfig => Self::INCLUDE_GLOBALS | Self::CASCADE_CONFIG,
}
}
}
fn something(flag: OpenFlags) {
let flag = flag.to_cpp();
...
}
利点
- 完全にenumです。
- 文書化は期待通りに機能します。
欠点
- RustからC++に渡すたびに関数呼び出しをする必要があります。パフォーマンスへの影響はそれほどないと思いますが、言及する価値はあります。
- 複数のフラグを一度に設定することができません。例:
OpenFlag::IncludeGlobal | OpenFlag::CascadeConfig
は不可能です。
bitflagsクレートを使用する
実装4:最終的にたどり着いた実装方法です。実装は以下の通りです。
use bitflags::bitflags
bitflags! {
/// システム全体の設定とユーザーのグローバル設定が設定の読み取りにどのように影響するかを決定します。
/// これはビットフラグです。したがって、`OpenFlags::INCLUDE_GLOBALS | OpenFlags::CASCADE_CONFIG`のようなオプションを渡すことができます。
#[repr(C)]
pub struct OpenFlags: u32 {
/// kdeglobalsを設定オブジェクトに統合する。
const INCLUDE_GLOBALS = 0x01;
/// システム全体の設定ファイルにカスケードする。
const CASCADE_CONFIG = 0x02;
/// 単独の設定ファイル。
const SIMPLE_CONFIG = 0x00;
/// ユーザーのグローバル設定は含むが、システム設定は省く。
const NO_CASCADE = Self::INCLUDE_GLOBALS.bits;
/// システム設定にカスケードするが、ユーザーのグローバル設定は省く。
const NO_GLOBALS = Self::CASCADE_CONFIG.bits;
/// グローバル設定を含み、システム設定にカスケードする完全な設定。
const FULL_CONFIG = Self::INCLUDE_GLOBALS.bits | Self::CASCADE_CONFIG.bits;
}
}
fn something(flag: OpenFlags) {}
利点
- 複数のフラグを一緒に使うことができます。
- 文書化が一貫しています。
欠点
- enumではなく、ドキュメントでは
struct
として表示されます。
文書スクリーンショット
結論
今後しばらくの間、kconfig内のすべてのQFlags
を表すために、bitflagsを使用するつもりだと思います。
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/ayush1325/bitflags-in-rust-2d2p