Stable Structures
はじめに
分散型クラウド環境『Internet Computer(IC)』で動作するCanisterをRust言語で開発するために参考となる情報をまとめています。
本記事では、Stable Structuresと呼ばれるライブラリを用いてCanister内のデータを管理する方法について解説します。
Heap MemoryとStable Memory
Canisterのデータ記憶領域にはHeap MemoryとStable Memoryの2種類があります。
Heap Memoryは一般的なアプリケーションの実行環境におけるメモリーに相当するものですので、扱いやすくアクセスコストも低いですが、Canisterのアップグレード時に内容はクリアされます。また、最大4GiBまでという制限があります。
一方、Stable Memoryはストレージに相当するものですので、Canisterのアップグレード時もデータは保持され、最大400GiBまでデータを保持することができます。
Caniserのアップグレード後も、データを保持し続ける方法として、以下の2つがあります。
- データ格納先にHeap Memoryを使用しアップグレード時にStable Memoryへ一時退避
- データ格納先にStable Memoryを使用
1の方法は、アプリケーションで扱うデータの規模が十分に小さい場合に有効です。
扱うデータ量が多いとHeap Memoryに収まらなくなったり、アップグレード時に時間がかかりすぎて失敗するなどの問題も発生しますので、その場合は、2の方法のようにデータ格納先としてHeap Memoryの代わりにStable Memoryを使用する設計にする必要があります。
Stable Structures
アプリケーションからStable Memoryへアクセスする際、個人的にはStable Memoryをファイルのように扱えるとうれしいのですが、現時点ではCanisterはWASIに未対応であり、ファイルI/Oやファイルシステムのような標準的な仕組みは用意されていません。
その代わりに、Stable Memoryをアプリケーションから扱いやすくしたRust用ライブラリがいくつか公開されています。(ic-stable-memory、icfs、Stable Structures・・・)
本記事で取り上げる『Stable Structures』はDfinityが開発しているライブラリであることと、RustのBTreeMap
相当のI/Fを備えたStableBTreeMap
というコレクション型が用意されていて、アプリケーション側はStable Memoryを意識することなく利用できますので、執筆時点では最適なライブラリであると考えています。
サンプル概要
作成するサンプルは、Canisterの公開インタフェースとしてregister
メソッドとget
メソッドを用意し、UserデータをStable Memoryへ格納、取得するものです。
Stable Structuresの使い方を例示したサンプルにすぎませんので、Principalによるアクセス制御などは行わない単純なつくりとしています。
自動採番するid値をキー、以下のUser構造体データを値としてStableBTreeMap
へ格納します。
開発
1. プロジェクトの作成
dfx new
コマンドでプロジェクトのひな型を生成します。
$ dfx new icptest
✔ Select a backend language: · Rust
✔ Select a frontend framework: · No frontend canister
✔ Add extra features (space to select, enter to confirm) ·
Fetching manifest https://sdk.dfinity.org/manifest.json
Creating new project "icptest"...
︙
作成されたプロジェクトディレクトリに移動します。
$ cd icptest
シリアライズを行うserde
と、ic-stable-structures
を追加します。
$ cargo add serde ic-stable-structures
本サンプルは、2024年7月時点で最新のv0.6.5を利用しています。
今後のバージョンアップにより仕様が変わる可能性がありますのでご注意ください。
2. 編集
src/icptest_backend/icptest_backend.did
Canisterの公開インタフェースは以下とします。
type User = record {
name : text;
age : nat8;
profile : text;
};
service : {
"get": (nat64) -> (opt User) query;
"register": (User) -> (nat64);
}
icptest_upgrade/src/icptest_backend/src/lib.rs
use candid::{CandidType, Decode, Deserialize, Encode};
use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory};
use ic_stable_structures::{
storable::Bound, DefaultMemoryImpl, StableBTreeMap, Storable,
};
use std::{borrow::Cow, cell::RefCell};
type Memory = VirtualMemory<DefaultMemoryImpl>;
#[derive(CandidType, Deserialize)]
struct User {
name: String,
age: u8,
profile: String,
}
impl Storable for User {
fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
Cow::Owned(Encode!(self).unwrap())
}
fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
Decode!(bytes.as_ref(), Self).unwrap()
}
const BOUND: Bound = Bound::Bounded {
max_size: 100,
is_fixed_size: false,
};
}
thread_local! {
static MEMORY_MANAGER: RefCell<MemoryManager<DefaultMemoryImpl>> =
RefCell::new(MemoryManager::init(DefaultMemoryImpl::default()));
static USERS: RefCell<StableBTreeMap<u64, User, Memory>> = RefCell::new(
StableBTreeMap::init(
MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))),
)
);
}
#[ic_cdk::query]
fn get(id: u64) -> Option<User> {
USERS.with(|users| users.borrow().get(&id))
}
#[ic_cdk::update]
fn register(user: User) -> u64 {
USERS.with(|users| {
let id = users.borrow().len() as u64;
users.borrow_mut().insert(id, user);
id
})
}
プログラムの解説
StableBTreeMap
に格納するUser
構造体には、Storable
のtraitを実装します。
to_bytes()
、from_bytes
でbyte列へのSeriaize / Deserializeの処理を記述し、Bound
には、データの最大サイズと可変長/固定長の指定を行います。
thread_local!
内では、MemoryManager
型の変数を宣言し、データはStableBTreeMap
型で、キーはu64のid、値はUser
構造体とします。
StableBTreeMap
のinit()
関数の引数には、MemoryManager#get()
の復帰値を渡します。
MemoryManager#get()
の引数には、本サンプルでは管理するデータはStableBTreeMap
1件のみなのでMemoryId::new(0)
を指定し、もし複数データを管理する場合には、MemoryId::new(1)
、MemoryId::new(2)
、…と指定するようです。
なお、StableBtreeMap
の値にStableBtreeMap
を持つといったNest構造には対応していないようですのでご注意ください。
3. Local Canister実行環境の起動
Local Canister実行環境が起動していない場合には、dfx start
コマンドで起動します。
$ dfx start --background --clean
4. Deploy
アプリケーションをビルドし、Local Canister実行環境にDeployします。
$ dfx deploy
Deploying all canisters.
Creating canisters...
Creating canister icptest_backend...
︙
URLs:
Backend canister via Candid interface:
icptest_backend: http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai
5. プログラム実行
dfx deploy
コマンドを実行した後に表示されるCandid UI画面のURLにアクセスします。
Casniterのプログラムを適当に修正して再度Deployした後もデータは消えずに保存されていることが確認できます。
6. Local Canister実行環境の停止
$ dfx stop
Discussion