C/C++のプリプロセッサを使ってHTMLやJavaScriptのデバッグ用コードを操作する。
HTMLやJavaScriptにデバッグやテストのためだけのコードを書いておき、リリース時は消しておきたいといというシチュエーションがあると思います。 gulpなどを使うのかなと思って、調べたら意外にC/C++のプリプロセッサを使う方法が出てきました。
前提
プリプロセッサってなに?
Wikipediaによると
コンパイル処理において、プリプロセッサ(preprocessor)とは、コンパイラがソースコードをコンパイルする前に、一旦ソースコードに処理を施すためのプログラムである。
処理内容
以下のようなものがある。
とのことです。 今回はコンパイル条件によるソースコードの部分的選択という機能を使ってデバッグ用コードをフィルタして取り除いてしまいます。(ということがオプションで選択できるので良いねという話)
準備
MacOSXの場合、XCodeのインストール時にclangというコンパイラがインストールされます。 コンソールからも確認できます。
$ which clang /usr/bin/clang
Unix/Linuxの場合ディストリビューションによってclangやgccがあると思います。 今回使う程度の機能はclangでもgccでも大差なく備えている機能なので、好きな方を使いましょう。 大体の環境ではccコマンドでシステムデフォルトのコンパイラが使えます。
$ which cc /usr/bin/cc $ cc --version Apple LLVM version 6.1.0 (clang-602.0.53) (based on LLVM 3.6.0svn) Target: x86_64-apple-darwin14.3.0 Thread model: posix
ソースコードを編集
例えば以下の様なJavaScriptソースコードがあったとします。
function is_bigger(a, b) { console.log('is_bigger(' + a + ',' + b ');'); // デバッグ用 return a > b; }
ここでデバッグ用の行を以下のように囲みます。
function is_bigger(a, b) { #ifndef NDEBUG console.log('is_bigger(' + a + ',' + b ');'); // デバッグ用 #endif return a > b; }
なんとなくわかると思いますが、#ifndef ~ #endif部分はNDEBUGという定数が定義された場合、出力されなくなります。ここでのNDEBUGはJavaScriptの変数とは関係ないプリプロセッサのオプションみたいなものです。
プリプロセッサを実行する
ソースコードをsample.jsなどの名前で保存しておきます。
コマンドラインから以下のようにccを起動すると、
$ cc -E -C -P -x c -Wno-invalid-pp-token sample.js
function is_bigger(a, b) { console.log('is_bigger(' + a + ',' + b ');'); // デバッグ用 return a > b; }
また、以下のようにccを起動すると、
$ cc -E -C -P -x c -Wno-invalid-pp-token -DNDEBUG sample.js
function is_bigger(a, b) { return a > b; }
-DNDEBUGオプションでプリプロセッサにNDEBUGという定義を渡しています。これがifndefで評価されることで該当部分のソースコードの出力を制御しています。 引用元には-Eオプションと-Wno-invalid-pp-tokenがありませんでした。-Eばプリプロセッサだけ実行(指定しないとコンパイルしようとしてエラーになる)、-Wno-invalid-pp-tokenは余計な警告(C/C++だと文法的におかしいトークン)を無視しています。 同様の操作でHTML(というか、行頭の#記号に意味が与えられていないファイル)でもデバッグ用コードを操作することができます。 プリプロセッサ自体にはフィルタ以外にもイロイロ機能があります。
活用すればもっと楽ができそうです。 欠点として、
- 出力結果のJavaScriptやHTMLの行がプリプロセッサ分変化するので、ブラウザで出力された行数と修正対象の行数が異なる
- エディタによってはインデントがおかしくなる(かもしれない)
チョット使う場所を選びそうです。
Socket.IO C++ Client over HTTPS
先日投稿したSocket.IO C++ Clientが早くもTLSに対応しました。これでHTTPSを使ったセキュアな通信が可能になります。
Release 1.4.0 Release · socketio/socket.io-client-cpp · GitHub
前提
- OS X Yosemite 10.10.3
- Node.js 0.12.2
- Socket.IO 1.3.5(サーバ側) Socket.IO C++ Clientを利用するにはver1.0以降が必須です。
- Socket.IO C++ Client 1.4.0 + α 以下sio(C++のnamespaceより)と省略
sioのインストール方法の変更点
- コンパイル方法自体は前回と変わりませんが、OpenSSLを利用するようになりました。sioのコンパイル前にOpenSSL(と開発用パッケージ)をインストールしておいてください。
- make install まで終わると build/lib/Release/libsioclient_tls.aなるファイルができています。
サンプルアプリケーションの変更点
以前のサンプルプログラムもTLSに対応しました。
llamerada-jp/socket.io-cpp-client-sample · GitHub
- TLSを利用するにはコンパイル時にlibsioclient.aでなくlibsioclient_tls.aをリンクします。cmakeなどのビルド環境を変更します。起動オプションをみてENABLE_SSLが指定された場合はlibsioclient_tls.aを利用し、OpenSSLのライブラリをリンクするようにしています。
if (NOT ENABLE_SSL) list(APPEND extra_libs "sioclient") else() list(APPEND extra_libs "sioclient_tls") find_package(OpenSSL) list(APPEND extra_libs ${OPENSSL_LIBRARIES}) endif()
cmakeへは以下のようにオプションを指定します。
$ cmake -D SIO_DIR=<sioを設置したディレクトリ>/socket.io-client-cpp/build \ -D BOOST_ROOT=/usr/local/Cellar/boost/1.58.0/ \ -D ENABLE_SSL=on ..
$ ./client https://localhost:8000/ <マシン名>
サンプルサーバの変更点
以下のページを参考にサンプルプログラムを変更しました。
try catch and ...release: 自前認証局でSSL対応なNode.jsサーバアプリ
// index.js var app = require('express')(); var server = require('http').Server(app); var io = require('socket.io')(server); server.listen(8000);
↓
// index.js var fs = require('fs'); var app = require('express')(); var server; if (process.env.ENABLE_SSL) { // SSL通信路を作成 server = require('https').Server({ key: fs.readFileSync('server.key'), cert: fs.readFileSync('server.crt'), ca: fs.readFileSync('ca.crt'), requestCert: true, rejectUnauthrized: false }, app); console.log('enable ssl'); } else { // 非SSL通信路を作成 server = require('http').Server(app); } var io = require('socket.io')(server); server.listen(8000);
手順1, 2で作られるca.crt, server.key, server.crtをwebフォルダに格納し、起動時に環境変数ENABLE_SSLを設定するとhttpsで待受を開始します。
$ ENABLE_SSL nade index.js
ブラウザでアクセスする際にhttp://localhost:8000/では接続に失敗し、https://localhost:8000/で接続できるはずです。
Socket.IO C++ Clientを利用する
Socket.IOはwebブラウザ上のアプリケーション間でリアルタイム性の高い通信を実現するためのNode.jsのライブラリです。 Socket.IO C++ ClientはそのSocket.IOのサーバへネイティブアプリから接続するためのライブラリです。 コレを利用するとwebとネイティブアプリで同じサーバを介して通信ができます。 UnixのSocket通信とは異なりストリームではなく、1つ1つのメッセージ単位で授受します。
Socket.IO C++ Client over HTTPS - ~llamerada/memo/
前提
- OS X Yosemite 10.10.3
- Node.js 0.12.2
- Socket.IO 1.3.5(サーバ側) Socket.IO C++ Clientを利用するにはver1.0以降が必須です。
- Socket.IO C++ Client(2015/5/19) 以下sio(C++のnamespaceより)と省略
Node.jsやNode.jsでのSocket.IOの利用方法についてはこの記事では触れません(他にわかりやすい資料がいっぱいあるため)。
インストール(C++版だけ)
sioはBoostを使うので、Boostを用意します。バージョン指定は特になかったので最新版を入れました。
$ brew install boost
パッケージ等はまだ無いので、gitからclone、コンパイルします。 インストールしたboostの格納先に応じてcmakeのオプションは変更してください。
$ cd <設置先ディレクトリ> $ git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git $ cd socket.io-client-cpp/ $ cmake -D BOOST_ROOT=/usr/local/Cellar/boost/1.58.0/ . # ← '.'が抜けていたので5/23に追記 $ make -j8 $ make install # ← 抜けていたので5/23に追記 $ ls CMakeCache.txt CMakeFiles CMakeLists.txt LICENSE Makefile README.md cmake_install.cmake examples lib libsioclient.a # ←リンクに使う src build # ←ヘッダファイル、ライブラリが格納されている test
サンプルプログラム
webブラウザで入力したコマンドをサーバ経由でネイティブアプリで実行して結果を表示するサンプルプログラムを作りました。 認証しないユーザでコマンドを実行できるので、このままサービスを公開するなどということは絶対しないでください。
動作
全体では以下の図のような動作をします。 サンプルプログラムなため、sioのエラー処理以外でのパケットのフォーマットチェックなどは省いています。
サンプルプログラムのコンパイル、起動方法
gitからのclone、コンパイルは以下のようになります。
$ cd <ダウンロード先ディレクトリ> $ git clone https://github.com/llamerada-jp/socket.io-cpp-client-sample.git $ cd socket.io-cpp-client-sample $ cd src $ mkdir build $ cd build $ cmake -D SIO_DIR=<sioを設置したディレクトリ>/socket.io-client-cpp/build -D BOOST_ROOT=/usr/local/Cellar/boost/1.58.0/ .. # ← 5/23 make install後のパスに合わせて変更 $ make
サーバの起動は以下のとおりです。
$ cd <ダウンロード先ディレクトリ> $ cd web $ npm install # ←依存パッケージのインストール、1度だけで良い $ node index.js 待機開始
C++プログラムの起動は以下のとおりです。
$ cd <ダウンロード先ディレクトリ> $ cd src/build $ ./client <サーバ起動先、http://localhost:8000/とか> <マシン名> 待機開始
webブラウザからhttp://localhost:8000/などにアクセスします。 テキストボックスにlsなどのコマンドを入力し、実行ボタンを押すと、以下のように実行結果が表示されます。
C++での処理の流れ
SampleClient::runがメインの処理になっています。 on_close、on_fail、on_openはrunの最初でsioにリスナとして登録しておきます。
connectでの接続処理
connectに接続先URLを渡すと、別スレッドで接続処理の待ち合わせが行われます。 socketの取得は接続処理が終わった後でなければなりません。 接続処理が終わると別スレッドでon_openが呼ばれます。 そのため、mutexとcondition_variable_anyを使ってon_openでの処理が終わるまでメインスレッドを待たせています。
// メインスレッドでの処理(runより抜粋) client.connect(url); { // 別スレッドで動く接続処理が終わるまで待つ std::unique_lock<std::mutex> lock(sio_mutex); if (!is_connected) { sio_cond.wait(sio_mutex); } } socket = client.socket();
// 接続スレッドでの処理(on_openより抜粋) { std::unique_lock<std::mutex> lock(sio_mutex); is_connected = true; // 接続処理が終わったのち、待っているメインスレッドを起こす sio_cond.notify_all(); }
onでのリスナの登録
web版同様、onメソッドでリスナを登録します。
socket = client.socket(); socket->on("run", std::bind(&SampleClient::on_run, this, std::placeholders::_1));
登録される側は以下のようになっています。処理内容は受信処理の欄で詳しく説明します。
void on_run(sio::event& e) { std::unique_lock<std::mutex> lock(sio_mutex); sio_queue.push(e.get_message()); // イベントをキューに登録し、待っているメインスレッドを起こす sio_cond.notify_all(); }
emitでの送信処理
メッセージの送信は以下のとおりです。 C++ではJavaScritptでのJSONのような柔らかい(?)構造は取れないため、std::mapを使ってobject相当の構造を作ります。 詳しくはJSONに対応するデータ構造の欄にまとめました。 sio::messageさえ作れれば送信のしかたはweb版と同じです。
sio::message::ptr send_data(sio::object_message::create()); std::map<std::string, sio::message::ptr>& map = send_data->get_map(); // objectのメンバ、typeとnameを設定する map.insert(std::make_pair("type", sio::string_message::create("native"))); map.insert(std::make_pair("name", sio::string_message::create(name))); // joinコマンドをサーバに送る socket->emit("join", send_data);
受信処理
sioではpooling(受信ソケットを監視するなど)の処理を入れる必要はありません。ライブラリ内で別スレッドが起動してサーバとの通信を監視しています。
onメソッドで登録した関数も接続処理と同様、別スレッドで動きます。 サンプルでは受信時はメインスレッドと共有しているキューにデータを登録して、メインスレッドでキューにデータが入っていた場合に解析することで、マルチスレッドでの排他制御を回避しています(イベント受信時と解析時のみ考慮している)。
// sioのメッセージを貯めるためのキュー std::queue<sio::message::ptr> sio_queue;
void on_run(sio::event& e) { std::unique_lock<std::mutex> lock(sio_mutex); sio_queue.push(e.get_message()); // イベントをキューに登録し、待っているメインスレッドを起こす sio_cond.notify_all(); }
メインスレッドではキューが空の場合はon_runでsio_cond.notify_all()が呼び出されるまでまち、再度キューの状態を確認します。 キューにデータが入っていることが確認できたら処理に移ります。 処理終了のタイミングでキューからデータを削除します。
while(true) { // イベントキューが空の場合、キューが補充されるまで待つ std::unique_lock<std::mutex> lock(sio_mutex); while (sio_queue.empty()) { sio_cond.wait(lock); } // イベントキューから登録されたデータを取り出す sio::message::ptr recv_data(sio_queue.front()); <取り出したデータに対応して何らかの処理をする> // 処理が終わったイベントをキューから取り除く sio_queue.pop(); }
この方式ではサーバからの受信とキューの消化処理を平行して実行しません。 並行した処理が無いため、マルチスレッド由来のデッドロックは不安定な状態は発生しませんが、on_runで処理を行う場合に比べて性能が低いです。 作りたいアプリケーションの構造によって適宜構造を考えたほうが良いです。 今回はsioメインのサンプルなので一番安全そうな処理にしたつもりです。
JSONに対応するデータ構造
sioではJSONの値をsio::messageクラスを継承したクラス(sio:: string_messageなど)に格納しています。
C++で取り出すときは、
data->get_string();
値を作るときは
string_message::create("文字列"); // sio::message::ptrを戻す
のようにします。 src/sio_message.hで定義されており、まとめると以下のとおりです。
*JavaScriptの型 | C++の型 | sioでの対応クラス | getter | get_flagの値 |
---|---|---|---|---|
Null | nullptr, NULL | - | - | - |
Number(整数) | int64_t | int_message | get_int() | flag_integer |
Number(小数、NaNなども?) | double | double_message | get_double() | flag_double |
String | std::string | string_message | get_string() | flag_string |
Object | map<string,message::ptr> | object_message | get_map() | flag_object |
Array | vector |
array_message | get_vector() | flag_array |
Blob, ArrayBuffer, (File?) | shared_ptr |
binary_message | get_binary() | flag_binary |
nullの取り扱い
JavaScriptでオブジェクトにnullを設定した場合、C++側ではsio::message::ptrがnullptrを指しています。 そのため、型の判定は以下のようになります。
void print_type(sio::message::ptr data) { if (data.get() == nullptr) { std::cout << "null"; } else { switch(data->get_field()) { case sio::message::flag_integer: std::cout << "integer"; break; case sio::message::flag_double: std::cout << "double"; break; case sio::message::flag_string: std::cout << "string"; break; case sio::message::flag_binary: std::cout << "binary(string)"; break; case sio::message::flag_array: std::cout << "array(vector)"; break; case sio::message::flag_object: std::cout << "object(map)"; break; } } std::cout << std::endl; }
boolの取り扱い
bool値の取り扱いには対応していないようです。JavaScript側でbool値を設定した場合、C++側ではnullとなってしまいました。 既存プログラムでbool値がある場合は、プログラムを書き換えるか、Node.jsで判定して整数に変換してからクライアントに渡すなどの対応が必要です。
その他
- httpsにはまだ対応していないです。現在実装中だそうです。
- クライアントがネイティブだけでサーバに大したロジックが不要であればFlatBuffersを梱包して渡すのもありな気がします。ルーティング情報とバイナリ(FlatBuffers)だけを持ったパケットをNode.js+Redisで転送するだけとか。
CMake導入
前提
経緯
PROCESS WARPのビルドは元々Makefileを手作りしていました。 Linux/MacOSXごとにライブラリのパス、デバック用かリリース用かなどををハードコードしており、Makefileのメンテナンスの必要がありました。 他のOSSプロジェクトでよしなに動いているので、どうやっているのか調べたら、Automake - GNU Project - Free Software Foundation (FSF)、CMakeといツールを使っている場合が多かったです。特徴を調べてCMakeを使うことにしました。
CMakeの特徴
- Unix、XCode、Windowsなどクロスプラットフォームでのビルドを補助する。
- ビルドを直接行うわけでなく、Makefile、XCode、VisualStudioのプロジェクトを出力するなど、各ネイティブ開発環境との親和性が高そう。(automakeにはないメリット)
- ソースとは独立した場所でビルドが可能なので、ソースを格納している場所を汚さずにすむ。
使い方は参考ページの方がよくわかるので、自分が特に引っかかったところだけ書きます。
利用ライブラリのコンパイルオプションの取得方法
C/C++のプログラムのコンパイルにはコンパイルオプション(依存するヘッダファイルの場所、最適化オプション、プログラムに渡す定義など)とリンカオプション(依存するライブラリファイルの場所、リンカオプション)が必要になります。それぞれ利用ライブラリに合せて追加する必要があります。 CMakeでは調べた限り以下の3つの方法でオプションを集めることができます。
find_package
依存しているライブラリがFind◯◯.cmakeというファイルを含んでいる場合、ソレを読み込んでオプションを取得することができます。大体のライブラリで使い方は統一されているようですが、LLVMなど複雑なライブラリは、必要な引数を渡すとソレに対応するオプションが戻ってくるなどの少し複雑な動作をする場合もあります。
find_package(LLVM REQUIRED CONFIG) # llvmのオプションを取得する処理(個別) include_directories(${LLVM_INCLUDE_DIRS}) llvm_map_components_to_libnames(llvm_libs support core irreader) list(APPEND extra_libs ${llvm_libs}) add_definitions(${LLVM_DEFINITIONS})
pkg_check_module/pkg_search_modules
Find.cmakeファイルがなくてもpkg-configコマンドに対応している(.pcファイルを提供している)ばあい、そこから情報を取得することができます。
pkg_search_module(FFI REQUIRED libffi) include_directories(${FFI_INCLUDE_DIRS}) string(REPLACE ";" " " FFI_CFLAGS_STR "${FFI_CFLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${FFI_CFLAGS_STR}") list(APPEND extra_libs ${FFI_LIBRARIES})
find_library
libmなど、基本的すぎてコンパイルオプションなどほとんどなく、find_packageやpkg-configにも対応していないライブラリはfind_libraryでオプションを取得することができます。
find_library(FFI_LIBRARY NAMES ffi) set(extra_libs ${extra_libs} ${FFI_LIBRARY})
環境ごとにcmake、pkg-configどちらがあるか分からないので、組み合わせて書くことができます。
# REQUIREDがついていない場合、エラーにはならない find_package(FFI) # 読み込みに成功するとFFI_FOUNDなどが設定される if (FFI_FOUND) # FindFFI.cmakeなどがあった場合の読み込み処理 else() # REQUIREDが付いているので、pkg-configでlibffiが見つからない場合、停止する pkg_search_module(FFI REQUIRED libffi) # pkg-configがあった場合の読み込み処理 endif()
ListとStringが混同してしまう罠
CMakeではListとStringの2つのデータ形式があります。
# Listの結合 set(変数名 A B) # 文字列としての結合 set(変数名 "${A} B")
Listの内部形式は"A;B"となっており文字列をセミコロンで結合したものです。 そのため、リストと文字列を混同すると"A B;C"のようになり、意図したものと異なる可能性があります。 find_packageで読み込んだ値は.cmakeファイルの作りによりListか文字列か変わるようです。 pkg_search_moduleの場合、〜CFLAG、〜LIBRARIESはどちらもListで渡されます。 CMake中では、CMAKE_C(XX)FLAGSは文字列、ライブラリはListとして渡す必要があるので、 〜CFLAGをCMAKE_C_FLAGSに結合すると意図しないセミコロンが混入してコンパイルに失敗します。 Listを文字列に結合する場合は以下のように変換する必要がります。
string(REPLACE ";" " " FFI_CFLAGS_STR "${FFI_CFLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${FFI_CFLAGS_STR}")
参考にしたページ
LLVM-IRの作り方
PROCESS WARPではLLVM-IRを読み込んでプログラムを実行しています。 この記事では、LLVM-IRを作成する環境(MacOSX/Linux)と実際にLLVM-IRを生成する簡単な手順を説明します。
内容
では始めましょう。
clang/llvmをインストールする(MacOSX + homebrew)
XCodeのインストール
MacOSXでclangを使う最も手っ取り早い方法はXCodeに付属の物を使うことです。AppStoreからXCodeを検索し、インストールします。
homebrewのインストール
llvmはhomebrewからインストールする方法が簡単です。(configure & makeに慣れていなければ) homebrewをインストールしていない場合、homebewをインストールします。 Homebrew — OS X用パッケージマネージャー 5/14現在のインストール方法は、ターミナルの起動して以下のコマンドを実行します。 最新のインストール方法はオフィシャルページから確認しましょう。
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
llvmのインストール
homebrewからllvmをインストールします。 ターミナルから以下のコマンドを実行します。(PROCESS WARPではversion 3.5での動作を確認しているためllvm35を指定しています)
$ brew install llvm35
インストールしただけではインストール先がPATHに入っておらずコマンドを呼び出すのが面倒です。 ターミナルから以下のコマンドを実行し、環境変数PATHにインストール先を追加します。(パスはバージョンに合わせて変更します)
$ PATH=$PATH:/usr/local/Cellar/llvm/3.5.1/bin/ $ export PATH
~/.bashrcに書き加えるとターミナル起動時に設定されます。 コマンドが使えるようになったことを確認します。
$ which llvm-dis
/usr/local/Cellar/llvm/3.5.1/bin/llvm-dis
clang/llvmをインストールする(Linux)
Linux環境でclang/llvmをインストールするにはディストリビューションごとのパッケージマネージャを利用するのが簡単です。ubuntuの場合、以下のコマンドを実行すればインストールされます。
$ sudo apt-get install clang-3.5 llvm-3.5
ubuntuではclang/llvmを複数バージョン同時インストールするできるようにコマンドの末尾にバージョン番号が付いているようです。例えば、clangは以下のようになっています。
$ which clang-3.5 /usr/bin/clang-3.5
LLVM-IRを生成する
LLVM-IR(.llファイル)の生成に入ります。 ここではMacOSX環境下でC/C++で書いたプログラムの変換を例示します。 Linux環境ではコマンドを読み替えてください。
clangでソースファイルをllvmのバイナリ形式までコンパイルします。
$ clang -O2 -emit-llvm -fno-exceptions -fno-vectorize -c <ソースファイル>
オプションは以下のとおりです。
- -O2:最適化の指定です。普通のC/C++のコンパイル同様です。LLVM-IRにも最適化が適用されます。Iオプションなども指定できますがPROCESS WARPは現在他の標準ライブラリとOpenGL以外のライブラリの呼び出しに対応していないので省略します。
- -emit-llvm:出力をllvmのバイナリ形式にするためのオプションです。
- -fno-exceptions:例外処理を使わないことを明示します。PROCESS WARPではまだC++の例外を使えないために指定しています。
- -fno-vectorize:SIMDを使った最適化を抑止します。PROCESS WARPではまだSIMD命令に対応していないため指定しています。
- -c:llvmのバイナリ形式で出力する場合、リンクを行わないため指定しています。
実行すると<ソースファイル>.bcファイルが出力されます。llvm-disでLLVM-IRに変換します。
$ llvm-dis <ソースファイル>.bc
変換が成功すると、<ソースファイル>.llファイルが出力されます。
以上です、お疲れ様です。
socket.ioでUin8Arrayを転送するときの注意事項
前提
- socket.io 1.3.5
socket.io(1.3.5)でUint8Arrayを転送しようとしたところ、既存ArrayBufferの一部を参照するviewとして利用している場合、うまく転送できませんでした。sliceを使って転送したい部分だけを抜き出すとうまくいきました。
MessagePack on socket.ioでのバイナリ転送案
前提
- socket.io 1.3.5
socket.ioのroomかnamespaceか悩んだ末、問題はバイナリの転送だけなので、MessagePackなどを使えば良いのではないかと考えました。プロトコルも素直になるので。
MessagePack:JavaScript向けのライブラリではいずれもバイナリを文字列としてデコードする(MessagePackの仕様上は文字列とバイナリデータを区別しない)ので、バイナリを扱おうとすると処理系によってデータを変更されてしまう可能性があることがわかりました。
BSON:配列を使うとデータサイズが大きくなる(配列のインデックスを添え時にしたmapとして扱っている)という欠点があります(あとJavaScriptに依存しすぎた型も多いですが使わなければ良い)。配列バンバン使うのでまずいです。
Base64:C++からJavaScriptにバイナリデータを渡すときにBase64エンコーディングをかける方法もありますが、単純にサイズが増えます。 (´・ω・`)ウ~ンMessagePackのライブラリを改造してUint8Arrayを戻す形にすれば良いかな?と調べたら改造済みのものがあるようです。
追記
などと調べていたら、socket.ioの1.0以降ではバイナリ転送(Blog, Uint8Array)を含むオブジェクトの転送が可能だそうです。