Simply Patrick

Learning Rust

關於 Rust

會知道 Rust 是因為之前公司團隊的工作是跟瀏覽器引擎 (Webkit/Blink) 的優化有關,那時只知道 Mozilla 正在用 Rust 這個新的程式語言實驗性地開發 Servo 引擎。同時間 Go 已經到達 1.0 的里程碑,同時有著優異的編譯及執行效能,很快地 Go 就變成我個人主要使用的程式語言,工作上需要的一些小工具也都是用 Go 來實現。

開始對 Rust 重新關注大概是它在 2015 年釋放 1.0 版本後,那時只稍微了解一下語法及主要的語言特性,當時覺得這程式語言的學習曲線蠻陡峭的,必須全面地對所有的語言特性有一定了解後才能駕馭它。那時 Go 用的正是順手,所以就沒什麼動力把 Rust 完整學起來。

去年開始工作上需要了解 Fuchsia OS 的實作,赫然發現裡面蠻多系統服務都是用 Rust 開發的,數量上甚至比 Google 自家的 Go 還多,這也是我決定多投入時間學習 Rust 的轉捩點。

學習資源

我主要使用下列幾本書來學習 Rust:

  • 深入浅出 Rust 這本書的電子版: 它的特色是側重於解釋 Rust 主要概念的設計思想,而不只是單純語法或用法的說明;且因為是中文的緣故,讀起來速度還是比較快,比較像是 Rust 的內功心法入門。
  • 30 天深入淺出 Rust 系列 則是以淺顯易懂的範例把 Rust 主要的特色講解了一遍,內容適合對於想要快速地對 Rust 有個全面性的概觀的人。
  • 對 Rust 有了基本的了解後,A Gentle Introduction to Rust 則是適合有 C/C++ 經驗的開發者來學習如何寫 idiomatic Rust。

至於最經典的 The Rust Programming Language 呢? 我只把了一些我覺得重要的章節看完,其他剩下的就直接動手實作,遇到有細節不清楚或是已經忘記時再花時間來看。

其他資源

上面主要是比較完整的 Rust 學習資源,下一篇再分享一些我覺得不錯的 blog post,通常是針對某個特定主題或進階功能的解釋或教學。


C++ 套件管理: 使用 Conan

手動管理 C/C++ 套件

想像你需要使用 LLVM 開發一個程式,如果在 macOS 上,最簡單的安裝方法是用 Homebrew,一行就搞定: brew install llvm。但 Homebrew 上的版本不一定是最新的而且也無法同時安裝不同的版本 (例如 LLVM 5.0/6.0 共存) 或同版本但不同設置(例如 LLVM 6.0 的 debug/release 版本);而在 Linux 及 Windows 上也有各自不同的安裝問題。

安裝完畢後,麻煩才剛開始:通常第一步是設置編譯環境的 CPPFLAGS 及 LDFLAGS:

For compilers to find this software you may need to set:
    LDFLAGS:  -L/usr/local/opt/llvm/lib
    CPPFLAGS: -I/usr/local/opt/llvm/include

根據不同的開發環境或編譯器,設置的方法也都不同。最後設置完後編譯及連結也不一定能成功,因為 LLVM 本身可能又依賴其他套件,還需要把它的 dependencies 一一安裝及設置。

Conan 介紹

Conan 企圖幫助 C/C++ 脫離這個窘境:沒有一個像樣的套件管理 (package management) 工具。

我建議先讀一下 Conan 的文檔來了解如何安裝 Conan 以及利用 Conan 來使用現成的程式庫

如果你是使用 CMake,事情會簡單一些,因為 Conan 提供的 CMake 整合還算好用,使用範例可以參考這個基於 LLVM 的小程式: clike

下載套件

除了預設的 conan-center,Conan 官網建議也可以到 bincraftersconan-community 裡尋找,可以用下列指令新增這兩個 remote:

$ conan remote add conan-community https://api.bintray.com/conan/conan-community/conan
$ conan remote add bincrafters https://api.bintray.com/conan/bincrafters/public-conan

之後就可以指定 remote 搜尋 package:

$ conan search ffmpeg -r=bincrafters
Existing package recipes:

ffmpeg/[email protected]/stable

目前 server 上面的套件數量不算多,有可能你會找不到你想要的,因此你不能期待很快地有人會幫你做出來,相反地你必須學會自己創建套件才能充分得到使用 Conan 的好處。

創建 package

自己創建一個套件其實也不難,基本的概念如下:

整個建置過程需要的目錄結構及流程會是由 Conan 來控制,但你需要提供一個 conanfile.py 描述每個步驟裡的需要執行的動作。Conan 會把打包的套件放在一個 local cache 裡讓其他的程式來使用。

以下示範如何下載 LLVM 6.0 的原始碼, 編譯,最後把它包裝成套件:

from conans import ConanFile, CMake
import glob
import os

class LlvmConan(ConanFile):
    name = "LLVM"
    version = "release_60"
    license = "LLVM Release License"
    url = "https://github.com/p47r1ck7541/llvm-60"
    description = "%s %s" % (name, version)
    settings = "os", "compiler", "build_type", "arch"
    generators = "cmake"

    def source(self):
        self.run("git clone https://github.com/llvm-mirror/llvm -b %s --depth 1 src" % self.version)

    def build(self):
        cmake = CMake(self)
        cmake.configure(source_folder="src")
        cmake.install()

    def package(self):
        # nothing to do here now because we reuse 'cmake install' to package files
        pass

    def package_info(self):
        self.cpp\_info.libs = \[os.path.basename(a) for a in glob.glob(os.path.join(self.package\_folder, "lib", "*.a"))\]
        self.cpp_info.cppflags = \["-std=c++11", "-fno-rtti"\]
        self.cpp_info.exelinkflags = \["-lcurses", "-lz"\]

幾個關鍵步驟:

  1. source(): 利用 git clone 下載原始碼
  2. build(): 負責編譯,這裏我們直接使用 Conan 提供的 CMake class 來控制 cmake 的執行,值得注意的是我們直接利用 cmake.install() 來取代實際打包套件的工作。
  3. package(): 如上解釋,不需做任何事。
  4. package_info(): 提供套件的相關使用資訊。例如 cppflagsexelinkflags 是寫死的,但 libs 則是運行 Python script 搜尋編譯結果找出來的。

完整範例可以參考這個 GitHub project: conan-llvm-60

實際運作過程如下(中間請自行快轉):

總結

就簡化自己手動管理 C/C++ 套件的工作來看,我認為 Conan 提供的好處值得你花時間去學它,至於它是否能成為主流的 C/C++ 套件管理工具,那就等待時間來驗證了。


C++ 單元測試: 使用 Catch2

Catch2 介紹

介紹一下最近有在使用的一個 C++ test framework, Catch2,它對自己的描述是:

A modern, C++-native, header-only, test framework for unit-tests, TDD and BDD - using C++11, C++14, C++17 and later (or C++03 on the Catch1.x branch)

我主要的用途是做一些 C++ 小程式的簡單驗證,因此簡單易用是我首要的選擇條件,而 Catch2 應該是目前找的到最好的選擇,例如這是個 Huffman coding 的測試代碼:

TEST_CASE("HuffmanEncoding") {
    vector<Symbol> symbols{
            {'a', 8.17}, {'b', 1.49}, {'c', 2.78}, {'d', 4.25},
            {'e', 12.7}, {'f', 2.23}, {'g', 2.02}, {'h', 6.09},
            {'i', 6.97}, {'j', 0.15}, {'k', 0.77}, {'l', 4.03},
            {'m', 2.41}, {'n', 6.75}, {'o', 7.51}, {'p', 1.93},
            {'q', 0.10}, {'r', 5.99}, {'s', 6.33}, {'t', 9.06},
            {'u', 2.76}, {'v', 0.98}, {'w', 2.36}, {'x', 0.15},
            {'y', 1.97}, {'z', 0.07},
    };

    HuffmanEncoding(symbols);

    REQUIRE(symbols['a' - 'a'].code == "1110");
    REQUIRE(symbols['e' - 'a'].code == "100");
    REQUIRE(symbols['t' - 'a'].code == "000");
    REQUIRE(symbols['z' - 'a'].code == "001001000");
}

可以看到代碼非常地簡潔而易懂,你只要學怎麼用 TEST_CASEREQUIRE 就好了。

如何使用 Catch

中文的介紹可以參考 “C++ 的单元测试工具 —— Catch | 时习之”,只是關於如何 #include "catch.hpp" 的部分我覺得描述的不是很精確,建議可以讀一下 “Why do my tests take so long to compile?”

真的很介意 compile time 受到影響的人,可以考慮關掉一些沒用到的功能來加速編譯: Catch2/configuration

Catch2 的原理

神奇的 REQUIRE

有用過其他的 C++ test framework 例如 googletest 的人,應該會對 Catch 的 REQUIRE 感到好奇:

為什麼 googletest 需要使用較不自然的 ASSERT_EQ(a, b), ASSERT_NE(a, b) 而 Catch 只需要寫 REQUIRE(a == b)REQUIRE(a != b)

拆解表達式 (Decomposing an expression)

考慮下列這個有問題的 Factorial 實作及它的測試代碼:

int Factorial( int number ) {
   return number <= 1 ? number : Factorial( number - 1 ) * number;  // fail
}

TEST_CASE( "Factorial of 0 is 1 (fail)", "[single-file]" ) {
    REQUIRE( Factorial(0) == 1 );
}

REQUIRE(Factorial(0) == 1); 會輸出下列的結果:

010-TestCase.cpp:14: failed: Factorial(0) == 1 for: 0 == 1

這表示 REQUIRE 需要從 Factorial(1) == 1 拆解出下列五個資訊:

  1. 整個表達式 “Factorial(0) == 1” 的字串值
  2. 表達式左邊 Factorial(0) 的結果,也就是 0
  3. 使用的 operator, 也就是 “==”
  4. 表達式右邊 1 的值,也就是常量 1
  5. 表達式的結果,在這個例子是 false

也就是說 Catch2 必須在 compile time 拆解這個表達式,這裡需要用到一些 template metaprogramming 的技巧。

REQUIRE

REQUIRE 是用 macro 來定義的,所以整個表達式的字串值可以簡單用 stringification 拿到。

#define REQUIRE( ... ) INTERNAL_CATCH_TEST( "REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__  )
#define INTERNAL_CATCH_TEST( macroName, resultDisposition, ... ) \
    do { \
        Catch::AssertionHandler catchAssertionHandler( macroName, CATCH_INTERNAL_LINEINFO, CATCH_INTERNAL_STRINGIFY(__VA_ARGS__), resultDisposition ); \
        INTERNAL_CATCH_TRY { \
            CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS \
            catchAssertionHandler.handleExpr( Catch::Decomposer() <= __VA_ARGS__ ); \
            CATCH_INTERNAL_UNSUPPRESS_PARENTHESES_WARNINGS \
        } INTERNAL_CATCH_CATCH( catchAssertionHandler ) \
        INTERNAL_CATCH_REACT( catchAssertionHandler ) \
    } while( (void)0, false && static_cast<bool>( !!(__VA_ARGS__) ) ) // the expression here is never evaluated at runtime but it forces the compiler to give it a look
    // The double negation silences MSVC's C4800 warning, the static_cast forces short-circuit evaluation if the type has overloaded &&.

Catch::Decomposer

而 2, 3, 4 的部分就要靠 Catch::Decomposer,這裡用的 template metaprogramming 技巧叫作 expression template

struct Decomposer {
    template<typename T>
    auto operator <= ( T const& lhs ) -> ExprLhs<T const&> {
        return ExprLhs<T const&>{ lhs };
    }

    auto operator <= ( bool value ) -> ExprLhs<bool> {
        return ExprLhs<bool>{ value };
    }
};

Decomposer<= 跟表達式的左邊結合變成 ExprLhs<T const &>ExprLhs 是個 C++ template:

ExprLhs

template<typename LhsT>
class ExprLhs {
    LhsT m_lhs;
public:
    explicit ExprLhs( LhsT lhs ) : m_lhs( lhs ) {}

    template<typename RhsT>
    auto operator == ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
        return { compareEqual( m_lhs, rhs ), m_lhs, "==", rhs };
    }
    auto operator == ( bool rhs ) -> BinaryExpr<LhsT, bool> const {
        return { m_lhs == rhs, m_lhs, "==", rhs };
    }

    template<typename RhsT>
    auto operator != ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
        return { compareNotEqual( m_lhs, rhs ), m_lhs, "!=", rhs };
    }
    auto operator != ( bool rhs ) -> BinaryExpr<LhsT, bool> const {
        return { m_lhs != rhs, m_lhs, "!=", rhs };
    }

    template<typename RhsT>
    auto operator > ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
        return { m_lhs > rhs, m_lhs, ">", rhs };
    }
    template<typename RhsT>
    auto operator < ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
        return { m_lhs < rhs, m_lhs, "<", rhs };
    }
    template<typename RhsT>
    auto operator >= ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
        return { m_lhs >= rhs, m_lhs, ">=", rhs };
    }
    template<typename RhsT>
    auto operator <= ( RhsT const& rhs ) -> BinaryExpr<LhsT, RhsT const&> const {
        return { m_lhs <= rhs, m_lhs, "<=", rhs };
    }

    auto makeUnaryExpr() const -> UnaryExpr<LhsT> {
        return UnaryExpr<LhsT>{ m_lhs };
    }
};

ExprLhr 利用 operator overloading 來知道是用那個 operator 跟表達式的右邊做運算,最後把整個表達式轉換成一個 BinaryExpr

BinaryExpr

template<typename LhsT, typename RhsT>
class BinaryExpr  : public ITransientExpression {
    LhsT m_lhs;
    StringRef m_op;
    RhsT m_rhs;

public:
    BinaryExpr( bool comparisonResult, LhsT lhs, StringRef op, RhsT rhs )
    :   ITransientExpression{ true, comparisonResult },
        m_lhs( lhs ),
        m_op( op ),
        m_rhs( rhs )
    {}
};

之前我們提到的 2, 3, 4, 5 的資訊就存在 BinaryExprm_lhs, m_op, m_rhs, 及 ITransientExpression 裡面。

搭配 CLion 使用

另外我還蠻推薦使用 JetBrains 的 C++ IDE: CLion,它目前有內建 Catch2 的支援,用它執行測試非常地簡單方便:


Kotlin Coroutines and Threads