こんにちは、Qiita学生対抗アドベントカレンダー23日目を担当します。logiteca7(ろじ)です。
はじめに
WebAssemblyを使って、簡単なJavaScript以外の言語を使ったChrome拡張機能を開発しました。
今回の記事ではその手法の解説と次の開発目標についてお話したいと思います。
また今回はRubyを使いましたが、理論上どんな言語でも出来るはずです。(多分Pythonも出来る)
動くデモはこちら↓(リポジトリにインストール方法が書いてあります。)
それではいってみましょう!
前提知識
Webassemblyとは
ブラウザ上で実行できるアセンブリ。
ブラウザで動くJavaScript(以下JS)以外の言語の1つ。
Chrome拡張とは
Chromeにインストールして使うプラグインのこと。
Chrome以外のFirefoxや他ブラウザもサポートしているためブラウザ拡張と呼ばれることもある。
Content Scriptとは
指定したサイトで任意のJSを読み込むChrome拡張の機能
技術概要
今年、ついにRubyがWebAssemblyに対応し、ブラウザ上でCRubyが動くようになりました。
↓既にRubyインタプリタのオンラインデモも公開されています。
で、仕組みをざっくり説明すると次の図。
このようにWebAssembly上でRubyが動いているイメージです。(一部操作はJSを介する必要がある)
僕はこれを使ってChrome拡張機能でもRubyが利用できるようになるのではないかと考えました。
デモ画像
ということで今回は拡張機能のContent Scriptを使い、2つのRubyスクリプトを読み込ませてみました。
1つ目はCSSセレクタで最初に見つかったh1タグの中身を書き換えるもの。
# コンソールにメッセージ表示
puts "Hello, this is content_script1!"
# 見出しの文字を変える
document = JS.global[:document]
document[:querySelector].call(:call, document, "h1")[:innerText] = JS.try_convert("Rubyで動いたよ!")
# ホントは`JS.global[:document].querySelector("h1")[:innerText] = "something"`って書けます。
2つ目はbodyのbackgroundColorをredに変えるコード。
# コンソールにメッセージ表示
puts "Hello, this is content_script2!"
# 背景色を赤にする
JS.global[:document][:body][:style][:backgroundColor] = JS.try_convert("red")
実行結果
うまくいきました、背景色の変更とコンソールのメッセージが確認できます。
他にもfetch APIを使ったり、addEventListenerを呼んだり、Promiseのawaitもできます。
ファイル構成の解説
デモのリポジトリのファイル構成は以下のようになっています。
/
|- ruby.wasm
|- manifest.json
|- unloosen.config.json
|- package.json
|- dist/
| |- content-script.umd.js
| |- index.umd.js
|- src/
|- content-script.js
|- index.js
|- content_script1.rb
|- content_script1.rb
...他のファイル省略
それでは重要なファイルについて解説していきたいと思います。
index.js
index.jsの中身はコンパイル、RubyVMの起動、Rubyコードの実行ツールが入ったスクリプトです。
ブラウザは配布されたWASMを直接実行することは出来ないため、一度JSでブラウザのPC用にコンパイルしてからRubyVMを実行します。
import { DefaultRubyVM } from "ruby-head-wasm-wasi/dist/browser.umd.js";
export var UnloosenRubyVM;
// メッセージ出力
export const printInitMessage = () => {
evalRubyCode(`
puts <<-"INF"
Ruby Browser Extension by logiteca7/aaaa777
Ruby version: #{RUBY_DESCRIPTION}
INF
`);
};
// ファイルのパスをchrome拡張用のURL形式に変換
const buildExtensionURL = (filepath) => {
return new URL(chrome.runtime.getURL(filepath));
}
// RubyVMを初期化する関数
export const initRubyVM = async () => {
const response = await fetch(buildExtensionURL("ruby.wasm"));
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const { vm } = await DefaultRubyVM(module);
UnloosenRubyVM = vm;
};
// URLからrubyファイルを実行
export const evalRubyFileFromURL = async (url) => {
await fetch(url)
.then((response) => response.text())
.then((text) => evalRubyCode(text));
};
// Chrome拡張のrubyファイルを実行
export const evalRubyFileFromExtension = async (filepath) => {
await evalRubyFileFromURL(buildExtensionURL(filepath));
}
// 文字列のrubyコードを実行
export const evalRubyCode = async (code) => {
UnloosenRubyVM.eval(code);
}
ruby-head-wasm-wasi/dist/browser.umd.js について
index.js
の一文目のimport "ruby-head-wasm-wasi/dist/browser.umd.js"
はHTMLに埋め込んでRubyを実行するスクリプトです。
(RubyをWebassembly対応させた)kateiさん作で、RubyVMの起動に利用させて頂きました。
↓リンクからexploreタブの/ruby-head-wasm-wasi/dist/browser.umd.js
でソースが確認できます。
content-script.js
Chrome拡張機能のうち、特定のサイトでJSを読み込ませる機能をContent Scriptといいます。
このJSが読み込まれると、後述のunloosen.config.json
に従ってRubyスクリプトをロードします。
index.js
の関数を利用しました。
実行時はwebpackでdist/content-script.umd.js
に纏めたものを使っています。
import { initRubyVM, evalRubyFileFromExtension, evalRubyCode, evalRubyFileFromURL, printInitMessage } from "./index.js";
// rubyVMの起動
await initRubyVM();
printInitMessage();
// unloosen.config.jsonを読みこむ
fetch(chrome.runtime.getURL("unloosen.config.json"))
.then((response) => response.json())
.then((json) => {
// content_scriptからURLとマッチしたcontent_scriptsを返す処理
return json["content_scripts"].filter((rule) => {
// 簡易matchesフィルタ
// <all_urls>かそうでないかのみチェックする
return rule.matches.filter((match) => {
return match === "<all_urls>"
}).length;
})
})
.then((content_script) => {
content_script.map((rule) => {
// rbプロパティの配列にあるスクリプトを全て非同期実行
rule.rb.map(async (ruby_script) => {
await evalRubyFileFromURL(chrome.runtime.getURL(ruby_script));
})
});
});
この2つのファイルはnpm install && npm run build
でビルドできます。
manifest.json
WASMを読み込むJSや拡張機能のアクセス権を定義する、Chrome拡張の設定ファイルです。
content_scriptsでRubyVMを起動するスクリプトを起動するのがミソです。
この例ではhttp://www.example.com
とhttps://www.google.com
のサイトで拡張機能が有効になります。
また、Webassemblyを動かすためにはcontent_secrity_policy
の設定が必須です。
{
"manifest_version": 3,
"name": "ruby-demo-extension",
"description": "ruby demo extension",
"version": "0.0.1",
"content_scripts": [
{
"js": ["dist/content-script.umd.js"],
"matches": [
"http://www.example.com/",
"https://www.google.com/*"
]
}
],
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
},
"web_accessible_resources": [
{
"resources": ["*"],
"matches": ["<all_urls>"]
}
]
}
unloosen.config.json
content script.jsが読み込まれた後に読み込むRubyスクリプトを設定するファイルです。
Chrome拡張機能の仕様ファイルではなく、僕が設置しました。
ちなみにUnloosenは今開発中の拡張機能フレームワークのコードネームだったりします。
{
"content_scripts": [
{
"rb": ["src/content_script1.rb"],
"matches": ["<all_urls>"]
},
{
"rb": ["src/content_script2.rb"],
"matches": ["<all_urls>"]
}
]
}
まとめ
ということで、実行するRubyファイルはこのような順序で参照されています。
現在開発中のフレームワーク
ここまでのテストでRubyで拡張機能の開発ができることがわかったので、次はChrome拡張のRubyフレームワークを作ってみようかなと思ってます。
現時点ではこんな感じでルーティング機能のついたものを考えてます。
タスク
- VFSを使ってライブラリをruby.wasmに組み込む形で配布出来るようにする。
- ロードした時URLから読み込むスクリプトを分ける。(ルーティング機能)
- Sinatraライクな書き方でイベントフック出来るようにする。
- ruby.wasmのバージョン上げる(JSの関数呼び出しがなんかおかしい)
暇な時に進めていきます。
その他雑記
雑記①
ちなみにこの拡張機能はあの悪名高きManifest Version 3(MV3)で動くように作りました。
MV3でJSのevalは使えないですが、WASM上のrubyではevalが使えます。(MV3でeval使えなくした意味とは?)
雑記②
WASM対応の功労者、kateiさんは実は同い年っぽい・・・
JSを利用する仕組みやirbのデモサイトのソースコードが美しかったので、いつかこれくらい書けるようになりたいと思うのでした。
最後に
今回僕はRubyで拡張機能を作ってみましたが、他言語でも同じように調査してみるつもりです。
ここまで読んで頂きありがとうございました。