Rc<RefCell<&'a mut [u8]>> in Solana
この記事はSolana Advent Calendar2024 15日目の記事です。
はじめに
Solanaブロックチェーン上で動作するSolanaのプログラム(スマートコントラクト)は、eBPF(Extended Berkeley Packet Filter)上で実行されます。SolanaプログラムはC言語風のFFIインターフェースを介して、Solana Runtimeからアカウント情報や実行に必要なデータを受け取ります。この低レベルな設計により高いパフォーマンスが得られる一方で、Rustに慣れたエンジニアでも戸惑います。
その一つがAccountInfo
構造体が持つ非常に複雑な型、特にRc<RefCell<&'a mut [u8]>>
です。
本記事では、Rc<RefCell<&'a mut [u8]>>
という型が必要とされる理由やその背後にある考え方について解説します。
1. Solanaプログラムと低レイヤー実行環境の概要
Solanaは、ハイスループットを実現しているブロックチェーンであり、トランザクション処理をeBPF上で実行します。プログラムはバイナリ(eBPF形式)としてオンチェーンにデプロイされ、Solana RuntimeがこれをBPF Loader
を介して呼び出します。
典型的なRust開発では、コンパイラが所有権と借用を静的に検証します。しかし、Solanaプログラムでは、Runtimeが用意したメモリをFFI境界で受け取り、その安全性は一部実行時に保証されることになります。AccountInfo
というオンチェーン上に保存されるデータを表す型は、このFFIで渡された生ポインタを安全にラップし、Rustが保証する安全性と結びつける役割を果たします。
特にAccountInfo
は、アカウントのアドレス、所有者、lamports
、データ領域への参照を保持し、プログラムがアカウントデータを読み書きするための抽象化レイヤーを提供しています。
Rc<RefCell<&'a mut [u8]>>
が出現する理由
2. AccountInfoが提供する抽象化とAccountInfo
には以下のようなフィールドがあります
ここで注目すべきはlamports
およびdata
フィールドの型です。data
はRc<RefCell<&'a mut [u8]>>
という複雑な型になっています。この型が必要とされる理由は次の通りです。
-
FFI境界で渡される生ポインタを安全な参照へ変換するため:
Solana RuntimeはFFIで生ポインタを渡します。これをRustで安全に扱うには、&'a mut [u8]
などの適切なライフタイム付きの可変参照が必要です。しかし、単なる参照では所有権が一意となるため、複数箇所で同じアカウントを扱う場合に不便が生じます。 -
内部可変性を確保するため(RefCell):
Rustの借用規則は強力ですが、FFI経由で得たデータを柔軟に更新するためには、常に可変参照を取得できるわけではありません。RefCell
は内部可変性を提供し、不変参照からでも実行時に可変アクセスを可能にします。これは、AccountInfo
が同時に複数箇所で使われる状況で役立ちます。 -
複数所有権を確保するため(Rc):
&'a mut [u8]
などの可変参照は、通常一人の所有者しか許さず、ライフタイム内で一つの可変借用のみが有効です。しかし、SolanaプログラムではCPIなどによって、複数のプログラムが同じアカウントデータを共有する必要があります。Rc
を使えば、同じデータへの複数の共有参照を可能にし、クローンを作成することで複数のAccountInfo
インスタンスが同一の内部データを共有できます。
これらを総合すると、Rc<RefCell<&'a mut [u8]>>
は「FFI経由の生データを安全で可変な共有参照として扱うための仕組み」と言えます。
3. Solana RuntimeとBPF Loaderの役割
Solana Runtimeは、トランザクション実行時に指定されたプログラムをBPF Loader
を介して実行します。その際、プログラムID、利用するアカウント情報、インストラクションデータといった必要な情報を、BPF VM上のメモリ領域に展開します。
BPF Loader
はSolana Runtime内のネイティブプログラムの一種であり、指定されたeBPFプログラム(ユーザが書いたRustプログラムをeBPF用にコンパイルしたもの)をロードし、そのentrypoint
関数を呼び出す役割を担います。
Runtime側では、アカウントのlamports
やdata
はRuntimeが所有し、プログラム実行中のみ、&'a mut u64
や&'a mut [u8]
としてプログラムに貸し出されます。この貸し出しはFFI経由で行われるため、プログラム側はunsafe
ブロックやRefCell
によるランタイムチェックで安全性を確保します。
4. entrypoint!マクロとdeserialize関数
entrypoint!
マクロは、Solanaプログラムが公開するエントリーポイントを定義するためのマクロです。このマクロは、BPF Loaderが呼び出すentrypoint
というC言語風のシンボルを定義し、その中でdeserialize
関数を呼んでFFI経由で渡された生ポインタinput: *mut u8
をパースします。
deserialize
関数では、input
から
program_id: &Pubkey
accounts: Vec<AccountInfo<'a>>
instruction_data: &'a [u8]
を復元します。この過程で、lamports
は&'a mut u64
として、data
は&'a mut [u8]
としてメモリを解釈します。しかし、これらは直接AccountInfo
構造体に埋め込むのではなく、Rc<RefCell<...>>
で包まれます。これにより、借用と参照カウントの仕組みを利用して、プログラム内で安全に複数回アクセスできるようになります。
実際のdeserialize
関数のコードでは、RefCell::new(...)
やRc::new(...)
を用いてAccountInfo
フィールドを構築する箇所が確認できます。
5. AccountInfo構造体のライフタイム保証
AccountInfo
は、アカウントを表す抽象化であり、以下のようなメンバーを持ちます(再掲)
ここで、'a
というライフタイムパラメータが重要です。'a
は、このAccountInfo
が参照するメモリが有効な間だけ存在することを保証します。Solanaプログラムはentrypoint
が呼ばれている間、Runtimeが提供するバッファやメモリ領域は有効です。この期間中、AccountInfo
はそのライフタイム'a
を通じて安全な参照を保持できます。
6. RefCell<T>とRc<T>
RefCell<T>:内部可変性と実行時借用チェック
RefCell<T>
は内部にT
を保持し、不変借用と可変借用の状態を実行時カウンタで管理します。
-
borrow()
が呼ばれるたび、内部の不変借用カウントがインクリメントされます。 -
borrow_mut()
が呼ばれると、可変借用カウントが1になる必要があります。また、このとき不変借用カウントが0であることが要求されます。 -
borrow()
とborrow_mut()
から返されるRef<T>
やRefMut<T>
は、スコープ終了時にドロップされ、借用カウントが減少します。
これにより、コンパイラでは検出が困難なFFI境界を含む複雑な借用関係でも、実行時に安全性を保証できます。不正な同時可変借用が発生するとパニックするため注意が必要です。
例えばlamports
はRc<RefCell<&'a mut u64>>
ですが、account_info.lamports.borrow_mut()
を呼ぶとRefMut<&'a mut u64>
が取得でき、そこからさらに*
演算子を適用することで実際のu64
値を変更できます。
Rc<T>:複数所有権と参照カウント
Rc<T>
は、参照カウントによる共有所有権を提供します。
Rc<T>
は参照カウントを持ち、Rc::clone(&rc_value)
を呼ぶと参照カウントが増え、同じデータへの参照を複数の変数で保持できます。AccountInfo
をCPIで他のプログラムに渡す際にも、同じRc
をクローンして引き回すことで、すべてが同一のu64
や[u8]
バッファを指し示すことができます。
Rc
はデータが必要なくなった時点(参照カウントが0になったとき)にのみデータを解放します。Solanaプログラムのエントリーポイント実行中にアカウントデータが無効になることは基本的にないため、Rc
の利用でunsafe
になることはありません。
なぜ2つ同時に必要なのか
&'a mut u64
という可変参照は、Rustの厳格な借用ルールに基づき、基本的にある時点において一つの可変参照しか許されません。しかし、この制約だけではSolanaプログラム開発における要求を満たすには柔軟性が不足します。
そこでRefCell
の出番です。RefCell
は内部可変性という機能を提供し、実行時の借用チェックをパスすることを条件に、一見不変に見えるデータへの可変アクセスを可能にします。これにより、複数の箇所から同一のデータへアクセスできる道が開かれます。
さらに、Rc
を組み合わせることで、そのRefCell
を複数のAccountInfo
間で共有することが可能になります。つまり、Rc
によって参照カウント方式の共有所有権を実現することで、複数のAccountInfo
が同一のデータを指し示すことができるのです。
この、RefCell
による内部可変性とRc
による共有所有権の組み合わせは、SolanaのCPIを行うのに必要になります。
7. CPI時のAccountInfoクローン問題
単純なインストラクション内でAccountInfo
を使う場合はクローンは必要ありません。borrow()
とborrow_mut()
を適宜呼ぶことで、data
やlamports
にアクセスできます。
しかし、CPI(Cross Program Invocation)でSolanaプログラムから他のSolanaプログラムを呼び出すさいに、AccountInfo
を渡す必要があり、クローンが必要になることがあります。
Rc
が存在するため、クローンは参照カウントを増やすだけの軽量な操作です。ただし、CPI先での利用を想定して、借用状況が整合性を取らなければなりません。これはとても重要でRefCell
に対して複数の可変借用が同時に行われると実行時にパニックを発生させます。CPI先や複数箇所でborrow_mut()
を乱用しないように注意してください。
8. AccountInfoを用いた生データアクセスコード例
ここで実際のコード例を示します。例えば、AccountInfo
からlamports
を加算するコードは以下のようになります。
use solana_program::account_info::AccountInfo;
use solana_program::entrypoint::ProgramResult;
fn add_lamports(account: &AccountInfo, amount: u64) -> ProgramResult {
let mut lamports_ref = account.lamports.borrow_mut(); // RefMut<&mut u64>を取得
let lamports_ptr = &mut **lamports_ref; // &mut &mut u64から&mut u64を取得
*lamports_ptr += amount;
Ok(())
}
borrow_mut()
によって実行時チェックされた上で可変参照が取得でき、&mut u64
としてlamports
を操作できます。
アカウントデータ(data
フィールド)の場合は、以下のようになります。
use solana_program::account_info::AccountInfo;
use solana_program::entrypoint::ProgramResult;
use solana_program::program_error::ProgramError;
fn write_byte_to_data(account: &AccountInfo, offset: usize, value: u8) -> ProgramResult {
let mut data_ref = account.data.borrow_mut(); // RefMut<&mut [u8]>を取得
let data_slice = &mut **data_ref; // &mut &mut [u8]から&mut [u8]を取得
if offset < data_slice.len() {
data_slice[offset] = value;
Ok(())
} else {
Err(ProgramError::InvalidArgument)
}
}
RefCell
経由で&mut [u8]
を取得し、バイト単位で書き込みが可能です。
9. borshを用いた複雑データ構造の読み書き
生のバイト列を直接扱うのはエラーが生じやすく煩雑です。borsh)を利用すれば、struct
定義を元にシリアライズ・デシリアライズを容易に行うことができます。
use borsh::{BorshSerialize, BorshDeserialize};
use solana_program::account_info::AccountInfo;
use solana_program::entrypoint::ProgramResult;
use solana_program::program_error::ProgramError;
#[derive(BorshSerialize, BorshDeserialize, Debug)]
struct MyData {
counter: u64,
flag: bool,
}
fn increment(account: &AccountInfo) -> Result<MyData, ProgramError> {
let mut data_ref = &mut account.data.borrow_mut(); // RefMut<&mut [u8]>を取得
let state = borsh::from_slice::<MyDataに変換>(data)?; // MyDataにSiriアライズ
state.counter = data.counter + 1;
data.copy_from_slice(borsh::to_vec(&state)?.as_slice()); // dataにデシリアライズして書き込み
}
borsh
を使うと、&[u8]
や&mut [u8]
を任意の構造体へシリアライズ/デシリアライズでき、コードが簡潔かつ安全になります。
まとめ
Rc<RefCell<&'a mut [u8]>>
という型は、Solana特有の実行環境とRustの所有権・借用モデルを統合するための必要な型です。
前提となる知識はここまでの解説で十分なので、ここからより深めるには実際のコードを読む、書くということをお勧めします。
Solanaの入門でよく使われるEscrowという題材を実装したコード、解説もあるのでぜひこちらを参照しながら実装にチャレンジしてみてください。
また、Solanaが公開しているプログラムのコードも参考になりますのでお勧めです。
動くものを作るという観点ならAnchorをお勧めしますが、学習という観点では低レイヤー部分を理解できると幅が広がるため、一度はぜひ挑戦してください。
Discussion