Rustにおけるビットフラグ

背景

KDEのシーズン2022でKConfigのRustバインディングに取り組んでいたときに、QFlagsをRustでどう表現するかという問題に直面しました。

  1. 多くのQFlagsは、複数のメンバーが同じ値を持つC++のenumとして定義されています。Rustのenumではこれは不可能です。
  2. ビット単位の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) {}

利点

  1. 定数はコンパイル時に置き換えられるため、パフォーマンスへのコストはありません。
  2. 全ての値をRustのコメントを使って同じように文書化できます。
  3. 複数のフラグを有効にできます。

欠点

  1. 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) {}

利点

  1. ほとんどがenumです。

欠点

  1. 文書化が一貫していません。定数はenumのバリアントとして表示されません。
  2. 複数のフラグを同時に有効にすることはできません。

実装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();
    ...
}

利点

  1. 完全にenumです。
  2. 文書化は期待通りに機能します。

欠点

  1. RustからC++に渡すたびに関数呼び出しをする必要があります。パフォーマンスへの影響はそれほどないと思いますが、言及する価値はあります。
  2. 複数のフラグを一度に設定することができません。例:OpenFlag::IncludeGlobal | OpenFlag::CascadeConfigは不可能です。

実装4:bitflagsクレートを使用する

最終的にたどり着いた実装方法です。実装は以下の通りです。

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) {}

利点

  1. 複数のフラグを一緒に使うことができます。
  2. 文書化が一貫しています。

欠点

  1. enumではなく、ドキュメントではstructとして表示されます。

文書スクリーンショット

文書スクリーンショット

結論

今後しばらくの間、kconfig内のすべてのQFlagsを表すために、bitflagsを使用するつもりだと思います。

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/ayush1325/bitflags-in-rust-2d2p