4. ic_cdk::call()

あるCanisterから別のCanisterの機能を呼び出す方法について解説します。

以下の公式サンプルを参考にしています。

https://internetcomputer.org/docs/current/developer-docs/backend/rust/intercanister

このサンプルでは、2つのCanister間のやりとりを、Publisher-Subscriberパターンを使って実現しています。

Publisher-Subscriberパターンとは、送信側と受信側を結合せずに、アプリケーションから関心を持っている複数のコンシューマーに対して非同期的にイベントを通知できるようにする仕組みです。

  • メッセージを送信する側:Publisher

  • メッセージを受け取る側:Subscriber

Publisherが送信したメッセージはトピックという送信先に送られます。トピックに送信先のCanister Idを紐づける「subscribe」を行うことで、送信したいSubscriberへと送るようにします。

1. Rust Workspaceの作成

(1) ディレクトリ構成

以下の2つのCanisterを作成するため、Rustのcargo workspaceを使用します。

  • publisher

  • subscriber

icptest
 ├─ src
 │   ├─ publisher
 │   │   ├─ src/lib.rs
 │   │   └─ Cargo.toml
 │   └─ subscriber
 │       ├─ src/lib.rs
 │       └─ Cargo.toml
 ├─ Cargo.toml
[workspace]
members = [
    "src/publisher",
    "src/subscriber"
]

※個人的には、icptest直下のsrcディレクトリは不要として各プロジェクトディレクトリでもよいかと考えていますが、公式サンプルと同じ構成としています。

(2) プロジェクトディレクトリ作成

$ mkdir icptest
$ cd icptest

(3) Cargo.toml作成

[workspace]
members = [
    "src/publisher",
    "src/subscriber"
]

(4) dfx.json作成

{
  "version": 1,
  "canisters": {
    "publisher": {
      "package": "publisher",
      "candid": "src/publisher/src/publisher.did",
      "type": "rust"
    },
    "subscriber": {
      "package": "subscriber",
      "candid": "src/subscriber/src/subscriber.did",
      "type": "rust"
    }
  }
}

2. 'publisher' Canister作成

(1) Rustプロジェクト作成

$ mkdir src
$ cd src
$ cargo new publisher --lib
$ cd publisher

a. crate-type追加

ライブラリセクションを追加して、crate-typeにcdylibを指定します。cdylib を指定することで最終成果物の .wasm ファイルを動的ライブラリにします。

[lib]
crate-type = ["cdylib"]

b. dependencies追加

$ cargo add candid ic-cdk serde

type Counter = record {
    topic:text;
    value:nat64;
};
type Subscriber = record {
    topic:text;
  };
service : {
     "subscribe": (subscriber:Subscriber) -> ();
     "publish": (counter : Counter) -> ();
}

publisher側では、2つの関数を定義しています。

  • subscribe()

  • publish()

a. subscribe()

subscribe()関数は、'subscribe' canisterから呼び出されることが想定されており、topicと通知すべきsubscriberの組をpublisher側に登録する処理です。呼び出し元はic_cdk::caller()でPrincipal Idを取得し、それをキー、topicを値として SUBSCRIBERS へ追加しています。

#[update]
fn subscribe(subscriber: Subscriber) {
    let subscriber_principal_id = ic_cdk::caller();
    SUBSCRIBERS.with(|subscribers| {
        subscribers
            .borrow_mut()
            .insert(subscriber_principal_id, subscriber)
    });
}

b. publish()

publish()関数は、フロントエンドなど外部からメッセージを受け取り、subscriberへ通知する処理です。この公式サンプルでは、Counterという構造体が定義されていてpublish()関数の引数として渡されます。

他のCanisterへの通知には、ic_cdk::notify()関数を使用しています。Canisterの呼び出しは時間かかりますので、publish()関数がasync (非同期)で定義されている点や、#[update]である点もご注意ください。

#[update]
async fn publish(counter: Counter) {
    SUBSCRIBERS.with(|subscribers| {
        // In this example, we are explicitly ignoring the error.
        for (k, v) in subscribers.borrow().iter() {
            if v.topic == counter.topic {
                let _call_result: Result<(), _> =
                    ic_cdk::notify(*k, "update_count", (&counter,));
            }
        }
    });    
}

3. 'subscriber' canister作成

(1) Rustプロジェクト作成

$ cd src
$ cargo new subscriber --lib

a. crate-type追加

ライブラリセクションを追加して、crate-typeにcdylibを指定します。cdylib を指定することで最終成果物の .wasm ファイルを動的ライブラリにします。

[lib]
crate-type = ["cdylib"]

b. dependencies追加

$ cargo add candid ic-cdk serde

type Counter = variant {
    topic : text;
    value:nat64;
};
type Subscriber = variant {
    topic:text;
  };
service : {
     "setup_subscribe": (publisher_id:principal,topic:text) -> ();
     "update_count": (counter : Counter) -> ();
     "get_count": () -> (nat64);
}

subscriber側では、3つの関数を定義しています。

  • setup_subscribe()

  • update_count()

  • get_count()

a. setup_subscribe()

publisher側へsubscribe登録を行うための設定を行うために用意された関数です。この関数を呼び出すことで、引数で指定した'publisher' Canisterのsubscribe()関数を呼び出して、指定したtopicの場合にメッセージを通知してもらうようにしています。

Canisterの呼び出しには、ic_cdk::call()関数を使用します。setup_subscribe()関数は時間がかかるため、asyncであること、および、ic_cdk::call()は非同期で呼ばれますがその応答を待つため 関数呼び出しで.awaitが付与されている点にご注意ください。

#[update]
async fn setup_subscribe(publisher_id: Principal, topic: String) {
    let subscriber = Subscriber { topic };
    let _call_result: Result<(), _> =
        ic_cdk::call(publisher_id, "subscribe", (subscriber,)).await;
}

b. update_count()

受け取った数値をカウンタに加算しています。

#[update]
fn update_count(counter: Counter) {
    COUNTER.with(|c| {
        c.set(c.get() + counter.value);
    });
}

c. get_count()

現在のカウンタ値を取得します。

#[query]
fn get_count() -> u64 {
    COUNTER.with(|c| {
        c.get()
    })
}

4. サービス起動

$ dfx start --clean --background

5. deploy

dfx.jsonのあるディレクトリに移動した上で、以下を実行して下さい。

$ dfx deploy
Deploying all canisters.
Creating a wallet canister on the local network.
The wallet canister on the "local" network for user "default" is "bnz7o-iuaaa-aaaaa-qaaaa-cai"
Creating canisters...
Creating canister publisher...

もし、Cargo.lockファイルがなければ、dfxコマンドがエラーとなりますので、以下で作成しておくとよいでしょう。

$ cargo generate-lockfile

6. 動作確認

(1) setup_subscribe呼び出し

第1引数にPublisherのCanister Id、第2引数にTopicを指定して、subscribeの設定を行います。

$ dfx canister call subscriber setup_subscribe "(principal \"$(dfx canister id publisher)\",\"ICP\")"
()

Publisherのcanister Idは、dfx canister id publisherで取得できますから、シェルスクリプトのCommand Substitution(コマンド置換)を使って第1引数に指定するとよいでしょう。それに合わせて、dfx call コマンドの引数は単一引用符(')から、二重引用符(")に変更し、引数内の文字列を括る二重引用符(")の前にエスケープ記号 (\)を指定するとよいでしょう。

topicは自由に設定できますが、ここでは ICP とします。

(2) publish呼び出し

publisherに対して、指定したトピックでpublishしてみます。

$ dfx canister call publisher publish '(record {topic="ICP"; value=1})'
()

(3) get_count呼び出し

subscriberに対して、指定したトピックのカウントを取得してみます。publishした値が反映されていることが確認できます。

$ dfx canister call subscriber get_count
(1 : nat64)

最終更新