Boostのビルド、あるいは共有ライブラリの悲劇(終)

【結論そのいち】Boostのビルドなんか、しなくたって生きていけるさ。
【結論そのに】Boostのライブラリをリンクするときは静的ライブラリをリンクしよう。


にもかかわらず、あなたがBoostの共有ライブラリをリンクしたいと考えるならば、あなたは茨の道を往くことになるだろう。もしくは既に茨の道にあるだろう。ぼくが知っているのは、茨の道が停止するかどうかぼくたちには証明できないという事実だけだ。


というところまで書いて放りだしていたのだけれど、結論としては、ELFシステムではbjamのオプションにdll-path=...を渡してrpathを使う。Mach-Oを使うシステムではinstall_name_toolでがりがり書き換えるという話になる。

エクストリームテキストプロセシング

C++0xをdisってる人をdisる闇のC++軍団たち - Togetterに吹いた。


だがちょっと待ってほしい。C++0xは変態過ぎはしないか。たとえば増田からは「正規表現使いたければそれこそ、Perl使え」と主張するような声もある。このような声に闇のC++軍団たちは謙虚に耳を傾けるべきではないか。


傾けてみた。まず、perlfaq9 - Networking - metacpan.orgをコピペして、

#! /usr/bin/perl

use strict;

my $atom       = qr{[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+};
my $dot_atom   = qr{$atom(?:\.$atom)*};
my $quoted     = qr{"(?:\\[^\r\n]|[^\\"])*"};
my $local      = qr{(?:$dot_atom|$quoted)};
my $domain_lit = qr{\[(?:\\\S|[\x21-\x5a\x5e-\x7e])*\]};
my $domain     = qr{(?:$dot_atom|$domain_lit)};
my $addr_spec  = qr{$local\@$domain};

foreach (qw(roll.me.@example.com roll.you@example.com)) {
    print "$_: ", (/^$addr_spec$/o ? "true" : "false"), "\n";
}

実行した。

$ time perl rfc2822_addr_spec.pl 
roll.me.@example.com: false
roll.you@example.com: true

real	0m0.018s
user	0m0.003s
sys	0m0.005s

Boost.Xpressiveで書いてみた。適当にexpressionにしたので正しいかどうかは判らない。

#include <iostream>
#include <string>
#include <vector>
#include <boost/assign.hpp>
#include <boost/foreach.hpp>
#include <boost/xpressive/xpressive.hpp>

int main(int argc, char* argv[]) {
    using boost::xpressive::_ln;
    using boost::xpressive::_s;
    using boost::xpressive::_w;
    using boost::xpressive::range;
    using boost::xpressive::set;
    using boost::xpressive::sregex;

    const sregex atom(+(_w|(set='!','#','$','%','&','\'','*','+','/','=','?','^','`','{','}','~','|','-')));
    const sregex dot_atom(atom >> *('.' >> atom));
    const sregex quoted('"' >> *('\\' >> ~_ln | ~(set='\\','"')) >> '"');
    const sregex local(dot_atom | quoted);
    const sregex domain_lit('[' >> *('\\' >> ~_s | set[range('\x21','\x5A') | range('\x5E','\x7E')]) >> ']');
    const sregex domain(dot_atom | domain_lit);
    const sregex addr_spec(local >> '@' >> domain);

    const std::vector<std::string> data = boost::assign::list_of
        ("roll.me.@example.com")
        ("roll.you@example.com")
        ;

    std::cout << std::boolalpha;
    BOOST_FOREACH(const std::string& i, data) {
        std::cout << i << ": " << regex_match(i, addr_spec) << "\n";
    }

    return 0;
}

下記の環境でコンパイルした。ちなみにcodepad.orgではコンパイルできなかった。

$ time g++ -fast rfc2822_addr_spec.cpp -o rfc2822_addr_spec

real	0m9.188s
user	0m8.617s
sys	0m0.538s

実行した。

$ time ./rfc2822_addr_spec
roll.me.@example.com: false
roll.you@example.com: true

real	0m0.019s
user	0m0.001s
sys	0m0.002s

C++使いはもう正規表現をblogに書くな」と言われても良いレベルのコンパイル時間。たった35行のプログラムをコンパイルするのに10秒近くかかるとか、マゾかと。


だが、それがいい

Boostのビルド、あるいは共有ライブラリの悲劇 (2)

Boostをふつうにビルドすると、たぶん静的ライブラリと共有ライブラリが生成される。静的ライブラリはオブジェクトファイルを寄せ集めただけなので、問題はない。すくなくとも共有ライブラリほど問題はない。共有ライブラリはおおいに問題である。Mac OS X (Darwin)の共有ライブラリは特に。


以下、Mach-Oを使うシステムとしてMac OS X、ELFを使うシステムとしてGNU/Linuxについて話す(Microsoft Windowsのことは知らない)。Finkの文書はMach-Oの暗部を簡潔にまとめている。

Mach-O の仕様の一つであり、多くの人を驚かせるものとして、共有ライブラリと 動的ローダブル・モジュールを厳密に区別します。ELFシステムでは両者は同質で、共有コードのどの部分でも、ライブラリとしても動的ローディングにも使うことができます。

Fink - Porting - Shared Code

共有ライブラリを作成する場合、実行時に検索するライブラリの名前を指定することができます、これはよく行われることで、これによって、ライブラリのいくつかのメジャーバージョンをインストールすることが可能になります。ELFシステムでは、これはsonameと言われています。Darwinの場合、ファイル名の他にフルパスを指定することができ(またする必要があり)ます。これにより、"rpath"オプションとldconfig/ld.so.cacheシステムが不要になります。まだインストールされていないライブラリを使うには、DYLD_LIBRARY_PATH環境変数を設定することもできます。詳細はdyldのmanページをご覧ください。

Fink - Porting - Shared Code

「(またする必要があり)ます」。ここは暗部、試験に出る。要約すると、rpathを基本的に使わない(使いたければさらなる暗部に踏みこむ覚悟が必要)。暗部に踏みこみたくなければ、解決方法は簡単。


【結論そのに】Boostのライブラリをリンクするときは静的ライブラリをリンクしよう。


ただし、静的ライブラリと共有ライブラリが両方使えそうに見える場合、共有ライブラリを優先することに注意。てっとり早い解決策は、共有ライブラリをビルドしない、あるいは削除してしまうこと。完全な静的リンクはお勧めしかねる(そもそもMac OS Xはlibcが提供されていないので静的リンクできない)。

Boostのビルド、あるいは共有ライブラリの悲劇 (1)

Boostをビルドするのは、かなりの難行である。ビルドシステムは意味不明だし、長い時間をかけてビルドしたライブラリは半分も使えば良いほうなのではないか。正直、ぼくが使うのは、せいぜいBoost.GraphとBoost.IostreamsとBoost.TestとBoost.Threadあたりくらいだ。これらでさえ、リンクはしてるけど、本当は使っていないことのほうが多い。たとえばBoost.Iostreamsを使うのは適当にストリームをでっちあげたいからであって、いつもいつもbzip2やzlibの入出力をするわけはない。Boost.PythonやBoost.MPIを使えば人生が変わるかもしれないが、今の人生もけっこう気に入っている。


【結論そのいち】Boostのビルドなんか、しなくたって生きていけるさ。

random_deviceの実装

もしあなたがセンシティヴだとしたら、random_deviceは自分で実装するべきかもしれない。この場合のセンシティヴは、熟練者 (expert) とか古強者 (veteran) とか芸術家 (artist) とほとんど同義かもしれない。


さて、C++0xで導入される乱数ライブラリについては本の虫: C++0xの新しい乱数ライブラリ、randomで解説されている。以下にrandom_deviceのoperator()()を引用する。

result_type operator()();

Returns:

A non-deterministic random value, uniformly distributed between min() and max(), inclusive. It is implementation-defined how these values are generated.

Throws:

A value of an implementation-defined type derived from exception if a random number could not be obtained.

返り値

min()とmax()の間(max()も含む)に一様に分布する非決定性の乱数。どのように値が生成されるかは実装定義である。

例外送出:

乱数が得られなかった場合に、exceptionを継承した実装定義の値。

libstdc++-v3の実装を眺める。

    result_type
    operator()()
    {
#ifdef _GLIBCXX_USE_RANDOM_TR1
      result_type __ret;
      std::fread(reinterpret_cast<void*>(&__ret), sizeof(result_type),
		 1, _M_file);
      return __ret;
#else
      return _M_mt();
#endif
    }
std::tr1::random_device::operator()()

_M_fileには/dev/urandomか/dev/randomをfopenした結果の妥当なFILE*が格納されている。このとき、freadが必ず成功するという保証はどこで与えられているのか、調べてみたけれどよく判らない。C file input/output - Wikipediaなんか、読むとむしろ不安になる(というか、この記述はどんなもんだろう)。

N3000 - P.897

SSH経由でNFSv3を使う

おおむねdarwinMacBook ProのうえのSnow Leopard)のうえで仕事したり遊んだり、仮想機械またはネットワーク(あるいはその両方)のうえのUNIXもしくはUNIXみたいなシステムにSSHでログインして暮らしている。リモートからローカルのファイルにアクセスしたいときがときどきある。

ときどきあるので、NFSSSHで転送している。問題はdarwinがNFSv4に対応していない点にある。対応していないので、NFSで利用するポートを限定する必要がある。Linux JFのhttp://www.linux.or.jp/JF/JFdocs/NFS-HOWTO/security.html#NFS-SSHが参考になる。以下、darwinと書いたとき、Snow Leopardのことを指す。


darwinのnfsdはlaunchdから起動される。/etc/exportsにエクスポートの設定が書かれているとnfsdが起動する。ちなみに初期状態だと/etc/exportsにはなにも書かれていない。たとえば、feedfaceというユーザの$HOME/Shareというディレクトリをエクスポートする設定を

/Users/feedface/Share -mapall=feedface 127.0.0.1

と書く(http://developer.apple.com/Mac/library/documentation/Darwin/Reference/ManPages/man5/exports.5.html参照)。


次に、nfsdが利用するポートを限定する必要があるので/etc/nfs.confに設定する。ちなみに初期状態だと/etc/nfs.confは存在しない(他の方法としてnfsdの引数で指定する方法もある)。nfsdが6049番ポートを、mountdが6050番ポートを利用し、非特権ポートからのマウントを許可するために

nfs.server.mount.port = 6050
nfs.server.mount.require_resv_port = 0
nfs.server.port = 6049

と書く(http://developer.apple.com/Mac/library/documentation/Darwin/Reference/ManPages/man5/nfs.conf.5.html参照)。もちろん指定するポートはなんでもいい。


ここまで設定したら、

ssh -R 6049:localhost:6049 -R 6050:localhost:6050 remote

とかやってリモート転送付きでSSHする。リモートからのマウントは、たとえばLinuxの場合、

sudo mount -t nfs -o port=6049,mountport=6050,nolock,tcp \
    localhost:/Users/feedface/Share /mnt/feedface-share

とかする。

Non-modern Shell Programing (4)

2.6.3 コマンド置換

コマンド置換は、コマンドの出力がコマンド名自体の代わりとして置換されることを許す。コマンド置換はコマンドが下記によって囲まれている時に起こるものとする。

$(コマンド)

または(バッククォートされたバージョン):

`コマンド`

シェルはサブシェル環境内でコマンドを実行し、コマンドの標準出力によってコマンド置換(コマンドのテキストと、それを囲んでいる"$()"やバッククォート)を置き換えることでコマンド置換を展開する。展開の際に、置換の終端の1個以上の<改行>文字群からなる列を取り除く。出力終端より前の埋め込まれた<改行>文字群は取り除かれない。しかし、IFSの値と、実施されているクォートによっては、フィールドデリミタとして扱われ、フィールド分割の間に除去されるかもしれない。出力がヌルバイトを含んでいたら、振る舞いは不定である。

バッククォート方式のコマンド置換内では、'$'や'`'や<バックスラッシュ>が後に続かない限り、<バックスラッシュ>は字義通りの意味を持つものとする。マッチするバッククォートの検索は、クォートされていない最初の非エスケープバッククォートによって満たされる。シェルコメントやヒアドキュメントや$(コマンド)方式の埋め込まれたコマンド置換やクォートされた文字列のなかで、非エスケープバッククォートに遭遇した場合、未定義の結果が起こる。"`...`"列内で、開始しているが終了していないシングルクォートまたはダブルクォート文字列は未定義の結果を引き起こす。

$(コマンド)方式内では、開き括弧からそれにマッチする閉じ括弧までのすべての文字群がコマンドを構成する。リダイレクションからなるスクリプト不定の結果を引き起こす)を除いて、あらゆる妥当なシェルスクリプトをコマンドとして用いることができる。

コマンド置換の結果は、さらなるチルダ展開やパラメータ展開やコマンド置換や算術展開の処理をされないものとする。コマンド置換がダブルクォート内で起きたら、フィールド分割とパス名展開はコマンド置換の結果に対して実行されないものとする。

コマンド置換は入れ子になりうる。バッククォートされたバージョン内で入れ子を指示するために、アプリケーションは内側のバッククォートに<バックスラッシュ>文字を先行させるものとする。例えば、

\`コマンド\`

下記のようにコマンド置換が1個のサブシェルからなるならば、

$( (コマンド) )

準拠アプリケーションは"$("と'('を(つまり、空白で分割されている)2個のトークンに分割するものとする。これは算術展開との不明確さをさけるために必要である。

2.6.3 Command Substitution