Vcpkg-rsでCargoからVcpkgをいい感じに利用する

github.com

以前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でやるとなんかつまづきが発生したのでメモする。
WindowsLinux(Ubuntu)両方の手順載せます。
LinuxはWSLで検証したけどそこまで突っ込んだことしてないので多分大丈夫。

リポジトリにまとめた。見ながら読むとわかりやすいかも
github.com

目次

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をインストール。MicrosoftOSSとして開発してて準公式的な雰囲気を感じる。
インストール方法の詳細は下記の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つで、要素の挿入と探索と削除。

play.rust-lang.org

Rust-二分探索木

ノードの削除について

Node構造体に生えてるdeleteメソッドのことです。insertとかと比べてありえんほど難しくて困った。
どんな実装が一般的なんだろう?自分の実装だと削除するノードをxとすると、

  1. ノードx以下の子ノードのデータをリストに保存する。
  2. ノードxを子ノード諸共削除する(dropさせる)。
  3. リストに保存されたデータを全部insertメソッドに投げる。
  4. 元の状態からノード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としてでたらめな文字列が入っているのでテストが落ちる。それでもこれはサンプルコードとして意図して記述されたものなので間違いじゃない。
なのでテストは見逃してくれ~というシチュエーションがあり今回調べるに至った。

参考サイト

http://ykomatsu.github.io/rust/book-ja/documentation.html

Kotlinからネイティブコードを呼んでAndroidで動かす

totechite.hatenablog.com 前回は共有ライブラリをつくって終わりました。

今回はAndroidで動かす編です。

はじめに

環境

  • Windows10 Home
  • Android Studio3.2.1
  • Kotlin 1.3.2

この記事のゴール

  • 共有ライブラリをAndroid/Kotlinで利用できるようになる

やることの流れ

  1. .soファイルの配置
  2. Kotlinでネイティブ関数を呼ぶ
  3. アプリで動かしてみる .soファイルの配置

.soファイルの配置

JavaやKotlinではSystem.loadLibrary()を使って共有ライブラリを読み込むことでネイティブコードを利用することができる。
JNIを利用しコンパイルした共有ライブラリをAndroidProjectフォルダ内に配置する。

まずAndroidStudioで新規プロジェクトを適当に立ち上げ、<ProjectName>\app\src\mainjniLibsフォルダを新規作成する。(<ProjectName>は適宜読み替えてほしい)
作成したら jniLibs\arm64 jniLibs\arm64-v8a jniLibs\armeabi jniLibs\armeabi-v7a jiniLibs\x86と各プラットフォームのフォルダをjniLibsディレクトリ下に作成し、前回生成した.soファイルをそれぞれのフォルダにコピペする。
f:id:totechite:20190106225027p:plain

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!という文字列が表示されれば成功だ。

するとこんな画面になる。
f:id:totechite:20190106233547p:plain
どうやらちゃんと動いてる様子。やったね

フィボナッチ数を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が実行される感じです。

出来上がったものはこちら

f:id:totechite:20190107004026j:plain

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ライブラリの詳しい説明、使い方

手順

  1. 下準備

  2. CargoProjectの立ち上げとコンパイラの設定

  3. 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で利用できるようにするためリンカーを手に入れましょう。

  1. AndroidNDKのインストールをする
    AndroidStudioの設定画面の Appearance & Behaviour > System Settings > Android SDK > SDK ToolsからNDKにチェックを入れてインストールする。 AndroidStudio-Setting
    もしくは直接ダウンロード。 NDK のダウンロード  |  Android NDK  |  Android Developers

  2. スクリプトの確認
    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というJavaFFIするための仕様の一つで、詳しくは以下。

Java Native Interface仕様の目次

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内にインストールしていきます。

f:id:totechite:20190106213600p:plain ここまで来たらこんな風になるかとおもいます。

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

参考元・勉強になったページ

Building and Deploying a Rust library on Android

jni - Rust

rustlingsでRust筋のストレッチ

github.com

今夏からRustの筋トレをしてて、いまいち所有権やポインタ周りが咀嚼できてない感があり、
良さげなチュートリアルで手を動かしたいと色々ググってたらみつけたやつです。

なにこれ

  • Rustの基礎ドリルみたいなの
  • 構文が間違ってたりなんか足りてない出題コードをコンパイルが通るよう編集する、といった感じ
  • 問題が書いてるRust PlaygroundのリンクがREADMEに貼られてるので、気軽に始められる
  • 問題がわからなくてもヒントを用意してくれているので、自分のようなビギナーでも気負わずにやれた

rustlings、読みはラスリング?コンパイラと取っ組み合うってニュアンス?
あとjekyllのコミッターの方がメンテしてるっぽい。

キラっとやってみた

Move semanticsの自分の解答を貼ってみる
2番目の問題はヒントにある3パターンの解答を載せた
My Rustlings answers

Todo

残りのセクションも近日中にやるぞ