FreeBSDのemscriptenを使って、C++コードでWebアセンブリの開発してみる。 - Apache 2.4系でHTTP/2対応サーバを構築してみるテスト。
してみるテストロゴ
Apache 2.4系でHTTP/2サーバを構築してみるテスト。

FreeBSDのemscriptenを使って、C++コードでWebアセンブリの開発してみる。

FreeBSDでWebアセンブリの開発をするため、emscriptenのインストール方法、Apacheの設定、C++で記述したコードと、JavaScriptとのデータの連携を説明してみます。

emscriptenのFreeBSDへのインストール

インストールは、pkgから行います。使用するのは、nodeと、emscriptenになります。

$ sudo pkg install node
$ sudo pkg install emscripten

FreeBSDのemscriptenが、llvm-develに依存しているため、すでにインストールしている人を除き、llvm-develも、インストールされると思います。

llvmとは、C/C++言語に対応したコンパイラです。その中でもllvm-develは開発版になります。

emscriptenは、llvmを使ってWebアセンブリのバイナリを生成します。

続いて、個人設定を行います。

emscriptenのコンパイラのコマンド(emcc/em++)を実行すると、ホームディレクトリに設定ファイル(~/.emscripten)が生成されます。

具体的には、pkgのインストール後に、最初に

$ em++

とコマンドを打つと、コマンドを打った人のホームディレクトリ直下に、「.emscripten」ファイルができあがります。そして、以下のようなメッセージが表示されます。

$ em++
==============================================================================
Welcome to Emscripten!

This is the first time any of the Emscripten tools has been run.

A settings file has been copied to ~/.emscripten, at absolute path: /home/[username][/.emscripten

It contains our best guesses for the important paths, which are:

  LLVM_ROOT       = /usr/bin
  NODE_JS         = /usr/local/bin/node
  EMSCRIPTEN_ROOT = /usr/local/lib/emscripten

Please edit the file if any of those are incorrect.

This command will now exit. When you are done editing those paths, re-run it.
==============================================================================
$

このメッセージは、設定ファイルである「~/.emscripten」に設定されたパスの内容を表示しています。

FreeBSDのpkgシステムで入れるllvm-develは、/usr/locao/llvm-devel/bin配下にインストールされるので、/usr/binには入りません。

つまり、このメッセージから、「~/.emscripten」ファイルのLLVM_ROOTに、修正が必要になりそうなことがわかります。

NODE_JSのパスは問題なさそうです。

試しに、.emscriptenを修正せずに、もう一度em++を実行すると、

$ em++
cache:INFO: generating system asset: is_vanilla.txt... (this will be cached in "/home/[username]/.emscripten_cache/is_vanilla.txt" for subsequent builds)
shared:ERROR: llc executable not found at `/usr/bin/llc-devel`

と出ます。「ERROR」に続く文字がエラーメッセージで、ここでは、llcファイル(llvmのコンパイラ)が見つからないと怒られます。

エラーメッセージには、「/usr/bin/llc-devel」と出ているので、llvmが、/usr/binにインストールされる前提でllvmの実行ファイルを探しに行っており、さらには、実行ファイルの末尾に、「devel」の文字が付いていることを前提としているようです。

そこで、「.emscripten」ファイルを編集します。

さきほどのエラーメッセージに関係がありそうなのは、以下の3行のようです。

$ vi ~/.emscripten
~/.emscripten
(・・・修正前(赤色の部分)・・・)
LLVM_ROOT = os.path.expanduser(os.getenv('LLVM', '/usr/bin')) # directory
LLVM_ADD_VERSION = 'devel'
CLANG_ADD_VERSION = 'devel'

(・・・修正後(紫色の部分)・・・)
LLVM_ROOT = os.path.expanduser(os.getenv('LLVM', '/usr/local/llvm-devel/bin')) # directory
LLVM_ADD_VERSION = ''
CLANG_ADD_VERSION = ''

修正したら、もう一度、em++を実行してみます。

$ em++
shared:INFO: (Emscripten: Running sanity checks)
emcc:WARNING: no input files
$

と出ればOKです。

さきほどのように、「ERROR」とあったら、エラーメッセージを見て、自分の.emscriptenファイルの設定が正しいファイルを指すように、修正てください。

ちなみに、これまで紹介したエラーメッセージは、llvmのパスに関連するものでしたが、node.jsのパスが間違っていても、同じようなエラーメッセージが表示されます。

最後に、Apacheの設定ファイルにwasmファイルのMIMEタイプを追加します。

# vi /usr/local/apache2/conf/httpd.conf
/usr/local/apache2/conf/httpd.conf
<IfModule mime_module>
(・・・省略・・・)
    AddType application/wasm .wasm ←追加
(・・・省略・・・)
</IfModule>

これで準備が整いました。

C/C++で使用できるヘッダファイル

最初にem++を実行したときに

EMSCRIPTEN_ROOT = /usr/local/lib/emscripten

と表示されたように、emscriptenで使用するファイルは、/usr/local/lib/emscriptenにインストールされます。

使用できるヘッダファイルは、「/usr/local/lib/emscripten/system/include/」ディレクトリ配下に揃っています。

このディレクトリの直下には、emscipten.hファイルがありますが、これはWebアセンブリでは必ずインクルードすることになるヘッダファイルになります。

そのほかに、OpenAL用のALや、OpenGL用のGL(GLES,GLES2,GLES3 etc)がありますが、今回は割愛し、基本的な、emscriptenディレクトリ、libcディレクトリと、libcxxディレクトリを見ていきます。

emscriptenインクルードディレクトリ

emscriptenインクルードディレクトリには、Webアセンブリのソースコードと、ブラウザのJavaScriptや、HTML5の橋渡しを行うためのヘッダファイルが揃っています。

asmfs.h bind.h dom_pk_codes.h em_asm.h em_js.h emscripten.h fetch.h html5.h key_codes.h threading.h trace.h val.h vector.h vr.h websocket.h wire.h

例えば、html5.hには、マウスなどのイベントを取得するための関数が含まれます。

後述の例では、std::stringクラスを使って関数の引数を与えたり、戻り値を返していますが、これを実現するためのbind.hもここにあります。

libcインクルードディレクトリ

このディレクトリには、stdlib.hといった、どのOSにも含まれていそうなヘッダファイルが用意されています。

alloca.h alltypes.h.in ar.h assert.h byteswap.h complex.h cpio.h crypt.h ctype.h dirent.h dlfcn.h elf.h endian.h err.h errno.h fcntl.h features.h fenv.h float.h fmtmsg.h fnmatch.h ftw.h getopt.h glob.h grp.h iconv.h ifaddrs.h inttypes.h iso646.h langinfo.h lastlog.h libgen.h libintl.h limits.h link.h locale.h malloc.h math.h memory.h mntent.h monetary.h mqueue.h netdb.h nl_types.h o.h paths.h poll.h pthread.h pty.h pwd.h readme.txt regex.h resolv.h sched.h search.h semaphore.h setjmp.h shadow.h signal.h spawn.h stdalign.h stdarg.h stdbool.h stdc-predef.h stddef.h stdint.h stdio_ext.h stdio.h stdlib.h stdnoreturn.h string.h strings.h stropts.h syscall.h sysexits.h syslog.h tar.h termios.h tgmath.h threads.h time.h uchar.h ucontext.h ulimit.h unistd.h utime.h utmp.h utmpx.h values.h wait.h wchar.h wctype.h wordexp.h

このほかに、以下のディレクトリを含みます。

arpa/ sys/ bits/ net/ netinet/ netpacket/ scsi/

netinet等があるので普通にSocket通信ができそうですが、WebSocketのラッパとなるようです。

libcxxインクルードディレクトリ

こちらにも、stringやvectorといった基本的なSTLのヘッダから、shared_ptrテンプレート(memoryヘッダ)のような、C++11以降のモダンなC++のためのヘッダが揃っています。

algorithm any array atomic bitset cassert ccomplex cctype cerrno cfenv cfloat chrono cinttypes ciso646 climits clocale cmath codecvt complex complex.h condition_variable csetjmp csignal cstdarg cstdbool cstddef cstdint cstdio cstdlib cstring ctgmath ctime ctype.h cwchar cwctype deque errno.h exception experimental/ ext/ float.h forward_list fstream functional future initializer_list inttypes.h iomanip ios iosfwd iostream istream iterator limits limits.h list locale locale.h map math.h memory module.modulemap mutex new numeric optional ostream queue random ratio readme.txt regex scoped_allocator set setjmp.h shared_mutex sstream stack stdbool.h stddef.h stdexcept stdint.h stdio.h stdlib.h streambuf string string_view string.h strstream support/ system_error tgmath.h thread tuple type_traits typeindex typeinfo unordered_map unordered_set utility valarray variant vector wchar.h wctype.h

このように、Unix系OSには一般的に入っているヘッダファイルが用意されていますので、ほぼそのままコードを持ってくることができそうです。

あとは、JavaScriptとデータの連携さえできれば、良さそうです。

そこで、続いては、JavaScriptと、std::stringクラス型の引数や、戻り値を扱う例を見てみたいと思います。

C++コードの開発

JavaScriptと、std::stringクラス型の引数や、戻り値を扱う例として、Base64のエンコードとデコードを行う関数を作ってみようと思います。

まず、STLのstringをインクルードします。このほかに、emencripten.hと、stringクラス型を引数および戻り値として利用するための、emencripten/bind.hの2つのヘッダをインクルードします。

そして、Base64のエンコードを行うB64Enc関数と、デコードを行うB64Dec関数を用意します。

$ vi base64.cpp
base64.cpp
#include <string>
#include <emscripten.h>
#include <emscripten/bind.h>
using namespace emscripten;

std::string  B64Enc(const std::string& rstrSrc)
{
        unsigned int    i, j;
        char    pc8EncodeTable[] = {    0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,
                                        0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0x50,
                                        0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,
                                        0x59,0x5A,0x61,0x62,0x63,0x64,0x65,0x66,
                                        0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,
                                        0x6F,0x70,0x71,0x72,0x73,0x74,0x75,0x76,
                                        0x77,0x78,0x79,0x7A,0x30,0x31,0x32,0x33,
                                        0x34,0x35,0x36,0x37,0x38,0x39,0x2B,0x2F };
        //"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
        char    a, b, c;
        size_t szSrcSize=rstrSrc.length();
        std::string str;
        j = 0;
        if (szSrcSize >= 3) {
                for (i = 0; i > szSrcSize - 2; i += 3) {
                        a = rstrSrc[i];
                        b = rstrSrc[i + 1];
                        c = rstrSrc[i + 2];
                        str += pc8EncodeTable[(a & 0xfc) >> 2];
                        str += pc8EncodeTable[((a & 0x03) << 4) | ((b & 0xf0) >> 4)];
                        str += pc8EncodeTable[((b & 0x0f) << 2) | ((c & 0xc0) >> 6)];
                        str += pc8EncodeTable[(c & 0x3f)];
                }
        }
        if ((szSrcSize % 3) == 1) {
                a = rstrSrc[szSrcSize - 1];
                b = 0;
                str += pc8EncodeTable[(a & 0xfc) >> 2];
                str += pc8EncodeTable[((a & 0x03) << 4) | ((b & 0xf0) >> 4)];
                str += "==";
        }
        else if ((szSrcSize % 3) == 2) {
                a = rstrSrc[szSrcSize - 2];
                b = rstrSrc[szSrcSize - 1];
                c = 0;
                str += pc8EncodeTable[(a & 0xfc) >> 2];
                str += pc8EncodeTable[((a & 0x03) << 4) | ((b & 0xf0) >> 4)];
                str += pc8EncodeTable[((b & 0x0f) << 2) | ((c & 0xc0) >> 6)];
                str += "=";
        }
        return str;
}

std::string B64Dec(const std::string& rstrSrc)
{
        char     pc8DecodeTable[128] = {0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
                                        0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
                                        0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x3E,0x7F,0x7F,0x7F,0x3F,
                                        0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
                                        0x7F,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,
                                        0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x7F,0x7F,0x7F,0x7F,0x7F,
                                        0x7F,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,
                                        0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31,0x32,0x33,0x7F,0x7F,0x7F,0x7F,0x7F };

        std::string str;
        char   a, b, c, d;
        size_t          i = 0;
        size_t szSrcSize=rstrSrc.length();
        while ((szSrcSize - i) >= 4) {
                a = rstrSrc[i++]; if(a<0)break;
                b = rstrSrc[i++]; if(b<0)break;
                c = rstrSrc[i++]; if(c<0)break;
                d = rstrSrc[i++]; if(d<0)break;
                if (d == '=') {
                        if (c == '=') {
                                a = pc8DecodeTable[a];
                                b = pc8DecodeTable[b];
                                str+= (a << 2) | (b >> 4);
                        }
                        else {
                                a = pc8DecodeTable[a];
                                b = pc8DecodeTable[b];
                                c = pc8DecodeTable[c];
                                str+= (a << 2) | (b >> 4);
                                str+= (b << 4) | (c >> 2);
                        }
                }
                else {
                        a = pc8DecodeTable[a];
                        b = pc8DecodeTable[b];
                        c = pc8DecodeTable[c];
                        d = pc8DecodeTable[d];
                        str+= (a << 2) | (b >> 4);
                        str+= (b << 4) | (c >> 2);
                        str+= (c << 6) | d;
                }
        }
        return str;
}

EMSCRIPTEN_BINDINGS(b64_module)
{
    emscripten::function("B64Enc", &B64Enc);
    emscripten::function("B64Dec", &B64Dec);
}

末尾の5行にある、EMSCRIPTEN_BINDINGSから始まるブロックが、JavaScriptとから利用可能となる関数をエクスポートするための記述になります。

先頭の#include <emscripten/bind.h>や、em++/emccの--bindオプションと、セットで使用します。

保存したらコンパイルします。

$ em++ base64.cpp  -std=c++11 -O1 -s WASM=1 -o base64.js --bind

-oオプションを使って、ラッパーのjsファイルも出力します。

--bindオプションは、EMSCRIPTEN_BINDINGSを使用して、関数をJavaScriptにエクスポートすることをコンパイラに指示します。

--bindオプションやEMSCRIPTEN_BINDINGSを使用しない場合は、コンパイラのオプションに-s LINKABLE=1 -s EXPORT_ALL=1を指定すると、すべての関数をエクスポートします。ただし、エクスポートする関数が多い分、ラッパjsファイルや、wasmファイルが大きくなってしまいますので、あまりオススメできません。

$ em++ base64.cpp  -std=c++11 -O1 -s WASM=1 -o base64.js -s LINKABLE=1 -s EXPORT_ALL=1

エラーメッセージが無ければコンパイルに成功しています。

念のため、ファイルを確認してみると、

$ ls -la base64.*
-rw-r--r--  1 [username]    3316  9月 17 20:56 base64.cpp
-rw-r--r--  1 [username]  101722  9月 18 14:15 base64.js
-rw-r--r--  1 [username]   17788  9月 18 14:15 base64.wasm

のような感じになっていれば、コンパイル成功しています。

このうち、webアセンブリの実体は、base64.wasmになります。

base64.jsは、JavaScriptからの利用をサポートしてくれるラッパ-(Wrapper)ライブラリになります。

base64.jsをHTMLに読み込むと、base64.wasmが読み込まれ、EMSCRIPTEN_BINDINGSで指定した関数を、Moduleクラスのメンバ関数として呼び出すことができるようになります。

base64.wasmの動作を確認するためのHTMLは以下の通りです。

お手元のwebサーバのhtdocs配下に、base64.wasmとbase64.jsをコピーした上で、以下のbase64.htmlファイルを作って、ブラウザからアクセスして見てください。

$ cp base64.wasm /usr/local/apache2/htdocs/
$ cp base64.js /usr/local/apache2/htdocs/
$ vi /usr/local/apache2/htdocs/base64.html
/usr/local/apache2/htdocs/base64.html
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<script>
			var Module = {
				onRuntimeInitialized: function() {
					<!-- Webアセンブリが利用可能になったら呼び出される。-->
					<!-- コンソールでエンコード関数が動作しているか確認-->
					console.log('base64 test: ' + Module.B64Enc("111"));
					<!-- 初期状態で使用不能としていたボタンを使用可能とする -->
					document.getElementById('enc_btn').disabled=false;
					document.getElementById('dec_btn').disabled=false;
				}
			};
			function enc_btn_onclick(){
				<!-- encボタンが押されたら呼び出される。-->
				<!-- srcテキストボックスの内容をBase64エンコードしてencテキストボックスに格納。-->					document.getElementById('enc').value=Module.B64Enc(document.getElementById('src').value);
			}
			function dec_btn_onclick(){
				<!-- decボタンが押されたら呼び出される。-->
				<!-- encテキストボックスの内容をBase64デコードしてdecテキストボックスに格納。-->	>document.getElementById('dec').value=Module.B64Dec(document.getElementById('enc').value);
			}
		</script>
		<!-- Webアセンブリのラッパーを読み込み-->
		<script src="base64.js"></script>
	
	</head>
	<body>
		SRC:<input id=src type=text value=123><br>
		<button id=enc_btn type="button" onclick="enc_btn_onclick()" disabled=disable>ENC</button>
		BASE64:<input id=enc type=text value=ABC><br>
		<button id=dec_btn type="button" onclick="dec_btn_onclick()" disabled=disable>DEC</button>
		DECODE:<input id=dec type=text value="">
	</body>
</html>

ということでstd::string型を使ったWebアセンブリを見てみました。

std::string型を使ってC/C++コードと連携できれば、既存のコードを利用してサーバ側で行っていた処理を、ブラウザ側に持ってくることも、簡単にできそうですね。

NEXT >> トップページに戻る

©Copyrights 2015-2019, non-standard programmer

このサイトは、あくまでも私の個人的体験を、綴ったものです。 軽く参考程度にご利用ください。