11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-05-30

(この記事は、「fukuoka.ex(その2) Elixir Advent Calendar 2017」の11日目、Webパフォーマンス Advent Calendar 2017 2017の4日目です)

昨日は@tuchiroさんのElixirでSI開発入門 #7 Multiで使う関数を再利用可能な粒度に分割するでした!

本連載の記事はこちらです。
|> ElixirのGenStageに入門する #1
|> ElixirのGenStageに入門する#2 バックプレッシャーを理解する
|> Elixir並列処理「Flow」の2段ステージ構造を理解する
|> Elixirから簡単にRustを呼び出せるRustler #1 準備編
|> Elixirから簡単にRustを呼び出せるRustler #2 クレートを使ってみる
|> Elixirから簡単にRustを呼び出せるRustler #3 いろいろな型を呼び出す

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

4/27から、30日間に渡り、毎日お届けしている「季節外れのfukuoka.ex Elixir Advent Calendar」「季節外れのfukuoka.ex(その2) Elixir Advent Calender」ですが、Qiitaトップページトレンドランキングに8回入賞、Elixirウィークリーランキングでは5週連続で1/2/3フィニッシュを飾り、各種ランキング通算で、トータル69回ものランクイン(前週比+60.1%)を達成しています

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

image.png


はじめに

今回は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を使います。

Rust用語

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

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

##Elixirのバイナリの扱い

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

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

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

Rustのバイナリの扱い

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

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

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

コード解説

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

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クレート公式サイト内の"EUC-KR"をDecodeするテストコードを参考にしました。

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


郵便番号データ

さて、今回のNIFを使ってShift-JISデータの代表格である、日本郵便の郵便番号データをコンバートしてみましょう。

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

  • ken_all.csv

(ソース全体はGithubにアップしました。)
2018/9/29 パッケージ名mbcs_rsでhex公開しました !
NifExample.sjis2utf8MbcsRs.decode!で実現されております。)

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

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

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を起動して、動作を確認します。

$ iex -S mix
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のコードは書いてみると数行なのですが、動作するようになるまでに4時間を要しました。
encodingクレートの使い方でもencoding_from_whatwg_label関数を使用するということがわからずに、かなり悩みました。Rustは簡単に書けそうなラスト1マイルがすごく遠い印象です。その過程は楽しいのですが、万人向けではないですね^^。

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

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

明日は@zacky1972さんの「ZEAM開発ログv0.1.6 Elixir から Rustler で GPU を駆動しよう〜ElixirでAI/MLを高速化」になります!

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

:stars::stars::stars::stars::stars: 満員御礼!Elixir MeetUpを6月末に開催します :stars::stars::stars::stars::stars:
※スミマセン、増枠分も埋まってしまいました...が、ほんの多少なら、もうちょい入れるかも

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

fukuokaex.png

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?