C言語への感謝の正拳突き えっと今日は9日目ですね。。
macroネタ、結構ウケるかな?と思ってたんですけどね、、
本日はAPIネタです。
C言語で書く目的
組み込み系などの制約の強い環境や、bootloaderなどのCPUやデバイスに非常に近いレイヤを除き、
リソースの豊富なサーバサイドのアプリケーション(or ライブラリ)をCで書く目的というのはどういうものがあるのでしょうか。
※上記組み込み系やCPUやデバドラまわりをC++で書くことを否定するわけではないです。
思いつく限り挙げると、
(1) マルチプラットフォーム対応かつ依存コンポーネントを減らしたい(portability?)
C++を利用する場合、マルチプラットフォーム対応したライブラリは結構あって、そういうライブラリ(例えばBOOST)は非常に便利なのだけど、そのライブラリ(例えばBOOST)への依存が発生する。
JavaやScalaを利用する場合、JVMがマルチプラットフォーム対応してくれるけれども、、以下略。
Erlangを利用する場合、以下略。
Goのマルチプラットフォーム対応も素晴らしいですが、外部アプリケーションから利用しやすいライブラリにし難い。
(Go1.5からshared libraryとして提供できるようですが、何かしら制約があるでしょうし、まだ試せてないです。)
Cの場合、マルチプラットフォーム対応なんて自前で実装して、かつ依存コンポーネントも減らしてやるよ。。という話が多い気がします。
(2) ABI互換とライブラリとして組み込む際の運用性
これを考えたらCかC++しかないのですが、Cのほうが他言語から利用しやすいのではないでしょうか。
C++もC APIにwrapすれば似たようなものだと思いますが、一手間ありますね。
Java/Scala、ErlangはVMに依存するため、ABI互換(Bytecode?)はVMが保証しますが、
ライブラリとして組み込む範囲を狭めてしまいます。
利用範囲が明確であり、いろいろと諦めざるところを理解していれば、非常によい選択肢だと思います。
(3) 0から組んで、限界性能を引き出したい
これによく該当するのはC/C++だと思います。
C++のほうが遅いみたいな話、今ではほとんど聞かないですね。
あと、検索してみると結構でてくるのは、
Why was XXX written in C and not C++?
って質問ですね。
gitとか
http://cpplover.blogspot.jp/2013/05/linus-torvalsc.html
h2oとか
https://github.com/h2o/h2o/issues/179
zeromqとか(これは逆でなぜC++なのか)
http://250bpm.com/blog:4
redisとか
https://www.quora.com/Why-is-Redis-written-in-C-not-C++
grpc coreは、portabilityを理由にあげていたかな。
grpc coreの外側はC++だし。
nginxはこの手の話がみつからないな。。
おっと、これ以上はいけない!
今回は特にプラットフォーム依存をどのように吸収しているのかに関して焦点を当てたいと思います。
マルチプラットフォーム対応する場合、モバイル環境への対応が問題になりがちですが、
ここでは、主にWindows Linux BSD OSXなどのマルチプラットフォーム対応に絞ります。
最近Cで書かれたプロダクトに、WiredTigerっていうのがあります。
Berkeley DataBaseの後継っぽいNoSQLDatabaseで、2015年頭 MongoDBに買収されたところです。
WiredTigerの概要
https://github.com/wiredtiger/wiredtiger
特徴とかはこのへんが詳しい
http://www.slideshare.net/wiredtiger/wired-tiger-overview
BTreeとLSMTreeベースだなんて、今風じゃないですか。
大体はwikiみれば書いてる
https://github.com/wiredtiger/wiredtiger/wiki
WiredTigerを具体例にして、Cで実装されたプロダクトがどのようにマルチプラットフォーム対応しているのか、
どのようなAPI設計になっているのかを調べてみました。
具体例
WiredTigerの内部では、プラットフォームを抽象化したレイヤが存在し、ディレクトリを分けて定義されていました。
src :: wiredtiger/src
src/async async apiの定義
src/block block manager(ioを管理してるとこ)
src/bloom bloom filter(LSMTree用)
src/btree BTreeのlayoutとかcursorの定義
src/config config
src/conn conn?
src/cursor 各種cursorの定義。column cursorとかある。
src/docs docs
src/evict eviction、txnとかreconcileに依存があって辛い
src/include include
src/log log
src/lsm LSMTreeのlayoutとかcursorの定義
src/meta metadataの定義
src/os_posix posix用のAPI <-- ここに注目します。
src/os_win windowns用のAPI <-- ここに注目します。
src/packing packing?
src/reconcile inmemory pageからdiskへの追い出し、読み込み制御
src/schema 内部のtableのschema定義
src/session session context
src/support support api,checksumとかcryptoとはcityhashとかhuffmanとか
src/txn transactionまわり
src/utilities wiredtigerのライブラリを制御するutilityコマンドの定義かな
os_posixやos_winの中には、WiredTigerが使用する最小限のAPIが定義されていて、主にfile ioのAPIですね。
Win向けとPosix向けに実装がわかれていて、WiredTigerの他コンポーネントは、プラットフォームを抽象化したAPIを呼び出しています。
1つ例をあげると、src/dir_list.cで定義されているwt_exist()ですね。
filenameで指定したファイルの存在を確認するAPIです。
/*
* __wt_exist --
* Return if the file exists.
*/
int
__wt_exist(WT_SESSION_IMPL *session, const char *filename, int *existp)
{
WT_DECL_RET;
char *path;
WT_RET(__wt_filename(session, filename, &path));
ret = GetFileAttributesA(path);
__wt_free(session, path);
if (ret != INVALID_FILE_ATTRIBUTES)
*existp = 1;
else
*existp = 0;
return (0);
}
/*
* __wt_exist --
* Return if the file exists.
*/
int
__wt_exist(WT_SESSION_IMPL *session, const char *filename, int *existp)
{
struct stat sb;
WT_DECL_RET;
char *path;
*existp = 0;
WT_RET(__wt_filename(session, filename, &path));
WT_SYSCALL_RETRY(stat(path, &sb), ret);
__wt_free(session, path);
if (ret == 0) {
*existp = 1;
return (0);
}
if (ret == ENOENT)
return (0);
WT_RET_MSG(session, ret, "%s: fstat", filename);
}
APIは__wt_exist()に統一されていて、
Windowsのほうは、GetFileAttributesA()を呼び出していて、
Linuxのほうは、stat()を呼び出しているようです。
それ以外にもいろいろと見どころはあって、
(1) 第一引数はtop level contextであり、第2引数がパラメータ、第3引数が結果をかえす。
(2) return typeがintかつ、0か 0以外のいずれかしか返さない
(3) WT_RETやWT_RET_MSGってなによ。。 返値が0以外だったら、即返値をreturnするマクロ
なかなかクセがあるAPIだと思います。
WiredTigerは、0が成功で、0以外が失敗、かつ様々なコードを割り当ててそうな気がします。
Windowsの場合、return typeはboolでtrue/falseのいずれか、詳細なエラーはGetLastError()やWSAGetLastError()で取得しろってのに多少は似ていますね。
WiredTigerの場合、0かそれ以外か、かつ関数呼び出しの返値は基本的にWT_RETマクロで検査し、0以外だったら即returnするようですね。
ここまで把握できたら、file ioを中心につかうWiredTigerがどのようなAPIを切っていて、WindowsとPosixにおいてどのようなAPIを呼び出しているのかを把握するだけでよいと思います。
ざっとAPIの一覧を示します。
file os_win os_posix
os_alloc.c calloc
realloc
posix_memalign
free
os_dir.c FindFirstFileA opendir
FindNextFileA readdir
FindClose closedir
os_dlopen.c GetModuleHandleExA dlopen
GetProcAddress dlsym
FreeLibrary dlclose
os_errno.c GetLastError errno
os_exist.c FileAttributeA stat
os_fallocate.c fallocate
os_filesize.c GetFileSizeEx fstat
GetFileAttributeExA stat
os_flock.c LockFile fcntl
UnlockFile
os_fsync.c FlushFileBuffers fcntl fdatasync fsync sync_file_range
os_ftruncate.c SetFilePointerEx ftruncate
SetEndOfFile
os_getenv.c GetEnvironmentVariableA getenv
os_map.c CreateFileMappingA mmap
MapViewOfFile posix_madvise
CloseHandle munmap
UnmapViewOfFile
os_mtx_cond.c InitializeCriticalSection pthread_mutex_init
InitializeConditionVariable pthread_cond_init
EnterCriticalSection pthread_mutex_lock
LeaveCriticalSection pthread_mutex_unlock
SleepConditionVariableCS pthread_cond_timedwait pthread_cond_wait
WakeAllConditionVariable pthread_cond_broadcast
DeleteCriticalSection pthread_cond_destroy
os_once.c InitiOnceExecuteOnce pthread_once
os_open.c CreateFileA open
os_remove.c RemoveFileA remove
os_rename.c GetFileAttributeA rename
os_rw.c ReadFile pread
WriteFile pwrite
os_sleep.c Sleep select
os_snprintf.c w vsnprintf
os_thread.c beginthreadex pthread_create
WaitForSingleObject pthread_join
os_time.c GetSystemTimeAsFileTime clock_gettime gettimeofday
os_vsnprintf.c w vsnprintf
os_yield.c SwitchToThread sched_yield
WiredTigerの特徴としては、基本的にマルチスレッドで動作して、pollとかは一切しない。
IOはWindowsの場合はfilehandle+overlapped、Linuxの場合はpread/pwriteのみ実行し、
IOとマルチスレッドの制御は、block managerがいろいろやっているようでした。
まとめ
C(やC++)でマルチプラットフォーム対応する場合、多方面のAPIを叩く予定がなければ、
自前でプラットフォームを抽象化したAPIを用意するのもありかもしれませんね。。
マルチプラットフォーム対応したVMやライブラリはこういうことを頑張ってたんですねー。
こういうのはBOOSTとかJVMとかBEAM VMとかGolangにお任せしたいです。
(BOOSTとOracleJVMはC++製、BEAM VMはC製、GolangはGo製です)
以上