ククログ

株式会社クリアコード > ククログ > ノータブルコード7 - Rustのif式を賢く使う

ノータブルコード7 - Rustのif式を賢く使う

組み込みGeckoプロジェクトでRustに本格的に触れ始めた畑ケです。 今回は、組み込みGeckoプロジェクトでフィードバックしたgit2-rsのコミットから「これは!」、と思ったコードを見つけたので紹介します。

まず、git2-rsというRustのcrate1 は、libgit2というライブラリのRustバインディングです。git2-rsはRustのパッケージマネージャーのcargoの依存crateの一つで、cargoはRustのパッケージ情報を取得するときにgitの操作が必要になる場面があります。git2-rsというcrateはcargoに必要なgitの操作を担当します。

背景

git2-rsのCライブラリのバインディングを担当しているlibgit2-sysというcrateの中に、libgit2のエラーメッセージを取得する操作が書かれていなかったため、 cargoの中でgitの操作が失敗したときにはエラーコードしか報告されないという問題がありました。

そこで、次節のパッチを提出しました。

ノータブルコードとなる前のパッチ

diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs
index eba2077..9d1afba 100644
--- a/libgit2-sys/lib.rs
+++ b/libgit2-sys/lib.rs
@@ -7,6 +7,8 @@ extern crate libz_sys as libz;
 use libc::{c_char, c_int, c_uchar, c_uint, c_void, size_t};
 #[cfg(feature = "ssh")]
 use libssh2_sys as libssh2;
+use std::ffi::CStr;
+use std::ptr;
 
 pub const GIT_OID_RAWSZ: usize = 20;
 pub const GIT_OID_HEXSZ: usize = GIT_OID_RAWSZ * 2;
@@ -3551,7 +3553,25 @@ pub fn init() {
         openssl_init();
         ssh_init();
         let r = git_libgit2_init();
-        assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
+        if r < 0 {
+            let git_error = git_error_last();
+            let mut error_msg: *mut c_char = ptr::null_mut();
+            if !git_error.is_null() {
+                error_msg = (*git_error).message;
+            }
+            if !error_msg.is_null() {
+                assert!(
+                    r >= 0,
+                    "couldn't initialize the libgit2 library: {}, error: {}",
+                    r,
+                    CStr::from_ptr(error_msg).to_string_lossy()
+                );
+            } else {
+                assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
+            }
+        } else {
+            assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
+        }
 
         // Note that we intentionally never schedule `git_libgit2_shutdown` to
         // get called. There's not really a great time to call that and #276 has

特に以下の部分がこのパッチの要点です。

            let git_error = git_error_last();
            let mut error_msg: *mut c_char = ptr::null_mut();
            if !git_error.is_null() {
                error_msg = (*git_error).message;
            }

このパッチでは、一度error_msg変数を可変なものとして宣言し、条件分岐の中で値を変更するという書き方になっています。これはC言語のライブラリなどでよく見かける書き方です。

リファイン後のパッチ

前述のパッチは無事取り込まれましたが、当該箇所を改めてプロジェクトオーナー側でリファインされました。ミュータブルな変数を使うよりも変数束縛を使う方がRustらしいコードです。 その考えに則って修正されたパッチが以下の通りです。

diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs
index 9d1afba840..a5998af84c 100644
--- a/libgit2-sys/lib.rs
+++ b/libgit2-sys/lib.rs
@@ -8,7 +8,6 @@ use libc::{c_char, c_int, c_uchar, c_uint, c_void, size_t};
 #[cfg(feature = "ssh")]
 use libssh2_sys as libssh2;
 use std::ffi::CStr;
-use std::ptr;
 
 pub const GIT_OID_RAWSZ: usize = 20;
 pub const GIT_OID_HEXSZ: usize = GIT_OID_RAWSZ * 2;
@@ -3552,30 +3551,25 @@ pub fn init() {
     INIT.call_once(|| unsafe {
         openssl_init();
         ssh_init();
-        let r = git_libgit2_init();
-        if r < 0 {
-            let git_error = git_error_last();
-            let mut error_msg: *mut c_char = ptr::null_mut();
-            if !git_error.is_null() {
-                error_msg = (*git_error).message;
-            }
-            if !error_msg.is_null() {
-                assert!(
-                    r >= 0,
-                    "couldn't initialize the libgit2 library: {}, error: {}",
-                    r,
-                    CStr::from_ptr(error_msg).to_string_lossy()
-                );
-            } else {
-                assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
-            }
-        } else {
-            assert!(r >= 0, "couldn't initialize the libgit2 library: {}", r);
+        let rc = git_libgit2_init();
+        if rc >= 0 {
+            // Note that we intentionally never schedule `git_libgit2_shutdown`
+            // to get called. There's not really a great time to call that and
+            // #276 has some more info about how automatically doing it can
+            // cause problems.
+            return;
         }
 
-        // Note that we intentionally never schedule `git_libgit2_shutdown` to
-        // get called. There's not really a great time to call that and #276 has
-        // some more info about how automatically doing it can cause problems.
+        let git_error = git_error_last();
+        let error = if !git_error.is_null() {
+            CStr::from_ptr((*git_error).message).to_string_lossy()
+        } else {
+            "unknown error".into()
+        };
+        panic!(
+            "couldn't initialize the libgit2 library: {}, error: {}",
+            rc, error
+        );
     });
 }
 

このパッチのコードの要点を以下に抜粋します。 ミュータブルな変数のerror_msgを削除し、代わりに以下のRustのコードでは、if式の性質を使いエラーメッセージの値へerrorというラベルを付けるコードになっています。

        let git_error = git_error_last();
        let error = if !git_error.is_null() {
            CStr::from_ptr((*git_error).message).to_string_lossy()
        } else {
            "unknown error".into()
        };

このように、Rustではifは文ではなく式なので、値を返すことができます。そのため、ifで分岐した時の結果の値に上記のようにラベルづけをすることができます。 変数束縛を意識的に使い、値にラベル付けをしていくのがRustらしいコードとなるため紹介しました。

  1. Rustでは、ライブラリの事をcrateと呼びます。