PythonとRustを使ってPythonの拡張モジュールを書く
Hideo Hattori(以下、Hattori):ありがとうございます。このようなお話させていただく機会をいただきまして、ありがとうございます。今日は「RustとPython」ということでお話をさせていただきます、Hattori Hideoと申します。よろしくお願いします。
自己紹介を改めてさせていただきますと、Twitter IDはhhattoというのでやっていまして、Hattori Hideoといいます。
今年スポンサーをさせていただいていますテックビューロで、ソフトウェアエンジニアをしています。Pythonを主に使って、仮想通貨の取引所を開発しているようなかたちになっています。
私自身はPythonは1.5ぐらい、大学の頃から使っていまして、いまも非常に進化し続けてて、type hintingとか、async/awaitとか、新しい機能が入りつつ、好きですごく使っています。
いろいろPythonに関してもOSSのプロジェクトもやっていまして、有名なものではautopep8とか、フォーマッタを開発したり、最近であればpyramidのflamegraphを出力するようなパッケージを作ったりしています。
僕は1年ぐらい前からRustを使い始めてまして、非常におもしろい言語だということで、今日はPythonとRustを使って、Pythonの拡張モジュールを書く方法を紹介させていただけたらと思っております。Rustをご存知というか、使ったことがある方、どれぐらいおられますか?
(会場挙手)
お、結構おられますね。ありがとうございます。僕自身もまだ勉強したてな部分があるので、間違いがもしかしたらあるかもしれないんですが、また後ほど資料を出したときにご指摘いただければ幸いです。
本日のアジェンダとしては、Rustの簡単な紹介をさせていただいて、なぜRustとPythonを使うのかということで、Pythonの拡張モジュールの書き方た1どういったモチベーションで使っているのかを紹介させていただき、実際の使っている事例を出して、良いところ・悪いところを紹介させていただこうかなと思っております。
Rustの特徴
まずRustとはなんぞやというところなんですが、こういうロゴでHello Worldはこんなかたちです。
fnファンクションメインで、letなになにで変数に代入したりして、printlnで標準出力に出力したりできます。
Rustなんですが、主にMozillaがサポートしていて、FirefoxやWebブラウザで組み込んでいくように開発された言語で、今最新のバージョンは1.29になっています。半年スパンぐらいでバージョンが上がっていってるような状況で、現在開発がどんどん進んでいっている状況です。
特徴はいろいろあるんですが、大きく2つ挙げさせていただきます。メモリ安全であるということと、高速であるというところが非常に特徴かなと思っております。
Rustのインストールはこういうコマンドで簡単にできるようになっています。rustupというツールを使えば簡単にインストールできるような感じになっています。
rustupとは何かと言うとRustのインストーラーになっていて、Pythonで言うとvenvとかpyenvのようなものです。
Rust自体に大きく3つのチャンネルがありまして、stableチャンネルと言う安定版のチャンネルと、ここには書いていないんですが、ベータチャンネルと、実験的な機能をどんどん入れていくnightlyというチャンネルがあって、僕自身よく使うのはstableとnightlyなので、ここに書かせていただきました。
そういうチャンネルという概念があるので、インストールするときはrustup。チャンネルとかをやると、その環境に応じたRustがインストールされるという感じになってます。
もう1つRustの機能と言うか、package managerなんですけど、cargoとcrates.ioというものがありまして、package managerとpackageのrepositoryになっています。
これはsetup.pyやpipとか、あとはPyPl.orgとか、そういったものに近いものになっています。cargoで依存packageを管理したりとか、その外部packageを管理しているのがcrates.ioになっていたりします。
実際にRustのプロジェクトを始めるときは、cargo newとか、プロジェクト名とかをやると、すぐにRustのプロジェクトが始められるようになっています。
「cargo new 〇〇 プロジェクト名」とやると、その「〇〇」というディレクトリができて、その下にCargo.tomlというリクワイアメンツテキストとか、そういったものに近いパッケージ管理、依存関係を記述するようなものができて、あとはソースの下にメインで、ここに実際に処理を書いていったりするようなかたちになっています。
さっきのHello worldなどをmain.rsに書くと、cargo buildして、cargo runすると、実際にバイナリーが動くというような感じになっています。
Ownershipについて
一応、言語機能の特徴として先ほど言ったメモリの安全性を話したんですが、結構使われている方がいらっしゃるみたいなんであれなんですが、Ownership。所有権とBorrowingと言って借用という概念があります。
ソースコードで少し示してみると、変数aがあって、その2行目にb=aでbにaを代入しているんですが、その時点でaの所有権がbに移るので、これを動かそうとすると、コンパイラに怒られるという状況になります。
これを解決するには、この場合だったらaを参照するというかたちにして、参照にするとその所有権は何も変わらないので、実際にあとでaとかbを使えるというようなかたちになったりします。
もう1つ例で言うと、並行プログラミングなどでthreadを作るという例になるんですけども、この場合だと4で4個threadを作ってiを渡します。
それをprintするという例になります。これをbuildしようとすると、ちょっとわかりにくいかもしれないんですけど、外から渡しているiがthread内でも使えるし、そのthreadのあとでも使えるので、コンパイラに怒られちゃうんです。
なので、この場合はthreadを作るspawnのときにmoveを書いてあげて、iの所有権とかを全部threadの中に限定してあげてやると、そのthreadの中と、そのthread外のiは別のものになるので、コンパイラに怒られずに処理ができるかたちになります。
Rustが高速で動作する理由
もう1つの言語の特徴として、すごく高速というのがあげられています。この要因としては、もともとのRust自体のruntimeが小さいので、いらないことをしなくてCとかC+とかに、すごい近いかたちで実行できます。
あとはzero-cost abstraction。ゼロコスト抽象化と言われているんですけど、C++とかと同じような考え方で、抽象化のコードを書いても、コストがかからない。実行するときに、抽象化したからと言ってその分のコストが、実行コストかからないという考え方も取り入れてます。
あとは、先ほど出てきたthreadとか、外部のパッケージになるんですけど、rayonとかcrossbeamというパッケージがあって、並行処理を簡単に書けるようなパッケージがあったりします。
こちらにベンチマークのリンクがあるので、またC++などとどれぐらい差があるのかというと、結構近い値が出てたりとか、C++よりも早い場合がすでにあったりします。
ほかの特徴としては、TraitsとかPattern MatchとかMacrosとか、いろいろあるんですがこれを紹介していると長くなってしまうので、興味があったらご自身で調べたりしていただければなと思います。
Rustの使用領域
Rustの使用領域ということで書かせていただいたんですが、Command-Line Toolsだったり、Web Applicationとか、あとは拡張モジュールなんかもよく書かれたりしています。
あとはOSを書いている人がいたりとか、いろんな分野でRustがどんどん使われていってるんですが、Command-Line Toolsだとそういう高速に実行できる部分で、grepのtoolがリライトされていったりとか、ls commandのツールがリライトされたりとか、そういったものに使われたりもしています。
Web Applicationで言うと、これはWeb Applicationフレームワークのベンチマークサイトなんですが、すでにRustのフレームワークではすごく高速に処理できていて、ランキングの上位に入ってきたりとか、機能がすごく充実したものが出てきたりもしています。
これがあとあと関わってくる部分なんですが、拡張モジュールが書かれたりもしています。
Redisの最近のバージョンに入ったモジュールとか、Nginxのモジュールとか、Varnishのモジュールなどの使用例が出たりもしています。
Programming Language、言語のPythonだったりRubyだったりの拡張モジュールを書くことに使われたりもしています。Cの代わりに使っていくという事例が少しずつ出てきています。
PythonとRubyを使うモチベーション
ここでPythonが出てきたので、今回はPythonの拡張モジュールをRustで書くというところ少し掘り下げて紹介させていただこうかなと思っております。
なぜRustとPythonを使うのかというところで、モチベーションとしては、Python単体で考えるとやっぱりどこかでCPUバウンドの部分があると思うので、そういったところでC拡張を使って拡張モジュールを書いたりするんです。
そういったときに結構めんどくさかったりします。ナマのCの拡張をAPIを使って書くとなると、そういうところでRustで書いた処理でPythonの拡張モジュールを書きたいというのが、1つのモチベーションです。
Rustのパッケージ、cratesというものになるんですが、そうした資産がすでにいろいろ出てきているので、それをPythonの世界でも使いたいというのが1つのモチベーションになっています。
実装方法
どういったかたちで実装するのかというと、現在3つぐらい方法があります。
Python側でctypesを使って、ナマの共有ライブラリ「.so」とかを作って呼び出す方法。
あとはrust-cpythonというツールを使う方法と、PyO3という、これもツールがあるんですがそれを使う方法で、今回はPyO3を使う方法を紹介させていただきます。
実際に必要なものはRustのNightlyチャンネルが必要になっています。あとはsetuptools-rustというRustの拡張を簡単に組み込みやすくするパッケージがあるので、そちらを使います。
Python拡張を書くプロジェクトをスタートするのは、先ほど、初めに説明したrustupというものがあるんですが、それでチャンネルをdefaultでnightlyにするために、rustup default nightlyとコマンドを実行して、常にNightlyチャンネルのRustが使われるようにします。
cargo newで作るんですが、そのときにライブラリとして作る場合は--lib オプションを作ればいいので、「- - lib example」で実行します。
実際にこういったディレクトリ構成で、このあとファイルの構成は出ます。
Cargo.tomlというもので先ほど言ってたみたいなパッケージの依存関係とかを書くんですが、初めのpackageは、package名とかversionとかを書くんですが、libというセクションに、こちらも拡張モジュールの名前と、このcrate-typeというパッケージのタイプを指定するところがあるので、他言語から使えるライブラリでcdylibという記述にしています。
あとはPyO3というパッケージcratesを使いたいので、dependenciesのPyO3を指定して、versionと拡張モジュールを書くためのおまじないみたいなものなんですが、featuresのextension-moduleを指定します。
そして setup.pyなんですが、こちらは先ほど言っていたいつものsetuptoolsのimportを使うと思いますが、それに加えてsetuptools_rustを使ってこういう形で書きます。
rust_extentionのところにextention名とどのCargo.tomlを使うかを指定して、あとはバインディングのところにPyO3を使うようにBinding.Pyo3を指定して、これでビルドすると実際に動きます。
実際の拡張モジュールのソースはsrc/lib.rsというところに書くんですが、ライブラリを指定するときはlib.rsに書きます。
ファイルを分けたりすることはできるんですが、まずはここに書くというルールがあります。はじめのfeatureのところは今のRustを使うおまじないみたいなものなんですが、Pythonのモジュールをinitするのを指定する記述と、実際のメソッドですね。この場合はhelloというメソッドで、nameの文字列をインプットして、ここは単にHello〇〇という名前を出力するという処理になっています。
これを$pip install.とするとBuildできて、実際にモジュールを呼び出すことができるような形になっています。Packagingに関しては、ちょっと詳細は説明しないんですが、pyo3-packというのがあり、簡単にPackageが作れるようになっています。
実際の利用例
実際の利用例を3つほどご紹介させていただきます。まずはfast-woothee-pythonというプロジェクトがありまして、これは僕が書いたんですが、Project Wootheeというのはユーザーエージェントの文字列をパースするツールがあります。
それはPythonやRubyなどいろいろな言語で実装されているんですが、その中でwoothee-rustというのを僕が書いて、それをPythonでも使えるようにしたのがfast-woothee-pythonというものです。
このベンチマークが、UAPという古くからあるユーザーエージェントパーサーがあって、そちらとwhootheeとfast-wootheeを比べて見ました。
UAP自体は内部でキャッシュする機構があるので、それなしの場合は一番遅くて、fast-wootheeがwootheeよりすこし早いというベンチマーク結果が出ています。
もう1つの例が、fcsvです。
Rustのcsv creatをラップするようなパッケージになっています。これをやりたかったのは、PythonのClassをどうやってRustで書けるのかということで、csvのReaderとWriterを実装してみました。
実際にやってみた例がこんな感じです。
Pythonのclassを定義、Rustで言うところの構造体を定義して、classがあるということをinit_modで登録してやるという処理なんですが、実際のclassのメソッドの実装がこんな感じになっています。
newとwritrowというPythonのcsvパッケージに似せたメソッドを生やすような感じで書いています。
イテレーターも一応実装できて、この場合だと、詳細は省きますがReader Object自体をIterator Objectと見立てるようなこともできます。forなどで使うと、Iterator Objectとして扱えるようなかたちになっています。
こちらもベンチマークを取ってみたところ、Writerだとどちらも標準のPytohnのモジュールより早い。
Readerに関しても標準のものより早いという結果になっています。
もう1つの例がfpathというものです。これも書いてみたんですが、Pythonのos.pathをRustでリライトするというプロジェクトで、現在50パーセントくらいリライトしました。この速さの秘訣は、memchrというCの文字列検索用のfunctionがあるんですが、それを使ってそれでラップしているものがあるので、高速で文字列を検索できるので早くなっているみたいな感じです。
こういうかたちで使っています。
こちらのベンチマーク結果はこのような形です。
実際のファイルシステムを読みに行くような処理は、やはりRustでも遅いというかPythonと同じなので、CPUバウンドのところだけ早くなっているような形ですね。abspathとかbasenameとか、pathの名前だけをいじるような処理は早くなるという形です。
こちらは別の処理のベンチマークで、だいたい50%くらい早くなっています。
良いところ&悪いところ
良いところと悪いところというところでお話させていただきます。
良いところとしてはC並のパフォーマンスが出るというのが良い点かなとおもっています。とは、ぼくの意見ということで書いたんですが、Cの生のAPIを触るよりは全然書きやすいので、その辺りもいいかなと思っています。
csv crateなんかもそうですが、RustですでにあるものをPython拡張としてパッケージとして使えるのは良い点かなと思います。
悪い点としては、unstableと書かせていただいたんですが、PyO3に関してもRustに関しても、どんどん言語機能が拡張している段階なので、ついていくのは結構大変な印象です。あとは、RustではよくLearning curveと言われているんですが、Rustを学ぶ学習曲線なんですが、学ぶのが大変という側面があるので、そういった意味で始めるときはすこし大変なのかなと思います。
ということで、Rustという言語を簡単に紹介させていただいて、Pythonの拡張モジュールをRustで書いてみて、実際に良い点と悪い点を紹介させていただきました。ありがとうございました。
(会場拍手)
司会者:ありがとうございます。それでは、質問の時間があるので、質問のある方は挙手をお願いします。
質問者1:Rustで拡張ライブラリを作るということなんですが、Rustに限らずPythonの拡張ライブラリを作るとデバッグのところがうまく実現できなかったり追いきれなかったりすることがああったんですが、Rustに関してはその辺りのデバッグ環境はどういった感じでしたか?
Hattori:Rustに関してはデバッグが難しいという状況があります。Rustのスタックトレースみたいなものが出るんですが、それがMacだとソースと全然対応していなかったりするので、Linuxだったら追えたりするんですが、そういった部分はまだまだ言語として成長している部分なのかなと思います。
質問者2:setuptoolsの話で、setuptools-rustについてお話されていましたが、あれはもちろん標準で入っていないと思うので。
Hattori:そうですね。リポジトリとしてはPyo3がオーガナイゼーションを作っていて、setuptools-rustも作っているという感じです。
質問者2:それと、Pyo3がNightlyでしか動かないというお話もされていましたが、まだstabel版がないということだったので、これって結局自分で用意したマシンに同じものを移すのであれば便利ですが、PyPIのように世の中に作ったものを広げるという意味では広げるのは難しいという理解で良いんでしょうか?
Hattori:一応、最近pyo3-packというものがあり、それでBuildすると簡単にwhile packingが作れます。それを、今だとTwineでしたかね、それでアップロードしてTravisなどと組み合わせればMacやLinuxで使う準備はできます。
質問者3:Rustのメモリー保護の話がありましたが、Pythonの拡張機能にした場合、例えばPythonから来たメモリーはそのままだと思うんですが、Rustで確保したメモリーはPython側ではどうなるんですか? ただ単にPythonに扱えるようになるだけで、そこはRustの保護機能は働かないと考えて良いんですか?
Hattori:そうですね。Rustで処理していて最後にメソッドに帰ってくる時などはすでにPythonの世界に。
質問者3:それがさらにRustで書かれた拡張機能に来た場合は、もう1度変数保護をし直す感じですかね?
Hattori:そうですね。
質問者3:わかりました、ありがとうございます。
司会者:それでは時間になりましたので、Hattoriさん、ありがとうございました。