Student wall 🎨
2023年5月に開催されたMotoko Bootcamp Day 3のプロジェクトをRust言語で実装します。
1. Rustプロジェクト作成
Rustのプロジェクト「day3」を作成します。cargo new
コマンドを--lib
オプションを付与して実行します。
$ cargo new day3 --lib
$ cd day3
生成されたファイルは以下の通りです。
day3
├── Cargo.toml
└── src
└── lib.rs
2. Cargo.tomlの編集
(1) IC関連ライブラリ追加
ic-cdkライブラリを使用します。最新バージョンでよいかと思いますので、以下のように実行ます。
$ cargo add candid ic-cdk serde
(2) crate-type設定
Canister上から関数が正しく呼び出させるようcrate-typeをcdylib
にします。
[lib]
crate-type = ["cdylib"]
3. dfx.jsonの作成
Canisterの定義を行います。
{
"canisters": {
"day3": {
"candid": "./day3.did",
"package": "day3",
"type": "rust"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"version": 1
}
4. candidの作成
dfx.jsonの [canisters] > [day3] > [candid]項目に指定したファイルに、Canisterに配置するDappが提供する関数のI/Fを定義します。
Motoko Bootcamp Day3 📺 Interfaceに相当するcandidを用意します。
※MotokoのResult (ok, err)とRust標準のResult (Ok, Err)で大文字小文字に違いがあります。MotokoとI/Fを合わせたい場合には別途Result型を定義してください。
type Content = variant {
Text: text;
Image: blob;
Video: blob;
};
type Message = record {
content: Content;
creator: principal;
vote: int;
};
type Result = variant {
Ok;
Err: text;
};
type ResultMessage = variant {
Ok: Message;
Err: text;
};
service : {
writeMessage: (Content) -> (nat);
getMessage: (nat) -> (ResultMessage) query;
updateMessage: (nat, Content) -> (Result);
deleteMessage: (nat) -> (Result);
upVote: (nat) -> (Result);
downVote: (nat) -> (Result);
getAllMessages: () -> (vec Message) query;
getAllMessagesRanked: () -> (vec Message) query;
};
5. lib.rsの編集
cargo new
コマンドで生成されたlib.rsの中身をクリアして、day3用のプログラムを作成します。
Motoko Bootcamp Day 3と同じように、以下の関数を実装します。
writeMessage()
getMessage()
updateMessage()
deleteMessage()
upVote()
downVote()
getAllMessages()
getAllMessagesRanked()
Rust言語仕様の理解が十分でないため、作成したソースコードは所有権まわりをはじめ最適化されていない可能性がありますのでご注意ください。もしも、おかしな実装等が見つかりましたらが、ご指摘いただけますとさいわいです。
ソース説明
(a) 関数名
Canisterが提供する関数の名前がcamelCase
形式なのに対し、Rustは一般的にSnake_case
形式を推奨しているため、コンパイル時に以下のような警告が出ます。
warning: variable `xxx` should have a snake case name
先頭行に以下を入れておくことで、警告を抑止することができます。
#![allow(non_snake_case)]
(b) Content列挙型
扱うコンテンツを列挙型 (enum)として定義しています。
Textm Image, Videoのいずれかの値をとり、それぞれ異なるデータ型のデータを持ちます。
enum Content {
Text(String),
Image(Vec<u8>),
Video(Vec<u8>),
}
(c) Message構造体
Contentとvote、creatorから構成される構造体を定義します。
Principal型はICのPrincipal IDを示しており投稿者も記録します。
struct Message {
content: Content,
vote: i128,
creator: Principal
}
(d) Canisterの保持データ
Canister内に保持するデータは以下の2種類です。
メッセージ自動採番用
メッセージIDをキー、Messageデータを値とするBTreeMap
以下のようにスレッドローカルデータで保持するのが作法のようです。
thread_local! {
static MESSAGE_ID: RefCell<u128> = RefCell::new(0);
static WALL: RefCell<BTreeMap<u128, Message>> = RefCell::new(BTreeMap::new());
}
※MotokoではHashMapが使われておりますが、ここではgetAllMessages()で順序性を維持できるようBTreeMapを使用しています。
6. Unitテスト
TODO: IC色のあるUnitテスト方法について後日整理する
Day 1やDay 2のようにUnitテストを記述してcargo test
を実行したところ、ロジックにICのPrincipal型が含まれることが原因で、「xxxx should only be called inside canisters.」のようなエラーが出ました。
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writeMessage() {
let id = crate::writeMessage(Content::Text(String::from("TEST")));
︙
}
}
ソース中にIC色があるとUnitテストが上手く行えないようですので、以下のいずれかの方法でテストするとよいでしょう。
Canisterに配置してテストを行う
IC CDKのAPIを直接呼ばずに抽象化して、テスト時はスタブを使うようにする
後者の方法として、以下の記事が参考になりそうです。
Test your canister code even in presence of system API calls
7. Local Canisterの起動
Local Canisterを起動します。
--background
オプションでサービス常駐でき、--clean
を付与すると真っ新な状態でLocal canisterを起動できます。
$ dfx start --background --clean
8. Local Canisterへの配備
$ dfx deploy
Cargo.lockがディレクトリに存在しない場合dfx deploy
がエラーとなりますので、cargo generate-lockfile
を実行するとよいでしょう。
最終更新
役に立ちましたか?