PythonとRubyでWebAssembly――PyodideとPyScript、Ruby 3.2を体験する:いろんな言語で試す、WebAssembly入門(終)
第7回は、PythonとRubyによる開発事例を紹介します。これらの言語は、ここまでの回で紹介してきた言語とは異なった、実行環境をWebAssembly化するというアプローチでWebAssemblyに対応しています。PythonのPyodideとPyScript、Ruby 3.2でのWebAssemblyサポートを紹介します。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
PythonとRubyのWebAssemblyへのアプローチ
これまでの回では、TypeScript(AssemblyScript)、C++、Rust、Goといったプログラミング言語でのWebAssemblyサポートを紹介してきました。いずれも、各言語のソースコードのコンパイルによってWebAssemblyバイナリを生成するというもので、言語の特性を生かした高速な実行を期待できるというものでした。
今回紹介するのは、これらとは方向性が違っていて、実行環境そのものをWebAssembly化するというものです。対象言語は、PythonとRubyです。双方とも動的に型付けするスクリプト型の言語であり、実行時にコードが解釈、コンパイルされるので、WebAssemblyバイナリの生成に向いたものとはいえません。そこで、実行環境の方をWebAssemblyすることで、間接的にPythonやRubyのコードを実行しようというわけです。
実行速度という点だけを見れば、実行環境はプラットフォームネイティブに用意されているので、WebAssembly化には意味がないかも知れません。しかし、実行環境がWebAssembly化されてWebブラウザで動作するということには、以下のようなメリットがあります。
- 言語の実行環境がインストール不要でWebブラウザがあれば実行できる
- スマートフォンのような環境でも実行できる
- Webブラウザで実行できる言語の選択肢が増える
初学者が言語の学習をはじめる際の環境構築は意外とハードルが高いということもありますから、それが不要になるということは学習コストを下げることにつながります。また、スマートフォンやタブレットのように開発環境を入れにくい環境でも利用できるということは、学習手段の選択肢を広げることになります。さらに、サーバサイドでPythonやRubyを使っている開発者が、そのスキルをクライアントサイドの開発にも生かせます。このように考えると、高速化だけがWebAssemblyの利用目的にとどまらないことを理解していただけるのではないでしょうか。
以降、PythonとRubyの2つの節に分けて、WebAssemblyへのアプローチを紹介します。
PythonのWebAssemblyへのアプローチ
Pythonでは、Pyodide(パイオダイドと発音)とPyScriptが主な手段となります。PyScriptはPyodideを前提としているので、まずはPyodideから紹介します。
Pyodideとは
Pyodideプロジェクトによる、CPython(以下のNOTEを参照)をWebAssembly化した実行環境です。PyodideをWebページに読み込むことで、JavaScriptからPythonコードを実行することができるようになります。
連載第4回で紹介しましたが、C/C++のWebAssemblyサポートはEmscriptenというプロジェクトが担っています。CPythonはC言語で記述されているので、このEmscriptenを用いてWebAssembly化を果たしています。つまり、PyodideはCPythonをEmscriptenを用いてWebAssemblyに移植したものといえます。
ただし、単なる移植ではありません。micropip(Pyodideのために用意されたpipの簡易実装版)により、Pythonモジュールを読み込んで利用できますし、JavaScript側とPython側にAPIが用意されているので、お互いのスコープにアクセス可能です。例えば、PythonのコードからDOMの要素にアクセスするといったことが容易に実現できます。
【補足】CPythonとは?
CPythonとは、Python言語のレファレンス実装です。参考実装ともいい、他のPython言語の実装に対する標準といえるものです。CPythonの“C”は、実装がC言語によるものであることに由来するといわれています。PyodideのWebAssemblyサポートとは、このCPythonのバイナリをWebAssembly環境で動作させることをいいます。
PyodideのREPLを試す
Pyodideの公式サイトには、Webブラウザで動作するREPL(Read Evaluation Print Loop)環境が用意されており、PythonのインストールをしなくてもWebブラウザ上ですぐにPythonのプログラミングを試すことができます。
Pyodide REPL(https://pyodide.org/en/stable/console.html)
上記ページにWebブラウザでアクセスすると、少しの待ち時間の後に図2のような黒背景の画面が表示されます。これはPythonインタープリタをターミナルなどで実行した状態と全く同じ(もちろん、処理内容は異なります)なので、好きにPythonのコードを入力すればその場で実行結果が返ってきます。
ちなみに、このREPL画面はJavaScriptライブラリjQueryによるターミナル機能を使って、そこからPyodideがPythonのコードを取得、実行して反映するという仕組みになっています。
PyodideでPythonコードを実行する
ここで、Webブラウザ上でPythonのコードを実行してみましょう。以下のHTMLを作成します。これは、Pyodide公式サイトにあるサンプルを少しアレンジしたものです。
<!doctype html> <html> <head> <script src="https://cdn.jsdelivr.net/pyodide/v0.23.2/full/pyodide.js"></script> (1) </head> <body> <script type="text/javascript"> async function main(){ (2) const pyodide = await loadPyodide(); (3) console.log(pyodide.runPython("print('Hello, Pyodide World!!')")); (4) } main(); (5) </script> Pyodide Demo </body> </html>
見ての通り単純なHTMLファイルです。(1)では、CDN(Content Delivery Network)であるjsDelivrからPyodideを使うためのJavaScriptファイルを読み込んでいます。これにより、以下のloadPyodide関数が呼び出し可能になります。
(2)では、(5)で呼び出すmain関数を定義しています。asyncを指定してわざわざ非同期関数を定義して、直後でその関数を呼び出すようにしているのは、(3)のloadPyodide関数を非同期関数の配下で呼び出す必要があるからです。
(3)のloadPyodide関数の呼び出しでは、Pyodideオブジェクトが取得されます。これにはawait式が必須で、これがないとオブジェクトの取得前に次のコードが実行されてしまい、正しい結果が得られません。
(4)では取得したPyodideオブジェクトのrunPythonメソッドで、Pythonのコードを実行しています。このようにPyodideでは、実行するPythonのコードはrunPythonメソッドの引数として与える必要があります。
このHTMLファイルをWebブラウザで表示すると、コンテンツとして「Pyodide Demo」が表示されます。Pythonコードが出力した内容は、図3のようにWebブラウザ(この場合はGoogle Chrome)のデベロッパーツールのコンソールで確認できます。
PyodideでPythonとJavaScriptの相互参照を試す
PythonのコードからDOMを変更し、JavaScriptからPythonの変数を参照してDOMを変更する例も紹介します。
…略… <body> <h1>Pyodide Demo 2</h1> <div>By Python: <span id="output_py">wait for while...</span></div> (1) <div>By JavaScript: <span id="output_js">wait for while...</span></div> <script type="text/javascript"> async function main(){ const pyodide = await loadPyodide(); const code = ` (2) import js (3) elem = js.document.getElementById('output_py') (4) msg = 'Hello, Pyodide World!!' (5) elem.innerText = msg (6) `; pyodide.runPython(code); (7) const elem = document.getElementById('output_js'); (8) elem.innerText = pyodide.globals.get('msg'); (9) } main(); </script> </body> …略…
body要素のみの抜粋です。(1)からの2行には、それぞれPythonコードとJavaScriptコードから書き換えるspan要素を記述しています。
(2)からは、Pythonで実行するコードをまとめて変数codeに格納しています。バッククオートを使うことで、Pythonコードを改行しながら記述できるので、コードの見通しが悪くなるのをある程度防ぐことができます。
(3)ではDOM操作のためにjsモジュールをインポートし、(4)ではjsモジュールのgetElementByIdメソッドでspan要素を取得、(5)では書き換えるテキストを変数msgとして用意し、(6)のinnerTextメソッドで書き換えるなど、流れはJavaScriptで書く場合とほとんど同じです。
(7)では、生成したPythonコードを実行しています。この時点で、Pythonコード用のspan要素が書き換えられます。
(8)では、JavaScriptコードから書き換えるspan要素を取得し、(9)でPyodide.globalsオブジェクトのgetメソッドでPythonの変数msgを参照してinnerTextメソッドで書き換えています。このように、Pyodide.globalsオブジェクトを使うと、Pythonコードのグローバルオブジェクトにアクセスできるので、Python側で実行した複雑な計算結果をJavaScriptコードが受け取って処理したりすることが可能です。
このHTMLファイルをWebブラウザで表示すると、コンテンツとして「Pyodide Demo 2」と「By Python: wait for while...」などがまず表示されます。しばらくすると、「wait for while...」の部分がPythonコードとJavaScriptコードで書き換えられて、図4のようになることが確認できます。
PyScriptとは
このようにPyodideは優れた実行環境ですが、実行したいPythonのコードはrunPython関数の引数として与える必要があります。これには、コードが長くなると必然的に煩雑になり、可読性も低下するという問題があります。PyScriptを使うと、PythonのコードをJavaScriptと同様にタグで囲って記述すればよく、コードの見通しがはるかによくなります。PyScriptは、Pythonの科学計算ディストリビューションであるAnacondaで有名な、米国Anaconda社によるプロジェクトです。
PyScript | Run Python in your HTML
なお、公式サイトにあるように、PyScriptはα版扱いであり、開発も激しく(hardly)進行中という状況なので、現時点では使用方法の研究という段階にとどめておきましょう。
PyScriptでPythonコードを実行する
Pyodide同様に、Webブラウザ上でPythonのコードを実行してみましょう。PyScriptでは、Pythonのコードをpy-script要素の内部に記述していきます。以下のHTMLファイルを用意します。
<!doctype html> <html> <head> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" /> (1) <script defer src="https://pyscript.net/latest/pyscript.js"></script> (2) </head> <body> <py-script> (3) print('Hello, PyScript World!!') (4) </py-script> PyScript Demo </body> </html>
(1)と(2)は、公式サイトで案内されている、PyScript利用のためのCSSとJavaScriptファイルの読み込みのためのlink要素、script要素です。いずれも、PyScriptの公式サイトから最新版を読み込む指定になっています。
(3)は、PyScript独自のタグpy-scriptになります。このタグに囲まれた範囲がPyScriptによって実行されるコードとなります。JavaScriptにおけるscript要素と同じようなものと考えればよいでしょう。
(4)は、実行したいPythonのコードです。
このHTMLファイルをWebブラウザで表示すると、スプラッシュ画面が表示されて読み込み中であることが示された後、図5のようにコンテンツとして「PyScript Demo」とPythonコードが出力した内容が表示されます。Pythonコードが出力した内容は、既定ではその下のターミナルを模した黒背景のボックスに表示されます。
この振る舞いは、py-configタグの内部に記述する指定と、py-scriptタグのoutput属性で変更できます。上記のサンプルを少しだけ変更したのが、下記です。
…略… <head> …略… <py-config> (1) terminal = false </py-config> </head> <body> <py-script output="output"> (2) print('Hello, PyScript World!!') </py-script> PyScript Demo 2 <div id="output"></div> (3) </body> …略…
(1)はPyScript独自のpy-configタグです。ここには、PyScriptの既定の動作を変更したい場合の設定をTOML形式(主に「key = value」の形式)で記述します。ここのterminalは、既定で生成されるpy-terminalタグを無効にするという意味です。これにより、1個目のサンプルにあったターミナル風の出力が行われなくなります。
(2)は、py-scriptタグにoutput属性を指定しています。この属性は、内部のPythonコードの標準出力が書き出されるHTML要素のid属性を指定します。この場合は、(3)のdiv要素がそれに相当します。なお、(1)でpy-terminalを無効にしない限り、div要素とターミナル風の双方に出力されるので注意してください。
このHTMLファイルをWebブラウザで表示すると、図6のようにコンテンツとして「PyScript Demo 2」と「Hello, PyScript World!!」が表示され、図5にあったターミナル風のボックスは表示されなくなります。このように、Pythonコードの実行結果でDOMを書き換えるという程度なら、このように属性の指定だけで済みます。
RubyのWebAssemblyへのアプローチ
Rubyでは、2022年12月にリリースされたバージョン3.2.0において、WASIベースのWebAssemblyサポートが追加されました。WASI(WebAssembly System Interface)とは、OSリソースなどのアクセスを規定したAPIセットのことです(連載第1回の補足を参照)。これによって、Webブラウザやエッジ環境、その他のWebAssembly/WASI環境でCRubyのバイナリが利用できるようになります。
WASIに対応していることから、利用範囲はWebブラウザ上での実行にとどまりません。本稿では例は示しませんが、例えばVFS(Virtual File System)を利用して、Rubyのソースファイル(.rb)もWebAssemblyバイナリに含めて配布するといったことも可能になります。
【補足】CRubyとは?
CRubyとは、Ruby言語のレファレンス実装です。参考実装ともいい、他のRuby言語の実装に対する標準といえるものです。Ruby言語自体は2011年にJIS(日本工業規格)で標準化されていますが、その後は更新されていないのでCRubyが事実上の標準といえます。CRubyの“C”は、実装がC言語によるものであることに由来するといわれています。Ruby 3.2のWebAssemblyサポートとは、このCRubyのバイナリをWebAssembly環境で動作させることをいいます。
TryRuby playgroundでCRubyを実行する
Ruby 3.2のリリースとWebAssemblyサポートにより、Rubyの手軽な実行環境であるTryRuby playgroundもCRuby 3.2をサポートしました。これまでは、OpalというRubyコードをJavaScriptに変換するコンパイラを使っていましたが、Rubyのコードを直接実行できるようになったわけです。
上記にWebブラウザでアクセスし、右ペイン中段にある「Opal 1.7.1」を「CRuby 3.2.0」に選択しなおします。[EDITOR]欄には既定のRubyコードが入っているので、そのまま[Run]をクリックすると、[OUTPUT]欄に実行結果が表示されます(図7)。このとき、初回のみ「*** Loading...」と表示されて、結果の表示まで少し時間がかかりますが、これは他の言語のケースと同様に、WebAssemblyバイナリの読み込みにやや時間を要するためです。
なお、アドレス欄にある通り、コードや実行エンジンがURLに埋め込まれているので、コードのURL文字列で配布できて便利です。
WebブラウザでRubyコードを実行する
ここで、Webブラウザ上でRubyのコードを実行してみましょう。以下のHTMLファイルを用意します。これは、公式GitHubリポジトリにあるサンプルを少しアレンジしたものです。簡単なDOMアクセスの例になっています。
<!doctype html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@1.0.1/dist/browser.script.iife.js"></script> (1) <script type="text/ruby"> (2) require "js" (3) document = JS.global[:document] button = document.getElementById('button') output = document.getElementById('output') button.addEventListener "click" do |e| text = Time.now.to_s output[:innerText] = text puts text end </script> </head> <body> <h1>Ruby WebAssembly Demo</h1> <button id="button">押してみて!</button> (4) <div id="output"></div> </body> </html>
(1)では、CDNであるjsDelivrから、RubyのWebAssemblyバイナリを読み込むためのJavaScriptファイルを読み込んでいます。(2)は、script要素のtype属性にMIMEタイプ"text/ruby"を指定して、ここがRubyのコードであることを示します。読み込まれたCRubyは、この部分のRubyコードを探し出して実行します。(3)は実行すべきRubyのコードです。PyScriptと異なり、独自のタグを用意するのではなく、RubyではMIMEタイプを追加することで対応しています。内容としては、jsモジュールからJSオブジェクトを取得し、button要素の取得、output要素の取得の後、button要素のクリックで現在日時を取得してoutput要素にテキストとして設定するイベントリスナーを設定するというオーソドックスなものです。
(4)以降は、ボタンとRubyコードの実行結果を格納するdiv要素です。
このHTMLをWebブラウザで表示すると、図8のようにコンテンツとして「Ruby WebAssembly Demo」とボタンが表示されます。Rubyコードが出力した内容は、Webブラウザ(この場合はGoogle Chrome)のデベロッパーツールのコンソールで確認できます。
まとめ
最終回である今回は、PythonとRubyのWebAssemblyへのアプローチを紹介しました。
今回を以て、本連載は終了となります。WebAssemblyはWebブラウザをはじめとするプラットフォームを横断するアプリケーション実行技術として、今後も機能強化が続けられていくでしょう。読者がWebAssemblyに関心を持ち、自身のプロジェクトに導入が必要になったときに、少しでも本連載が役に立てば幸いです。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.