Solana Developer Hub Online #0: Rust入門 in Solana
この記事は2023/11/24に開催したSolana Developer Hub Online #0で話をさせていただいた「Rust入門 in Solana」を再編集したものになります。
Rust 入門 in Solana
私はRust大好きおじさんをやっています。元々、C言語からはじめ、C++、C#、Java、PHP、JavaScript、Scalaなどなど、いろいろな言語をやっており、最近ではRust、Golang、Python、TypeScriptを主に使っています。
Rustはよく難しい言語と言われています。
Solanaに関わる人たちにお会いしてRustのことを聞くとやはり難しい・・・と言われることが多いです。また、世間一般的にもRustは高難易度言語と言われることが多いです。
しかし、私の周りで難しいと言われることはあまりありません。先日あったRust.Tokyo 2023でも簡単とは言わないが、言われているほど難しくはないというような話もありました。
では、どこにこのギャップがあるのでしょうか。
そのためにSolanaのプログラム(スマートコントラクト)を例に、Rustに入門してみましょう。
また、これは余談ですが、LLMの登場によってエンジニアの開発環境は大きく変わりました。この変化によってもうあまりプログラミング自体の難しさにとらわれない状況にもなってきました。このあたりについてもご紹介できればと思います。
Solanaの仕組み
Rustを入門する前に、まずSolanaの仕組みについて簡単に説明します。
Solanaはブロックチェーンの一種です。ブロックチェーンについて簡単に説明すると分散データベース、特にKVS特性を持ったデータベースになります。
Solanaでは保存されるレコードのことをアカウントと呼びます。アドレスをキーに、レコードの情報を持つメタ、データを持つステートの3つの情報を持ちます。
このアドレスにはメタ上で所有するプログラムが設定され、そのプログラムからしか書き込みは行えません。
また、プログラム自体もアカウントになっており、メタが持つexecutableのフラグがtrueならプログラム、falseならデータがステートに保存されます。
Solanaでデータを操作するフローは上記のフローになります。
まず、クライアントがトランザクションをSolanaネットワークに送信します。このトランザクションには複数のインストラクションが含まれ、インストラクションには下記の3つの情報が含まれます。
プログラムアドレス: どのプログラムに操作の指示を出すのか
インストラクションデータ: 指定したプログラムでどのような操作をするのか
アカウントメタ: プログラムでどのアカウントを操作するのか
これらの情報がSolanaネットワークに送信されると、ネットワーク内で対象のプログラムがロードされ、実行されます。
プログラムには変更可能なバイト列を渡され、このバイト列を変更することでアカウントの変更が反映されます。
渡されたバイト列には下記の3つの情報が含まれます。
プログラムアドレス: 現在実行しているプログラムのアドレス
インストラクションデータ: クライアントから渡されたどのような操作をするのかのインストラクション
アカウントメタ+ステート: クライアントから指定されたメタに対応するステートを持ったアカウント
これらの情報はクライアントから渡されたトランザクション内の情報と対応するものが渡されます。
今回は触れませんが、プログラムからプログラムを呼び出し、一貫性のある変更をできます。
Rust入門
ではRust入門としてライブプログラミングを行なっていきます。
まず、Rustプロジェクトとして初期化をします。
? initialize rust project in this dir
とターミナルに入力することで必要なコマンドを提案してもらいます。
cargo init
これでRustプロジェクトの初期化方法がわかりましたね。
これは先日リリースされたCopilot for CLIというコマンドを使っています。私は多用することがわかっているのでエイリアスで「?」に設定していますが、実際には
gh copilot suggest -t shell initialize rust project in this dir
というコマンドを実行してRustプロジェクトの初期化に必要なコマンドを取得しています。
次に生成したCargo.tomlをSolana用の設定を行います。このCargo.tomlというのはRustプロジェクトで依存ファイルやコンパイルの設定を記述するファイルです。Node.jsで言うpackage.json、PHPで言うcomposer.jsonを想像してもらうとわかりやすいと思います。
Solanaのプログラムは少し特殊なので専用の設定と依存が必要になります。しかし、私もそう何度も書くものではないので何が必要かは覚えていません。
こういうときはGitHub Copilot Chatを使って教えてもらいましょう。
依存はあっていそうですが、設定が足りていないようですね。追加で質問をして正しいCargo.tomlを出力します。
まだ少し設定は足りないのですが、コンパイルが成功する最低限の設定にはなったので一旦これで進めます。
(リハーサルでは一発で期待したCargo.tomlがでました)
余談ですがLLMでバージョン情報があるような出力は古いものが出やすいです。IDEの機能を使って最新のバージョンに変更しましょう。
この状態でコンパイルをすると失敗します。これはCargo.tomlに[lib]を追加したためです。
Rustでは一般的に実行可能なバイナリファイルではmain.rsを作り、ダイナミックリンクライブラリではlib.rsを作ります。SolanaではBPF形式のライブラリを作るため、後者のlib.rsに書き換えましょう。
lib.rsができたら、先ほどのSolanaの説明したバイト列を受け取るエントリポイントを作成します。このシグネチャも特に覚えていないのでGitHub Copilot Chatに聞いてしまいます。
GitHub Copilot Chatの提案したコードをそのまま使う必要はありません。例えばRustのコードではuseを使うことでnamespaceを省略できます。また、あとで何かをやるときはtodo!()マクロを使うのが一般的です。
この状態ではまだバイト列を受け取ることができません。entrypoint!()マクロを使い、作成したprocess_instruction関数をエントリポイントとして登録します。
このprocess_instruction関数の名前は慣例的なものであり、Solanaでよく使われる名前です。大事なのはentrypoint!()マクロに渡すさいに同じ名前であることなので、別の名前でも構いません。
このentrypoint!()マクロが何をしているかを知るにはマクロを展開しましょう。JetBrainsのIDEでは再帰マクロ展開の表示を実行することで表示できます。
unsafe extern "C"〜とずらずらと見知らぬワードが出てきて怖いですが、これはRustでダイナミックリンクライブラリを作るときの定型句なので特に気にする必要はありません。大事なのは先ほど登録したprocess_instruction関数が内部で呼び出されるということです。
このあたりの話を掘り下げると入門からだいぶ外れるので、興味がある方はご自身で確認ください。
Solanaのプログラムでは一般的に3つの要素を作ります。
State: アカウントのステートを表す
Instruction: インストラクションを表す
Processor: StateとInstructionから処理を行いStateの情報を書き換える
それぞれを書くためにstate.rs、instruction.rs、processor.rsとファイルを作りましょう。
ではまずStateから作成します。ここは簡単なのでGitHub Copilotを使ってさくさく書いていきます。
Solanaの一般的なステートの書き方として作成したstructに対して、IsInitialized、Sealed、Pack traitを実装します。私自身あまり良い書き方だとは思っていませんが、ステートとしてどういった機能があるかという点ではわかりやすいと思います。
何気なくpubというキーワードを使っていますが、こちらは公開範囲の設定です。pubが付いていない場合は外部から利用できません。
今回は例としてState(pub u64)としていますが、実際にはフィールドを公開するのはよくありません。getter/setterもしくは振る舞いを持たせてStateがありえない状態にならないようにするのが望ましいです。
動的型付け言語の経験が主な場合、こういった&[u8] → u64といった型変換は難しく感じると思います。ただ、Rustは慣例として型変換する関数の命名規則に慣例があるので比較的わかりやすいです。
変換元になる型が変換する関数を持っている場合
to_xxx
into/into_xxx
try_into
変換先の方が変換する関数を持っている場合
new
from/from_xxx
try_from
だいたいこのいずれかの関数を探せば変換できます。
今回のケースではu64がfrom_le_bytes(&[u8; size_of::<Self>( )]を持っているため、そちらを使うのが良さそうです。
実際にu64::from_le_bytesにsrcを渡すと型が一致しないというエラーが出ます。こういったコンパイルエラーはRustの開発中ではよく発生します。
一つずつエラーを調べても良いのですが、ここではJetBrainsのIDEの機能のAI Assistantでエラーの修正方法を教えてもらいましょう。
修正ができたらStateとしては完成です。次にInstructionを作成します。
こちらもStateと同様にGitHub Copilotでさくさく作成してしまいます。
作成できたら、次はProcessorを作成します。
これはGitHub Copilot Chatの優れた機能として、複数のファイルを指定できます。state.rsとinstruction.rsを指定してその型に合わせたProcessorを作成してもらいましょう。
その結果として微妙なコードがでてきました。もう書いた方が早いのでGitHub Copilotを駆使してサクサク書いていきましょう。
(リハーサルでは一発で期待したProcessorがでました。たぶん新規チャットで始めなかった、lib.rsを含めていなかったあたりに問題がありそうです)
Processorでいくつか初見のものがでてきたので解説します。
まず、mutというキーワードは変数の可変性です。このmutがつく変数は変更可能(Mutable)になり、つかない変数は変更不可能(Imutable)になります。
next_account_infoに渡しているところで&mutがついているのは可変参照を渡して、所有権は渡さないけど変更してよいとしています。ここでなぜ&mutが必要なのかというのはnext_account_infoが&mut Tを要求しているからですね。
次にborrow_mutです。これはRustでみんな大好きな所有権を取る関数です。
ここが少し難しいのですがRustでの所有権の有無はコンパイル時にチェックします。そのため、borrow/borrow_mutといった関数を呼び出すことはありません。では何故、このSolanaのプログラムで呼び出しているかと言うと
アカウントを表すAccountInfoという型でステート部を表すdataの型がRc<RefCell<&'a mut &[u8]>>となっているからです。RCは参照カウント方式のスマートポイント、RefCellは動的な所有権のチェックをする型になります。
そのため
let mut data1 = account.data.borrow_mut();
let mut data2 = account.data.borrow_mut();
と書くとdata2のところで所有権で問題があるとエラーが実行時に発生します。
このあたり掘り下げるか悩ましいのですが、簡単に何故こういった型が使われているかと言うとパフォーマンスのために最適化されているためだと思ってもらうと良いでしょう。(詳しく掘りたい型は#[derive(Clone)]に着目すると良いです)
RefCell<T>というかなり珍しい型をSolanaのプログラムでは扱いますが、一般的なRustプログラミングでは滅多に出てきません。おそらくSolanaのプログラムで一番難しく感じるのはここになると思います。
ここまで、書けたらSolanaのプログラムとして動作するため、完成・・・と言いたいところですが、エンジニアとしてはこれで完成とは言えません。ドックコメントとテストを書いていきましょう。
まず、ドックコメントをということでGitHub Copilot ChatのGenerate Docsを使います。
個人的な意見としては何を使ったとしてもあまり良いコメントになることは少ない印象です。稀に良いコメントが作成されますが、大抵はシグネチャやコードをそのまま文章にしたような情報量の増えないコメントになりがちです。
過度な期待はせずにないよりマシと割り切るか、生成したコメントを元に手で修正しましょう。
次にテストコードの生成でCoduim AIのIntegrity-Agent IDE Pluginを使います。
GitHub Copilot ChatやChatGPTなど他でもテストの生成はできますが、Integrity-Agentでは他のツールから群を抜いて良いテストパターンを生成してくれます。個人的な意見では2023/11時点で最も優秀なテスト生成機能だと思っています。
このIntegrity-Agentでは3つの機能があります。
テスト生成: 選択したコードに対してテストを生成
コードの説明: 選択したコードの説明を生成
コードの提案: 選択したコードの問題点の修正、リファクタリングの提案
これらを利用してコードの品質を高めます。
(リハーサルでは一発で動作するテストが出力できたけど本番ではダメでした)
まとめ
最後にまとめです。
Solanaのプログラム開発で、一般的にRustで難しいと言われる機能は所有権になると思います。特にSolanaのプログラムには動的な所有権が出てきますし。
本当は所有権のエラーの修正を見せれればよかったのですが、GitHub Copilotが事前に回避できるコードばかりを提案してくれたので見せる機会はありませんでした。
ただ、この所有権は難しいと言われますが、イミュータブルプログラミングを意識していればあまり出会う機会がないと思います。また、仮に所有権のエラーが出ても対応方法として3つしかないので簡単に回避できます。
clone()を呼び出す
&をつけて参照にする
渡し先の型を変更する
このあたりがパッと出てこなくてもJetBrainsのAI Assistantを呼び出せば済むので安心してください。
と、考えていくとはじめに話したギャップはどこにあるかというと、Rustで何を書くのかというところに尽きるのかと思います。
特にSolanaのプログラムでは低レイヤーな処理や、独特な部分があり、そこで難しさが出てしまっています。
ただ、この辺りはライブラリやフレームワークで回避可能な範囲でもあります。例えばSolanaプログラム用のフレームワークのAnchorを使う、バイト列の変換周りであればborsh、serdeなどを使うと隠蔽されて扱いやすくなります。
以上が「Solana Developer Hub Online #0: Rust入門 in Solana」で話した内容になります。