Elixirから簡単にRustを呼び出せるRustler #4 SHIFT-JIS変換を行う

---
（この記事は、[「fukuoka.ex(その2) Elixir Advent Calendar 2017」](https://adventar.org/calendars/2871)の11日目、[Webパフォーマンス Advent Calendar 2017](https://qiita.com/advent-calendar/2017/webperf) 2017の4日目です）

昨日は@tuchiroさんの[ElixirでSI開発入門 #7 Multiで使う関数を再利用可能な粒度に分割する](https://qiita.com/tuchiro/items/8048e38404e0968d9b6e)でした！

本連載の記事はこちらです。
|> [ElixirのGenStageに入門する #1](https://qiita.com/twinbee/items/12a61863ceef794996e0)
|> [ElixirのGenStageに入門する#2 バックプレッシャーを理解する](https://qiita.com/twinbee/items/eda7f70fa9e4651f2248)
|> [Elixir並列処理「Flow」の2段ステージ構造を理解する](https://qiita.com/twinbee/items/43b767520da666ee960d)
|> [Elixirから簡単にRustを呼び出せるRustler #1 準備編](https://qiita.com/twinbee/items/aabc11d0d667800fc0bb)
|> [Elixirから簡単にRustを呼び出せるRustler #2 クレートを使ってみる](https://qiita.com/twinbee/items/54e8a4ec73bc27abd10e)
|> [Elixirから簡単にRustを呼び出せるRustler #3 いろいろな型を呼び出す](https://qiita.com/twinbee/items/f94eb7f74ff39c781da0)

*お礼：各種ランキングに69回のランクインを達成しました*

4/27から、30日間に渡り、毎日お届けしている[「季節外れのfukuoka.ex Elixir Advent Calendar」](https://qiita.com/advent-calendar/2017/elixir-or-phoenix)と[「季節外れのfukuoka.ex(その2) Elixir Advent Calender」](https://adventar.org/calendars/2871)ですが、[Qiitaトップページトレンドランキング](https://qiita.com/trend)に8回入賞、[Elixirウィークリーランキング](https://qiita.com/tags/elixir)では5週連続で１／２／３フィニッシュを飾り、各種ランキング通算で、<font color="red"><b>トータル69回ものランクイン</b></font>（前週比+60.1%）を達成しています

みなさまの暖かい応援に励まされ、<font color="red"><b>合計452件ものQiita「いいね」</b></font>（前週差+103件）もいただき、fukuoka.exアドバイザーズとfukuoka.exキャストの一同、ますます季節外れのfukuoka.ex Advent Calendar、頑張っていきます

[![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/bbe9d349-c978-e9e8-ba7d-f8feda74be91.png)](https://adventar.org/calendars/2871)

---

# はじめに
今回はElixirのShift-JIS(SJIS)事情から始めます。

企業の受発注の現場ではWindowsの主要Encodingである、SJISが未だに幅を聞かせています。Elixirの文字列型はUTF8エンコードであり、SJISに対応した変換ライブラリも見当たらず、SI案件では導入の障壁の一つでした。

ここではRustlerの連載の一環としてBinaryを渡して、SJIS->UTF8変換を行う手法を提示致します。

Elixirをサービスのプラットフォームとして選択しない場合でも、Flowというライブラリを使用して、並列でShift-JIS => UTF8変換が可能になるので、バッチ処理内のツールとしてWebパフォーマンスの改善が図れます。

Rustに興味がない方は、途中飛ばして最後の結果だけでも読んで頂ければありがたいです。

---

さて、前回はコレクション型をElixirから取り出してきました。今回は最大の難関であるBinaryがテーマです。

## 今回使うRustのクレート

[encoding](https://crates.io/crates/encoding)を使います。


## Rust用語
以下Rust特有の用語が出てくるので、抜粋しておきます。

- トレイト(trait) オブジェクト指向のクラス的なものですが、メンバ変数がなく関数を集めたものです。
- Option型 値がないかもしれないことを表すラップ。
- スライス　部分配列のこと。必ず本体がある。

##Elixirのバイナリの扱い

実はElixirにはStringとBinaryは区別がありません。どちらもバイナリです。
文字列とバイナリの区別が必用になるのは、Rust側です。

```elixir
iex(1)> <<"あ",0>>  # 最後尾に0を足して、バイトコードを確認
<<227, 129, 130, 0>>

iex(2)> <<227,129,130>>
"あ"
```


## Rustのバイナリの扱い

`String型`は一般的な文字列型ですがElixirと異なるのは、UTF-8にないコードを代入すると例外が発生(panic)するところです。

Elixirから渡したSJISとして渡した文字列は、RustのString型やそのスライスのstr型で受けることはできません。


```rust
 let str : String = args[0].decode()?;   // NG
```


## コード解説
どう解決しているかは、コードを見ていきましょう。

```rust
fn sjis2utf8<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {

    let sjis_encoding = encoding_from_whatwg_label("sjis").unwrap();
    let in_binary : NifBinary = args[0].decode()?;
    let in_str = in_binary.to_owned().unwrap();
    
    let utf8_str = sjis_encoding.decode(in_str.as_slice(), DecoderTrap::Ignore).unwrap();
    Ok(utf8_str.encode(env))
}
```

前回までに紹介してきた他のプリミティブ型(数値型、文字列等）が、直接型指定をして値を取り出してきたのとは違って、一旦`NifBinary型`にデコードします。この型はまだバイトコードではありません。次に、Rustlerのトレイト関数`to_owned`でOption型に変換します。さらに、Option型から値を取り出すために、`unwrap()`関数を実行します。これだけの流れを経て、ようやくバイトコードが取り出せました。

この辺を理解するのはRustの型定義だけでなく、NIFで受け渡しされているデータ構造まで知る必用があります。

encodingクレートで`Shift-JIS`をUTF-8へ**デコード**する方法は、[encodingクレート公式](https://crates.io/crates/encoding)サイト内の"EUC-KR"をDecodeするテストコードを参考にしました。

コード中、`sjis_encoding.decode(in_str.as_slice(),･･･`の部分では第一引数に`&[u8]`型が求められているので、先ほど取り出したバイトコードのスライスを渡しています。また、戻り値はResult型なのでunwrap()で値を取り出しています。バイナリをStringやstr型ではなく、`&[u8]`で渡しているところがポイントです。



---
# 郵便番号データ

さて、今回のNIFを使ってShift-JISデータの代表格である、日本郵便の[郵便番号データ](http://www.post.japanpost.jp/zipcode/download.html)をコンバートしてみましょう。

ここでは全国一括データを使います。

- ken_all.csv

~~([ソース全体](https://github.com/enpedasi/rustler_shiftjis)はGithubにアップしました。)~~
2018/9/29 **パッケージ名[mbcs_rs](https://github.com/enpedasi/mbcs_rs)でhex公開しました　!**
（`NifExample.sjis2utf8`は`MbcsRs.decode!`で実現されております。)

プロジェクトディレクトリにダウンロード、解凍してElixirを起動します。テキストエディタで確認したところ、124184行あります。

以下のコードは、CSVの文字列フィルターのサンプルです。

```elixir:postal.ex
defmodule PostalCsv do
  def filter(word \\ "福岡県") do
    "ken_all.csv"
    |> File.stream!
    |> Stream.map(fn r -> NifExample.sjis2utf8(r) end)
    |> Stream.filter(fn r -> String.contains?(r, word) end)
    |> Enum.to_list
  end
end
```

iexを起動して、動作を確認します。

```bash
$ iex -S mix
```

```elixir
iex(1)> PostalCsv.filter("福岡市中央区")
・・・
 "40133,\"810  \",\"8100073\",\"ﾌｸｵｶｹﾝ\",\"ﾌｸｵｶｼﾁﾕｳｵｳｸ\",\"ﾏｲﾂﾞﾙ\",\"福岡県\",\"福岡市中央区\",\"舞鶴\",0,0,1,0,0,0\n",
・・・
 "40133,\"810  \",\"8100037\",\"ﾌｸｵｶｹﾝ\",\"ﾌｸｵｶｼﾁﾕｳｵｳｸ\",\"ﾐﾅﾐｺｳｴﾝ\",\"福岡県\",\"福岡市中央区\",\"南公園\",0,0,0,0,0,0\n",
 "40133,\"810  \",\"8100022\",\"ﾌｸｵｶｹﾝ\",\"ﾌｸｵｶｼﾁﾕｳｵｳｸ\",\"ﾔｸｲﾝ\",\"福岡県\",\"福 岡市中央区\",\"薬院\",0,0,1,0,0,0\n",
 ...]
iex(2)> PostalCsv.filter("福岡市中央区") |> Enum.count
53
iex(3)> PostalCsv.filter("0") |> Enum.count
124184
```

無事SJISのファイルがElixirで読めましたね。

Flowでも動作することは確認できました。このファイルの大きさでは、Flowの並列処理の力が発揮できないので、動作時間はStream版と同程度でした。(Flow版のソースもアップしております`PostalCsv.filter_flow`)

RustのクレートEncodingはSJISだけでなく、多様なエンコーディングに対応してます。
これでElixirで文字エンコードに悩むことはなくなったので、SI案件には朗報だと思います。

---
# 終わりに

バイトコードのスライスを使いだすと、Rust本来の難しさが出てきますね。
今回のRustのコードは書いてみると数行なのですが、動作するようになるまでに４時間を要しました。
encodingクレートの使い方でも`encoding_from_whatwg_label`関数を使用するということがわからずに、かなり悩みました。Rustは簡単に書けそうなラスト１マイルがすごく遠い印象です。その過程は楽しいのですが、万人向けではないですね^^。

それでも、一度コードを書いてしまえば鉄壁な安全性を享受できるというのは、NIFのような低レベルでは大きなアドバンテージとなります。

さて、今回でRustlerの連載は最後のつもりでしたが、OTP20の最新機能が盛り込まれていることがわかりましたのでもう少し深堀します。次回は[「Elixirから簡単にRustを呼び出せるRustler #5 NIFからメッセージを返す」](https://qiita.com/twinbee/items/cd818fd509ace2ae0d0a)です。

明日は@zacky1972さんの[**「ZEAM開発ログv0.1.6 Elixir から Rustler で GPU を駆動しよう〜ElixirでAI/MLを高速化」**](https://qiita.com/zacky1972/items/c998cad40ff9f2a42fde)になります！

PS. 次回予定を「コールバック」から「メッセージ送信」に変更しました。

:stars::stars::stars::stars::stars:　満員御礼！Elixir MeetUpを6月末に開催します　:stars::stars::stars::stars::stars:
<font color="red" bold>※スミマセン、増枠分も埋まってしまいました．．．が、ほんの多少なら、もうちょい入れるかも</font>

[「fukuoka.ex#11：DB／データサイエンスにコネクトするElixir」](https://fukuokaex.connpass.com/event/87241/)を6/22（金）19時に開催します。
私もETSとFlowを題材に発表いたしますので、ぜひ興味ある方はご参加ください！

![fukuokaex.png](https://qiita-image-store.s3.amazonaws.com/0/215989/16ba72c0-05ac-ac73-79bd-efc39216f436.png)