データ圧縮ライブラリ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のライブラリ扱うなんてこと出来たら面白そう