WebAssemblyでScratchプラグインを作ろう!

WebAssemblyで
Scratchプラグインを作ろう!

#0 Install Day

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

Uchio Kondo (@udzura)

  • 福岡市エンジニアカフェ ハッカーサポーター
  • フィヨルドブートキャンプ アドバイザー
  • RubyKaigi Speaker (2016 ~)
  • 共同翻訳: 『入門eBPF』(オライリージャパン)
  • うどんは資さんうどん過激派

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

今日やるゴール

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

今日やるゴール

  • 本講座の概要説明
  • 必要な環境の構築
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

概要

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

本講座の概要説明

  • 何をするの?
  • やることは?(言語、技術、他)
  • 想定する受講者のレベル
  • その他

※ 今後の説明において、時期、内容等あくまでも予定です

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

本講座では何をするのか?

  • WebAssembly の基本的なところを一緒に調べて、手を動かします(〜12月)
  • WebAssembly の応用の一環で、ScratchのブロックとしてWebAssemblyで作った機能を組み込みます(2025年1月〜)

※ 本講座を通して、WebAssemblyのことを短くWASMと呼ぶことがあります

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

イメージ

  • Scratchは自分でMODが作れるので、そこで動く何かを想定しています(予定)
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

具体的なやること

  • 今回を含め 全6回 の予定です(月一回程度のハイブリッド開催)
    • #0 インストールデイ(今回)
    • #1 小さな WebAssembly モジュールを動かそう
    • #2 WebAssembly モジュールとブラウザを連携させよう
    • #3 WebAssembly モジュールでより複雑なことをしよう
    • #4 Scratch をハックする方法を学ぼう
    • #5 Scratch とWebAssemblyを連携させよう
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

具体的に扱う技術・環境

  • ブラウザ ... 基本的に最新のChromeのみ動作確認します
  • 言語 ... Rust を使います
    • Rust は一般的にはやや学習が大変な言語ですが、Rustの中でも難しめの概念(特にライフタイム周り)はなんとか誤魔化して進めます
    • また、JavaScript/TypeScriptも必要に応じ扱うことになります
      • 全てサンプルコードは用意します
  • OS ... MacOS / Windows どちらのPCでもご参加いただけます
    • MacOS での開発を推奨します
    • Windows を選ぶ場合、ある程度の自力解決能力を期待します
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

想定する受講者のレベル

  • 以下のレベルを期待します
    • 最低でも一つ何かしらの普通のプログラミング言語を取り扱える
      • Scratch しか触れたことがない場合は少し難しいです
    • コマンドラインツールに触れたことがある
  • Scratch 自体を使ったことがあるかどうかは問いません
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

本講座の特徴

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

本講座の特徴#1

地味です

  • 少なくとも途中まで割と地味な感じになります🥺 あらかじめご了承ください
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

本講座の特徴#2

難易度やや高めです

  • あまり前例がない/資料がないことをやっていきます
  • 進度も正直速いです
  • Discord等もうまく活用したい気持ちがあります
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

本講座の特徴#3

一緒に調べて、作っていきます

  • 講師はフロントエンドエンジニアではありません‼️
    • RubyをWebAssemblyで動かす等いくつか実験をしたぐらい
  • 例えば質問内容にその場では答えられないことがあります
    (調べることはもちろんします)
  • 特に後半はReact/TypeScriptを取り扱い、むしろ識者と意見交換したい...
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

本講座の免責事項...

  • Windows マシンでの動作確認は十分に行っていません
    • 利用することは制限しません
    • 場合によってかなりの部分を自分でトラブルシュートする必要があるかもしれません
      • (Macでもそうなのですが、Windowsではそうなる確率が上がります)
    • コマンドライン操作をWSL上での実行に寄せると少し楽かも?
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

そういうわけで...

  • 難易度が高い、一緒に講座を作りたい、ということで

    • オフラインでの参加を推奨します
  • 全てをオフライン必須、というわけではありませんが...

  • 資料は全て公開(録画もあるそうです)、Discordで適切な内容であれば質問は可能です。自学にも対応した形にはなっています

  • また、テイカー気質の方は少し想定している参加者層とは違うこともご了承ください

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

オフライン推奨の話

  • もう少し解像度を上げます
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

トラブった時

  • 質問をすると思いますが
  • 実は、講師・コーチがそのものずばりの答えを知っていることは少ないです
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

何ができるか?

  • 学習者よりトラブルシュートが上手なはずです
  • その大きな違いは何かというと、手数です
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

オフラインならば

  • コーチと一緒にたくさんある確認事項・試行錯誤をその場で一つずつやっていくことができます

オンラインでは...

  • 数多く(場合によって10以上)の確認事項を一気に全部共有しないといけません
    • 情報量が多く「ワ、ワァ...」となってしまうと思います
    • かと言って一つずつやるとやり取りがとても遅いため大変です
    • しかしトラブルの原因はわからないため、見つかるまでやれることは全部
      確認する必要があります
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

まとめると

  • オフライン推奨 = 必ず起こるトラブルシュートを効率よくするため、です
  • 毎回オフライン参加でなくても大丈夫です...
    • 困ったことがあった場合、どこかでオフラインで質問に来た方が効率が良いことも多いでしょう
    • (その場合、例えば前回範囲の質問、なども歓迎します)
    • ある程度時間がかかってもOK、非同期ゆっくり目でもOK、ならオンライン質問も活用できるかと
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

オンラインを全面否定するわけではないです

  • いろいろな事情もあります
  • 選択肢は多い方がいいです
  • しかし、さまざまな制約からオンライン学習では難しい要素もあるので、
    ある意味でより自律的、積極的な学習が求められることは理解してください。
    • 自分一人でも基本的な手数を稼げるようになるとベター
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

FYI: オンラインでの質問のコツ

  • 以下の3項目を意識して言語化しましょう
    • (1) やろうとしていること・意図
    • (2) 実際にやったこと(コマンドなど)
    • (3) 結果として起こったこと・何が意図と反するか
  • すべてのログを共有しましょう
    • めちゃくちゃ長くても大丈夫です、 @udzura はちゃんと読み(め)ます
    • 変に忖度して削った結果重要な情報が消えているパターンの方が困ります
    • 同様に、打ったコマンド、書いたコード、検索したワードなども省略せずにありのままに伝えましょう
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

参考になるページ

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

いろいろ言いましたが...

  • 基本楽しくやっていきましょう
  • トラブルも楽しむ!(?)
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

その他補足情報

  • ハッシュタグ
    • #EngineerCafeLabWASM です!
  • Discordのチャンネル
    • イベント -> #webassembly でどうぞ!
    • URLその他もここに貼ります
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

環境構築

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

はじめにコピペ元を掲示

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

インストール/セットアップする環境

  • 前提となるツール
    • ✅ Mac: Homebrew
    • ✅ Windows: WSL2
  • ✅ ブラウザ: Chrome
  • ✅ Rustの開発環境
  • ✅ WASMの開発環境
  • ✅ エディタ
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

Mac: Homebrewのセットアップ

参考情報

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

Windows: WSL2環境の立ち上げ

更なる便利な情報

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

ブラウザ

#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

Rustの開発環境

  • Rustの環境一式をセットアップしておきましょう
  • 選択肢は全てデフォルトで大丈夫です
  • PATH環境変数を設定する のを忘れずに
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • Windowsの方: この作業はWSL2のコンソールで実行してください
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

Rustの開発環境(続き)

  • RustでWASMをビルドできるよう以下のコマンドも追加で打ちます
rustup target add wasm32-wasi
rustup target add wasm32-unknown-unknown
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

Rustの動作確認

cargo new test-crate --bin
cd test-crate 
cargo build
./target/debug/test-crate 

#=> Hello, world! と表示されればOK
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

WASMの開発環境/Mac

  • Macの方...
    • Homebrew経由で簡単にインストールできます
    • wasmtime
    • WABT (WebAssembly Binary Toolkit)
    • wasm-tools
brew install wasmtime wabt wasm-tools
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

WASMの開発環境/Win or Linux

  • Windowsの方...
    • WSL2 の方に以下をインストールします。
    • wasmtime
    • WABT (WebAssembly Binary Toolkit)
      • Linux用のバイナリを選択
    • wasm-tools
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

WASMの開発環境/Win or Linux(コマンド)

# wasmtime
curl https://wasmtime.dev/install.sh -sSf | bash
. /home/ubuntu/.bashrc
# wabt (x64前提)
wget https://github.com/WebAssembly/wabt/releases/download/1.0.36/wabt-1.0.36-ubuntu-20.04.tar.gz
tar xzf wabt-1.0.36-ubuntu-20.04.tar.gz
sudo mv wabt-1.0.36/bin/* /usr/local/bin
# wasm-tools
cargo install --locked wasm-tools
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

WASMツールの動作確認

  • 以下のwasmバイナリをRustで作ってみる
# 先ほどhello worldしたプロジェクトで実行
cargo build --target wasm32-wasi
file target/wasm32-wasi/debug/test-crate.wasm

# target/wasm32-wasi/debug/test-crate.wasm: WebAssembly \
# (wasm) binary module version 0x1 (MVP)
# ... のように表示される(1行で)
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

WASMツールの動作確認(2)

  • wasmtimeで実行しましょう
wasmtime target/wasm32-wasi/debug/test-crate.wasm
# Hello, world! と同じように出力される
  • 他のツールも試しましょう
wasm-objdump -x target/wasm32-wasi/debug/test-crate.wasm || echo NG!
# NG! と出なければOK
wasm-tools validate -v target/wasm32-wasi/debug/test-crate.wasm || echo NG!
# NG! と出なければOK
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

エディタ

  • 基本的に自由です
  • こだわりがなければ VSCode が良いかと思います
  • VSCodeの場合、以下の拡張を入れます(すごく種類があるがこれがいいみたい)
    • rust-analyzer
    • TypeScriptについてはVSCodeは組み込みでサポートされています
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

今日のまとめ

  • 講座の雰囲気や狙い、学習者の心得的な話をしました
  • 必要と思われるツールをまずはセットアップしました
    • 今後の展開で随時追加するかもしれませんが
  • 一緒にWASM講座、作っていきましょう
#0 Install Day
WebAssemblyでScratchプラグインを作ろう!

次回

  • #1 小さな WebAssembly モジュールを動かそう
    • 予定: 10/27(日) 14:00 start
#0 Install Day
\n
\n
\n `.split(/\n\s*/).join(""),this.wrapper=null!==(t=this.shadowRoot.querySelector(`div[${e}]`))&&void 0!==t?t:void 0;const l=this.svg;this.svg=null!==(o=null===(s=this.wrapper)||void 0===s?void 0:s.querySelector(`svg[${i}]`))&&void 0!==o?o:void 0,this.svg!==l&&(this.svgComputedStyle=this.svg?window.getComputedStyle(this.svg):void 0),this.container=null!==(a=null===(r=this.svg)||void 0===r?void 0:r.querySelector(`span[${n}]`))&&void 0!==a?a:void 0,this.observe()}disconnectedCallback(){this.svg=void 0,this.svgComputedStyle=void 0,this.wrapper=void 0,this.container=void 0,this.observe()}attributeChangedCallback(){this.observe()}flushSvgDisplay(){const{svg:t}=this;t&&(t.style.display="inline",requestAnimationFrame((()=>{t.style.display=""})))}observe(){this.containerObserver.disconnect(),this.wrapperObserver.disconnect(),this.wrapper&&this.wrapperObserver.observe(this.wrapper),this.container&&this.containerObserver.observe(this.container),this.svgComputedStyle&&this.observeSVGStyle(this.svgComputedStyle)}observeSVGStyle(t){const e=()=>{const i=(()=>{const e=t.getPropertyValue("--preserve-aspect-ratio");if(e)return e.trim();return`x${(({textAlign:t,direction:e})=>{if(t.endsWith("left"))return"Min";if(t.endsWith("right"))return"Max";if("start"===t||"end"===t){let i="rtl"===e;return"end"===t&&(i=!i),i?"Max":"Min"}return"Mid"})(t)}YMid meet`})();i!==this.svgPreserveAspectRatio&&(this.svgPreserveAspectRatio=i,this.updateSVGRect()),t===this.svgComputedStyle&&requestAnimationFrame(e)};e()}updateSVGRect(){var t,e,i,n,s,o,r;let a=Math.ceil(null!==(e=null===(t=this.containerSize)||void 0===t?void 0:t.width)&&void 0!==e?e:0);const l=Math.ceil(null!==(n=null===(i=this.containerSize)||void 0===i?void 0:i.height)&&void 0!==n?n:0);void 0!==this.dataset.downscaleOnly&&(a=Math.max(a,null!==(o=null===(s=this.wrapperSize)||void 0===s?void 0:s.width)&&void 0!==o?o:0));const c=null===(r=this.svg)||void 0===r?void 0:r.querySelector(":scope > foreignObject");if(null==c||c.setAttribute("width",`${a}`),null==c||c.setAttribute("height",`${l}`),this.svg&&(this.svg.setAttribute("viewBox",`0 0 ${a} ${l}`),this.svg.setAttribute("preserveAspectRatio",this.svgPreserveAspectRatio),this.svg.style.height=a<=0||l<=0?"0":""),this.container){const t=this.svgPreserveAspectRatio.toLowerCase();this.container.style.marginLeft=t.startsWith("xmid")||t.startsWith("xmax")?"auto":"0",this.container.style.marginRight=t.startsWith("xmi")?"auto":"0"}}}const o=(t,{attrs:e={},style:i})=>class extends t{constructor(...t){super(...t);for(const[t,i]of Object.entries(e))this.hasAttribute(t)||this.setAttribute(t,i);this.attachShadow({mode:"open"})}static get observedAttributes(){return["data-auto-scaling"]}connectedCallback(){this._update()}attributeChangedCallback(){this._update()}_update(){const t=i?``:"";let e="";const{autoScaling:n}=this.dataset;if(void 0!==n){e=`${e}`}this.shadowRoot.innerHTML=t+e}};let r;const a=Symbol();let l;const c="marpitSVGPolyfill:setZoomFactor,",d=Symbol(),g=Symbol();const h=()=>{const t="Apple Computer, Inc."===navigator.vendor,e=t?[u]:[],i={then:e=>(t?(async()=>{if(void 0===l){const t=document.createElement("canvas");t.width=10,t.height=10;const e=t.getContext("2d"),i=new Image(10,10),n=new Promise((t=>{i.addEventListener("load",(()=>t()))}));i.crossOrigin="anonymous",i.src="data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%2210%22%20viewBox%3D%220%200%201%201%22%3E%3CforeignObject%20width%3D%221%22%20height%3D%221%22%20requiredExtensions%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxhtml%22%3E%3Cdiv%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxhtml%22%20style%3D%22width%3A%201px%3B%20height%3A%201px%3B%20background%3A%20red%3B%20position%3A%20relative%22%3E%3C%2Fdiv%3E%3C%2FforeignObject%3E%3C%2Fsvg%3E",await n,e.drawImage(i,0,0),l=e.getImageData(5,5,1,1).data[3]<128}return l})().then((t=>{null==e||e(t?[u]:[])})):null==e||e([]),i)};return Object.assign(e,i)};let p,m;function u(t){const e="object"==typeof t&&t.target||document,i="object"==typeof t?t.zoom:t;window[g]||(Object.defineProperty(window,g,{configurable:!0,value:!0}),document.body.style.zoom=1.0001,document.body.offsetHeight,document.body.style.zoom=1,window.addEventListener("message",(({data:t,origin:e})=>{if(e===window.origin)try{if(t&&"string"==typeof t&&t.startsWith(c)){const[,e]=t.split(","),i=Number.parseFloat(e);Number.isNaN(i)||(m=i)}}catch(t){console.error(t)}})));let n=!1;Array.from(e.querySelectorAll("svg[data-marpit-svg]"),(t=>{var e,s,o,r;t.style.transform||(t.style.transform="translateZ(0)");const a=i||m||t.currentScale||1;p!==a&&(p=a,n=a);const l=t.getBoundingClientRect(),{length:c}=t.children;for(let i=0;i{null==t||t.postMessage(`${c}${n}`,"null"===window.origin?"*":window.origin)}))}function v({once:t=!1,target:e=document}={}){const i=function(t=document){if(t[d])return t[d];let e=!0;const i=()=>{e=!1,delete t[d]};Object.defineProperty(t,d,{configurable:!0,value:i});let n=[],s=!1;(async()=>{try{n=await h()}finally{s=!0}})();const o=()=>{for(const e of n)e({target:t});s&&0===n.length||e&&window.requestAnimationFrame(o)};return o(),i}(e);return t?(i(),()=>{}):i}p=1,m=void 0;const b=Symbol(),w=(e=document)=>{if("undefined"==typeof window)throw new Error("Marp Core's browser script is valid only in browser context.");if(((e=document)=>{const i=window[a];i||customElements.define("marp-auto-scaling",s);for(const n of Object.keys(t)){const s=`marp-${n}`,a=t[n].proto();null!=r||(r=!!document.createElement("div",{is:"marp-auto-scaling"}).outerHTML.startsWith("
{t.outerHTML=t.outerHTML.replace(new RegExp(`^<${n}`,"i"),`<${s}`).replace(new RegExp(`${n}>$`,"i"),`${s}>`)})))}window[a]=!0})(e),e[b])return e[b];const i=v({target:e}),n=()=>{i(),delete e[b]},l=Object.assign(n,{cleanup:n,update:()=>w(e)});return Object.defineProperty(e,b,{configurable:!0,value:l}),l},y=document.currentScript;w(y?y.getRootNode():document)}();

講座というより、学生の自主ゼミみたいな感じかもしれない