♾️

はじめてのCanister開発 (Rust)

2024/05/05に公開

はじめに

Internet Computer(IC)は、Blockchain技術を応用した分散クラウド環境です。

Canisterと呼ばれるSmart Contractから構成されるDapp(Decentralized Application; 分散型アプリケーション)がICの複数ノード上で動作します。
CanisterにはWebAssemblyモジュールとデータが保持されており、データ更新の際は複数ノード間のコンセンサスによってデータの一貫性が保たれます。

Canisterの開発には、Canister Development Kit(CDK)を使用します。プログラミング言語として、IC用に作られたMotoko以外に、Rust、Python(Kybra)、TypeScript(Azle)に対応しており、本記事ではRust言語での開発方法を解説します。

サンプル概要

解説するHelloサンプルは、CDKのdfx newコマンドを実行すると生成される最も基本的なプロジェクトです。
サンプルは、以下の2つのCanisterから構成されています。

  • Backend Canister
  • Frontend Canister

Backend Canisterにはパラメータとして<名前>を受け取ったら『Hello, <名前>!』という文字列を返すgreet()メソッドを実装しています。
また、Frontend CanisterはWebユーザーインタフェースを提供しており、フォームに<名前>を入力して[Submit]ボタンをクリックすると、Backend Canisterのgreet()メソッドを呼び出し、その戻り値を画面に表示する処理を実装しています。

開発環境準備

まずは開発環境を整えます。
筆者はWindows PCを使用していますので、Windows Subsystem for Linux (WSL)のUbuntu 24.04 LTS上に開発環境を構築しています。ご使用の環境に応じて適宜読み替えてください。

CDKインストール

以下のコマンドを実行して、dfxコマンドやそのバージョン管理を行うdfxvmコマンドなど開発に使用するツールをインストールします。

$ sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)"

参考サイト

Installing tools

Rustセットアップ

以下のコマンドを実行してRust環境をセットアップします。

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

WebAssemblyモジュールをビルドするため、wasm32をターゲットに追加しておいてください。

$ rustup target add wasm32-unknown-unknown

ビルド時、cmakeなど一部の開発用ツールがないとエラーになります。お使いの環境にインストールしておいてください。
Ubuntuの場合は以下のコマンドを実行しておくとよいでしょう。

$ sudo apt install -y build-essential

参考サイト

Developer environment

プロジェクト作成

開発環境が整ったらプロジェクトを作成します。

ターミナル上でdfx new <プロジェクト名>と入力して実行すると、Backend開発に使用するプログラミング言語やFrontendのフレームワークなどについて対話形式で訊かれた後、Helloサンプルが生成されます。

<プロジェクト名>には作成するプロジェクトの名称を指定します。例えば、プロジェクト名を『hello』にする場合、以下のコマンドを実行してください。

$ dfx new hello

Backendのプログラミング言語の選択

Backend Canisterの開発に使用するプログラミング言語を訊いてきますので、『Rust』を選択してください。

? Select a backend language: ›  
  Motoko
❯ Rust
  TypeScript (Azle)
  Python (Kybra)

Frontend frameworkの選択

Frontend Canisterの開発に使用するWebフレームワークを訊いてきますので、お好みに応じて選択してください。デフォルトは『SvelteKit』です。

? Select a frontend framework: ›  
❯ SvelteKit
  React
  Vue
  Vanilla JS
  No JS template
  No frontend canister

Extra featuresの選択

追加する機能があればチェックしてください。本解説では不要です。

? Add extra features (space to select, enter to confirm) ›
⬚ Internet Identity
⬚ Bitcoin (Regtest)

ディレクトリ構成

dfxコマンドが完了すると、hello(プロジェクト名)のディレクトリが作成されてファイルが生成されます。

hello
├── .git
│    ︙
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── README.md
├── dfx.json
├── node_modules
│    ︙
├── package-lock.json
├── package.json
├── src
│   ├── hello_backend
│   │    ︙
│   └── hello_frontend
│        ︙
└── tsconfig.json

Local Canister実行環境の起動

開発時はIC上ではなくローカルPC上にデプロイしてデバッグ等を行うとよいでしょう。dfx startコマンドで、Local Canister実行環境を起動します。--backgroundオプションはサービスをバックグラウンド起動するものですので指定しておくとよいでしょう。また、--cleanオプションを指定すると前回起動時の状態を破棄します。

$ cd hello
$ dfx start --background --clean

ビルド・デプロイ

Local Canister実行環境が稼働中の状態でdfx deployコマンドを実行すると、ビルドが行われてデプロイされます。

$ dfx deploy
Deploying all canisters.
Creating canisters...
Creating canister hello_backend...
  ︙
Deployed canisters.
URLs:
  Frontend canister via browser
    hello_frontend:
      - http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai
      - http://bd3sg-teaaa-aaaaa-qaaba-cai.localhost:4943/
  Backend canister via Candid interface:
    hello_backend: http://127.0.0.1:4943/?canisterId=be2us-64aaa-aaaaa-qaabq-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai

※ポート番号やCanisterIdや使用する環境、設定等によって変わります

サンプルの実行

dfx deployコマンド実行時に表示されるFrontend CanisterのURLをブラウザからアクセスします。
テキストボックスに適当な名前を入れて[Click Me!]ボタンをクリックすると、Backend Canisterが呼び出され、応答文字列『Hello, <名前>!』が表示されます。

ファイルの解説

dfx.json

プロジェクトに関するJSON形式の設定ファイルです。
Backend Canisterがhello_backendという名称で定義され、Frontend Canisterがhello_frontendという名称でそれぞれ定義されています。

dfx.json
{
  "canisters": {
    "hello_backend": {
      "candid": "src/hello_backend/hello_backend.did",
      "declarations": {
        "node_compatibility": true
      },
      "package": "hello_backend",
      "type": "rust"
    },
    "hello_frontend": {
      "dependencies": [
        "hello_backend"
      ],
      "source": [
        "src/hello_frontend/dist"
      ],
      "type": "assets",
      "workspace": "hello_frontend"
    }
  },
  "defaults": {
    "build": {
      "args": "",
      "packtool": ""
    }
  },
  "output_env_file": ".env",
  "version": 1
}

Backendディレクトリ構成

src/hello_backendディレクトリにBackend Canister用の設定ファイルとソースファイルが出力されます。Rustを選択した場合は、以下の通りです。

hello
├── src
│   └── hello_backend
│       ├── Cargo.toml
│       ├── hello_backend.did
│       └── src
│           └── lib.rs

src/hello_backend/Cargo.toml

Rustのビルド設定ファイルです。パッケージ名『hello_backend』がdfx.jsonのpackage項目に対応づいています。
crate-type = ["cdylib"]を指定し、少なくともcandidic-cdkライブラリを追加する必要があります。

src/hello_backend/Cargo.toml
[package]
name = "hello_backend"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

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

[dependencies]
candid = "0.10"
ic-cdk = "0.13"
ic-cdk-timers = "0.7" # Feel free to remove this dependency if you don't need timers

src/hello_backend/hello_backend.did

Canisterの公開インタフェースを定義します。Backend Canisterではgreetというqueryメソッドを公開しています。

src/hello_backend/hello_backend.did
service : {
    "greet": (text) -> (text) query;
}

参考サイト

Candidの記述方法の詳細は、公式ドキュメントが参考になります。

Candid reference

src/hello_backend/src/lib.rs

src/hello_backend/src/lib.rs
#[ic_cdk::query]
fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}

Frontendディレクトリ構成

src/hello_frontendディレクトリにFrontend Canister用の設定ファイルとソースファイルが出力されます。
Frontend frameworkに『SvelteKit』を選択した場合は以下の通りです。

hello
├── src
│   └── hello_frontend
│       ├── .svelte-kit
│       │    ︙
│       ├── package.json
│       ├── src
│       │   ├── app.d.ts
│       │   ├── app.html
│       │   ├── index.scss
│       │   ├── lib
│       │   │   └── canisters.js
│       │   ├── routes
│       │   │   ├── +layout.js
│       │   │   └── +page.svelte
│       │   └── vite-env.d.ts
│       ├── static
│       │   ├── .ic-assets.json5
│       │   ├── favicon.ico
│       │   └── logo2.svg
│       ├── svelte.config.js
│       ├── tsconfig.json
│       └── vite.config.js

src/hello_frontend/svelte.config.js

SvelteKitなどのWebフレームワークではStatic Site Generation(SSG)とServer Side Rendering(SSR)の方式がありますが、Frontend Canisterは基本的にHTTPリクエストで指定されたパスに対してstaticコンテンツを返すWebホスティングでのでSSGを選択する必要があります。

SvelteKitでは、svelte.config.jsでSSGの設定を行います。ビルドファイルはsrc/hello_frontend/distディレクトリに出力される設定となっており、これはdfx.jsonのsource項目と対応づいています。

src/hello_frontend/svelte.config.js
import adapter from '@sveltejs/adapter-static';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
    // If your environment is not supported or you settled on a specific environment, switch out the adapter.
    // See https://kit.svelte.dev/docs/adapters for more information about adapters.
    adapter: adapter({
      pages: 'dist',
      assets: 'dist',
      fallback: undefined,
      precompress: false,
      strict: true,
    }),
  },
};

export default config;
参考サイト

Static site generation

src/hello_frontend/src/routes/+page.svelte

SvelteKitはファイルシステムベースのルーティングを採用しており、src/routesがリクエストパスのルート(/)になります。
また、ディレクトリ内にある+page.svelteコンポーネントでWebページが定義されています。

$lib/canisters.jsからbackendをimportして、onSubmit()関数内でBackend Canisterのgreet()メソッドを呼び出しています。

src/hello_frontend/src/routes/+page.svelte
<script>
  import "../index.scss";
  import { backend } from "$lib/canisters";

  let greeting = "";

  function onSubmit(event) {
    const name = event.target.name.value;
    backend.greet(name).then((response) => {
      greeting = response;
    });
    return false;
  }
</script>

<main>
  <img src="/logo2.svg" alt="DFINITY logo" />
  <br />
  <br />
  <form action="#" on:submit|preventDefault={onSubmit}>
    <label for="name">Enter your name: &nbsp;</label>
    <input id="name" alt="Name" type="text" />
    <button type="submit">Click Me!</button>
  </form>
  <section id="greeting">{greeting}</section>
</main>

src/hello_frontend/src/lib/canisters.js

dfx deployコマンドを実行するとsrc/declarationsディレクトリにCanisterの公開インタフェースに応じたJavaSciprt呼び出し処理が自動生成されますので、それをimportします。

HelloサンプルのSvelteKit版ではcanisters.jsというファイル内で、自動生成されたcreateActor()という関数を実行して呼び出しできるようにしています。

src/hello_frontend/src/lib/canisters.js
import { createActor, canisterId } from 'declarations/hello_backend';
import { building } from '$app/environment';

function dummyActor() {
    return new Proxy({}, { get() { throw new Error("Canister invoked while building"); } });
}

const buildingOrTesting = building || process.env.NODE_ENV === "test";

export const backend = buildingOrTesting
    ? dummyActor()
    : createActor(canisterId);

Discussion