mizdra's blog

ぽよぐらみんぐ

ゆかりスロットをWeb Share APIに対応させた

iOSのSafariにWeb Share APIが来るという話を聞いたので, 趣味で作っている「ゆかりスロット」で試してみることにした. 2月2日時点でまだiOS 12.2はリリースされていないので, Stableな環境で動かしたいならChrome 61+がインストールされたAndroid端末が必要.

developers.google.com

yukari-slot.mizdra.net

www.mizdra.net

実装の詳細

navigator.sharetitle, url, text の3つのoptionalなパラメータを渡して呼び出すと, 共有可能なアプリ一覧が載ったOSネイティブのあのシートが画面下から生えてくる. それぞれのパラメータの対応状況は共有先のアプリに依存していて, 処理されたり無視されたりする. 自分が試した限りではSlack for Androidは3つとも見てくれたけど, Twitter for Androidは texturl しか見てくれなかった.

function shareResult(leftEye: number, rightEye: number) {
  const text =
    leftEye === 1 && rightEye === 1
      ? 'ゆかりちゃん完成!!!'
      : 'ゆかりスロット失敗 😥'
  const url = `https://yukari-slot.mizdra.net/share/${leftEye}${rightEye}`

  navigator
    .share({
      title: 'ゆかりスロット',
      text,
      url,
    })
    .then(() => console.log('shared!'))
    .catch((e) => console.error(e))
}

navigator.share に渡す引数が不正の場合は TypeError , ユーザアクション契機による呼び出しでなければ NotAllowedError , シェア先のアプリが無い場合やユーザがシェアをキャンセルした場合は AbortError が投げられる. ゆかりスロットでは navigator.share がない環境やシェアに失敗した場合はは Twitter Web Intentでシェアするようfallbackしている.

function createShareData(leftEye: number, rightEye: number) {
  const text =
    leftEye === 1 && rightEye === 1
      ? 'ゆかりちゃん完成!!!'
      : 'ゆかりスロット失敗 😥'
  const url = `https://yukari-slot.mizdra.net/share/${leftEye}${rightEye}`
  return { text, url }
}

function createTweetLink(text: string, url: string) {
  const encodedText = encodeURIComponent(text)
  const encodedHashtags = encodeURIComponent('ゆかりスロット')
  const encodedUrl = encodeURIComponent(url)
  // Twitter Web Intentの場合は hashtags パラメータでハッシュタグを設定する
  return `https://twitter.com/intent/tweet?text=${encodedText}&hashtags=${encodedHashtags}&url=${encodedUrl}`
}

async function share (
  leftEye: number | undefined,
  rightEye: number | undefined,
) {
  const { text, url } = createShareData(leftEye, rightEye)

  try {
    await navigator
      // ハッシュタグを付加して共有
      .share({ text: `${text} #ゆかりスロット`, url })
  } catch (e) {
    // AbortError はユーザがシェアをキャンセルした場合の
    // エラーなので無視する
    if (e.name === 'AbortError') return

    // navigator.share がない環境やシェアに失敗した場合は
    // Twitter Web Intentにfallbackする
    window.open(createTweetLink(text, url))
  }
}

今の所雑に text パラメータにハッシュタグを含めているのでSlackにシェアするとチャンネルへのリンクになって残念なことになっている. 真面目に実装するならTwitterにシェアするボタンとWeb Share APIを呼び出すボタンは別々にしてユーザが選択できるようにすると良さそう.

f:id:mizdra:20190202020352p:plain:w300
Slackにシェアした様子. ハッシュタグが上手く機能していないことが分かる.

ローカルでデバッグする

Web Share APIは (現状Stableなバージョンでは) Chrome for Androidでしか動かないため, Android端末を用いてデバッグすることになる. またAPIはhttpsなホスト or localhost にのみ提供されており, Web Share APIに対応しているブラウザであってもhttpなホスト上で navigator.share を参照すると undefined が返ってくる. つまり, APIをローカルでデバッグするには開発マシン上の開発サーバに対して, Android端末から localhost ホストでアクセスする必要がある.

これはChrome Dev ToolsのPort Fowarding機能を使うと簡単に実現できる. Remote devices タブの Settings から以下のように開発マシンのportとモバイルデバイスのportのマッピングを指定できる.

f:id:mizdra:20190202020023p:plain
開発マシンの localhost:8080 とAndroid端末の localhost:8080 をマッピングしている様子

developers.google.com

『プログラミングRust』輪読会における取り組みについて

はじめに

この記事はwhywaita Advent Calendar 23日目の記事です.

adventar.org

今日はwhywaitaさんと僕が所属している技術系学生サークル「MMA」ので行われている活動である『プログラミングRust』輪読会について, 開催に至った経緯とその様子を紹介します.

『プログラミングRust』輪読会 とは

弊サークルでは部員の技術力向上, 部員同士の交流などを目的に, 部員有志の間で技術勉強会が定期的に開催されています. とある分野を勉強したい部員が集まってその分野について勉強するという形式で, ただ黙々と作業するものから講義スタイルのものまで様々です. 以下のようなテーマの勉強会がここ数年で開催されてきました.

  • Haskell講習 (2016/4 〜 2016/6)
    • Haskellの入門書であるすごいH本を読み進めていく会
  • Scala Collection Library Code Reading (2017/4 〜 2017/8)
    • Scalaのコレクションライブラリのコードを読む会
  • CTF勉強会 (2017/4 〜 現在)
    • 2017年は講義形式, 2018年はCTFの常設問題に取り組みCTFの勉強をしている
    • メンバー同士でオンラインコンテストへ出場
  • 競技プログラミング勉強会 (2018/11 〜 現在)
    • 競技プログラミングの常設問題を一緒に解いたり, アルゴリズムの勉強をする
    • メンバー同士でオンラインコンテストへ出場
  • 『プログラミングRust』輪読会 (2018/10 〜 現在)

『プログラミングRust』輪読会はこの勉強会の一環として開催されているものです. 元々, 僕がRustという言語を勉強したくて『プログラミングRust』を書籍を購入したのですが中々読む時間が取れず, いわゆる積読本となっていました. また書籍自体もとても分厚く (約600P), またRustという急な学習曲線を持つ言語がテーマであることも読書を躊躇する理由となっていました.

プログラミングRust

プログラミングRust

  • 作者: Jim Blandy,Jason Orendorff,中田秀基
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2018/08/10
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

流石にそのまま放置していても良くないので, 何かしら手立てを考えて読み進める方法を模索することにしました. そこで取った選択肢が「輪読会」です. 個人で分厚い本を読むと途中で飽きたりしがちですが, 輪読会駆動で定期的に読む仕組みを取り入れることで, 読書を強制することができます. それ以外にも以下のような様々な効果が期待できると言われています.

  • 一人で読むにはパワーが必要な本を全員で読みきる
    • 分担して読むことで負担を減らせる
  • 理解が難しい本を全員で理解する
    • 普段触れていない分野が題材の場合でも、有識者の補足をもらえる
    • 別の分野の視点から良いところ、悪いところなどの意見や議論ができる
    • より実際に運用しているサービスに当てはめて考えやくすなる
  • 触れたことのない分野を皆でキャッチアップ
    • 全員が一度に知識をキャッチアップできるので属人化になりにくい

輪読会がうまく回りはじめた話 - LCL Engineers' Blog

幸いにも部員の中にRustに興味がある人が多数居たので, すぐに開催へと漕ぎ着けることができました.

輪読会を始める

開催に関するコンセンサスが取れたので, 輪読会のルールについて決めていきます. 具体的には開催日はどうするか (週1か週2か隔週か), 1回あたりの分量はどうするか (節ごとか章ごとか), 音読するか, 予習は必須か, 予習した範囲を要約した資料を課題として作るかどうか, などといったことを決定します. これらは「実際に本の中で紹介されているコードを試しながら読み進めて欲しいので予習必須」, 「分量が多いので要約資料必須」など, 本の特性に合わせて決めていくと良いかと思います. 今回は以下のような基準で輪読会のルールを決めていきました.

  • スケジュールが調整しやすいので週1ごと開催
  • 言語機能ごとにまとめて学びたいので章ごと読む
  • 1章あたりの分量が多いので音読は無し
  • 本の中で紹介されているコードを試しながら読み進めて欲しい&当日読む時間が勿体無いので, 予習は必須
  • 分量が多いので要約資料必須
  • 属人化を防ぐため, また主催の負荷を軽減するため要約資料は持ち回り制の担当者が作成

ルールが決まったら, 輪読会の概要と共にアナウンスを流します. この際, 輪読会の難易度や雰囲気を明確に伝えるため, またミスマッチを防ぐため「対象者」という項目を書いておくのがポイントです. 以下は実際にサークルのメーリングリストに流したアナウンスです.

「『プログラミングRust』輪読会」開催のお知らせです. Rustの入門書が最近発売されたのですがとっても分厚いので,
輪読会駆動で頑張って読み進めようという会です.

- 輪読する本: https://www.oreilly.co.jp/books/9784873118550
- Rustについて: https://employment.en-japan.com/engineerhub/entry/2017/07/10/110000

Rustは"非常に急な"学習曲線を持つ難しい言語ですが, 沢山人が集まれば誰かしらは分かっている人が居るので質問し合いましょう.
議論して様々な視点から理解を深め合えるかも.

参加希望者は以下の調整さんから都合の合う日を入力して下さい :pray:

- https://chouseisan.com/s?h=XXXXXXXXXXXX

## 概要
- 輪読会駆動で『プログラミングRust』を最後まで読む
    - 毎週1章ずつ読んで半年〜1年で全章読破という気持ち
- 開催日
    - 週1で開催予定
    - 調整さんの結果を見て決めます
- 対象者
    - 参加は自由ですが, プログラミング初心者にはRustは難しすぎるのでオススメしません
    - 以下のような経験があると初学が少し楽になります
        - C/C++などのシステムプログラミング言語を触ったことがある
        - 式指向言語を触ったことがある
        - 関数型プログラミングを取り入れた言語を触ったことがある
        - パターンマッチングがある言語を触ったことがある
        - ジェネリクス(ジェネリック/総称型)がある言語を触ったことがある
- 場所: 部室 (予定)
- 持ち物: 『プログラミングRust』の物理本または電子書籍 (各自で用意)
- 連絡用Slackチャンネル: #programming-rust


## 進め方
- 当日は質問や議論に集中しましょう
- そのために…
    - 参加者は予習をお願いします :pray:
        - 参加者は当日扱う範囲を事前に読んできて下さい
        - 完全に理解できなくてもOK. 1人で無理せず当日質問して解決しましょう
    - "担当者"は予習範囲の要約をお願いします
        - 担当者は予習範囲を要約したものを当日持ってきて下さい
        - 当日は主にその内容を元に議論をします
        - 担当者は毎週持ち回りします
- 当日の流れ
    1. 担当者が要約した内容について話す
    2. 要約を元に皆で議論する


## 補足
- 予習について
    - 予習範囲で分からないことがあったらSlackで聞いたり, メモして当日共有すると良いです
        - 1人で悩む時間は勿体無いので相談しよう! きっと誰か教えてくれる!
- 要約について
    - 形式は自由
        - `.txt` でも `.md` でもOK
        - スライドでもただの文章でもOK
        - 箇条書きでもOK
    - 本に書いてあることを要約に全部含める必要はないです
        - 自分が特に重要だと思った所を要約すると良いかも
        - 元の文章の主張や全体像を簡単に把握できればOK
            - 詳細は書籍を当たれば良い


## 第1回の案内
- 読む範囲: 1〜2章
- 担当者: @mizdra
- 開催日
    - 決まり次第 #programming-rust に流します
    - 早ければ来週から, 難しければ再来週から

輪読会の進行の仕方

当日になったら部室に集まって, 初めに要約資料を担当者が読み上げます. 要約と言っても分量が多い章だとA4サイズで30Pくらいあるので, 要約資料もそれなりの分量になります. そのため, 大抵は質問&小休憩タイムは最後にまとめて取るのではなく, 節ごとに挟みながら30分〜80分程度掛けて進めています. 要約資料の読み上げが終わったら, 途中で拾いきれなかった疑問やちょっとした余談, 全体を通した感想などについて議論していきます. 要約資料の読み上げ中にSlackチャンネルに予めネタを放り投げておくと, 最後の議論タイムで話すネタをスタックできたりして便利です.

f:id:mizdra:20181223234620p:plain
6章の要約資料の読み上げ中のやり取り

実際にどのような質問と議論が行われたのかは, 第一回の輪読会の資料にログが残してあります *1. 以下で輪読会の資料の一部が公開されていますので, 参考までにどうぞ.

感想や反省など

まだ輪読会は7章までしか進んでいませんが, 各自が予習し, 資料を作成し, それを元に議論するというフローが上手く回っており, 無事軌道に乗ることができているようです. 自分は以前からTRPLでRustの勉強をしていて多少のRustの知識はあったのですが, 実際に書籍を読んだり他の参加者に指摘されたりして自分の誤解に気づく, という場面も何度かありました. 「お互いに理解を深め合う」ということが実現できており, 開催して良かったなと思っています.

一方で質問や議論のログが残っておらず, 輪読会に来ていない/来れなかった人に知見を共有できていない, 振り返りの時間が確保できていないなど反省点もあります. 今後はこうした改善点をきちんと洗い出し, 次回以降の勉強会に役立てていきたいですね.

以上, whywaita Advent Calendar 23日目の記事でした. 24日目は @yu_suke1994 さんの担当です!

*1:他の回は質問の議事録まで取っていなかったため, 残念ながら残っていません.

日向縁さんの誕生日をお祝いしてゲームを作った話

はじめに

この記事はゆゆ式 Advent Calendar 2018 9日目の記事です.

adventar.org

今回は日向縁さんの誕生日をお祝いしてゲームを作ったので, その紹介をします.

日向縁さんと目

皆さんは「日向縁」というキャラクターを聞いて初めに何を連想するでしょうか. ゆるふわ, お姫様, よく笑う, などなど. 人によって様々なイメージが浮かぶと思います. ちなみに僕は「1の目」を最初に連想しました. そう, 日向縁さんといえば作中でよく数字の「1」のような目をすることで知られています. 原作者の三上小又氏のツイートなどで「1の目」をネタにされていることで印象深い方も多いでしょう.

また, 日向縁さんの誕生日が11月11日であることに因んでこんなイラストも描かれています.

ゆかりスロット

かくいう僕も日向縁さんの誕生日をお祝いしてこんなゲームを作ってみました.

yukari-slot.mizdra.net

日向縁さんの目を揃えるスロットゲームです. 任意のタイミングで「とめる!」ボタンを押すと任意の数字が目として揃っていきます. 「11」が揃えば完成です.

またスロットの結果をシェアしたいというご期待に答えて, シェアボタンを用意してあります. 皆さんのお気に入りの縁さんをシェアできる最高便利機能です.

こんな感じで画像付きでツイートできる.

難易度調整に少し拘っていて, 目で追える速度で回っているけど中々狙って止められない程度になっています. 何度も挑戦して是非完成させてみて下さい.

開発時のちょっとした小ネタ

こちらはスロットを停止させた時に止めた目の取得に失敗して, 片目が undefined になった「undefinedゆかり」です.

f:id:mizdra:20181209232800p:plain:w400

そしてこちらはレイヤの重ね合わせの順番をミスった結果誕生した「ゆかりモンスター」です. 怪談とかに出てきそう.

f:id:mizdra:20181209233124p:plain:w400

おわりに

以上が ゆゆ式 Advent Calendar 2018 9日目「日向縁さんの誕生日をお祝いしてゲームを作った話」となります. いかがでしたでしょうか. ただ「11」を揃えて完成させるのを狙うだけでなく, 色んな目を揃えて日向縁さんの表情を楽しむのも良いかもしれません. 皆さんも是非ゆかりスロットで遊んで貰えればと思います.

10日目は@ekme_brbrさんの担当です!

ポケモンGO プレイ体験記

この記事はPokémon RNG Advent Calendar 2018 一日目の記事です.

adventar.org

今年もPokémon RNG Advent Calendarの季節がやって来てしまいましたが, 皆さんはいかがお過ごしでしょうか. 僕はポケモン最新作を発売初日に購入して真っ先に 「知的財産の表記」を読み, 起動して主人公とライバルの名前を決めた所で満足して寝ました. 主人公の部屋からまだ脱出できていない引きこもり太郎です.

さて今年は初日に何を書くのか色々悩んだのですが, 例年1日目の記事はボリュームのあるものとなっていて (参考1, 参考2), 「Advent Calendarに記事を投稿したことがない人にとって敷居が高いと思われてしまうのではないか」との声が寄せられていました. そこで今回は「ポケモンGO プレイ体験記」というテーマでゆるふわな記事を書いていこうと思います.

ポケモンGOとの出会い

僕がポケモンGOを初めて知ったのは, ポケモンGOの正式サービス開始前のフィールドテスト(ベータテスト)に参加していた知人から話を聞いたのがきっかけでした. 実はその知人はIngressをそこそこやり込んでいるプレイヤーで, 旗から見ていて随分楽しそうにプレイしていたので, 以前から位置情報ゲーム自体には興味を持っていました.

Ingressは陣取り合戦のために船を出して遥々離島まで行ったり, 海外まで行ったりする, あまり活発でない僕にとって近づきにくいゲームという印象 (偏見です) がありましたが, ポケモンGOはアメリカでの先行リリースでも社会現象を引き起こす程多くの人に遊ばれていたようなので, 特に抵抗感もなく始められました. 僕が前から好きだった「ポケモン」*1がベースになっているという点も後押しになっていたりします.

最近のプレイの様子

基本は大学への通学中にプレイしているという感じです. 電車での移動中は速度超過によるペナルティでボックス整理程度しかできませんが, 電車を降りた後の徒歩での移動で毎日40分程度ポチポチしてます. とはいってもそこそこ人通りの多い場所を通って通学しているので, 安全を考慮して最近は「おでかけモード」をONにした状態でスリープモードにして歩くことが多いです. おでかけモード, 便利ですよね…

またイベントなどに時期に合わせて, 時々公園 (お台場とか) や駅 (新宿駅/錦糸町駅) に行って数時間じっくりプレイするといったこともしています. リリース初期の頃は大宮公園 (地元から比較的楽に行くことができた) に行くことが多く, 午前午後ぶっ通しで遊んで帰る, みたいなムーブをしていました. 大宮公園は大宮駅からそこそこ距離があるものの, 第一・第二・第三公園の3つの公園からなる広大な敷地を持ち, それぞれ異なるポケモンの巣が設定されていたので, お気に入りの場所でした.

大宮第三公園. めっちゃ良い感じに撮れていますが, これはたまたま本格的なカメラを使って写真を撮っている人が近くに居て, その構図を真似して撮ってみたら良い感じになっただけです.

めちゃめちゃに歩いた様子が映ってる.

ちょっとしたテクニック

10kmタマゴは普段孵化させずスタックしておいて, アメや星の砂のボーナスが掛かっているイベント時にまとめて孵化させると, アメや星の砂を効率的に稼げて便利です. 星の砂は孵化以外でも手に入れる方法がありますが, 10kmから出てくるポケモンのアメは中々手に入らないので, 星の砂よりもアメボーナスがある時に孵化させるのがオススメです.

アメ2倍デーに5つ10kmを孵化させて, プテラx1/ヒンバスx1/ヨーギラスx2/ラッキーx1が出てきた様子.

おわりに

以上が Pokémon RNG Advent Calendar 2018 1日目「ポケモンGO プレイ体験記」となります. いかがでしたでしょうか. 最新作でポケモンGOと連携できるらしいのですが, まだ主人公の部屋から脱出できていないので, その機能を体験できていません. 頑張って進めようと思います…

それでは今日からクリスマスまでの間, Pokémon RNG Advent Calendar 2018 を楽しんでいきましょう. 良いクリスマスを!

2日目は @oupo さんの担当です!

*1:最新作はまだ主人公の部屋から出てない程度にはやってませんが…

WebAssemblyを使って乱数調整ツールをWebに移植した話

tl;dr

  • C++のツールをWebAssemblyを使ってWebに移植した
  • WebAssemblyへコンパイルする言語としてRustを, JS-WebAssembly間のバインディングにwasm-bindgenを採用した
  • 乱数計算処理をWebAssemblyで実装することで, C++実装と比べて0.2〜0.7倍, JS実装と比べて1〜13倍の性能が出た

はじめに

枠だけ確保してずっと放置していた去年のAdvent Calendarの記事がようやく書けたと思ったら, もう少しで今年のAdvent Calendarがやってくる季節になってしまいました. この記事は Pokémon RNG Advent Calendar 2017 23日目の記事です.

adventar.org

皆さんは「乱数調整」という言葉を知っているでしょうか? 乱数調整とは簡単に言うと計算機によってゲームのランダムな事象を予測し, 狙った結果を引き起こす行為のことです. 例えば, 乱数調整を行うことで珍しいアイテムや強いキャラを狙って入手したり, 貴重なイベントを狙って発生させたりできるようになります.

www.mizdra.net

この Pokemon RNG Advent Calendar はポケモンにおける乱数調整についての記事を書く Advent Calendar です. 今回はAdvent Calendarのネタとして7世代の孵化乱数*1に関するツールを作ったので, その紹介をします.

背景

計算機によってゲームのランダムな事象を予測するためには, 現在の乱数生成器の状態を特定することが不可欠です. 現在でいくつかの特定手法が確立されており, その内の1つに乱数生成器の初期化方法に注目して初期Seedを特定する方法があります. これは乱数生成器が32bitの初期Seedで初期化されていることを利用して, 32bit値の総当たりにより初期Seedを特定するものです.

これを利用した初期Seed特定ツールがおだんぽよ氏開発の search-tinymt-seed です. search-tinymt-seed では乱数生成器の初期化直後に生成された8つのタマゴの個体の性格を元に, 初期Seedを 2^32 通り総当たりで検索して初期Seedを特定します.

しかしこのツールはC++によって実装されており, 公式サイトで配布されているビルド済みバイナリが動かない場合はユーザが自力でビルドしなければなりません. そこで今回はWebAssemblyを使って search-tinymt-seed をWebへと移植し, ブラウザさえあれば誰でも簡単に利用できるようにしてみました.

ツールの紹介

以下のリンクからアクセスできます.

search-tinymt-seed-for-web.netlify.com

github.com

乱数生成器の初期化直後に生成された8つのタマゴの個体の性格を入力すると, その条件にマッチする初期Seedを出力してくれます. また計算方式としてJavaScriptとより高速に計算が可能なWebAssemblyの2種類の方式が選べます. デフォルトの入力 [れいせい, きまぐれ, のんき, おっとり, すなお, おだやか, まじめ, てれや] では初期Seed候補として [0x0B76DDAF, 0x261C6F52] が出力されるようになっています.

技術面について

ツールのコアとなる乱数計算処理にWebAssemblyを使用することで, Webツールでありながら高速で, (WebAssemblyをサポートする) ブラウザがインストールされたあらゆる環境から利用できるというポータビリティを実現しています. PC版のChromeやSafari, Firefoxに加え, モバイルのChromeやSafariなどでも動くはずです *2.

WebAssemblyへとコンパイルできる言語には C/C++/Rust/AssemblyScript などがありますが, 今回はRustを採用しました. エコシステムの枯れ具合を考慮するとC/C++でも良いですが, 最近になってRust/WebAssembly周りのエコシステムが充実してきたため, 素振りがてらRustを使ってみることにしました.

www.mizdra.net

WebAssemblyを使って開発する際にまず問題になるのはJS-WebAssembly間でのデータの受け渡しです. 現在のWebAssemblyの仕様では整数やIEEE754準拠の浮動小数点数などの基本的な値しか直接JS-WebAssembly間で受け渡しできないため, 構造体や文字列といったデータを受け渡す際は一工夫必要です. こうしたデータバインディングをWebAssemblyの仕様として取り入れる Host Bindings という Proposal も出ていますが, 現在仕様策定中でまだまだブラウザで利用できるような状況ではありません. そこで今回は Host Bindings のポリフィル的な位置づけにあるwasm-bindgenを使用し, データバインディングを実現しています.

hacks.mozilla.org

例えばツールでは以下のようにRust側からJavaScript側でexportされた関数を呼び出して, 進捗の表示を実装しています.

// /src/worker/worker.ts

// `foundSeeds` の型として `number[]` の代わりに `Uint32Array` を使い,
// Rust側から見えるようにexportする
export function postProgressAction (foundSeeds: Uint32Array, seed: number) {
  // UI スレッドに進捗を送信
  postMessage({
    type: 'PROGRESS',
    payload: {
      foundSeeds: Array.from(foundSeeds),
      calculatingSeed: seed,
    },
  } as Progress)
}
// /src/wasm/lib.rs

// wasm_bindgen アトリビュートで wasm-bindgen による
// データバインディングを利用することを宣言し,
// 合せてimportしたいJavaScriptの関数のシグネチャと
// その関数が宣言されている場所 (JavaScriptのモジュール) を指定する
#[wasm_bindgen(module = "../worker/worker")]
extern {
    // `&[u32]` と `Uint32Array`, `u32` と `number` が
    // それぞれ wasm-bindgen によって対応付けられる
    fn postProgressAction(foundSeeds: &[u32], seed: u32);
}

#[wasm_bindgen]
pub fn search_tinymt_seed(natures: &[u32], has_shiny_charm: bool) -> Vec<u32> {
    let mut found_seeds: Vec<u32> = Vec::new();
    let param = tinymt32::Param {
      mat1: 0x8F7011EE,
      mat2: 0xFC78FF1F,
      tmat: 0x3793FDFF,
    };

    each_u32!(seed => {
        let mut rng = tinymt32::from_seed(param, seed);

        let found = natures
            .iter()
            .all(|&nature| nature == get_egg_nature(&mut rng, has_shiny_charm));


        if seed % 0x0100_0000 == 0 && seed != 0 {
            // 通常のRustの関数と同じように呼び出す
            postProgressAction(found_seeds.as_slice(), seed);
        }

        if found {
            found_seeds.push(seed);
            postProgressAction(found_seeds.as_slice(), seed);
        }
    });

    found_seeds
}

またツールでは初期Seed検索中にUIスレッドがブロックされるのを防ぐため, 初期Seed検索処理をWebWorker上で実行しています. Safariなどの一部のブラウザには一定期間UIスレッドがブロックされるとブロックの原因になっているスクリプトを停止する機構が備わっているため, 「どうせ計算ボタン1つしかないツールだしUIスレッドくらいブロックされても困らないでしょ」という気持ちでWebWorker無しで雑に実装すると, 計算が途中で停止してしまいます. WebWorkerの導入はその対策にもなっている訳です.

スレッドと計算の流れを示したアーキテクチャの図

計算時間の比較

計測時間の比較. MacOS/Android/iOSごとに違うデバイスで測定. 異なるデバイス間の値は単純に比較できないので注意.

移植元のC++実装はマルチスレッドで並列化されているため, JS/WebAssembly実装と同じシングルスレッド構成になるよう改変したものの結果を載せています. 環境やブラウザによりますがWebAssembly実装はC++実装と比べて0.2〜0.7倍, JS実装と比べて1〜13倍と, 十分ツールとしては実用的なレベルの性能が出ています. ちなみにこの計算時間はシングルスレッドでの計測値なので, マルチスレッドにより乱数計算処理を並列化すればさらなる性能の向上が見込めます.

困ったこと

Webpack で WebWorker 上から wasm を Dynamic Import できない

現状 worker-loader を使ってWebWorkerをバンドルしているプロジェクトにて, wasm-bindgen で生成されたスクリプトを WebWorker 上から Dynamic import すると実行時エラーになります.

github.com

例えば次のようなコードが実行時エラーになります.

// /app.js
import Worker from './worker.js'
const worker = new Worker()
worker.onmessage = (event) => {
  console.log(event.data) // expected: 3
}
// /worker.js
import('./lib.js').then((module) => {
  const result = module.add(1, 2)
  postMessage(result)
})
// /lib.rs
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
// /webpack.config.js
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const rootPath = resolve(__dirname, '.')
const distPath = resolve(rootPath, './dist')

module.exports = {
  entry: {
    app: resolve(rootPath, './app.js'),
  },
  output: {
    path: distPath,
    filename: '[name].[hash].js',
    // WebWorkerの実行コンテキストでは `window` グローバルオブジェクトが無いので,
    // 代わりに `this` でグローバルオブジェクトを参照するようにする
    globalObject: 'this',
  },

  resolve: {
    extensions: ['.js', '.wasm'],
  },

  module: {
    rules: [
      { test: /worker\.js$/, loader: 'worker-loader' },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin(),
  ],
}

この問題はWebpackのBootstrapコード *3 の一部がエントリポイントだけに埋め込まれてしまい, WebWorker 内には埋め込まれないことに起因しています. WebWorkerとそれ以外のモジュールの実行コンテキストは分かれているので, 片方だけにBootstrapコードが埋め込まれているともう片方からそのコードを参照することが出来ず, エラーとなるという訳です. WebpackのWebWorker/WebAssemblyサポートはまだまだ未熟なため, こうしたエッジケースに嵌まることが度々あります.

今回はIssueのコメントにもあるように, worker-loaderに頼らず webpack.config.js で WebWorker を明示的に別のエントリポイントとして分けることで問題を回避しました. こうすることで Webpack が WebWorker とそれ以外のモジュールの実行コンテキストが分かれていることを正しく認識し, WebWorker にも初期化コードを埋め込んでくれるようになります.

// /webpack.config.js
const { resolve } = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpackMerge = require('webpack-merge')

const rootPath = resolve(__dirname, '.')
const distPath = resolve(rootPath, './dist')

const WORKER_PATH = '/worker.js' // ビルド後のworkerのパス

const baseConfig = {
  resolve: {
    extensions: ['.js', '.wasm'],
  },

  plugins: [
    new webpack.DefinePlugin({
      WORKER_PATH: JSON.stringify(WORKER_PATH),
    }),
  ],
}

// app用にentry/outputフィールドをカスタマイズした設定
const appConfig = webpackMerge(baseConfig, {
  entry: {
    app: resolve(rootPath, './app.js'),
  },
  output: {
    path: distPath,
    filename: '[name].[hash].js',
  },

  plugins: [
    new HtmlWebpackPlugin(),
  ],
})

// worker用にtarget/entry/outputフィールドをカスタマイズした設定
const workerConfig = webpackMerge(baseConfig, {
  target: 'webworker',
  entry: {
    'worker': resolve(rootPath, './worker.js'),
  },
  output: {
    path: distPath,
    filename: 'worker.js',
    // target フィールドで WebWorker 用に `globalObject` フィールドが
    // 自動で書き換えられているので, `globalObject` フィールドを手動で指定する必要はない
    // globalObject: 'this',
  },
})

module.exports = [appConfig, workerConfig]
// /app.js
const worker = new Worker(WORKER_PATH)
worker.onmessage = (event) => {
  console.log(event.data) // 3
}

おわりに

WebAssemblyを使ってC++の乱数調整ツールをWebに移植することで, ブラウザさえあれば誰でも簡単に利用できるポータビリティと, (ブラウザにより多少差はあるものの) 十分ツールとしては実用的なレベルの性能を実現することができました. 個人的にはもっと低い性能が出ると思っていたので, C++実装とそれほど大差のない性能が出た時は驚きました. WebAssemblyスゴイ.

また記事では触れていませんでしたが, 実はRustを使って型安全性/メモリ安全性のあるDXの良いWebアプリケーション開発環境の構築に挑戦する, というのも今回の開発で掲げたテーマの1つだったりします. リポジトリをよく見ると, ちゃっかりTypeScriptを導入していたり, JS/Rustの自動コンパイルやNetlifyによるpush駆動deployを実現していたりします. ですから今回の開発で小さいプロジェクトながらもそうした開発環境を構築できることが分かったのは大きな成果だったと思います. 今度はもっとガッツリRust/WebAssemblyを使って大きなものを作ってみたいですね. 最近は wasm-bindgen 以外にも js-sysweb-sys といったRustからJavaScript APIやWeb APIを直接利用できるcrateが登場しているので, 合せてそちらも触っていけると良さそうです.

*1:育て屋から貰えるタマゴの生成に関する乱数調整を対象とする分野のことです.

*2:ちなみに手元のEdge for Windowsでは動きませんでした

*3:参考: https://github.com/webpack/webpack/blob/a4e5f63d9d370b191bb9586570bc22bf2947e390/lib/MainTemplate.js

はてなサマーインターン2018に参加してクイズ大会で優勝してきた

はじめに

大学3回生の id:mizdra です. 普段はJavaScriptを触っていて, Webアプリケーションを作って遊んでいます.

8月から9月にかけて約1ヶ月間, はてなサマーインターン2018に参加してきました. この記事はその体験記となります.

developer.hatenastaff.com

f:id:mizdra:20180924133406j:plain:w400

応募

僕は普段からはてなブログやはてなブックマークを利用している「はてなユーザ」の一人で, はてなに愛着を持っています. また, Hatena Developer Blogに見られるはてなの技術に対する姿勢が大好な「はてなのファン」でもあります. はてなユーザ/はてなのファンとして, はてなのサービスに直接関わってみたい, はてなの社内の様子を直接見てみたい. そうしたことが動機で, インターンに応募しました. また, 大学の先輩方 (id:miki_bene, id:hogashi) が過去のはてなインターンに参加していて, とても楽しそうにしている様子を見ていたことも応募のきっかけの1つだったりします.

志望動機やポートフォリオを書いて応募すると, 選考面談を受けることになります. 面談ではCTOの id:motemen さんと共に何故はてなをインターンシップ先に選んだのか, 今までどんな技術を学び/扱ってきたかなどについて話しました. 自分が今までに作ったアプリやツールの話をしながら「良い〜」「良いですね〜」などと楽しくお話をさせて頂きました.

その後無事インターンに参加できることが決まったので, 新幹線で埼玉から京都へと向かいました.

新幹線車内で朝食を食べている時の様子です.

前半 (1週目〜2週目)

インターンの前半では技術やはてなに関する講義を受け, その講義で学んだことを活かして演習を解きます. メンターの補助を受けながら, はてなの中の人による講義を受けることができます. *1

特に, 初めの1週間はWebアプリケーションに関する講義があり, 演習を進めていくと日記サービスが完成するというカリキュラムになっています. 加えて2周目ではAWSから講師をお招きしてAWSハンズオンが開催され, 1週目で作った日記サービスを本番環境にデプロイします. 講義でWebサービスを支える技術を網羅的に学び, 学んだことを元に自分の力で実際に稼働するWebサービスを作っていく. このような体験が出来るのは, はてなインターンならではだと思います. 特に, 僕のように普段Webフロントエンドばかりやっていて, DBやWebサーバについてあまり学習する機会が無い, という人にとっては絶好の学習の機会です. はてなインターンの講義は, そうした「Webサービスを支える技術を網羅的に学びたいけど, まだ手が出せてない人」に是非オススメしたいと思える内容でした.

f:id:mizdra:20180924184608p:plain
GraphQLの講義を受けている時の様子です.

また, 今回のはてなインターンでは従来のPerlやScalaをベースとする講義資料が, Golangをベースとするものへと一新されています. 新しいものを触ることが好きな人にもオススメです.

後半 (3週目〜4週目)

配属

インターンの後半2週間は開発チームに配属され, 実際に稼働しているサービスの開発を行います. 僕は第一志望だったコンテンツプラットフォームコースに当たるマンガチームに配属されました. マンガチームではスマホ向けのマンガアプリや, Web向けのマンガビューワ「GigaViewer」などを開発をしています. 今回は一緒に配属された id:YaaMaa さんと共に, GigaViewerの開発を行いました.

GigaViewerについては, 以下の記事で紹介されているので是非読んでみて下さい *2.

developer.hatenastaff.com

開発の様子

マンガチームでは, 主に id:hitode909 さんや id:onk さんとメンタリングしながら開発しました. まずは実際にサービスを使ってみて, どんな機能があったら嬉しいかというユーザ視点の思いをチーム内で共有し, 次にその機能はどういうデザインであるべきか, 実現するにはどういう技術を用いれば良いのか, という議論をしていきます. デザインについては, デザイナーの id:swimy1113 さんと相談しながら決めていきます. その後, 本当に挙げられた技術で機能を実現できるかどうかを詳しく調査し, 実装へと移ります.

実装の過程では, 同じチームに配属された id:YaaMaa さんと2人で手分けしながら開発を進めました. 僕がのんびり開発していたら id:YaaMaa さんがシュッと仕上げていることが多々あり, ヒッとなっていました. すごかった.

ちなみに僕はWebフロントエンドのロジック周りを主に担当していて, id:Pasta-K さんや id:hitode909 さんに相談しながら開発していました. お2方とも的確なアドバイスをして下さり, 爆速で開発を進めることができました. ありがとうございました.

リリースした機能

3つの機能をリリースしました. 全てとなりのヤングジャンプでのみ利用可能です.

  1. 閲覧済み作品の新着エピソード更新通知機能
  2. 新着エピソードのレコメンド機能
    • ビューワの最後のページに更新通知で表示されているエピソードがレコメンドされる
  3. ビューワページのSPA化
    • ビューワページ間の遷移に限定して, シームレスな遷移ができる
    • 遷移速度の改善以外に, 全画面表示状態が遷移後も維持されるなどの効果もある

f:id:YaaMaa:20180910221611p:plainf:id:YaaMaa:20180910221909p:plain
リリースした機能のスクリーンショット*3

「閲覧済み作品の新着エピソード更新通知機能」は「作品の更新を知りたい」という僕の思いから実装されたものです. トップページにアクセスするだけで, 以前見た作品の最新話が出ているかどうか, ひと目で判断できます. 実装にあたっては, 会員未登録のユーザにも更新通知を出したいという難しい要件や, 利用したいAPIがブラウザで実装されていない問題など, 様々な困難に遭遇しましたが, 機能を制限するなど問題を回避できるよう方向転換して*4何とか実装することができました. とても思い入れのある機能です.

また, レコメンドの改善やSPA化は id:YaaMaa さんが提案したアイデアを元に実現されました. SPAの実装は, 技術的に非常に面白い課題でした. どちらもユーザの体験を改善する良い機能だと思います.

f:id:mizdra:20180924152908g:plain
機能をリリースすると, オフィスに転がってるくす玉でお祝いします.

マンガチームの世界観

マンガチームのスペースの中央には木魚が置いてあり, 叩くとチームメンバーがバッと集まってきます. 困ったらこの木魚を叩いて助けを呼びます. また, 木魚の横には id:Pasta-K さんのアクリルブロックが置いてあります. 何だかよく分からないですが, そういう世界が広がってます.

f:id:mizdra:20180924133555j:plain
Suzuriで買えます.

同じ階にはあの「飲み会IoTボタン」が置いてあります.

成果発表

インターン最終日には「ほたて」と呼ばれる「ットなスクをがけた」チームを社員が投票して投票し, 表彰する会があります. インターン生はインターンでの成果をここで発表し, 🏆表彰🏆されます *5.

cybozushiki.cybozu.co.jp

マンガチームは5チーム中, 2位という結果になりました. 準優勝めでたい 🎉🎉🎉. 優勝した id:gazimum さん, おめでとうございます. 次は負けません ⚔️.

f:id:mizdra:20180924133701j:plain

おわりに

今まで経験したことのない最高の夏を過ごすことができました. こんなにインターン生のことを考えているインターンは他には無いと思います. この記事を読んでいる皆さんも, 是非来年以降応募して最高の夏を過ごしましょう.

最後になりますが, インターンを通して本当に色々な方のお世話になりました. メンターや講師の方々, マンガチームの方々, その他インターンを支えて下さった方々, 本当にありがとうございました!

おまけ

白熱する優勝争いの様子です.

https://twitter.com/meteor_saan/status/1032952399256670208

※Mackerelチームのインターン生です.

※大規模チームのメンターです.

https://twitter.com/meteor_saan/status/1032982397539643392

※マンガチームのインターン生 (僕) です.

あわせて読みたい

e-ntyo.hatenablog.com

https://blog.meteors.me/entry/2018/09/08/112050blog.meteors.me

tomoyaf.hatenablog.com

yaamaa-memo.hatenablog.com

turtar-fms.hatenablog.com

noahorberg.hatenablog.com

guni1192.hatenablog.com

*1:僕の場合は僕の大学の先輩にあたる id:miki_bene さんがメンターで, とても丁寧に質問などに対応して下さいました. ありがとうございました 🙏.

*2:余談ですが, GigaViewerのことはインターンに行く前にこの記事を読んで知っていて, 「はてな社ではこんな最高便利なマンガビューワを作っていたのか〜」という感想を持っていました. まさかその開発に携われるとは思っていなかったので, 配属が決まった時は嬉しかったです.

*3:引用元: はてなインターン2018に参加した - 見返すかもしれないメモ

*4:個人的にはインターンシップ一番のハイライトでした.

*5:実は僕はインターンの最後の2日間体調を崩してしまい, 残念ながら成果発表会に参加することができませんでした. 色んな人にめちゃめちゃ心配されたり気遣いして頂いて, お陰さまで今は元気です. ありがとうございました 🙇

Chrome拡張機能でコールバック地獄を解決する

Chrome拡張機能の非同期APIはコールバックにより実装されています. 例えば, 拡張機能ごとに用意されるストレージからデータを取得する場合, ストレージへのアクセス中にJavaScriptの処理が中断されるのを防ぐため, 非同期APIが用意されています.

// background.js
chrome.storage.local.get(['admin'], (result1) => {
  chrome.storage.local.get([`user/${result1.admin}/name`], (result2) => {
    console.log(result2)
  })
})

このコールバックによる非同期APIはJavaScriptにおいては一般的な非同期APIの実装手法ですが, 「コールバック地獄」という問題を引き起こします. これを解決する手法としてES2015で追加されたPromiseとジェネレータ, もしくはES2017で追加されたasync/awaitを利用する手法が知られています.

numb86-tech.hatenablog.com

numb86-tech.hatenablog.com

susisu.hatenablog.com

しかしChrome拡張機能の非同期APIはコールバックにより実装されているため, Chrome拡張機能の中では直接的にはPromiseasync/awaitを利用することが出来ません. そのため, 手動で非同期APIをPromiseでWrapするか, 以下のようなWrapperライブラリを利用する必要があります.

この記事ではmozilla/webextension-polyfillを使ってコールバック地獄を解決する方法を紹介します.

mozilla/webextension-polyfillを使ってコールバック地獄を解決する

ブラウザ間で統一的なAPIで拡張機能を作成できるよう策定されたWeb Extension APIというものがあり, 現時点でMozilla FirefoxとMicrosoft Edgeでサポートされています. mozilla/webextension-polyfillはこのWeb Extension APIのChrome向けのPolyfillとなっています. Web Extension APIの非同期APIは全てPromiseで提供されているため, Chrome拡張機能でコールバック地獄を解決する方法として用いることができます.

mozilla/webextension-polyfillnpm.comからもインストールできますが, モジュールバンドラなどを挟む必要があるため, 少々手間です. これに変わるより手軽なものとして, unpkg.comから直接ライブラリをダウンロードし, manifest.jsonbackground.scriptsなどから読み込む方法があります.

$ cd extension-project
$ wget https://unpkg.com/webextension-polyfill/dist/browser-polyfill.min.js
// manifest.json
{
  // ...

  "background": {
    "scripts": [
      "browser-polyfill.min.js",
      "background.js"
    ]
  },

  "content_scripts": [{
    // ...
    "js": [
      "browser-polyfill.min.js",
      "content.js"
    ]
  }]
}

このようにPolyfillを読み込むことでbackground.jsなどからbrowserという名前空間からWeb Extension APIにアクセスできます.

// background.js
browser.storage.local.get(['admin'])
  .then((result) => {
    return browser.storage.local.get([`user/${result.admin}/name`])
  })
  .then((result) => {
    console.log(result)
  })

非同期APIはPromiseを返すので, 次のようにasync/awaitを利用したコードにも書き換えることもできます.

// background.js
(async () => {
  const result1 = await browser.storage.local.get(['admin'])
  const result2 = await browser.storage.local.get([`user/${result1.admin}/name`])
  console.log(result2)
})()

以上, Chrome拡張機能でコールバック地獄を回避するTIPSでした.

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.