Vcpkg-rsでCargoからVcpkgをいい感じに利用する
以前SnappyをFFIで利用するプログラムをつくった際、ビルドスクリプトを書く機会があって下のようなコードを書いた
Before
//build.rs fn main{ // static library(*.lib) println!("cargo:rustc-link-search=native=<path_to_vcpkg>/installed/x64-windows/lib/"); // dynamic library(*.dll) println!("cargo:rustc-env=PATH=<path_to_vcpkg>/installed/x64-windows/bin/"); }
シンプルだけどちょっと長々しい
After
//build.rs fn main(){ extern crate vcpkg; vcpkg::find_package("snappy").unwrap(); }
Vcpkg-rsを使うとこのように短くなる。
準備 ※Windowsの場合
公式ドキュメントに大体書いてる。大体3ステップで済んだ
https://docs.rs/vcpkg
Cargo.tomlに[build-dependencies]
とvcpkg
を追記
[package] ... [dependencies] ... [build-dependencies] vcpkg = "0.2"
次に、PowerShellを起動しvcpkg integrate install
を実行
Vcpkgの/installed/x64-windows/bin
にある.dllをプロジェクトのルートディレクトリ直下に置く。
もしくは環境変数VCPKGRS_DYNAMIC
をつくり、
値としてVcpkgのパッケージの.dllが置かれたディレクトリpathを設定する。
こんな感じ↓
set VCPKGRS_DYNAMIC=<path_to_vcpkg>/installed/x64-windows/bin
setだと今起動しているインスタンス限りで死んでしまうので、
システムの詳細設定>環境変数
に設定してもよさそう。勇気ある人はset
の代わりにsetx
使うといいと思う。
使ってみる
パッケージをインストールvcpkg install snappy
find_package()
メソッドにパッケージ名を指定する
//build.rs fn main(){ extern crate vcpkg; vcpkg::find_package("snappy").unwrap(); }
ややスッキリした。
データ圧縮ライブラリSnappyをRustで利用する
概要
タイトル通りです。というかTRPLのここの内容まんまです。
旧版日本語訳:https://doc.rust-jp.rs/the-rust-programming-language-ja/1.9/book/ffi.html
本家:https://doc.rust-lang.org/nomicon/ffi.html
いわゆるやってみた系ですがWindowsでやるとなんかつまづきが発生したのでメモする。
WindowsとLinux(Ubuntu)両方の手順載せます。
LinuxはWSLで検証したけどそこまで突っ込んだことしてないので多分大丈夫。
リポジトリにまとめた。見ながら読むとわかりやすいかも
github.com
目次
Snappyについて
Snappyのインストール
Snappyとのバインディングを書く
実行
Snappyについて
GitHub - google/snappy: A fast compressor/decompressor
C++で書かれたデータ圧縮解凍ライブラリ
zlibと比べて圧縮率は低い?が速度は大分速いらしい
MongoDBやChromeのIndexedDB(LevelDB)など著名なNoSQLデータベースの中で動いている http://google.github.io/snappy/
ちなみにubuntuのパッケージ管理ツールに同名のソフトウェアがあるがこれとは別物なので注意。
Snappyのインストール
Snappyのライブラリファイルを取得しましょう。
自分はここに9割ぐらい消耗しました。勢いに任せてソースコードをcmakeしようとするのはやめよう。(1敗)
もっと楽でわかりやすい方法があります。
Linux
sudo apt install libsnappy-dev
標準のパッケージマネージャでinstallする。
apt系以外の場合snappy
でパッケージの検索をかけてそれっぽいのを探す。
Windows
Windowsのパッケージ管理ツール Vcpkgをインストール。MicrosoftがOSSとして開発してて準公式的な雰囲気を感じる。
インストール方法の詳細は下記のqiita記事を参照。
Vcpkgを使用してOpenCVを導入する - Qiita
次にPowerShellを開いてVcpkgでSnappyをインストールする。
vcpkg install snappy:x64-windows
vcpkg install snappy
の様にパッケージ名のみの指定も可能だがデフォルトは32bitバージョンを取ってくるのでお好みで指定する。
Snappyとのバインディングを書く
cargo new --bin --edition 2018
してコードを書きます。取り敢えず下記の公式ドキュメントをみながら写経する。
他言語関数インターフェイス
※Windowsのみ
本質的なコードとは別にWindowsの場合はビルドスクリプトを書く必要があります。
コンパイル時、rustcに呼び出されたリンカはSnappyのライブラリファイルを見つけようとしますが、そのライブラリファイルが入っているVcpkgへのpathをリンカは知りません。
なので./srcがコンパイルされる前にリンカに教える必要があります。
Cargoがその辺をいい感じにしてくれる方法を提供しています。
Build Scripts - The Cargo Book
というわけでbuild.rsをプロジェクトのルートディレクトリに新規作成して、以下をコピペしましょう。
#[cfg(target_os = "windows")] fn main() { // Reference to https://doc.rust-lang.org/cargo/reference/build-scripts.html // Please rewrite <path_to_vcpkg> to your setted path. // static library(*.lib) println!("cargo:rustc-link-search=native=<path_to_vcpkg>/installed/x64-windows/lib/"); // dynamic library(*.dll) println!("cargo:rustc-env=PATH=<path_to_vcpkg>/installed/x64-windows/bin/"); } #[cfg(target_os = "linux")] fn main() { // Do nothing }
これがbuild.rsの内容になります。
専用の記法で標準出力することでライブラリのpathを設定することができます。
<path_to_vcpkg>
は自分のVcpkgへの絶対パスに書き換えてください。
余談
ぱっと全体をみて、まずmain関数が複数あることに違和感があるかもしれません。僕はありました。これはcfgアトリビュートにより条件ごとに実行対象のmain関数が変わるというものでvalidな書き方です。
下のmain関数の様になにもしなくてもエラーにはなりませんが、逆に実行できるmain関数がない場合はエラーとなります。
なので例えばWSLでこのプロジェクトをcargo runした場合、下のmain関数がないとビルドが落ちます。なにもしない関数があるのはその為です。
実行
ファイルの文字列を読み込んで圧縮したり解凍するサンプルコードです。
Snappyとのバインディングコードはbind_snappy.rsとして切り分けました。
mod bind_snappy; use bind_snappy::{compress, uncompress, max_compressed_length}; use std::io::{BufReader, Read}; use std::io::{BufWriter, Write}; use std::fs::File; fn main(){ /* something */ let file_path = "./src/bind_snappy.rs"; let mut file: File = File::open(file_path).unwrap(); let mut reader = BufReader::new(&mut file); let mut buf = String::new(); reader.read_to_string(&mut buf).expect("Could not read!"); let src: String = buf; let result: Vec<u8> = compress(src.as_bytes()); println!("Origin data: {:?}", src); println!("Origin length: {:?}", src.as_bytes().len()); println!("Compressed length: {:?}", result.len()); let bytes: Vec<u8> = uncompress(&result).unwrap(); // encode bytes to UTF-8 let utf8 = String::from_utf8(bytes.to_vec()).unwrap(); println!("Uncompressed: {:?}", utf8); assert_eq!(src, utf8) }
動いた。やったね
やった感想
ちゃんと圧縮されててスゲーって思った(小並感)
Cのライブラリ他にも使ってみたい
WebAssemblyに変換してJSからCのライブラリ扱うなんてこと出来たら面白そう
Rustで二分探索木かいた
最近C言語勉強してみたりしてアルゴリズムとデータ構造に興味持ってきたんだけど、そういや二分木かいたことないなーと思いRustでやってみた。
「二分探索木」で検索して一番上にレコメンドされたサイトみながらやったけど、正直雰囲気でかいたので自分のこれが二分探索木と呼べるのかいまいち不安だったりする。
実装した機能は3つで、要素の挿入と探索と削除。
ノードの削除について
Node構造体に生えてるdeleteメソッドのことです。insertとかと比べてありえんほど難しくて困った。
どんな実装が一般的なんだろう?自分の実装だと削除するノードをx
とすると、
- ノード
x
以下の子ノードのデータをリストに保存する。 - ノード
x
を子ノード諸共削除する(dropさせる)。 - リストに保存されたデータを全部insertメソッドに投げる。
- 元の状態からノード
x
のみが削除された状態になる。
こうすることで大小関係の順序を保ったまま指定の要素のみを削除できた。
ただこの、全部削除して全部insertしなおすのめっちゃ効率悪そう。今更だけども
かいた感想
削除処理むずかしかった
再帰力が高まった気がする。ハノイの塔より好きだ
AVLtreeとBtreeもどんななのかちらっと見たけど難しそう
次はBtreeかいてみたい
ドキュメンテーションテストでコードスぺニットを実行しないには
事情あってテストで実行したくないコードスぺニットが出てきた時の解決記事です。
結論
no_run
アトリビュートを使う。以上。詳細は以下
doc.rust-lang.org
余談
Rustにはrustdocというドキュメント自動生成ツールがある。
markdownを書くとhtmlとしていい感じに吐き出してくれる便利ツールで、主に使われる場面はライブラリを公開する際だろうか。
ソースコードにドキュメント用のテキストやサンプルコードを直接書き込めるようになってる
コードスぺニットに対応しており、Rustのコードを埋め込むことができて、それらはcargo testによるユニットテストの際に自動実行される。そのためドキュメントのサンプルコードが動かない等の悲劇を回避できる。
僕も開発でお世話になってるが、ときにどうしてもテストにかけたくないサンプルコードがでてくる。
例えば自分の場合は,、
let client = Client::set_token("access_token");
のような、webサービスのtokenを取る関数に、例としてでたらめな文字列を当てているサンプルコードだった。
このコード片は通常、リクエストを送りレスポンスを返すのに使われるがtokenとしてでたらめな文字列が入っているのでテストが落ちる。それでもこれはサンプルコードとして意図して記述されたものなので間違いじゃない。
なのでテストは見逃してくれ~というシチュエーションがあり今回調べるに至った。
参考サイト
Kotlinからネイティブコードを呼んでAndroidで動かす
totechite.hatenablog.com 前回は共有ライブラリをつくって終わりました。
今回はAndroidで動かす編です。
はじめに
環境
- Windows10 Home
- Android Studio3.2.1
- Kotlin 1.3.2
この記事のゴール
- 共有ライブラリをAndroid/Kotlinで利用できるようになる
やることの流れ
- .soファイルの配置
- Kotlinでネイティブ関数を呼ぶ
- アプリで動かしてみる .soファイルの配置
.soファイルの配置
JavaやKotlinではSystem.loadLibrary()を使って共有ライブラリを読み込むことでネイティブコードを利用することができる。
JNIを利用しコンパイルした共有ライブラリをAndroidProjectフォルダ内に配置する。
まずAndroidStudioで新規プロジェクトを適当に立ち上げ、<ProjectName>\app\src\main
にjniLibs
フォルダを新規作成する。(<ProjectName>
は適宜読み替えてほしい)
作成したら
jniLibs\arm64
jniLibs\arm64-v8a
jniLibs\armeabi
jniLibs\armeabi-v7a
jiniLibs\x86
と各プラットフォームのフォルダをjniLibs
ディレクトリ下に作成し、前回生成した.soファイルをそれぞれのフォルダにコピペする。
Kotlinでネイティブ関数を呼ぶ
共有ライブラリを読み込むため、クラスの初期化メソッドでSystem.loadLibrary()をする。
実行時に前節で準備した、各プラットフォームの.soファイルを読み込んでいる様子。
class MainActivity : AppCompatActivity() { companion object { init { System.loadLibrary("greetings") } } override fun onCreate(savedInstanceState: Bundle?) { // anything } }
System.loadLibrary()の引数には文字列を取り、利用する共有ライブラリの名前を入れる。今回はlibgreetings.so
なので"greetings"
とした。
次に、読み込んだ共有ライブラリに定義されたネイティブ関数を利用するため、Kotlinコードに外部関数を定義する。
適当に呼び出し専用のクラス等をつくり、その中で外部関数と別に、間接的に呼び出させるためにpublicなメソッドを用意してみる。
class RustGreetings { private external fun greeting(to: String): String; fun sayHello(to: String): String { return greeting(to); }; ......... }
これにより外部関数はネイティブ関数のJNIにバインドされ、Kotlin側からはいい感じに使えるらしい…(?) なるほどわからん
アプリで動かしてみる
本当に動くのかみてみよう。
新規のAndroidプロジェクトを作成し、minSdkVersionは26にする。
MainActivityがonCreateした際、
TextViewにRustGreetings.sayHello()のStringを表示するアプリをつくってみる。
class MainActivity : AppCompatActivity() { companion object { init { System.loadLibrary("greetings") } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val g = RustGreetings() val hello: String = g.sayHello("Rust!") findViewById<TextView>(R.id.rustgreeting).text = hello } }
sayHello()(もとい呼び出し先のgreeting())は文字列を取り、文字列を返す関数で、return "Android meets" + arg ;
みたいな文字列が返る。
なので今回はAndroid meets Rust!
という文字列が表示されれば成功だ。
するとこんな画面になる。
どうやらちゃんと動いてる様子。やったね
フィボナッチ数をKotlinとネイティブ関数で求める
ベンチマークとかで親の顔より見るあれです。
プログラムの実行時間を計測して表示させるアプリをつくってみましょう。
RustGreeting.kt
class RustGreetings { private external fun greeting(to: String): String; private external fun fibo(to: Int): Int; fun sayHello(to: String): String { return greeting(to); }; fun rsfibo(to: Int): Long { val elapsed = measureTimeMillis { fibo(to); } return elapsed } fun ktfibo(to: Int): Long { val elapsed = measureTimeMillis { ktfibo_re(to) } return elapsed } fun ktfibo_re(n: Int): Int { if (n <= 1) { return n } else { val x = n - 2 val y = n - 1 return ktfibo_re(x) + ktfibo_re(y) } } }
rsfibo()とktfibo()はそれぞれ実行時間を返す。
MainActivity.kt
class MainActivity : AppCompatActivity() { companion object { init { System.loadLibrary("greetings") } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val g = RustGreetings() val hello: String = g.sayHello("Rust!") findViewById<TextView>(R.id.rustgreeting).text = hello val button: Button = findViewById(R.id.excute) as Button button.setOnClickListener(object : View.OnClickListener { override fun onClick(v: View?) { val fibo_num = Integer.parseInt(findViewById<TextView>(R.id.editfibo).text.toString()) val k = g.ktfibo(fibo_num) findViewById<TextView>(R.id.kotlinfibo).text = k.toString() + "ms" val r = g.rsfibo(fibo_num) findViewById<TextView>(R.id.rustfibo).text = r.toString() + "ms" } }) } }
ボタンのonClickイベントでfiboが実行される感じです。
出来上がったものはこちら
fibo(40)だと4倍ぐらいはやい
総括
知見がとぼしすぎて、はやーい!すごーい!以外の感想がない
使い所だけど、GoogleのAndroidNDKのサイトにCGとか機械学習の推論につかう例があってなるほどと思った。
なにかうまいこと応用したいですね
RustをクロスコンパイルしてAndroidで動かす
RustのコードがAndroidで動くとか動かないとかいう話をtwitterでみて興味が湧きました。
調べたところmozillaが記事を書いててくれてたので読みながら手を動かしてみた記事です。
読みながらといっても自分はフィーリングでやってた感が多分にあるので、英語読める方は元記事みたほうがいいと思います。
Building and Deploying a Rust library on Android
はじめに
環境
- Windows10 Home
- Android Studio3.2.1
- Rust 1.31.1
やること
RustのプログラムをAndroidのネイティブコードにクロスコンパイルする
やらないこと
JNIライブラリの詳しい説明、使い方
手順
下準備
CargoProjectの立ち上げとコンパイラの設定
Rustコードから共有ライブラリを生成する
下準備
Rustコンパイラツールチェインの取得
対象のAndroidデバイスのCPUアーキテクチャに向けてツールチェインが用意されているのでrustupでインストールする。
ネイティブコードにコンパイルするのに必要になります。
ここではとりあえずメジャー所のARMv7とARM64、x86をインストールする。
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
公式サイトに対応プラットフォームの一覧があるので適宜書き換えてほしい。
forge.rust-lang.org
AndroidNDKのインストール
先のツールチェインにより各CPUのネイティブコードにコンパイルできるようになったぽい。
次にこれをAndroidで利用できるようにするためリンカーを手に入れましょう。
AndroidNDKのインストールをする
AndroidStudioの設定画面のAppearance & Behaviour > System Settings > Android SDK > SDK Tools
からNDKにチェックを入れてインストールする。
もしくは直接ダウンロード。 NDK のダウンロード | Android NDK | Android Developersスクリプトの確認
NDKのディレクトリndk-bundle\build\tools
に、make-standalone-toolchain.sh
またはmake-standalone-toolchain.py
があることを確認しましょう。
(ndk-bundle
の配置ですが、AndroidStudioでインストールした場合、WindowsであればC:\User\<username>\AppData\Local\Android\sdk
に配置されています。)
このスクリプトを実行することでツールチェインをインストールできます。ツールチェインはのちにRustのコンパイラにリンカーとして指定するのに必要となります。
スタンドアロン ツールチェーン | Android NDK | Android Developers
CargoProjectの立ち上げとコンパイラの設定
デスクトップかどこかにcargo new --lib greetings
とかしてCargoプロジェクトを作る。
Cargo.tomlは以下のように追記する。
[package] name = "greetings" version = "0.1.0" authors = ["hogehuga <hogehuga@gmail.com>"] [target.'cfg(target_os="android")'.dependencies] jni = { version = "0.10.2", default-features = false } [lib] name = "greetings" crate-type = ["cdylib"]
依存クレートにjniとありますが、これはJava Native InterfaceというJavaがFFIするための仕様の一つで、詳しくは以下。
JNI | Java | IT用語辞典 | 日立ソリューションズ
これにのお陰でJVM言語からRustが生成したネイティブコードを呼んだり、ネイティブコードからJavaバイトコードを呼んだりできるらしい。すごい(こなみ)
.cargo/config
cargo buildの際NDKのリンカ―と連携させるようにcargoの設定ファイルをつくります。
プロジェクトのルートディレクトリに.cargo
フォルダとその下にconfig
ファイルをつくります。このとき.cargo/config
となります。config
ファイルは拡張子無しです。
config
の記述は以下の通りです。ちなみに僕はWindows10でやってるのでMacとか他のOSと拡張子が違ったりするかも。いい感じに読み替えてください。
[target.aarch64-linux-android] ar = "NDK/arm64/bin/aarch64-linux-android-ar" linker = "NDK/arm64/bin/aarch64-linux-android-clang.cmd" [target.armv7-linux-androideabi] ar = "NDK/arm/bin/arm-linux-androideabi-ar" linker = "NDK/arm/bin/arm-linux-androideabi-clang.cmd" [target.i686-linux-android] ar = "NDK/x86/bin/i686-linux-android-ar" linker = "NDK/x86/bin/i686-linux-android-clang.cmd"
toml風な記法だけどconfig.toml
と書いても読み込まれないので注意。
NDKツールチェインのインストール
次に先ほど確認したndk-bundle\build\tools
にあるmake-standalone-toolchainスクリプトを実行し、ARMv7とARM64、x86のツールチェインをインストールしていきましょう。
シェルでCargoプロジェクトに入って作業します。
下記は例です。
set NDK_HOME=C:\Users\<your_name>\AppData\Local\Android\sdk\ndk-bundle cd greetings mkdir NDK %NDK_HOME%/build/tools/make_standalone_toolchain.py --api 26 --arch arm64 --install-dir NDK/arm64 %NDK_HOME%/build/tools/make_standalone_toolchain.py --api 26 --arch arm --install-dir NDK/arm %NDK_HOME%/build/tools/make_standalone_toolchain.py --api 26 --arch x86 --install-dir NDK/x86
プロジェクトのルートディレクトリにツールチェインを置きたいのでフォルダをつくります。名前はNDK
にします。
次にmake_standalone_toolchainスクリプトを実行してNDK
内にインストールしていきます。
ここまで来たらこんな風になるかとおもいます。
Rustコードから共有ライブラリを生成する
ということで<cargo_project_name>\src\lib.rs
にコードを書きましょう。
デモに適当なプログラムを用意します。
use std::os::raw::c_int; #[no_mangle] pub extern "C" fn rust_fibo(n: *mut c_int) -> *mut c_int { if n <= 1 as *mut i32 { n } else { let x = n as i32 - 2; let y = n as i32 - 1; (rust_fibo(x as *mut i32) as i32 + rust_fibo(y as *mut i32) as i32) as *mut i32 } } #[cfg(target_os = "android")] pub mod android { extern crate jni; use self::jni::objects::{JClass, JString}; use self::jni::sys::{jint, jstring}; use self::jni::JNIEnv; use super::*; #[no_mangle] pub unsafe extern "C" fn Java_com_totechite_rustpractice_RustGreetings_greeting( env: JNIEnv, _: JClass, input: JString, ) -> jstring { let input: String = env .get_string(input) .expect("invalid pattern string") .into(); let output = env .new_string(format!("Android meets {}", input)) .expect("Couldn't create java string!"); output.into_inner() } #[no_mangle] pub unsafe extern "C" fn Java_com_totechite_rustpractice_RustGreetings_fibo( _env: JNIEnv, _: JClass, java_int: jint, ) -> jint { let fibo = rust_fibo(java_int as *mut i32); fibo as jint } }
書きました。
mod android
内にあるのがJava等から呼び出し可能なネイティブ関数の部分です。
ナントカgreeting関数はStringをとって、"Android meets"という文字列をくっつけて返します。
ナントカfibo関数はフィボナッチ数を求めるやつです。
命名が超長いのは恐らくJNIの仕様で、呼び出し元のprojectのディレクトリ構造とクラス名、メソッド名に対応させるぽいです。
AndroidProjectであれば<android_project_name>\app\src\main
以下のディレクトリになります。
例えばfn Java_com_totechite_rustpractice_RustGreetings_greeting()
なら、app\src\main
以下のjava\com\totechite\rustpractice
にあるRustGreetings
クラス内にgreeting()
外部呼び出しメソッドが定義されていることを期待してます。
関数名とAndroidProject側の構造が合致しないとno implementation
みたいなエラーになるので注意しましょう。
コンパイルしてみる
cargo build --target aarch64-linux-android --release cargo build --target armv7-linux-androideabi --release cargo build --target i686-linux-android --release
targetフォルダに各プラットフォームのフォルダと共有ライブラリの.soファイルが出力されていれば完了です。
次の記事で共有ライブラリをAndroidで動かします。 totechite.hatenablog.com
参考元・勉強になったページ
rustlingsでRust筋のストレッチ
今夏からRustの筋トレをしてて、いまいち所有権やポインタ周りが咀嚼できてない感があり、
良さげなチュートリアルで手を動かしたいと色々ググってたらみつけたやつです。
なにこれ
- Rustの基礎ドリルみたいなの
- 構文が間違ってたりなんか足りてない出題コードをコンパイルが通るよう編集する、といった感じ
- 問題が書いてるRust PlaygroundのリンクがREADMEに貼られてるので、気軽に始められる
- 問題がわからなくてもヒントを用意してくれているので、自分のようなビギナーでも気負わずにやれた
rustlings、読みはラスリング?コンパイラと取っ組み合うってニュアンス?
あとjekyllのコミッターの方がメンテしてるっぽい。
キラっとやってみた
Move semanticsの自分の解答を貼ってみる
2番目の問題はヒントにある3パターンの解答を載せた
My Rustlings answers
Todo
残りのセクションも近日中にやるぞ