凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Native Client の導入と Hello World までのまとめ

はじめに

Google Native Client とは、ブラウザ上でネイティブコードを安全に実行するためのサンドボックス化技術です *1。開発者側から見れば、Chrome 上で高速に動くアプリケーションを既存の資産を利用しながら C++ で記述でき、ユーザ側から見ればサクサクなアプリケーションがインストール不要でブラウザ上で動く、といった感じです。登場からかなり経ちますが、興味があるので調べてみました。

動作例

chrome://flags/ よりネイティブクライアントを有効にしておきます。

これで以下のデモを動かすことができます。

導入

まずは、本家より「Quick Start」を行なって開発環境を導入します。手順は以下を参考にします。

1. ダウンロード

からダウンロードして zip 展開します。

$ wget http://storage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/nacl_sdk.zip
$ unzip nacl_sdk.zip
2. バンドルをインストール

NaCl と FileIO や Render、Audio 周りや HTML/JS との仲立ちをする API セットである Pepper Plug-in API を入れます。Native Client の略称が NaCl で塩なのに対し、Pepper(胡椒)な訳ですね。いくつかバージョンがあるのですが、これは list で調べることができます。各バージョンは Chrome のバージョンと対応しています。

$ ./naclsdk list
Bundles:
 I: installed
 *: update available

  I  sdk_tools (stable)
     pepper_16 (post_stable)
     pepper_17 (post_stable)
     pepper_18 (post_stable)
     pepper_19 (post_stable)
     pepper_canary (canary)
     pepper_20 (post_stable)
     pepper_21 (post_stable)
     pepper_22 (post_stable)
  I  pepper_23 (stable)
     pepper_24 (beta)
  I  pepper_25 (beta)

All installed bundles are up-to-date.

インストール済みのものには「I」が付きます。例えば 25 を入れるには次のようなコマンドを打ち込みます。

$ ./naclsdk update pepper_25
Downloading https://commondatastorage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/25.0.1364.36/naclsdk_mac.tar.bz2
|================================================|
.... (以下略

example を動かしてみる

ここに手順が書いてあります。
まず、examples ディレクトリ下のサンプルたちを make します。

$ cd pepper_25/examples/
$ make

次に、これらのサンプルがブラウザから見えるように http サーバを立ち上げます。

$ ./httpd.py
INFO:root:Starting local server on port 5103
INFO:root:To shut down send http://localhost:5103?quit=1

5103 ポートで動きますので、http://localhost:5103 にアクセスすると以下のように各サンプルへのリンクが表示されます。ちなみに httpd.py を使わなくても、apache や nginx 等で見えているディレクトリへ放り込んでも OK です。

色々試してみて下さい。
http://localhost:5103/hello_world/index_newlib_Debug.html

http://localhost:5103/hello_world_gles/index_newlib_Debug.html

Hello World!

チュートリアルの Hello World をやってみます。

Step 4 から hello_tutorial.zip をダウンロードして展開、examples 以下へ配置します。tutorial には以下のファイルが含まれています。

  • Makefile
  • make.bat
  • hello_tutorial.html
  • hello_tutorial.cc
  • hello_tutorial.nmf

Make を行うとコレに加えて、

  • hello_tutorial_x86_32.nexe
  • hello_tutorial_x86_64.nexe

が出来ます。
NaCl では次のような流れでファイルを読み込みます。

  1. HTML から embed タグでマニフェストファイル(nmf)を読み込む
  2. マニフェストに記述されたアーキに応じた nexe を読み込む

実際のファイルベースで見ていきます。hello_tutorial.html に、以下の様な記述があります。

<embed name="nacl_module"
   id="hello_tutorial"
   width=0 height=0
   src="hello_tutorial.nmf"
   type="application/x-nacl" />

ここで nmf を見ています。nmf はアーキによって読み込む nexe を振り分ける json になっています。

{
  "program": {
    "x86-64": {"url": "hello_tutorial_x86_64.nexe"},
    "x86-32": {"url": "hello_tutorial_x86_32.nexe"}
  }
}

で、nexe は hello_tutorial.cc をコンパイルして出来たものでした。それでは、hello_tutorial.cc を見てみます。

#include <cstdio>
#include <string>
#include "ppapi/cpp/instance.h"
#include "ppapi/cpp/module.h"
#include "ppapi/cpp/var.h"

class HelloTutorialInstance : public pp::Instance {
public:
	explicit HelloTutorialInstance(PP_Instance instance) : pp::Instance(instance)
	{}
	virtual ~HelloTutorialInstance() {}
	virtual void HandleMessage(const pp::Var& var_message) {
		// TODO(sdk_user): 1. Make this function handle the incoming message.
	}
};

class HelloTutorialModule : public pp::Module {
public:
	HelloTutorialModule() : pp::Module() {}
	virtual ~HelloTutorialModule() {}
	virtual pp::Instance* CreateInstance(PP_Instance instance) {
		return new HelloTutorialInstance(instance);
	}
};

namespace pp {
	Module* CreateModule() {
		return new HelloTutorialModule();
	}
}

コメントは省いてあります。pp = pepper plug-in 名前空間下にある pp::Instance を継承した HelloTutorialInstance クラス、pp::Module を継承した HelloTutorialModule クラス、そしてそれを生成する pp::CreateModule 関数が定義されています。
このままでは何も起きないプログラムになっています。そこで、Tutorial に従ってコードを追加し以下のようにします。

#include <cstdio>
#include <string>
#include "ppapi/cpp/instance.h"
#include "ppapi/cpp/module.h"
#include "ppapi/cpp/var.h"

// 追加
namespace {
	const char* const kHelloString = "hello";
	const char* const kReplyString = "hello from NaCl";
}

class HelloTutorialInstance : public pp::Instance {
public:
	explicit HelloTutorialInstance(PP_Instance instance) : pp::Instance(instance)
	{}
	virtual ~HelloTutorialInstance() {}
	virtual void HandleMessage(const pp::Var& var_message) { // <-- 中身を実装
		if (!var_message.is_string())
			return;
		std::string message = var_message.AsString();
		pp::Var var_reply;
		if (message == kHelloString) {
			var_reply = pp::Var(kReplyString);
			PostMessage(var_reply);
		}
	}
};

class HelloTutorialModule : public pp::Module {
public:
	HelloTutorialModule() : pp::Module() {}
	virtual ~HelloTutorialModule() {}
	virtual pp::Instance* CreateInstance(PP_Instance instance) {
		return new HelloTutorialInstance(instance);
	}
};

namespace pp {
	Module* CreateModule() {
		return new HelloTutorialModule();
	}
}

文字列の追加と HelloTutorialInstance::HandleMessage の中身の追記をしています。HandleMessage は JS 側からのメッセージを受け取るハンドラで、var_message が JS 側から渡される変数です。JS では変数の型は自由なので AsString() メソッドを使って C++ 側で明示的に std::string に変換します。このあたりは Node.js とかと同じですね。
HandleMessage では、変換した message が "hello" であったなら "hello from NaCl" という文字列を JS 側に送る、という処理をしています。では、メッセージを送受信できるように JS 側の実装も行いましょう(1行追加するだけですが)。併せて関係のあるコードも一緒に見てみます。

function moduleDidLoad() {
  HelloTutorialModule = document.getElementById('hello_tutorial');
  updateStatus('SUCCESS');
  HelloTutorialModule.postMessage('hello'); // <-- コレ追加
}

function handleMessage(message_event) {
  alert(message_event.data);
}
<div id="listener">
  <script type="text/javascript">
    var listener = document.getElementById('listener');
    listener.addEventListener('load', moduleDidLoad, true);
    listener.addEventListener('message', handleMessage, true);
  </script>

  <embed name="nacl_module"
     id="hello_tutorial"
     width=0 height=0
     src="hello_tutorial.nmf"
     type="application/x-nacl" />
</div>

postMessage でメッセージを送って handleMessage で message イベントを捕まえて処理しています。
なお、load イベントリスナは、事前にリスナをアクティブにして load イベントを確実に捕まえられるように直接 embed につけるのでなく div につけています。これでブラウザから以下の URL にアクセスすれば hello from NaCl のアラートが表示されます。


これらの流れを図に描いてみるとこんな感じになります。

汚い図になってしまった…。
まず NaCl モジュールのロード時に CreateModule() を一度だけ実行してモジュールを読み込みます。以降、 タグが見つかり次第、その都度 CreateInstance() を実行してインスタンスを生成します。その後は postMessage (JS) / pp::Instance::PostMessage (C++) およびメッセージ毎のイベントハンドラ (JS) / pp::Instance::HandleMessage (C++) で、JS / C++ 間のメッセージの送受信を行う、という寸法になっています。

なお、CreateInstance に渡される引数がなぜ pp::Instance でなく PP_Instance なんだろうと思い調べてみましたら、ここではユニークな ID を与えているようです。

typedef int64_t PP_Instance;

なるほど。

おわりに

ブラウザで動くのにパフォーマンスも良いし、ネット連携も簡単な NaCl さん、流行って色んなアプリケーションが登場してくると面白くなりそうですね。

*1:[http://ja.wikipedia.org/wiki/Google_Native_Client:title]