Rustでブラウザからバイナリをパースする

この記事はRust Advent Calendar 2020 21日目の記事です。

お仕事のメインはRuby/TypeScriptを使っていますが、新しいことを学びたいなーと思って、Rustを勉強し始めてみました。
今回はWebAssemblyを使って、ブラウザからバイナリファイルをパースする処理を書いてみます。

RustをWebAssemblyに変換する

この手の記事は沢山あるので、ざっくりやり方だけ書いておきます。
wasm-bindgenwasm-packをインストールして、

# Cargo.toml
[package]
name = "app"
version = "0.0.1"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.63"
# src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
struct ApplicationRunner();

#[wasm_bindgen]
impl ApplicationRunner {
    pub fn new() {}
}
# index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <script type='module'>
      import init, { ApplicationRunner } from './app.js'

      (async () => {
        await init('/app.wasm')
        new ApplicationRunner()
      })()
    </script>
  </body>
</html>

あとは wasm-pack を使ってコンパイルすれば完成です。 めちゃ簡単。

$ wasm-pack build --no-typescript --dev --target web --out-dir ./public

ファイルを読み込む

続いて、ブラウザから、ファイルを指定してRustにパースさせる導線を作っていきます。
先ほど定義した ApplicationRunner をnewしたタイミングで、イベントをlistenしたDOMをレンダリングしておきます。

DOMの生成には virtual_dom_rsweb_sysjs_sys を使います。
すごく単純な例では、こんな感じでDOMをマウントすることができます。

let vdom = html! { <div></div> };
let window = web_sys::window().unwrap()
let dom_updater = DomUpdater::new_append_to_mount(vdom, &window.document().unwrap().body().unwrap());
dom_updater.update(vdom);

今回はファイルの読み込みをしたいので、vdomの中身を書き換えて、inputタグにイベントを仕込んだDOMにしてみます。

html! {
  <input
    type="file"
    onchange=move |event: web_sys::InputEvent| {
        let target = event.target().unwrap();
        let input = target.dyn_ref::<HtmlInputElement>().unwrap();
        let files = input.files().unwrap();
        let file = files.item(0).unwrap();

        let file_reader = web_sys::FileReader::new().unwrap();
        file_reader.read_as_array_buffer(&file).unwrap();

        let mut onload = Closure::wrap(Box::new(move |event: Event| {
            let file_reader: FileReader = event.target().unwrap().dyn_into().unwrap();
            let file = file_reader.result().unwrap();
            let file = js_sys::Uint8Array::new(&file);

            let mut bytes = vec![0; file.length() as usize];
            file.copy_to(&mut bytes);

            // ここにファイルの中身のパースを開始する処理を追加する
            do_something(&bytes)
        }) as Box<FnMut(_)>);

        file_reader.set_onload(Some(onload.as_ref().unchecked_ref()));
        onload.forget();
    }
  >
}

ファイルをパースする

ファイルの読み込みする処理が書けたので、次はファイルをパースしていきます。
単にバイナリを読んでいくだけであれば std::io::Cursor だけでいけますが、little endianを読み込もうとするとやや操作が面倒です。

そこで、byteorderを使って読み込んでいきます。
下記の例では、今後UTF8の文字列変換や特定のバイナリに特化した読み込みに対応する時に拡張しやすくするために、読み取り用のwrapperを用意しました。
単純なzipファイルのパースであれば、file name fieldやextra fieldなどの可変長のfieldがあるため、指定長でvecやstringを返すメソッドも用意するといいかもしれません。

use byteorder::{BigEndian, ByteOrder, LittleEndian, ReadBytesExt};

pub struct Binary<'a> {
    cursor: Cursor<&'a [u8]>,
}

impl<'a> Binary<'a> {
    pub fn new(bytes: &[u8]) -> Binary {
        Binary {
            cursor: Cursor::new(bytes),
        }
    }

    pub fn position(&mut self) -> u64 {
        self.cursor.position()
    }

    pub fn read_little_u16(&mut self) -> Result<u16, Error> {
        Ok(self.cursor.read_u16::<LittleEndian>().unwrap())
    }

    pub fn read_little_u32(&mut self) -> Result<u32, Error> {
        Ok(self.cursor.read_u32::<LittleEndian>().unwrap())
    }

    pub fn read_u16(&mut self) -> Result<u16, Error> {
        Ok(self.cursor.read_u16::<BigEndian>().unwrap())
    }

    pub fn read_u32(&mut self) -> Result<u32, Error> {
        Ok(self.cursor.read_u32::<BigEndian>().unwrap())
    }

    pub fn read_i16(&mut self) -> Result<i16, Error> {
        Ok(self.cursor.read_i16::<BigEndian>().unwrap())
    }

    pub fn read_i32(&mut self) -> Result<i32, Error> {
        Ok(self.cursor.read_i32::<BigEndian>().unwrap())
    }
}

パース処理を書いていく

過去にRubyでpsdやzipのパーサを書いたことがあるのですが、これらは仕様書通りの単純な構造だったので、同様に先頭から読み込んでstructに詰めていくだけでいけそうです。
今回はzipのヘッダーをパースする処理の書いてみます。

// == LocalFileHeader
//
// local file header signature     4 bytes  (0x04034b50)
// version needed to extract       2 bytes
// general purpose bit flag        2 bytes
// compression method              2 bytes
// last mod file time              2 bytes
// last mod file date              2 bytes
// crc-32                          4 bytes
// compressed size                 4 bytes
// uncompressed size               4 bytes
// file name length                2 bytes
// extra field length              2 bytes
struct LocalFileHeader {
    pub signature: u32,
    pub version: u16,
    pub general_purpose_bit_flag: u16,
    pub compression_method: u16,
    pub last_modified_file_time: u16,
    pub last_modified_file_date: u16,
    pub crc_32: u32,
    pub compressed_size: u32,
    pub uncompressed_size: u32,
    pub file_name_length: u16,
    pub extra_field_length: u16,
}

impl LocalFileHeader {
    pub fn from_bytes(bytes: &[u8]) -> Result<LocalFileHeader, Error> {
        let mut binary = Binary::new(bytes);

        let signature = binary.read_little_u32()?;
        let version = binary.read_little_u16()?;
        let general_purpose_bit_flag = binary.read_little_u16()?;
        let compression_method = binary.read_little_u16()?;
        let last_modified_file_time = binary.read_u16()?;
        let last_modified_file_date = binary.read_u16()?;
        let crc_32 = binary.read_u32()?;
        let compressed_size = binary.read_u32()?;
        let uncompressed_size = binary.read_u32()?;
        let file_name_length = binary.read_u16()?;
        let extra_field_length = binary.read_u16()?;

        Ok(LocalFileHeader {
            signature,
            version,
            general_purpose_bit_flag,
            compression_method,
            last_modified_file_time: last_modified_file_time,
            last_modified_file_date,
            crc_32,
            compressed_size,
            uncompressed_size,
            file_name_length,
            extra_field_length,
        })
    }
}

とても簡単ですね。

パースした内容をHTMLにレンダリングする

では、最後にパースした内容をHTMLへレンダリングしてみます。
諸々省略しているのでこのままでは動きませんが、雰囲気としてはこんな感じでDOMを生成して、初めに作ったDomUpdaterのupdateに返り値のVirtualNodeを渡せばOKです。

// let local_file_header = LocalFileHeader::from_bytes(&bytes)
// let vdom = render(...)
// dom_updater.update(vdom); という感じで使う
fn render(local_file_header: &LocalFileHeader) -> VirtualNode {
    html! {
      <div>
        <span>
          signature: {format!("{:X}", local_file_header.signature)}
          <br>
          version: {format!("{:X}", local_file_header.version)}
          <br>
          general_purpose_bit_flag: {format!("{:X}", local_file_header.general_purpose_bit_flag)}
          <br>
          compression_method: {format!("{:X}", local_file_header.compression_method)}
          <br>
          last_modified_file_time: {format!("{:X}", local_file_header.last_modified_file_time)}
          <br>
          last_modified_file_date: {format!("{:X}", local_file_header.last_modified_file_date)}
          <br>
          crc_32: {format!("{:X}", local_file_header.crc_32)}
          <br>
        </span>
      </div>
    }
}

まとめ

今回はwasm-bindgenを使ってブラウザでファイルをパースする処理を書いてみました。
もう少しパース処理を作り込んでいけば、画像変換やzipの再圧縮などいろいろな処理を書くことができそうです。

可能であれば、仕事でもちゃんとRust書いてみたいのでReal Worldの活用事例をもっと知りたいなぁと思いました(´・ω・`)

kamipoさんのアドバイスと拠点を跨いだ勉強会

この記事は京都開発拠点アドベントカレンダー 18日目の記事です。

マネーフォワードには開発拠点が複数ありますが、技術情報はSlackで議論・共有されています。
今日はそんなSlackの中から、東京拠点と京都拠点を跨いで開催した「Railsへコントリビュートする勉強会」について、東京拠点のアルパカ隊長が紹介します。

はじまりのSlack

ふとした時に、社内SlackのRubyチャンネルでこんな会話がありました。

@alpaca-tc @alpaca-tc

程よい難易度で、kamipoさんが「これ自分がやらんでもええやろ」みたいなissueがあれば

このslackに投げてもらって

誰かシニアエンジニアがサポートしつつ、新卒がRailsコントリビュートするみたいなのやりたい:eyes:

すると、kamipoさんからすぐ解決できるものは見た瞬間に直してしまっていると前置きがあった上で、 こんなissueを教えてもらいました。

@kamipo @kamipo

簡単じゃないけど直したほうがいいと思ってるやつだとたとえば rails/rails#35204 とか。

これはautomatic_inverse_ofがバグっててinverse_ofって一対一の関係になるはずなのに

belongs_to :user と belongs_to :writer の両方ともinverse_ofが :user と誤認されるってバグで、

起きてることに気づきづらいから直したほうがいいんだけど

inverse_of: false すれば回避できるから優先順位が低い、とかそういう系のはいっぱいある。

ふーむ、パッと見はよく分かりません。程よい難易度を感じます。
追加でかなり詳しいアドバイス(ほぼ答え)をいただいて、早速チャレンジですヽ(・∀・ )ノ

拠点を跨いで勉強会を開催

昨今のコロナ禍によってオンライン化が一気に進みました。(僕も、今年は数回しか出社していません)
おかげで、拠点間を跨ぐオンライン勉強会も突発的に開催できるようになりました。

当日の流れ

今回は、京都の開発メンバーを含めた数人のメンバーを募り、Discordで話しながらkamipoさんのアドバイスを紐解いていくことになりました。

まずはセットアップ

Contributing to Ruby on RailsからActiveRecordに関するセットアップ手順を抜粋して、各自でローカル環境をセットアップしていきます。
その上で、先ほどのissueを再現するテストコードを書いて、テストが失敗する状態を再現しました。

当日のセットアップ手順書

続いて調査

そして、全員の環境でテストが失敗する状態になったら、kamipoさんにいただいたアドバイスを調査していきます。

@kamipo @kamipo

このissueを参照してるここ rails/rails#36708 でも言及してるんですが、

automatic_inverse_ofは改善可能で改善すれば自ずとこの問題も解決されます。具体的には :foreign_key オプションをinverse_of判定に含めることでこれまで判定不能だったinverse_of判定も可能になります。

このissue自体はforeign_key未指定の同一クラス参照してるassociationがある場合にforeign_keyの一致を判定せずにクラスが一致するかどうかだけでinverse_of判定してることが問題なのだと思います。

ざっくり言うと、このissueはモデルの関連を指定した時に、判定の条件漏れがあることが原因のようです。
みんなでActiveRecord::Reflection#automatic_inverse_ofあたりを読み進めて、inverse_ofの判定処理に誤りがあることを発見しました。

PRが完成

わいわい話しながらPRができたところで、kamipoさんにPRを見てもらって更にアドバイスをいただきました。

  • よほどのことがない限り1PR 1commitになるようにするのがのぞましい contribution guide
  • レビュアーがまだ知らない情報は、ぜんぶPRに含めてくれるほうがレビューのハードルが下がるので頑張って欲しい
    • レビュアーが、わざわざこれは正しい変更だと解説コメントしなくてもいいぐらい書くといい
    • 完全さよりもコンテキストを共有する気持ちが重要

貴重なアドバイスをいただきながら、完成したPRがこちらです。

rails/rails#40643

このPRは無事マージされ、Rails6.1.0でリリースされています🎉

Slackでの四方山話

今回は勉強会の話を抜粋しましたが、普段からSlackでは様々なトピックの技術系の話題がなされています。
個人的にも、kamipoさんにアドバイスをいただけたおかげで、直近1ヶ月で4つほどPRを送ることができました。

ひとりでOSSへ取り組むのは難しさがありますが、Slackで議論ができるとハードルがグッと下がって踏み出しやすくなりますね。

まとめ

今回の勉強会の件もそうですが、弊社では拠点に関係なくこういった技術の話題でワイワイできます。 ぜひ皆さんも遊びにきてくださいヽ(・∀・ )ノ

まとめ 「kamipoさんはすごい人」


(※kamipoさんは弊社社員ではなく、善意でSlackのチャンネルに入ってくださっています。)

ピクシブを退職しました!

6月末で株式会社ピクシブを退社したので、退職エントリを書くゾヽ(・∀・ )ノ
エントリを通じて、エンジニアとして働く誰かの参考になれば嬉しいゾ

会社について

ピクシブは「お絵かきがもっと楽しめる場所を作ること」を基本理念として、
pixiv.netを中心とした創作活動にまつわるWebサービスを運営する会社です。

入社したタイミングは、 これから新規事業をどんどんやる! というタイミングでした。
(入社当時のエントリはこちら→めまぐるしい1年!)

これまでやってきたこと

イラストやアニメといった文化に全く触れてこなかった僕にとって、
ピクシブで過ごした3年間は全てが新鮮で楽しいものでした( ゚∀゚)o彡

入社してすぐにpixivFACTORYの立ち上げをしました。
開発リーダーになってから3年間で、pixivFACTORYの大きな機能の多くを担当させてもらいました。

我が子のような可愛い可愛いサービスですね。

新卒2年目ごろからは、Railsおじさんとして
レガシー殺すマン、Rails-Wayマンとして、社内のプロジェクトに関わってきました。

退職が決まってからの最後の3ヶ月は、pixivFACTORYを離れてpawooの開発をしていました。
Mastodon本家に高速化やバグ修正のパッチを投げていたら、GithubのGraphでトップ4に入るぐらいだったので、退社直前までそれなりにはやっていたと思います。

なんで辞めるの?

タイミングがたまたま良かったからです。

引き継ぎをして新しい挑戦をしようというタイミングで、お話をいただきました。
その事業ドメインが魅力的だったので、エイヤッと転職することにしました。

転職は何を期待しているの?

次は、いわゆるFintechの会社にいきます。
pixivに入った時と同じように、何も前提知識を知らない業界だったというところに惹かれました。

何も知らない、分からないというのは、なんでもできることの裏返しなのできっと面白いですね。

何をするかは決まってませんが、きっと新しい会社でも楽しめるかなと思います。(´・ω・)

これから何するの?

8月まで1ヶ月のお休みをいただいています!
元同僚がやっているアフリカの事業をお手伝いしつつ、 7/21からはSplatoonに費やしたいと思っております。

また、日本でまだ行ったことがない都道府県が残り5県なので
各地をまわり、地元のおっちゃんと地酒を飲み語らいながら過ごしたいと思います。

おわりに

新卒として東京に出てきて、
この3年間を、pixivで働けたことは最高の日々でした。

学生時代から何社も経験していますが
あんなに楽しい会社もなかなかない ので、本当にオススメします!

最後に例のもの貼っておきます。何かいただけたら飛び跳ねて喜びます。ヽ(・∀・ )ノ

財布を無くして絶望マンのウィッシュリスト
(旅行初日の本日、9万ぐらい入った財布とカードを落として地方で死に絶えそうです。マジで…。)

3年間、本当にありがとうございました。

シェアリンクの作り方

最近Railsでシェアリンクを実装したので紹介します。 みんなはどうやって実装しているのだろう?

↓こんなURLね

https://alpaca.tc/shares/uIx90S

実装面白かったので紹介してみます><

ちなみに僕の実装方法は、ポリモーフィックなカラムを持つshare_linksテーブルを作って、シェアしたいrecordを紐付けることで実現した。

ユニークなキーが必要なためidを使用していてに、2度クエリを発行しているのだけど、パフォーマンスを気にするならLAST_INSERT_IDを使って一度のクエリで済ます方が良い。

class CreateShareLinks < ActiveRecord::Migration
  def change
    create_table :share_links do |t|
      t.references :sharable, polymorphic: true, null: false
      t.string :uuid
      t.timestamps null: false

      t.index :uuid, unique: true
      t.index [:sharable_type, :sharable_id], unique: true
    end
  end
end
# lib/share_link_uuid.rb
require 'digest/sha2'

module ShareLinkUuid
  # URLで使用可能な文字列
  HEX64 = [*('0'..'9'), *('A'..'Z'), *('a'..'z'), '_', '-'].freeze

  def self.generate(id, length)
    digest = Digest::SHA512.hexdigest(id.to_s).to_i(16).to_s(8)

    uuid = digest.gsub(/\d{2}/) do |token|
      HEX64[token.to_i(8).to_s(10).to_i]
    end

    uuid[0...length]
  end
end

# app/models/share_link.rb
class ShareLink < ActiveRecord::Base
  UUID_LENGTH = 6

  belongs_to :sharable, polymorphic: true, required: true
  validates :uuid, absence: { if: :new_record? }, presence: { if: :persisted? }
  validates :sharable_type, inclusion: { in: %w(Item) }
  after_create :save_uuid

  private

  def save_uuid
    @uuid_length ||= UUID_LENGTH
    update_uuid!(@uuid_length)
  rescue ActiveRecord::RecordNotUnique
    # UUIDの長さを増やして再試行
    @uuid_length += 1
    retry
  end

  def update_uuid!(length)
    uuid = ShareLinkUuid.generate(id, length)
    update!(uuid: uuid)
  end

  module Sharable
    extend ActiveSupport::Concern

    included do
      has_one :share_link, as: :sharable, dependent: :destroy

      scope :shared, -> { joins(:share_link) }
      scope :not_shared, -> { where.not(id: shared) }
    end
  end
end

# app/models/item.rb
class Item 
  include ShareLink::Sharable
end
Rails.application.routes.draw do
  resources :shares, only: [:create], controller: :share_links
end

class ShareLinksController < ApplicationController
  def create
    @share_link = ShareLink.new(share_link_params)

    if @share_link.save
      render format: :json
    else
      render :error
    end
  end

  private

  def share_link_params
    params.require(:share_link).permit(
      :sharable_type, :sharable_id
    ).merge(user: current_user)
  end
end

うーん、もっと良い方法ありそうだナ…

東京に慣れてきた

昨年の6月に越してきて、あと3ヶ月ほどで東京に来て一年!?
時間が経つのは早いものですねー…。

今年の一年を振り返って。

憧れの東京に来て、なんだか目的もなく一年が過ぎかけてました。
ずっと東京に憧れていて、夢が叶っちゃったら長い間本当に無目的で生きてましたね:(

あの勉強と仕事のストイックな3ヶ月はどこへやら…

お仕事。

6月に入ってからBOOTHにアサインされて、数ヶ月を過ごし
その後、pixivFACTORYのメインエンジニアとしてコード書いてました。

pixivFACTORYが出た後に、ユーザーさん達が嬉しい言葉を言ってくださるのをTwitterで眺めていて、

ゆるやかーに…「ボッ」 っと火が着いた今日この頃です。

最近は、仕事が最高に楽しい。
実は入社する前にこの会社に期待していたことはずっとやらせて貰えていて、文句は無いはずなのにイマイチ目的が無くてやる気が出てませんでした。

でも、自分で作ったサービスを使ってくださるユーザーさんの声を聞いて、あーーーーーーやらねば…!!!とやる気が漲ってきている今日この頃です。

惜しむべくはpixivはホワイト企業なところ。もうブラック企業に生きていきたい。

プライベート

アートの趣味がかなり充実していました!

東京ってやっぱりすごいですよ!
ギャラリーが多いのでプラ〜っと出かけて適当にオシャレそうなカフェやギャラリーに入っても楽しめる。すごい!

就活前にギャラリーで展示されてた方と仲良くなって、そのままプライベートのお付き合いしてたり、友人伝いで色んな人と会えたり、すごい!!

岐阜や関西に居た頃では考えられないですね。

あと、不意打ちだったんですが、忘年会やなんやらと前職や前々職の方に声を掛けてもらえて嬉しかったです。

残りちょっと

もうすぐ入社式です。
新卒になるのでまたゆるく頑張ります。