LayerX エンジニアブログ

LayerX の エンジニアブログです。

RustなしでLayerX Labsの開発は語れない

はじめに

こんにちは。LayerX Labs(以下、Labs)エンジニアのきむ(@jkcomment)です。秘匿化モジュール「Anonify」の開発や大手金融機関や行政等との実証実験に携わっています。

Labsの開発はRustからはじめ、Rustで終わる

tech.layerx.co.jp

先日、恩田(さいぺ)の方からAnonifyとRustについての話をしましたが、Rustは速度・安全性・効率的な並行性を特徴とし、C,C++と同等な性能を発揮しつつ、システムプログラミングに適した言語です。Anonifyはハードウェアレベルの機密性を実現するために Trusted Execution Environment (TEE) の一種であるIntel SGXを活用しています。Intel SGXはIntelのCPUが提供しているメモリ上に「Enclave」と呼ばれるハードウェア的に厳重に保護された領域を生成することで、センシティブデータを保護しつつプログラムを実行する為のCPUの拡張機能です。そのため、システム制御が可能な言語を使う必要があります。また、機密性を担保する技術として高い安全性が求められます。そういう意味でRustはAnonifyにぴったりな言語とも言えるでしょう。また、LabsではAnonifyだけでなく、他の開発にもRustを使っています。大きく3つに分けてみると

  • Anonify
  • 実証実験
  • 関連ツール

となります。Rustをチーム共通言語として使うことで安全で質の高い開発ができるとともにメンバー間異なるプロジェクトを担当していても互いにレビューできたり、鉄火場(繁忙期を意味する、LayerXも社内用語)の場合、すぐにサポートに入れるなど開発業務全般に対する柔軟性が高まる効果もあります。

Anonify

我々の中心プロジェクトであるAnonifyです。詳しい話は割愛しますが、スマートコントラクト以外の部分はほぼRustで開発しています。Anonifyは、単独で動作も可能ですが、モジュールとしても使用可能なところが特徴です。

実証実験

Anonifyを用いた実証実験もRustを利用します。上記で話したようにAnonifyは独自で動かすがことが可能ですが、モジュールとしても利用できるため、実証実験の時はAnonifyをモジュールとしてimportし、Anonify以外の部分、例えばWebサーバ的な部分やユースケースによってはDBを使うことがあったりもするので、そこらへんはRustで実装していく感じです。よく使うライブラリとしてはactix-webやtokio、diesel、rust-sqlxなどがあります。

関連ツール

Anonify本体の開発や実証実験に役立つツール開発も全部Rustで行います。例えば性能試験を行う際、負荷をかけるコマンドツールや集計結果を出力するツールなどを開発したりします。cliツールを作ることが多いため、clapなどのcliライブラリをよく使っています。

毎日使うRust、少し注意を払うことでコードが良くなる&リソースを効率よく使える

上記でお話ししましたようにLayerX LabsではRustを軸にした開発業務を行なっています。Rust SGX SDKやrust-web3など特定の機能に特化した若干癖のあるライブラリを使うことが多いのですが、Rustは基本的にコンパイラがしっかりコードをチェックし、最適化してくれるため、コンパイルさえ通れば大体狙い通りプログラムが動きます。ですが、他の言語を書く時と同じようにコードの読みやすさを維持することまたはリソースをどう使うかはコードを作成するプログラマ次第で、少し注意を払うことでコードが読みやすくなったり、作成したプログラムがリソースを無駄なく効率よく使うことができます(経験上の話になりますが、プログラムの中で無駄なリソースを使うのはコアな処理より大体小さな不注意で書いてしまったコードの方が多かったりしました)。ここではRust SGX SDKやrust-web3などのライブラリの話よりRustそのものにフォーカスしてお話ししたいと思います。

不要なcloneは避ける

ぼーっとしてコードを書く際よくやりがちなのがclone()の使いすぎ問題です(特に私は文字列でよくやりがちでした)。オブジェクトのclone()を使う際は本当に必要かどうかをちゃんと判断する必要がありです。使うことは簡単ですが、変数に対して.clone()を呼び出すと、そのデータのコピーが作成され、コピーの作成にはリソースが必要となります。したがって、clone()はほとんどの場合、パフォーマンスに悪影響を及ぼすので、避けるべきです。多くの場合、clone()を使う必要はなく、同じ変数の参照を異なる関数に渡すことができます。

// clone()を使う場合
fn main() {
    let hoge = Hoge::new();
    foo(hoge.clone());
    foo(hoge);
}
 
// 参照を渡す場合
fn main() {
    let hoge = Hoge::new();
    foo(&hoge);
    foo(&hoge);
}
環境変数の有無によって処理を変えたい

開発を行う際に環境変数を使うことは多いでしょう。例えば、PASSWORDという環境変数が設定されている時は処理Aを、そうでない時は処理Bを実行したい。そういった時には環境変数の値をOptionとして受け取り、処理の呼び出し時に引数として渡し、処理の中で分岐処理を行うようにします。そうすることで環境変数が必要な時だけ用意しておくと済むでしょう。

PASSWORD=hogehoge
fn main() {
    // PASSWORD環境変数が存在なくてもエラーにならない
    let password: Option<String> = env::var("PASSWORD").ok();
    // Option::as_derefはOptionの中の型がDerefトレイトを実装しているときにDeref::Targetの参照へ変換できる
    hoge(password.as_deref());
}
 
fn hoge(password: Option<&str>) {
    // passwordが存在する場合
    if let Some(pw) = password {
        foo();
    // passwordが存在しない場合
    } else {
        bar();
    }
}
_を利用して大きい数値を読みやすくする

実際Anonifyでは、gasの指定など桁数の多い数値を使ったりしているため、数字の間に_を使って理解しやすくしています。

const GAS: u64 = 5_000_000;
unwrapの誘惑に負けるな!

すでにご存知かと思いますが、Rustでコードを書く際無意識でunwrapを使う方いらっしゃいませんか?笑(ここにいます) 結論から言いますとunwrapより?を使いましょう。?はパニックになる代わりにエラーを返すunwrapと想像できます。? は直接エラーを返すのではなく、実際には Err(From::from(err)) を返します(例はFromが実装されている場合です)。つまり、エラーが変換可能であれば、自動的に適切な型に変換されます。

単純にvecを使うのも良いけど、Bytesもありかも

actix-webなどのフレームワークを使わずTCPサーバを作る時、tokioを使うことが多いのではないでしょうか。tokioは軽量で非同期プログラミングに適しており、特にTCPサーバなどで高い性能を発揮します。私たちも要件によってはtokioでTCPサーバを実装したりします。TCPサーバの処理の流れをざっくり話しますと、クライアントから送られてきたデータをbufferに入れて該当する処理を呼び出すという流れになります。データを渡す際、シンプルにバイトデータ(&[u8]やVecなど)をあちらこちらに渡しながらメモリプールを使用することも良いのですが、極端な話、受信したデータが大きい場合(1GBとか)、それをVec<[u8]>にすると大量のメモリを占領してしまうハメになります(本当に極端な例です)。なのでそのかわりにBytesライブラリを活用するのも良い方法の一つでしょう。Bytesは、複数のBytesオブジェクトが同じ基本メモリを指すようにすることで、ゼロコピーのプログラミングを促進します。これは、参照カウント(Reference Counter)を使用し、メモリが不要になり、解放できるようになったときに追跡することで管理されます。

use bytes::{BytesMut, BufMut};
 
let mut buf = BytesMut::with_capacity(1024);
buf.put(&b"hello world"[..]);
buf.put_u16(1234);
 
let a = buf.split();
assert_eq!(a, b"hello world\x04\xD2"[..]);
 
buf.put(&b"goodbye world"[..]);
 
let b = buf.split();
assert_eq!(b, b"goodbye world"[..]);
 
assert_eq!(buf.capacity(), 998);

よく使われるものとしてBytesMutとBytesがあります。BytesMutはbufferを書き込むためのmemory buffer実装です。一般的にbytesMut::with_capacity(1024)のような感じで初期化しますが、メモリ不足時は自動的にメモリを確保します。BytesMutの特徴としては、Deep CopyではなくShallown CopyベースのポインタとReference Counterで実装されているためメモリを効率よく使います。Bytesは読み取り専用のmemory buffer実装です。 実際tokioでTCPサーバを実装する際、channelを利用してやりとりをするケースがよくあります。その場合、1:Nで同じデータを送信することが多いですが、Bytesを使うと、例えば1万個のchannelにデータを送ってもBytesを維持するための小さなスペースを除いては、実際にコピーされるデータはありません(データは全部同じメモリを参照している)

列挙型のサイズは、バリアントのサイズによって決まる

他の言語でも列挙型はありますが、Rustの列挙型は、柔軟性が高く、構造体のような振る舞いもできるためいろんな場面で使われることが多いです。そんな便利な列挙型ですが、効率よく使うためには注意が必要です。列挙型のサイズは、列挙型が持つバリアント(構成子)のデータ型によってサイズが決まるからです。したがって、最適ではないメモリレイアウトにならないように、同じようなサイズのバリアントを列挙することが良いでしょう。また、必要に応じて、大きなバリアントをBox化すると列挙型のサイズを小さくすることができます。

enum Hoge {
    Foo(u8),
    Bar([u8; 100]),
}
 
enum HogeBox {
    Foo(u8),
    Bar(Box<[u8;100]>)
}
 
fn main() {
    let x = Hoge::Foo(0);
    let y = HogeBox::Foo(0);
    println!("Hoge size {:?} bytes",  std::mem::size_of_val(&x));
    println!("HogeBox size {:?} bytes",  std::mem::size_of_val(&y));
 }

結果

Hoge size: 101 bytes
HogeBox size: 16 bytes

RustでSGXプログラミングするならここが最高!

RustでSGXプログラミングしてみたい!でもどうすればいいかわからないまたはどんなライブラリ使えばいいかわからない!そういう方のためのリンク集です。よかったらぜひ参考にしてみてください。

RustによるIntel SGXプログラミングとSDKの内部実装
https://qiita.com/Osuke/items/5e30132a7789955105d6

Rust SGX SDK
https://github.com/apache/incubator-teaclave-sgx-sdk

SGXで使用可能なライブラリ
https://github.com/mesalock-linux

すべての経済活動を、一緒にデジタル化しませんか?

最後に、LayerXでは引き続き、ミッションである「すべての経済活動を、デジタル化する」を一緒に実現する仲間を絶賛募集中です。まずはお話だけでも構いませんので、お気軽にご連絡ください。

open.talentio.com