libuvとは(特にイベントループについて)

f:id:k-kty:20190111205332p:plain
libuvのロゴ

http://docs.libuv.org/en/v1.x/design.html を読み進めていった際のメモです。

概要

libuvは「クロスプラットフォームサポートのためのライブラリ」で、Node.jsで使われることを想定して開発が進められてきた(現在はNode.js以外でも用いられている)。

「イベント・ドリブンな非同期I/Oモデル」をもとに設計されていて、以下のようなものを提供している

  • プラットフォーム間で異なるI/Oポーリングの仕組みの抽象化
  • (ハンドルとストリームによる)ソケットなどの抽象化
  • ファイルI/Oやスレッド周辺の各種機能

ハンドルとリクエスト

libuvでは、イベントループとともに、ハンドルとリクエストという2つの抽象化された概念が用意されている。

ハンドルは「(アクティブなときに)何らかの操作を行うことができる、長期的に存在するオブジェクト」を表す。その例としては以下のようなものがある。

  • イベントループが一周するたびに一度呼ばれるprepareハンドル
  • 新しいコネクションができるたびにコールバックを実行するTCPサーバーハンドル

リクエストは(一般的に)「短い期間で終わる有効な操作」を表す。 リクエストはハンドルを通して行う(例:writeリクエストはハンドルを通してデータを書き込む)ことも、スタンドアロンで行う(例:getaddrinfoリクエストはハンドルを用いず、ループの中で直接実行される)こともできる。

I/Oループ (イベントループ)

I/Oループ(イベントループ)は、libuvの中核をなしている。 イベントループはすべてのI/O操作を管理し、「一つのイベントループが一つのスレッドに対応する」ことが想定されている。

異なるスレッドで動作する限り、複数のイベントループを利用することができる。 ちなみに、イベントループ(そして、ループやハンドルに関係する他のAPI)は、基本的にはスレッドセーフではない。

イベントループは、I/Oに対してシングルスレッドの同期的な(一般的な)アプローチを用いている。 すべての(ネットワーク)I/Oはノンブロッキングなソケット上で行われ、それをプラットフォームに応じた最善の方法でポールしている(Linux上ではepoll, OSXやその他のBSDではkqueue, SunOSではevents port, WindowsではIOCP)。ループの中において、イベントループは(それ以前にポーラーに追加された)ソケット上でのI/Oアクティビティを待ち、ソケットの状況(読み込み可能, 書き込み可能, ハングアップ)とともにコールバックが呼ばれる、これを用いてハンドルはI/O操作を行える。

イベントループへの理解を深めるために、一回のループでどのようなことが行われているのかを確認する。

  1. 「現在時刻」が更新される。イベントループは、ループの最初に取得した時刻をキャッシュすることで、時刻関係のシステムコールの発行回数を減らしている。
  2. もしもループが生きているのなら、イベントループの周回がはじまる。もしも生きていないのなら、イベントループは終了する。(有効かつ参照されているハンドル、有効なリクエスト、またはclosingハンドルが存在する場合、イベントループは生きているとみなされる。)
  3. 適切なタイマーが実行される。イベントループの「現在時刻」よりも前に設定されていたコールバックが呼ばれる。
  4. pendingコールバックが呼ばれる。多くのI/Oコールバックは、I/Oのポーリングの直後に呼ばれることが多い。しかし、コールバックの実行を(イベントループの)次の周回に遅らせるということもある。そのようにして前の周回で後回しにされたI/Oコールバックが、ここで実行される。
  5. idleコールバックが呼ばれる。
  6. prepareハンドルのコールバックが呼ばれる。これはイベントループがI/Oブロックをする直前に呼ばれる。
  7. pollタイムアウト(どれほどの時間I/Oのブロックを行うか)を計算する。計算は以下の通り行われる。
  8. 一つ前のステップで計算した時間分だけ、I/Oのブロックをする。(読み書きのために)ファイルディスクリプタを監視しているI/O関連のハンドルはここで適当なコールバックを呼ぶ。
  9. checkハンドルコールバックが呼ばれる。checkハンドルのコールバックはI/Oのブロックの後に呼ばれ、prepareハンドルのコールバックと対を成している。
  10. closeコールバックが呼ばれる。もしもハンドルが uv_close() によってcloseされた場合、そのハンドルはcloseコールバックを呼び出す。
  11. UV_RUN_NOWAIT または UV_RUN_ONCE モードでループが走っている場合には、ループは終了する (uv_run() がreturnする)。もしも UV_RUN_DEFAULT モードでループが走っている場合には、ループが生きてれば次の周回に入り、そうでなければループは終了する。

注1: 7の計算は以下のように行われる

  • UV_RUN_NOWAIT フラッグとともにループが走っていた場合、0
  • uv_stop() が呼ばれている場合(イベントループがもうすぐ止まる場合)には0
  • アクティブなハンドルやリクエストが存在しない場合には0
  • もしもアクティブなidleハンドルが存在する場合には0
  • 「closeされるべきハンドル」が存在する場合には0
  • 上の場合全てにマッチしない場合には最も直近のタイマーの時間(もしもそれが存在しない場合には無限)

注2: libuvはファイルI/Oにおいてスレッドプールを用いているが、ネットワークI/Oは常にシングルスレッド(イベントループのスレッド)上で行う。

注3: pollingの機構はUnixとWindowsで大きく違うが、libuvは実行モデルに一貫性をもたせている。

ファイルI/O

ネットワークI/Oとは違い、(設計上の困難さから)ファイルI/Oはスレッドプールを用いてファイルのI/Oを行っている。

libuvは現在グローバルな(全てのイベントループが用いることのできる)スレッドプールを用意している。3種類の操作がこのプールで行われる。

  • ファイルシステムの操作
  • DNS関連の操作(getaddrinfo と getnameinfo)
  • ユーザーが定義した操作 (uv_queue_work() を利用する)

注: スレッドプールのサイズは限られているので、注意を配る必要がある

V8 JavaScriptエンジンとは(そしてその高速化について少し)

f:id:k-kty:20190111205205p:plain
V8のロゴ

V8とは

V8はGoogleが開発しているオープンソースのJavaScript実行エンジン(JavaScriptのコードを解析し、機械語まで落とし込んで実行してくれるもの)です。

C++で書かれていて、Google Chromeで使われているほか、Node.js(サーバーサイド向けのJavaScript実行環境)などにも組み込まれています。

競合?として、MicrosoftのChakraやMozillaのSpiderMonkeyなどがありますが、全体的にパフォーマンスはV8に分があります。

公式サイトは https://v8.dev/ です。

JavaScriptエンジンのパフォーマンスについて

JavaScriptの実行エンジンで最も重要なのは「いかに高速にJavaScriptを実行できるか」です。

「どれだけ広いJavaScriptの仕様に対応できるか」や「デバッグ機能の豊富さ」も忘れてはならない点ではあるものの、やはり一番差が出るのはパフォーマンスで、そして実行エンジンのパフォーマンスの差はブラウザのパフォーマンスに大きく影響します。

そのために、ブラウザ(熾烈なシェア争いを繰り広げている)の開発元は実行エンジンのパフォーマンスの向上に努めてきました。

そのおかげか、JavaScriptは動的型付けの言語にもかかわらず、実行エンジンのおかげでかなりのハイパフォーマンスで実行することができるようになっています。(似たようなCPUインテンシブな処理をPythonとJavaSriptで書いてみて比較したらJavaScriptのほうが早いことが多い。)

V8の高速化について

そこで実際、V8がどのようにしてJavaScriptの実行を高速化をしているのか(一例だけど)を軽くまとめてみます。

  1. JavaScriptのコードがパースされ、抽象構文木が作られる。
  2. Ignitionというインタープリタが、その抽象構文木を元に、実行を始める(最適化するよりも、とにかく早く実行を開始するという雰囲気)
  3. Ignitionでの実行中に、ホットな関数(多く呼び出される関数)を特定し、さらにその関数がどういった種類(文字列なのか、数値なのか)の引数とともに呼び出されているのか、というデータを収集する
  4. そのデータをもとに、TurboFanがホットな関数をコンパイルし、最適な機械語を生成する
  5. その機械語を用いた実行を始める
  6. もしも異なったタイプの引数が来てしまった場合(最適化した機械語を用いられない場合)は最適化前の処理に戻る

こういった感じでV8はJavaScriptを実行するらしいです。

特定の関数の引数には常に同じ型のものを渡してあげると、実行エンジンに優しいコードになるということですね。

Node.jsでノンブロッキングなコードを書くべき理由

f:id:k-kty:20190111210413p:plain
Node.jsのロゴ

Node.jsで「イベントループをブロックするコードを書いてはだめで、ノンブロッキングなコードを書かないといけない」とかよく言われます。Node.jsを用いて実際に開発をしている人にとっては至極当然のことですが、なぜノンブロッキングなコードを書かないといけないのか、というのをもう一度まとめてみます。

ブロッキングなコードの例

fsモジュールのreadFileSyncはブロッキングな関数の最たる例です。

const fs = require('fs');
const data = fs.readFileSync('/path/to/file');
// dataを使った操作

とても理解しやすいのですが、「こういうことはやってはいけません」とよく言われます。

ノンブロッキングなコードの例

fs.readFileはfs.readFileSyncのノンブロッキング版です(fs.readFileSyncがfs.readFileのブロッキング版と言ったほうがいいのかもしれないですが...)。

const fs = require('fs');
fs.readFile('/path/to/file', (err, data) => {
  // dataを用いた操作
});

なぜだめなのか

Node.jsにおいて、ユーザー(開発者)が書いたJavaScriptの処理は基本的に一つのスレッド(メインスレッド)の中で処理されます。メインスレッドではイベントループが走っています。

基本的に、一つのスレッドの中では同時に一つの処理(関数)しか実行できません。そのため、fs.readFileSync() の実行中(ファイル読み込み待ち中)は、他の処理が何もできません。例えばこのコードがサーバーアプリケーション内にあったとしたら、fs.readFileSync() の実行中はレスポンスを返すことができないのです。

fs.readFile() の場合はそうではありません。それ自体の関数の実行は一瞬で終わり、Node.js(に組み込まれているlibuv)がファイル読み込みのためのスレッドを立ち上げ、その処理が完了した時点(正確には少し後)でコールバック ((err, data) => { ... }) がメインスレッドで実行されます。よって、メインスレッドはファイルの読み込み待ちの間も他の処理をすることができます。

注: ファイルのI/Oは上のようにメインスレッド以外のスレッドで処理をするのですが、ネットワークI/Oの場合は他のスレッドを用いない処理も多いです。

ブロッキングなコードを書いてもいいとき

fs.readFileSync() が存在するくらいなので、ブロッキングなコードを書いてもいいときは存在します。たとえサーバーアプリケーションだとしてもです。

例えば、プログラムの起動時に config.txt を読み込んで、module.exports を用いて他ファイルから利用(require)したいとします。

そういったときにこういったことはできません。

fs.readFile('./config.txt', (err, data) => {
 // 何らかの処理
 module.expoorts = ...;
});

この問題は fs.readFileSync() などのブロッキングな操作を用いれば解決します。こういった場合が、ブロッキングなコードを書いてよい場合です。

おまけ

const f = (n) => {
  let ret = 0;
  for (let i = 0; i < n; i++) ret += 1;
  return ret;
};

もちろん、このコードはブロッキングな関数です。f(1000000000) みたいなことをするとイベントループはブロックされてしまいます。こういうときは(workerを使うか、C++のアドオンを使うかなどして)別スレッドを立てて処理するようにしないといけません。

V8をアプリケーションに組み込むためのガイド

f:id:k-kty:20190111205205p:plain
V8のロゴ

https://v8.dev/docs/embed を読み進めていったときのメモです。

概要

C++アプリケーション内に、V8 JavaScriptエンジンを組み込むためのガイド。

次の2つの方法を紹介する

  • C++のオブジェクトやメソッドをJavaScriptで使えるようにする
  • JavaScriptのオブジェクトやメソッドをC++で使えるようにする

重要な概念

Isolate(アイソレート)

自らのヒープをもった、VMインスタンス。

Local Handle(ローカルハンドル)

オブジェクトを参照するもの。V8のオブジェクトはすべてハンドルを介して利用される(GCの仕組みと関係がある)。

Handle Scope(ハンドルスコープ)

複数のハンドルの集合。使い終わったハンドルを一つ一つ削除するのではなく、スコープごと削除する、といったことができる。

Context(コンテキスト)

実行環境。複数の(関係のない)JavaScriptコードを一つのV8インスタンス内で走らせることを可能にする。

どのコンテキスト内でJavaScriptコードを走らせたいのか、常に明示的に指定する必要がある。

ハンドルとGC

ハンドルは、(ヒープ内にある)JavaScriptのオブジェクトの位置への参照を提供する。

V8のGCは、「今後アクセスされないオブジェクトによって使われているメモリ」を再利用できるようにする。その過程で、オブジェクトがヒープ内の別の位置に移動されることがあるが、そういった場合にハンドルは更新される。

オブジェクトがJavaScript内で参照されていない、かつどのハンドルからも参照されていないとき、オブジェクトはGarbageとみなされる。そして、(適当なタイミングで)GCによって削除される。

ハンドルの種類

ハンドルにはいくつか種類がある。

Local Handle(ローカルハンドル)

ローカルハンドルはスタックに保持され、適当なデストラクタによって削除される。

このハンドルのライフタイムは、「ハンドルスコープ」によって決定される。ハンドルスコープは、一般的には関数呼び出し時に作成される。

ハンドルスコープが削除されるとき、ハンドルスコープ内のハンドルによって参照されていたオブジェクト群は不要とみなされ、それらよって使われていたメモリは再利用可能となる。(JavaScriptコードまたは他のハンドルから参照されている場合にはオブジェクトは不要にならない。)

ローカルハンドルに対応するV8のクラスは Local<SomeType> である

注: ハンドルスタックはC++のコールスタックの中にはないが、ハンドルスコープはC++のコールスタックの中にある。ハンドルスコープはスタックにしかアロケートされない(new を用いてアロケートしない)。

Persistent Handle(永続ハンドル)

ローカルハンドルと同様、ヒープに置かれたJavaScriptオブジェクトへの参照を提供する。

2つのバリエーションが有り、それによって参照のライフタイムが異なる。

Persistent Handleは、以下のような場合に用いる。

  • オブジェクトへの参照を一つの関数を超えて保持したい場合
  • C++のスコープに対応しない形でオブジェクトを用いたい場合

例えばGoogle Chromeは、DOMノードへの参照を保持するのにPersistent Handleを用いている。

Persistent HandleはWeakにすることもできる。「オブジェクトがWeakなPersistent Handleからしか参照されていない場合には、GCによるコールバックを発動させる」といったことができる。

Persistent Handleの2つのバリエーションは、以下の通りである。

UniquePersistent

対象オブジェクトのライフタイムがC++のコンストラクタとデストラクタに依存する。

Persistent

コンストラクタによって作成できるが、 Persistent::Reset によって明示的にクリアしないといけない。

メモ

PersistentとUniquePersistentはコピーすることができず、C++11以前の標準ライブラリのコンテナ(std::vectorなど)とともに用いることには向いていない。そのため、PersistentValueMapPersistentValueVectorが用意されている。

その他

Eternal

削除されることのないオブジェクトのためのPersitent Handle。オブジェクトが生きているかをGCが判定する手間を省くことができるため、軽量。

ハンドルスコープ

オブジェクトを作成するときに、毎回ローカルハンドルを作っていては、ハンドルの数が膨大になってしまう。

そこで便利なのが、ハンドルスコープである。ハンドルスコープのデストラクタが呼ばれたとき、そのスコープ内に作られた全てのハンドルがスタックから削除される。(そして、参照されていたオブジェクトはGCによってヒープから削除することができるようになる。)

ハンドル・ハンドルスコープの例

HandleScope handle_scope(isolate);
Local<Context> context = Context::New(isolate);
Persistent<Context> persistent_context(isolate, context);
Context::Scope context_scope(context);
Local<String> source_obj = String::NewFromUtf8(isolate, argv[1]);
Local<Script> script_obj = Script::Compile(source_obj);
Local<Value> local_result = script_obj->Run();

注目すべき点は、

  • Context::New() はローカルハンドルを返しており、それを用いて永続ハンドルを作っている。
  • handle_scopeのデストラクタが呼ばれたとき、source_objやscript_objは不要になる
  • persistent_contextは永続ハンドルなので、ハンドルスコープから抜けても削除されない
  • persistent_contextを削除するには、persistent_context->Reset()をする必要がある

(ハンドルスコープを宣言している)関数内で定義したローカルハンドルを返り値として用いることはできない。

それに対処するためには、HandleScopeの代わりにEscapableHandleScopeクラスを用いる。そして以下のように、用いたいオブジェクトのローカルハンドルを引数として、Escapeメソッドを用いる。

Local<Array> NewPointArray(int x, int y, int z) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();

  EscapableHandleScope handle_scope(isolate);

  Local<Array> array = Array::New(isolate, 3);

  if (array.isEmpty())
    return Local<Array>();

  array->Set(0, Integer::New(isolate, x));
  array->Set(1, Integer::New(isolate, y));
  array->Set(2, Integer::New(isolate, z));

  return handle_scope.Escape(array);
}

Escapeメソッドは、引数に渡された値を、現在のスコープを含んでいるスコープにコピーし、ローカルハンドルを全て削除する。そして新しいハンドルを返す。

コンテキスト

コンテキストは、実行環境を意味する。V8の一つのインスタンス内で、独立した(関連性のない)複数のJavaScriptのアプリケーションを実行するために用意されている。

JavaScriptコードを実行するときには、どのコンテキストで実行するのかを明示的に指定する必要がある。

JavaScriptでは、ビルトインのユーティリティ関数やオブジェクトを、コード内で変更することができる。もしも2つの全く関係ないJavaScriptの関数が、(共有している)グローバルオブジェクトを同じように変更しようとしたら、予期しない結果を引き起こすはずである。こういった問題が、コンテキストによって解消される。

多くのビルトインオブジェクトがその中に含まれることを考えると、CPUやメモリへの負荷が大きいと思うかもしれない。 しかし、V8のキャッシュ機構のおかげで、初回以降のコンテキストの作成は初回のコンテキストの作成よりもかなり軽量である。ビルトインのJavaScriptコードのパースは、初回のみで十分なためである。 さらに、V8のスナップショット機能(snapshot=yes ビルドオプションで有効になる。デフォルトで有効)は、ビルトインのJavaScriptコードを予めコンパイルし、シリアル化されたヒープを含んだスナップショットを作成する。これを用いれば、初回のコンテキストの作成も高速化される。

コンテキストは一度作った後、何度も入ったり出たりできる。

注: コンテキストAからコンテキストBに入り、その後にコンテキストBを抜けると、コンテキストAに復帰する。

それぞれのウィンドウ、そしてiframeがそれぞれ個別の(他に干渉されない)JavaScriptの環境を作りたいというモチベーションにより、V8はコンテキストの仕組みを持っている。

テンプレート

テンプレートは、コンテキスト内での関数やオブジェクトの設計図のようなものである。 テンプレートでC++の関数やデータ構造をラップすることで、JavaScriptから扱えるオブジェクトを作ることができるようになる。 例えばGoogle Chromeは、テンプレートを用いてC++のDOMノードをラップし、JavaScriptオブジェクトとして利用できるようにしている。 テンプレートは一度作れば、複数のコンテキストで用いることができる。 また、テンプレートは必要なだけ作成することができるが、一つのコンテキスト内で、各テンプレートはそれぞれ一つのインスタンスしか持つことができない。

JavaScriptでは、関数とオブジェクトに強い二重性がある。 C++やJavaでは、新しい種類のオブジェクトを作成する際にはクラスを定義する。 一方、JavaScriptでは関数を作り、その関数をコンストラクタとしてインスタンスを作成する。そのため、JavaScriptオブジェクトのレイアウトや機能は、それを作成した関数に強く結びついている。この特徴は、V8のテンプレートの動作に反映されている。

テンプレートには2つの種類がある。

Function Template(関数テンプレート)

Function Templateは、関数のための下書きである。コンテキスト内でGetFunctionメソッドを呼ぶことで、JavaScriptの関数をインスタンス化することができる。JavaScriptの関数(インスタンス)が呼ばれたときにC++コールバックが呼び出されるようにすることができる。

Object Template(オブジェクトテンプレート)

Function Templateはそれぞれ、対応するObject Templateを持っている。関数をコンストラクタとして、オブジェクトを作成するように設計されている。Object Templateには(以下の)2種類のC++のコールバックを関連付けることができる。

Accessor Callbacks(アクセッサコールバック)

特定のオブジェクトのプロパティがアクセスされた際に呼ばれるコールバック

Interceptor Callbacks(インターセプタコールバック)

オブジェクトのプロパティがアクセスされた際に必ず呼ばれるコールバック

以下の例では、グローバルオブジェクトのテンプレートを作成し、ビルトインのグローバル関数を設定している。

Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
global->Set(String::NewFromUtf8(isolate, "log"), FunctionTemplate::New(isolate, LogCallback));
Persistent<Context> context = Context::New(isolate, NULL, global);

Accessors(アクセッサ)

JavaScriptによって、オブジェクトのプロパティがアクセスされたときに、計算を行い値を返すC++のコールバックのことをアクセッサという。 アクセッサは(SetAccessorメソッドを用いて)オブジェクトテンプレートで設定される。 SetAccessorメソッドは、関連付けたいプロパティの名前と、読み/書きの際にそれぞれ呼ばれる2つのコールバックを引数として受け付ける。

スタティクグローバル変数へのアクセス

C++内で2つの整数変数x, yをコンテキスト内のグローバル変数としてJavaScriptからアクセス可能にしたいケースを考える。 この場合には、スクリプトがそれらの値を読んだり書いたりする際に、C++のアクセッサを呼ぶ必要がある。 アクセッサ関数(読み/書きの2つ)は、C++の整数を(Integer::Newを用いて)JavaScriptの整数に変換し、JavaScriptの整数を(Int32Value)を用いて、C++の整数に変換する。 コードは以下のようになる。(yについては省略)

void XGetter(Local<String> property, const PropertyCallbackInfo<Value>& info) {
  info.GetReturnValue().Set(x);
}

void XSetter(Local<String> property, Local<Value> value, const PropertyCallbackInfo<void>& info) {
  x = value->Int32Value();
}

Local<ObjectTemplate> global_templ = ObjectTemplate::New(isolate);
global_templ->SetAccessor(String::NewFromUtf8(isolate, "x"), XGetter, XSetter);
Persistent<Context> context = Context::New(isolate, NULL, global_templ);

注: 上のコードで、オブジェクトテンプレートはコンテキストと同じタイミングで作られているが、「テンプレートは事前に作っておいて、それを後に複数のコンテキストで用いる」ということもできる。

ダイナミック変数へのアクセス

上の例では、変数が静的でグローバルなものだった。もしもブラウザのDOMツリーのように、操作されるデータがダイナミックだったらどうなるのであろうか。 まず、xとyが、C++のPointクラスのフィールドとしよう。

class Point {
public:
  Point(int x, int y) : x_(x), y_(y) { }
  int x_, y_;
}

C++のpointインスタンスを(ほしい分だけ)JavaScriptで使えるようにしたい。 そこで、C++のpointインスタンスそれぞれに対してひとつのJavaScriptオブジェクトをつくり、それらの間の関連付けをしないといけない。 これは、external valueとinternal object fieldsによって実現される。

まず、pointのラッパーオブジェクトのオブジェクトテンプレートを作成する。

Local<ObjectTemplate> point_templ = ObjectTemplate::New(isolate);

各JavaScriptのpointオブジェクトは、それをラップし、内部フィールドを持っているC++オブジェクトへの参照を保持するようになっている。 内部フィールドというのは、JavaScriptからはアクセスできずC++からアクセスできるフィールドである。 オブジェクトはいくつでも内部フィールドを持つことができ、その数は以下のように(オブジェクトテンプレートにおいて)設定できる。

point_templ->SetInternalFieldCount(1);

ここで、内部フィールドの数は1に設定されており、その内部フィールドのインデックス0は、C++のオブジェクトを指している。

次に、xとyのアクセッサをテンプレートに追加する。

point_templ->SetAccessor(String::NewFromUtf8(isolate, "x"), GetPointX, SetPointX);
point_templ->SetAccessor(String::NewFromUtf8(isolate, "y"), GetPointY, SetPointY);

そして以下のように、オブジェクトテンプレートからインスタンスを作成し、それを内部フィールドに設定してラップする。

Point* p =...;
Local<Object> obj = point_templ->NewInstance();
obj->SetInternalField(0, External::New(isolate, p));

Externalオブジェクトはvoid*のラッパーである。Externalオブジェクトは内部フィールドに参照を保存するためにのみ用いられる。 JavaScriptオブジェクトはC++オブジェクトへの参照を直接持つことはできず、橋渡しとして、Externalバリューが用いられる。 HandleがC++からJavaScriptオブジェクトへの参照を提供していたのに対し、ExternalはJavaScriptからC++オブジェクトへの参照を提供しているという点で、HandleとExternalは対照的である。

以下はxのための読み込み/書き込みアクセッサの定義である。

void GetPointX(Local<String> property, const PropertyCallbackInfo<Value>& info) {
  Local<Object> self = info.Holder();
  Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
  void *ptr = wrap->Value();
  int value = static_cast<Point*>(ptr)->x_;
  info.GetReturnValue().Set(value);
}

void SetPointX(Local<String> property, Local<Value> value, const PropertyCallbackInfo<void>& info) {
  Local<Object> self = info.Holder();
  Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
  void* ptr = wrap->Value();
  static_cast<Point*>(ptr)->x_ = value->Int32Value();
}

アクセッサはJavaScriptオブジェクトにラップされているpointオブジェクトへの参照を取り出し、対象のフィールドを読み書きしている。 このようにして、何個のpointオブジェクトに対しても使えるジェネリックなアクセッサを作成できる。

Interceptor (インターセプター)

オブジェクトのどのプロパティがアクセスされたときにも呼び出されるようなコールバックを指定することができる。これはインターセプターと呼ばれている。 インターセプターには2つの種類が存在する。

Named Property Interceptor (名前付きプロパティのインターセプタ)

文字列の名前を持ったプロパティがアクセスされたときに呼ばれるインターセプタ。 (文字列の名前を持ったプロパティの)例として、ブラウザでの document.theFormName.elementName などがある。

Indexed Property Intercepter (インデックスプロパティのインターセプタ)

インデックスプロパティがアクセスされたときに呼ばれるインターセプタ。 (インデックスプロパティの)例として、ブラウザでの document.forms.elements[0] などがある。

V8のソースコードにサンプルとして含まれているprocess.ccはインターセプターを使用している。 以下のコードで、SetNamedPropertyHandlerはMapGetとMapSetをインターセプタとして指定している。

Local<ObjectTemplate> result = ObjectTemplate::New(isolate);
result->SetNamedPropertyHandler(MapGet, MapSet);

MapGetインターセプタは以下のようになっている

void JsHttpRequestProcessor::MapGet(Local<String> name, const PropertyCallbackInfo<Value>& info) {
  map<string, string> *obj = UnwrapMap(info.Handler());
  string key = ObjectToString(name);
  map<string, string>::iterator iter = obj->find(key);
  if (iter == obj->end()) return;
  const string &value = (*iter).second;
  info.GetReturnValue().Set(String::NewFromUtf8(value.c_str(), String::kNormalString, value.length()));
}

セキュリティモデル

ブラウザでいう、same-origin policyは、あるオリジンから得たドキュメントやスクリプトが、他のオリジンから得たドキュメントを読み込んだり書き換えたりすることを防ぐためのものである。 ここで、オリジンというのは、ドメイン名、プロトコル(httpsなど)、そしてポートの組み合わせとして定義されている。つまり、www.example.com:81とwww.example.comは異なったオリジンであり、同じオリジンとして扱われるには、ドメイン名、プロトコル、ポートの全てが一致している必要がある。 このポリシーが無いと、悪意のあるウェブページが(ブラウザ内で)他のウェブページを書き換えたりすることができてしまう。

V8では、オリジンはコンテキストとして定義されている。呼び出し元のコンテキスト以外のコンテキストへのアクセスはデフォルトで禁止されている。もしも呼び出し元のコンテキスト以外のコンテキストにアクセスしたい場合には、セキュリティトークンまたはセキュリティコールバックを用いる必要がある。 セキュリティトークンは任意の値を取れるが、シンボル(他で使われていないカノニカルな文字列)を用いることが多い。 コンテキストを用意する際に、SetSecurityTokenを通して明示的にセキュリティトークンを指定することができる。もしもそうでない場合には、V8が自動で作成してくれる。

グローバル変数へのアクセスが試みられたとき、V8のセキュリティシステムは(アクセスの対象となっている)グローバルオブジェクトのセキュリティトークンと、(アクセスをしようとしている)コードのセキュリティトークンをチェックしている。 もしも一致していた場合、アクセスは許可される。もしも一致していない場合には、V8はアクセスを許可すべきか否かを判定するためのコールバックを呼び出す。 (オブジェクトテンプレートの)SetAccessCheckCallbacks関数を用いてオブジェクトに対してセキュリティコールバックを設定することで、オブジェクトへのアクセスを許可するか否かを指定することができる。 V8のセキュリティシステムは、(アクセスの対象となっている)オブジェクトのセキュリティコールバックを取得し、それを呼ぶことで他のコンテキストがそれにアクセス可能か否かを判定する。 このコールバックには、「アクセスされているオブジェクト」「アクセスされているプロパティ」そして「行われようとしている操作の種類(書き込み、読み込み、削除など)」が与えられ、「アクセスを許可するか」を返す。

例えばGoogle Chromeでは次のものに対して特別なコールバックが使用されている。window.focus(), window.blur(), window.close(), window.location, window.open(), history.forward(), history.back(), history.go()

例外

V8は、エラーが発生した際に例外を発生させる。例えば、スクリプトや関数が、存在しないプロパティを読み込んだり、関数じゃないものを関数として用いたりしたときである。 V8は、操作が成功しなかった場合に空のハンドルを返す。そのため、コード内で関数の返り値が空のハンドルで無いか逐一確かめる必要がある。それは、LocalクラスのIsEmpty関数を用いることによって可能である。 また、(以下のように)TryCatchを用いて例外をキャッチすることができる。

TryCatch trycatch(isolate);
Local<Value> v = script->Run();
if (v.IsEmpty()) {
  Local<Value> exception = trycatch.Exception();
  String::Utf8Value exception_str(exception);
  printf("Exception: %s\n", *exception_str);
}

もしも返り値として空のハンドルが返ってきて、TryCatchが存在しなかった場合、コードの実行はは終了する必要がある。もしもTryCatchが存在しており(例外がキャッチされているのであれば)、コードは終了しなくても良い。

継承

JavaScriptは、クラスのないオブジェクト指向言語である。伝統的なクラス継承の仕組みではなく、プロトタイプを用いた継承を用いる。C++やJavaなどの、一般的なオブジェクト指向の言語に慣れているプログラマにとって混乱しやすいかもしれない。

(JavaやC++などの)クラスに基づいたオブジェクト指向プログラミング言語は、クラスとインスタンスという異なった2つの概念をベースにしている。 プロトタイプベースのJavaScriptにはこの区別がない(オブジェクトがあるだけである)。 JavaScriptはクラスのヒエラルキーの宣言に対応していない。しかし、JavaScriptはプロトタイプの仕組みによって、カスタムのプロパティやメソッドをオブジェクトに追加することを簡単にしている。 例えば、以下のようにしてオブジェクトにカスタムのプロパティを追加することができる。

function bicycle() {}
var roadbile = new bicycle();
roadbike.wheels = 2;

このようにして追加されたカスタムプロパティは、当該オブジェクトにのみ存在する。もしほかのbicycle()インスタンス(mountainbikeとする)を作ったときには、mountainbike.wheelsはundefinedとなる。

このような動作が好ましいときもあるが、全てのオブジェクトインスタンスに共通してカスタムプロパティを追加したい場合も存在する。 その際に便利なのが、JavaScriptのプロトタイプオブジェクトである。プロトタイプオブジェクトは、以下のようにして利用することができる。

function bicycle() {}
bicycle.prototype.wheels = 2;

これにより、bicycleインスタンスはすべてwheelsプロパティを持つようになった。

このような機能は、V8のテンプレートにも用いられている。関数テンプレートにはPrototypeTemplateというメソッドがあり、それによってテンプレートがプロトタイプの機能を持つことができる。 プロパティを追加し、そしてそれらにPrototypeTemplate関数を用いてC++の関数を関連付けることで、対応するFunctionTemplateのインスタンスからプロパティが利用できるようになる。 例としては、次のようになる。

Local<FunctionTemplate> biketemplate = FunctionTemplate::New(isolate);
biketemplate->PrototypeTemplate().Set(
  String::NewFromUtf8(isolate, "wheels"),
  FunctionTemplate::New(isolate, MyWheelsCallback)->GetFunction()
);

これにより、biketemplateは、プロトタイプチェーンにwheelsメソッドを持つようになり、それが呼ばれたときにはC++の関数MyWheelsMethodCallbackが呼ばれるようになる。

そしてV8のFunctionTemplateクラスは、メンバー関数としてInheritを持っている。これにより次の例のように、ある関数テンプレートを継承して関数テンプレートを作ることができる。

void Inherit(Local<FunctionTemplate> parent);

ContentfulとexpressとNuxt.jsで簡単なブログを作ってみる

Contentfulというサービスを知る機会があったので、それを用いてブログを作ってみました。 Contentfulは、ユーザー向けのページを提供しないCMS(ヘッドレスCMS)というもので、コンテンツの追加・編集画面やデータベース、そしてそのコンテンツを利用するためのAPIなどを提供しています。ベルリンの会社で、Lyftとかも顧客にいるみたいです。

今回は

  • コンテンツの追加・管理 -> Contentful
  • Contentfulからデータを取得して整形 -> express
  • ユーザー向けの画面を生成 -> Nuxt.js といった形で簡単なブログを作ってみたいと思います。

コードはこちらで公開しています。

github.com

Contentful

まずはContentful周りの作業をします。

アカウントの作成

以下のページからContentfulのアカウントを作成できます。 少し使うだけなら無料です。

www.contentful.com

Spaceの作成

その後、Spaceというものを作ります。データベースの名前みたいなものです。今回は blogという名前の物を作ります。

f:id:k-kty:20181212113005p:plain

Content Modelの作成

次に、Content Modelというものを作ります。データベースで言うテーブルのようなものです。

Content Modelでは、複数のFieldを定義していきます。種類は以下のようなものがあります。

f:id:k-kty:20181212113132p:plain

今回はこのようにフィールドを定義していきました。

  • 記事ID ... Short text
  • タイトル ... Short text
  • 日付 ... Date & time
  • 本文 ... Rich text

Contentの作成

Content Modelの定義が終わったら、それに従ってContentを作成していきます。

編集画面はこのようになっています。シンプルでとても使いやすいです。

f:id:k-kty:20181212113632p:plain

express

次に、「Contentfulからデータを取得し、整形してレスポンスとして返す」APIをexpressで作成します。

ファイル構成

  • package.json
  • config.js 設定ファイル
  • contentful.js Contentfulのライブラリのラッパー
  • index.js ルーティングの設定・処理の定義
  • models.js 記事クラス(Post)の定義

package.json

{
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "contentful": "^7.0.5",
    "cors": "^2.8.5",
    "express": "^4.16.4"
  }
}

config.js

こちらに必要な値はContentfulで取得できます。

module.exports = {
  CONTENTFUL_SPACE: '...',
  CONTENTFUL_ACCESS_TOKEN: '...',
}

contentful.js

const contentful = require('contentful');

const {
  CONTENTFUL_SPACE,
  CONTENTFUL_ACCESS_TOKEN,
} = require('./config');

const client = contentful.createClient({
  space: CONTENTFUL_SPACE,
  accessToken: CONTENTFUL_ACCESS_TOKEN,
});

module.exports = client;

models.js

Rich text形式のデータをHTMLに変換するためのコードも含んでいます。

Rich text形式のデータについてはこちらを参照してください。

Rich Text – Contentful

const contentful = require('./contentful');

class Post {
  constructor({
    id,
    title,
    date,
    content,
  }) {
    this.id = id;
    this.title = title;
    this.date = date;
    this.content = content;
  }

  get contentHtml() {
    return Post.getHtml(this.content);
  }

  static getHtml(content) {
    if (content.nodeType === 'text') {
      const bold = content.marks.filter(i => i.type === 'bold').length > 0;
      const italic = content.marks.filter(i => i.type === 'italic').length > 0;
      const underline = content.marks.filter(i => i.type === 'underline').length > 0;

      const styles = {};

      if (bold) {
        styles['font-weight'] = 'bold';
      }

      if (italic) {
        styles['font-style'] = 'italic';
      }

      if (underline) {
        styles['text-decoration'] = 'underline';
      }

      if (Object.keys(styles).length) {
        return `<span style="${Object.entries(styles).map(i => `${i[0]}:${i[1]};`).join('')}">${content.value}</span>`;
      } else {
        return content.value;
      }
    }

    if (content.nodeType === 'hr') {
      return '<hr/>';
    }

    let tag = '';
    const attributes = {};

    if (content.nodeType === 'heading-1') {
      tag = 'h1';
    } else if (content.nodeType === 'heading-2') {
      tag = 'h2';
    } else if (content.nodeType === 'heading-3') {
      tag = 'h3';
    } else if (content.nodeType === 'heading-4') {
      tag = 'h4';
    } else if (content.nodeType === 'heading-5') {
      tag = 'h5';
    } else if (content.nodeType === 'heading-6') {
      tag = 'h6';
    } else if (content.nodeType === 'paragraph') {
      tag = 'p';
    } else if (content.nodeType === 'document') {
      tag = 'div';
    } else if (content.nodeType === 'unordered-list') {
      tag = 'ul';
    } else if (content.nodeType === 'ordered-list') {
      tag = 'ol';
    } else if (content.nodeType === 'list-item') {
      tag = 'li';
    } else if (content.nodeType === 'blockquote') {
      tag = 'blockquote';
    } else if (content.nodeType === 'hyperlink') {
      tag = 'a';
      attributes.href = content.data.uri;
    } else if (content.nodeType === 'embedded-asset-block') {
      tag = 'img';
      attributes.src = content.data.target.fields.file.url;
      attributes.alt = content.data.target.fields.title;
    }

    return `<${tag}${Object.entries(attributes).map(i => ` ${i[0]}="${i[1]}"`).join('')}>${content.content.map(Post.getHtml).join('')}</${tag}>`;
  }

  static async findById(id) {
    return contentful.getEntries({
      'fields.id': id,
      'content_type': 'post',
    })
      .then(i => i.items)
      .then((entries) => {
        if (!entries.length) {
          return null;
        }

        return new Post(entries[0].fields);
      });
  }

  static async list(params) {
    return contentful.getEntries(params)
      .then(i => i.items)
      .then(entries => entries.map(entry => new Post(entry.fields)));
  }
};

module.exports = {
  Post,
};

index.js

  • /posts で記事の一覧を取得
  • /posts/<記事id> で記事のデータを取得

といったようにしています。

const express = require('express');
const cors = require('cors');

const { Post } = require('./models');

const app = express();

app.use(cors());

app.get('/posts', async (req, res) => {
  const posts = await Post.list(req.query)
    .then(posts => posts.map(post => ({
      id: post.id,
      title: post.title,
      date: post.date,
    })));

  res.send(posts);
});

app.get('/posts/:id', async (req, res) => {
  const post = await Post.findById(req.params.id);

  // 投稿が存在しない場合には404エラー
  if (!post) {
    res.status(404).send({
      error: 'not found',
    });

    return;
  }

  res.send({
    id: post.id,
    title: post.title,
    date: post.date,
    content: post.contentHtml,
  });
});

app.listen(4000);

動かしてみる

以上のディレクトリで npm start を実行すると、サーバーが localhost:4000 で立ち上がります。

http://localhost:4000/posts で投稿のリストを取得します。以下のようなレスポンスが返ってきます。

[{"id":"what-is-contentful","title":"Contentfulを試してみた","date":"2018-12-01T00:00+09:00"}]

http://localhost:4000/posts/what-is-contentful で投稿の詳細を取得します。以下のようなレスポンスが返ってきます。

{"id":"what-is-contentful","title":"Contentfulを試してみた","date":"2018-12-01T00:00+09:00","content":"<div><h1>Contentfulとは</h1><p><a href=\"https://www.contentful.com/\">Contentful</a>は<span style=\"font-weight:bold;\">ヘッドレスCMS</span>です。</p><p>以下のように画像を挿入したりもできます。</p><img src=\"//images.ctfassets.net/gu6l9tv1vpsd/2kMHqVs6JS6wMkOqOWIGMU/fe98c0c3a6f207a4d9286171e348bd6b/____________________________2018-12-10_1.50.46.png\" alt=\"スクリーンショット 2018-12-10 1.50.46\"></img><p></p><p></p><p></p><p></p></div>"}

Nuxt.js

次に、expressからデータを取得して表示するページをNuxtで作成します。

フィアル構成

  • layouts/default.vue
  • pages/index.vue
  • pages/posts/_id.vue
  • nuxt.config.js
  • package.json

package.json

{
  "scripts": {
    "start": "nuxt"
  },
  "dependencies": {
    "@nuxtjs/axios": "^5.3.6",
    "nuxt": "^2.3.4"
  }
}

nuxt.config.js

Nuxtの設定ファイルです。axiosモジュールを使うようにしています。

module.exports = {
  modules: [ '@nuxtjs/axios' ],
};

layouts/default.vue

デフォルトのレイアウトを定義します。

<template>
  <div class="page">
    <section class="header">
      <nuxt-link to="/">
        <h1>Contentful + Express + Nuxt で作るブログ</h1>
      </nuxt-link>
    </section>
    <section class="main">
      <nuxt/>
    </section>
  </div>
</template>

<style scoped>
.page {
  max-width: 720px;
  margin: 0 auto;
  padding: 20px;
}

.header {
  padding: 20px 0;
}

.header a {
  text-decoration: none;
}

.header h1 {
  font-size: 18px;
  /* text-align: center; */
}

.main {

}
</style>

<style>
* {
  box-sizing: border-box;
  margin: 0;
  color: #333;
}
</style>

pages/index.vue

トップページ(記事の一覧ページ)です。今回は簡単のためにコンポーネント化などはしません。

<template>
  <div>
    <ul class="post-list">
      <template v-for="(post, idx) in postList">
        <li :key="'post'+idx">
          <nuxt-link :to="'/posts/' + post.id">
            <span v-text="post.title"/>
          </nuxt-link>
        </li>
      </template>
    </ul>
  </div>
</template>

<script>
export default {
  async asyncData({ app }) {
    const postList = await app.$axios.get('http://localhost:4000/posts')
      .then(i => i.data);

    return {
      postList,
    };
  },
}
</script>

<style scoped>
.post-list {
  margin: 0;
  padding: 0;
}

.post-list li {
  list-style: none;
}
</style>

pages/posts/_id.vue

記事の詳細ページです。存在しない記事を見ようとした場合には404エラーになるようにしています。

<template>
  <div>
    <p
      v-text="formatDate(post.date)"
      class="post-date"/>
    <h1
      v-text="post.title"
      class="post-title"/>
    <div
      v-html="post.content"
      class="post-content"/>
  </div>
</template>

<script>
export default {
  async asyncData({ app, params, error }) {
    const post = await app.$axios.get(`http://localhost:4000/posts/${params.id}`)
      .then(i => i.data)
      .catch(() => null);

    if (!post) {
      return error({
        statusCode: 404,
      });
    }

    return {
      post,
    };
  },
  methods: {
    formatDate(date) {
      return date.split('T')[0].split('-').map(Number).join('/');
    },
  },
};
</script>

<style scoped>
.post-date {
  font-size: 12px;
}

.post-title {
  font-size: 24px;
}

.post-content {
  padding: 20px 0;
}
</style>

<style>
.post-content h1 {
  font-size: 20px;
  margin: 5px 0;
}

.post-content h2 {
  font-size: 20px;
  margin: 5px 0;
}

.post-content h3 {
  font-size: 18px;
  margin: 5px 0;
}

.post-content h4 {
  font-size: 18px;
  margin: 5px 0;
}

.post-content h5 {
  font-size: 16px;
  margin: 5px 0;
}

.post-content h6 {
  font-size: 14px;
  margin: 5px 0;
}

.post-content p {
  font-size: 14px;
  margin: 5px 0;
}

.post-content img {
  max-width: 100%;
  margin: 5px 0;
}
</style>

動かしてみる

APIを起動させたまま、このディレクトリで npm start を実行すると localhost:3000 でサーバーが立ち上がります。

以下のように、記事の一覧ページ、記事の詳細ページを表示することができます。

f:id:k-kty:20181212115831p:plain

f:id:k-kty:20181212115856p:plain

Contentful、相当使いやすいですね。疑問点などあったらコメントなどでご質問ください。

node.jsでaws s3のデータを読み書きする

aws-sdkパッケージを使います。以下のコマンドでインストールします。

npm i aws-sdk

このドキュメントは https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html で見ることができます。適宜参照しましょう。

また、別途S3の読み書き権限のあるアクセスキー(とシークレットキー)を発行しておきます。

準備が終わったら、以下のように書き始めます。今後s3オブジェクトを操作することになります。

const aws = require("aws-sdk");

const S3_ACCESS_KEY = '...';
const S3_SECRET_KEY = '...';

const s3 = new aws.S3({
  apiVersion: '2006-03-01',
  accessKeyId: S3_ACCESS_KEY,
  secretAccessKey: S3_SECRET_KEY,
});

オブジェクトを列挙するための関数を作ってみます。

const listObjects = params => new Promise((resolve, reject) => {
  s3.listObjectsV2(params, (err, data) => {
    if (err) reject(err);
    else resolve(data.Contents);
  });
});

以下のようにすれば、'some-bucket'バケット内のオブジェクトを3つ列挙してくれます。並び順は辞書順のはずです。

listObjects({
  Bucket: 'some-bucket',
  MaxKeys: 3,
})
  .then((objectList) => {
    // 何らかの処理
  });

ここで、objectListはETag, Key, LastModified, Sizeなどをキーとして持つオブジェクトの配列です。

次に、バケット名・キー名からファイルを読み出す関数を作ってみます。ストリームを用いるケースもあると思いますが、今回は省略します。

const getObject = (bucket, key) => new Promise((resolve, reject) => {
  s3.getObject({
    Bucket: bucket,
    Key: key
  }, (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});

以下のようにすれば、'some-bucket'内の、'some-key'という名前のオブジェクトのデータを取得できます。

getObject('some-bucket', 'some-key')
  .then((object) => {
    // 何らかの処理
  });

ここでobjectはBody, ETag, LastModified, ContentLengthなどをキーとして持つオブジェクトです。

そして最後に、バケットに新しいデータを追加する関数を作ってみます。

const putObject = (bucket, key, data) => new Promise((resolve, reject) => {
  s3.putObject({
    Body: data,
    Bucket: bucket,
    Key: key
  }, (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});

以下のようにして、'some-bucket'内に'some-key'という名前のファイルを作成できます。中身は'some-content'となります。

putObject('some-bucket', 'some-key', Buffer.from('some-content'))
  .then(() => {
    // 何らかの処理
  });

dockerのコンテナ・ボリュームを掃除する

逐一きれいにしていくのが一番なのですが、dockerを用いていろいろやっていると、不要なファイルが残っていたりします。

そういうときには以下のコマンドを実行します。

docker rm `docker ps -qf "status=exited"`
docker volume rm `docker volume ls -qf "dangling=true"`

一行目のコマンドでは、停止しているコンテナを列挙し(docker ps...の部分)、それをdocker rmで削除しています。

また、二行目のコマンドでは、コンテナから参照されていない(≒不要な)ボリュームを列挙して(docker volume ls ...)、それをdocker volume rmで削除しています。