キーポイント
- Deno and Node.js both execute JavaScript on C/C++ based runtimes to achieve high performance.
- Deno is a single binary app incompatible with NPM modules, and has no easy way to incorporate native modules in apps.
- WebAssembly provides a way to run high performance code in Deno apps.
- WebAssembly is a safe, portable, and lightweight container for server-side applications.
- The Rust compiler toolchain provides great support for WebAssembly.
Denoプロジェクトがついに、待望のv1.0に到達しました!Node.jsのオリジナル作者であるRyan Dahl氏の手によるDenoは、氏の言う"Node.jsで後悔している10の事"に対処しています。
DenoはNPMと、悪名高いnode_modules
を排除して、TypeScriptとJavaScriptで記述されたアプリケーションを実行する、単一のバイナリ実行ファイルの形式となっています。
その一方で、TypeScriptやJavaSciptは、ほとんどのWebアプリケーションに適したものではありますが、ニューラルネットワークのトレーニングや推論、マシンラーニング、暗号処理といった、計算重視型のタスクには適切でない場合があります。事実として、Node.js自体、これらのタスクにネイティブライブラリの助けを借りることが少なくありません(暗号処理にopensslを使用するなど)。
ネイティブモジュールを取り入れるためのNPM的なシステムを持たないDenoでは、ネイティブパフォーマンスの必要なサーバサイドアプリケーションをどうやって書けばよいのでしょうか?そこで役に立つのが、WebAssemblyです!この記事では、Rustでハイパフォーマンスな関数を記述して、それをWebAssemblyにコンパイルし、Denoアプリケーション内で実行します。
TL;DR
このDenoスタータプロジェクトテンプレートをGitHubからクローンあるいはフォークしてください。インストラクションに従えば、Denoで動作する最初のWebAssembly関数(Rustで記述されている)が5分以内に完成するはずです。
ちょっとした背景説明
Node.jsが大きな成功を収めたのは、開発者に2つの世界で最高のものを提供できたからです。すなわち、JavaScriptによる、特にイベントベースの非同期アプリケーション開発における簡便さと、C/C++のハイパフォーマンスです。Node.jsアプリケーションはJavaScriptで記述されますが、実行は、Google V8 JavaSciptエンジンと多数のネイティブライブラリモジュールを含んだ、C/C++ベースのネイティブランタイムによって行われます。Denoはこの勝利の方程式を再現する一方で、そのプロセスを、TypeScriptとRustによる最新のテクノロジでサポートします。
DenoはV8を使用した、シンプルで現代的、そしてセキュアなJavaScriptおよびTypeScriptのランタイムで、Rustを使って開発されています。-- The deno.land website.
有名なプレゼンテーション"Node.jsで後悔している10の事 (10 things I regret about Node.js)"の中で、Node.jsの作者であるRyan Dahl氏は、Node.jsと競合し、さらには代替となるDenoを、最初に戻って開発する論拠について説明しました。Dahl氏の後悔は、おもにNode.jsがサードパーティのコードやモジュールを管理する方法に関するものです。
- CモジュールをNode.jsにリンクするための複雑なビルドシステム
package.json
、node_modules
、index.js
、その他のNPMアーティファクトに関わる不必要な複雑性
結果としてDenoでは、依存関係の管理について、いくつかの極めて意識的かつ独断的な選択が行われています。
- 単一のバイナリ実行体である
- アプリケーションはTypeScriptまたはJavaScriptで記述され、依存関係はコード内の
import
ステートメントとして、ソースコードにリンクするフルURLで明示的に宣言される - Node.jsのモジュールとは互換性がない
結構。ですが、もっとハイパフォーマンスが必要なアプリケーション、例えば複雑なニューラルネットワークモジュールを秒未満で実行する必要のある、AI・アズ・ア・サービスアプリケーションではどうなるのでしょう?DenoでもNode.jsでも、多くの関数はTypeScriptあるいはJavaScript APIを経由して呼び出されるのですが、実行部分はRustやCを使ってネイティブコードで記述されます。Node.jsでは、JavaScipt APIからサードパーティ製ネイティブライブラリをコールするという選択肢が常に存在します。しかしDenoでは、今のところ、これができないのです。
DenoのWebAssemblyサポート
WebAssemblyは、ポータブルなバイトコードをネイティブ並のスピードで実行できるように設計された、ライトウェイトな仮想マシンです。RustあるいはC/C++関数をWebAsemblyバイトコードにコンパイルすれば、TypeScriptからアクセスすることが可能になります。一部のタスクにおいては、TypeScript自体で記述された同等の関数を実行するよりも、はるかに高速であることが期待できるのです。例えば、IBMのこの研究によると、特定の処理アルゴリズムにおいては、RustとWebAssemblyによって、Node.jsの実行速度を1,200~1,500パーセント改善することが可能になります。
Denoは内部的にGoogle V8エンジンを使用しています。V8はJavaSciptランタイムだけでなく、WebAssembly仮想マシンでもあります。つまりDenoでは、WebAssemblyが最初からサポートされているのです。DenoはTypeScriptアプリケーションに対して、WebAsemblyの関数を呼び出すためのAPIを提供します。
実際に、ポピュラーなDenoコンポーネントの一部は、すでにWebAsemblyで実装されています。例えば、Denoのsqliteモジュールは、sqliteのCソースコードを、Emscriptenを使ってWebAssemblyにコンパイルすることで作成されているのです。Deno WASIコンポーネントは、WebAssemblyアプリケーションからファイルシステムなど、基盤となるオペレーティングシステムのリソースへのアクセスを可能にします。今回の記事では、RustとWebAssemblyを使ったハイパフォーマンスなDenoアプリケーションの記述方法を説明したいと思います。
セットアップ
最初はもちろん、Denoのインストールです!ほとんどのシステムでは、コマンドをひとつ実行するだけです。
$ curl -fsSL https://deno.land/x/install/install.sh | sh
今回はRustで関数を記述するので、Rust言語コンパイラとツールのインストールも必要です。
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
最後に、ssvmupツールがビルドプロセスを自動化し、すべてのアーティファクトを生成して、Denoアプリケーションが簡単にRust関数を呼び出せるようにしてくれます。ここでも、コマンドひとつでssvmupと依存関係をインストールすることができます。
$ curl https://raw.githubusercontent.com/second-state/ssvmup/master/installer/init.sh -sSf | sh
注記: ssvmupは、JavaScriptとRustソースコード間の"グルー(glue)"コードを自動生成するためにwasm-bindgen
を使用するので、それぞれのネイティブなデータ型を使ったコミュニケーションが可能になります。これがない場合、関数の引数と戻り値は、WebAssemblyがネイティブにサポートする、非常にシンプルな型(32ビット整数など)に制限されます。ssvmupとwasm-bindgen
がなければ、文字列や配列などは使用することはできません。
Hello world
最初は、Denoのサンプルからhello worldの例を見てみましょう。hello worldのソースコードとアプリケーションテンプレートはGitHubからダウンロードできます。
Rust関数はsrc/lib.rsファイルの中にあって、入力文字列の前に"hello"を加える簡単なものです。say()
関数が#[wasm_bindgen]
でアノテートされていることで、TypeScriptから呼び出すために必要な"配管"を、ssvmupが生成可能になっている点に注目してください。
#[wasm_bindgen]
pub fn say(s: &str) -> String {
let r = String::from("hello ");
return r + s;
}
Denoアプリケーションはdeno/server.tsファイルの中にあります。このアプリケーションは、ssvmupツールで生成したpkg/functions_lib.jsファイルから、Rustのsay()
関数をインポートしています。functions_lib.js
というファイルの名称は、Cargo.tomlファイルで定義した、Rustのプロジェクト名によって決まります。
import { serve } from "https://deno.land/std@0.54.0/http/server.ts";
import { say } from '../pkg/functions_lib.js';
type Resp = {
body: string;
}
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
let r = {} as Resp;
r.body = say (" World\n");
req.respond(r);
}
それではssvmupを実行して、Rust関数からDeno WebAssembly関数を生成しましょう。
$ ssvmup build --target deno
ssvmupが正常終了すれば、pkg/functions_lib.js
ファイルを調べて、コンパイルされたWebAsemblyファイルpkg/functions_lib.wasm
を実行するために、Deno WebAssembly APIがどのように使用されているかを確認することができます。
では、Denoアプリケーションを実行しましょう。WebAssemblyをロードするためには、Denoにファイルシステムの読み込みパーミッションが必要です。また、HTTP要求の受信と応答を行う必要があるので、ネットワークへのアクセスも可能でなくてはなりません。
$ deno run --allow-read --allow-net deno/server.ts
別のターミナルウィンドウを開いてDeno Webアプリケーションにアクセスすれば、HTTPコネクション経由でhelloを言わせることができます!
$ curl http://localhost:8000/
hello World
もう少し複雑な例
スターターテンプレートプロジェクトには、DenoのTypeScriptとRust関数の間で複雑なデータを受け渡しする方法を示した、もっと詳細なサンプルがいくつか含まれています。ここでは、src/lib.rsにRust関数をいくつか追加してみましょう。それぞれが#[wasm_bindgen]
アノテートされている点に注意してください。
#[wasm_bindgen]
pub fn obfusticate(s: String) -> String {
(&s).chars().map(|c| {
match c {
'A' ..= 'M' | 'a' ..= 'm' => ((c as u8) + 13) as char,
'N' ..= 'Z' | 'n' ..= 'z' => ((c as u8) — 13) as char,
_ => c
}
}).collect()
}
#[wasm_bindgen]
pub fn lowest_common_denominator(a: i32, b: i32) -> i32 {
let r = lcm(a, b);
return r;
}
#[wasm_bindgen]
pub fn sha3_digest(v: Vec<u8>) -> Vec<u8> {
return Sha3_256::digest(&v).as_slice().to_vec();
}
#[wasm_bindgen]
pub fn keccak_digest(s: &[u8]) -> Vec<u8> {
return Keccak256::digest(s).as_slice().to_vec();
}
最も興味深いのは、おそらくcreate_line()
関数でしょう。この関数は、Point
構造体で表現された2つのJSON文字列を取得して、Line
構造体で表現されたJSON文字列を返すものです。Point
とLine
はいずれもSerialize
とDeserialize
でアノテートされている点に注意してください。JSON文字列との相互変換をサポートするために必要なコードをRustコンパイラが自動生成する上で、これらが必要になるのです。
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: f32,
y: f32
}
#[derive(Serialize, Deserialize, Debug)]
struct Line {
points: Vec<Point>,
valid: bool,
length: f32,
desc: String
}
#[wasm_bindgen]
pub fn create_line (p1: &str, p2: &str, desc: &str) -> String {
let point1: Point = serde_json::from_str(p1).unwrap();
let point2: Point = serde_json::from_str(p2).unwrap();
let length = ((point1.x — point2.x) * (point1.x — point2.x) + (point1.y — point2.y) * (point1.y — point2.y)).sqrt();
let valid = if length == 0.0 { false } else { true };
let line = Line { points: vec![point1, point2], valid: valid, length: length, desc: desc.to_string() };
return serde_json::to_string(&line).unwrap();
}
#[wasm_bindgen]
pub fn say(s: &str) -> String {
let r = String::from("hello ");
return r + s;
}
次に、JavaScriptプログラムdeno/test.tsで、Rust関数を呼び出す方法について確認してみましょう。ここでString
と&str
は単純にJavaScriptの文字列、i32
は数値、Vec<u8>
あるいは&[8]
はJavaSciptのUin8Array
です。JavaScriptのオブジェクトをRust関数に渡したり、あるいは受け取ったりする前には、JSON.stringify()
あるいはJSON.parse()
を通す必要があります。
import { say, obfusticate, lowest_common_denominator, sha3_digest, keccak_digest, create_line } from '../pkg/functions_lib.js';
const encoder = new TextEncoder();
console.log( say("SSVM") );
console.log( obfusticate("A quick brown fox jumps over the lazy dog") );
console.log( lowest_common_denominator(123, 2) );
console.log( sha3_digest(encoder.encode("This is an important message")) );
console.log( keccak_digest(encoder.encode("This is an important message")) );
var p1 = {x:1.5, y:3.8};
var p2 = {x:2.5, y:5.8};
var line = JSON.parse(create_line(JSON.stringify(p1), JSON.stringify(p2), "A thin red line"));
console.log( line );
ssvmupを実行してRustライブラリを構築すれば、deno/test.tsをDenoランタイム内で実行することで、次のような出力が得られます。
$ ssvmup build --target deno
... Building the wasm file and JS shim file in pkg/ ...
$ deno run --allow-read deno/test.ts
hello SSVM
N dhvpx oebja sbk whzcf bire gur ynml qbt
246
Uint8Array(32) [
87, 27, 231, 209, 189, 105, 251, 49,
... ...
]
Uint8Array(32) [
126, 194, 241, 200, 151, 116, 227,
... ...
]
{
points: [ { x: 1.5, y: 3.8 }, { x: 2.5, y: 5.8 } ],
valid: true,
length: 2.2360682,
desc: "A thin red line"
}
その次は
Rust関数を作成して、Deno TypeScriptアプリケーションからアクセスすることができました。多数の計算重視のタスクをRust関数に置くことで、ハイパフォーマンスで安全なWebサービスをDenoによって提供することが可能になります。そのようなサービスには、例えばマシンラーニングやイメージ識別などがあります。
著者について
Michael Yuan博士は、ソフトウェアエンジニアリングに関する5冊の書籍の著者です。最新の著書"Building Blockchain Apps"は2019年1月、Addison-Wesleyから出版されました。博士は、WebAssemblyとRustテクノロジをクラウド、ブロックチェーン、AIアプリケーションに導入したスタートアップであるSecond Stateの創業者のひとりでもあります。Second Stateは、迅速で安全でポータブルなサーバレスRust関数のNode.jsへのデプロイを可能にします。WebAssembly.Todayニュースレターを購読して、最新の情報に触れてください。