2012年12月15日土曜日

Boost.Context on Cygwin

1.51.0 において Boost.Context がリリース入りしました。Boost.Context はコンテキスト切り替えのためのライブラリです。コンテキストの最も一般的な訳語は「文脈」ですが、この場合は実行中のアドレス、CPU のレジスタなど、実行時の状態情報とでもいうべきものです。(プリエンプティブ)マルチタスクは OS がこのコンテキスト情報を切り替えることで成立していますが、これを自前で切り替えられるようにするのが Boost.Context です。あるいは、setjmp/longjmp だと setjmp した時点に戻ることしかできませんが、longjmp した時に同時に setjmp が実行されその場所にまた戻ることが出来ると言っても良いかもしれません(スタックの取り扱いが違いますが)。 これが出来て何が嬉しいかというと、C# の yield みたいなことが実現できるわけですが、それはともかく。CPU のレジスタとか書いていることでお分かりかもしれませんが、Boost.Context は C/C++ の範囲では実現できません。ということでアセンブリ言語で実装されています。Windows だと MASM が要求されるのが面倒だったので gas に移植(というほどのこともないですが)し、Cygwin でビルドできるところまで到達したのですが、example の jump.cpp をコンパイル、動作させてみると、コンテキストを切り替えた先の文字列出力で詰まってしまいました。

//          Copyright Oliver Kowalke 2009.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>

#include <boost/assert.hpp>
#include <boost/context/all.hpp>

namespace ctx = boost::context;

ctx::fcontext_t fcm;
ctx::fcontext_t * fc1 = 0;
ctx::fcontext_t * fc2 = 0;

void f1( intptr_t)
{
        std::cout << "f1: entered" << std::endl;
        std::cout << "f1: call jump_fcontext( fc1, fc2, 0)" << std::endl;
        ctx::jump_fcontext( fc1, fc2, 0);
        std::cout << "f1: return" << std::endl;
        ctx::jump_fcontext( fc1, & fcm, 0);
}

void f2( intptr_t)
{
        std::cout << "f2: entered" << std::endl;
        std::cout << "f2: call jump_fcontext( fc2, fc1, 0)" << std::endl;
        ctx::jump_fcontext( fc2, fc1, 0);
        BOOST_ASSERT( false && ! "f2: never returns");
}

int main( int argc, char * argv[])
{
        ctx::guarded_stack_allocator alloc;

        void * base1 = alloc.allocate(ctx::guarded_stack_allocator::default_stacksize());
        BOOST_ASSERT( base1);
        fc1 = ctx::make_fcontext( base1, ctx::guarded_stack_allocator::default_stacksize(), f1);
        BOOST_ASSERT( fc1);
        BOOST_ASSERT( base1 == fc1->fc_stack.sp);
        BOOST_ASSERT( ctx::guarded_stack_allocator::default_stacksize() == fc1->fc_stack.size);

        void * base2 = alloc.allocate(ctx::guarded_stack_allocator::default_stacksize());
        BOOST_ASSERT( base2);
        fc2 = ctx::make_fcontext( base2, ctx::guarded_stack_allocator::default_stacksize(), f2);
        BOOST_ASSERT( fc2);
        BOOST_ASSERT( base2 == fc2->fc_stack.sp);
        BOOST_ASSERT( ctx::guarded_stack_allocator::default_stacksize() == fc2->fc_stack.size);

        std::cout << "main: call start_fcontext( & fcm, fc1, 0)" << std::endl;
        ctx::jump_fcontext( & fcm, fc1, 0);

        std::cout << "main: done" << std::endl;

        return EXIT_SUCCESS;
}

このコードは本来、

main: call start_fcontext( & fcm, fc1, 0)
f1: entered
f1: call jump_fcontext( fc1, fc2, 0)
f2: entered
f2: call jump_fcontext( fc2, fc1, 0)
f1: return
main: done

と出力されて終了するのですが、main: call start ... の行だけ出力されて詰まってしまう状態です。実際にハイライトされている 22 行目で詰まっていたわけですがこれは Cygwin 内部の仕組みのせいでした。

規格に定義されているわけではありませんが、一般的に、ローカル変数(自動変数)、関数の引数などはスタックと呼ばれる領域に格納されています。関数の呼び出しがネストするごとにスタックは伸びていきます。setjmp/longjmp ならば戻るだけ、なので伸びた先のことは忘れてしまえばいいわけですが、コンテキスト切り替えによってまた戻ってくるためにはスタックの状態が保存されていなければなりません。ということで Boost.Context ではスタック領域自体を別に用意した領域に切り替えます。この時、OS 側で管理している情報である NT TIB(Thread Information Block) のスタック情報(top と bottom)も切り替えています。一方、Cygwin ではスタックの底に cygtls というスレッド固有の情報を格納しており、NT TIB を経由して参照しています。このため、Boost.Context によるコンテキスト切り替えの結果、NT TIB が指すスタックの底に cygtls が存在しないことになり(恐らく排他制御に失敗して)詰まってしまっていたわけです。

実際、f1(), f2() 内の出力をコメントアウトすると、正しく切り替え出来 main: done も出力されます(NT TIB が元のスタックの底を指すため)。コンテキスト切り替え先で Cygwin のシステムコールがまともに使えないのはペナルティが大きすぎるので、NT TIB のスタック情報を切り替えをしない版を作ってみたところ正しく動作しているようです(Boost.Context gas on Windows パッチ)。

スタック領域のチェックをするだとかいったデバッグ系ツールと組み合わせられないでしょうが他は大丈夫だと思っているのですがどうでしょうか。

2012年12月8日土曜日

【C++ Advent Calendar 2012】 8日目 「C++ Compiler Farm の紹介」&「キャストの復習」

このエントリは C++ Advent Calendar 2012 8 日目の記事です。

C++ Advent Calendar 2012 というからには普通 C++ のネタを提供するものですが、一応、C++ 関連ではあるのですが言語仕様でもなければライブラリでもないネタです。それだけではなんなので一応小ネタとして「キャストの復習」という内容も書いてみます。

C++ Compiler Farm の紹介

既に一度 Twitter 上で流したネタではあるのですが、「C++ Compiler Farm」 というサイトを作ってみました。

C++ は言語仕様が複雑であり、かつ、標準実装や唯一の実装のようなものが存在しないこともあり処理系によって挙動がまちまちである、というのは C++er は身に染みて良く知っていることだと思います。それでは皆さんの周辺ではコンパイラは何種類くらい利用可能でしょうか?無償利用可能なコンパイラに限っても世の中に結構な数があるわけですがそんなにたくさん常用できる状態にはない、という人も結構いるのではないでしょうか? C++ Compiler Farm はオンラインで複数の C++ コンパイラによるコンパイル結果、実行ファイルの実行結果を確認できるサービスです。

以前 Twitter 上で流した時点ではコンパイル、実行結果の確認はできる、という状態でしたが、結果を後から参照することができませんでした。今回機能強化を果たし、http://ccf.myhome.cx:5000/result/1 のようなリンクで実行結果を後から参照することができるようになりました。これでコンパイラによって挙動が違う、などと言った時に他の人に結果を見せやすくなるんじゃないかな、と期待しています。

コンパイラ無選択状態でも実行可能だったり、全ての結果が保存されたり、編集できなかったりまだまだ低機能ですが利用者がちょっとでも居そうであればちょこちょこやっていこうかと思っています。なお関連ソースコードは https://github.com/yak1ex/ccf で公開しています。

C++ Advent Calendar で紹介しておきながらあるまじきことですが、ほとんど Perl で実装されています。なので概要だけ構成を説明しておくと以下の図のような構成になっています。

AWS EC2 上で Windows / Linux サーバ Micro instance 各1台。それぞれでコンパイルサーバが実行されており、Web サーバは Linux 側。ブラウザ上の Javascript と協調しながら処理する感じです。サーバ側では非同期処理フレームワーク AnyEvent を使っていますので、C++er としては Boost.Asio とかで書けると格好いいところなんですが。サンドボックス部分だけ、Google Chrome のオープンソース実装である Chromium の C/C++ コードを一部修正して使用しています。これで system("rm -rf /"); とか入力されても問題ないようになっています(http://ccf.myhome.cx:5000/result/7)。……そのはず、です。Windows 側はメッセージなしで黙って無視される形ですね。実行時間、使用メモリについても制限をかけています。相変わらず Amazon Web Services 無料範囲内での運用で、使用資源を絞った状態ですが以前よりはちょっと緩めました。この辺は調整だと思っていますので使用実績が増えれば考えるための材料も増えるかと思っています。

「C++ Compiler Farm」を紹介させて頂きました。皆様の C++ life に少しでも役立てば幸いです。

キャストの復習

ということで小ネタ「キャストの復習」です。最初に言っておきますが、規格上どうか、という話であって、実装上は概ね変わらないとかそういう割と役に立たない話になります。また、アラインメントについて省略したり正確な表現でなかったりします。

さて、C++ キャストは以下の 4 種類あります。

  • const_cast
  • dynamic_cast
  • reinterpret_cast
  • static_cast

このうち const_cast は const 外し、dynamic_cast は安全なキャストであるかどうかを判定できる、という点で位置づけは割と明快です(dynamic_cast の使いどころはどこか、というのはそれはそれで議論になりそうですが)。ということでたまに使い分けで議論になる reinterpret_cast と static_cast について、特にポインタの場合について掘り下げてみようと思います。

が、その前に C-style キャストについても確認しておきましょう。C++ においては C-style キャスト (type)value は以下の C++ キャストとして解釈可能なもののうち先にあるものと解釈されます(14882:2011 5.4p4)。

  • const_cast 1回
  • static_cast 1回
  • static_cast 1回 + const_cast 1回
  • reinterpret_cast 1回
  • reinterpret_cast 1回 + const_cast 1回

つまり文脈によってどのように解釈されるかが変わります。

C 言語において割と暗黙のうちに仮定されているんじゃないかという前提として、ポインタのキャストでは指す位置は変わらない(値は変わらず解釈が変わる)、というものがある気がします(注:C 言語においてもchar* への変換以外規格上その保証はありません(9899:1999, 9899:2011 6.3.2.3p7)。ついでに strict aliasing rule 的に char* 系以外の別の型へのポインタ経由でアクセスすると未定義動作です(9899:1999, 9899:2011 6.5p7)。一方で、C++ においてはポインタのキャストでその値(指している場所)が変わる場合が有り得ます。

以下のような継承がされているクラス群がある場合、

struct A { int n; };
struct B { int n; };
struct C : A, B { int n; };

C 型のオブジェクト内に A 型、B 型のオブジェクトが含まれる形となり、かつ先頭位置を C 型と共有可能なのは A 型か B 型かいずれか一つしかないことになります。規格上定義されていませんが典型的にはメモリレイアウトは以下の図のようになります。

図のようにA型、B型の順に並んでいるとして、C* を B* に static_cast すると C の中にある B の先頭を指すことになり、つまり、指す位置が変わります。逆に B* を C* に static_cast しても指す位置が変わります。この場合、もともと C 型のオブジェクト中の B 型オブジェクトを指していない場合などは不正な位置を指すことになります。つまりこのキャストはいつでも安全とは言えないのですが、static_cast はstandard conversion として規定されている型変換の逆方向のキャストもできると規定されている(14882:2003 5.2.9p6, 14882:2011 5.2.9p7)ため一律コンパイル可能です(もちろん不正な位置を指す場合には未定義動作ですが)。つまり「static_cast ならば安全なキャストだ」というわけではありません。

では static_cast の立ち位置とはなんでしょう?上の段落で単なる「キャストする」ではなく「static_cast する」と明示したことに気付かれたでしょうか?C++03 以前では、reinterpret_cast でのポインタのキャストについてはヌルポインタがヌルポインタのままであること(14882:2003 5.2.10p8)、T1* → T2* → T1* で元に戻ること(14882:2003 5.2.10p7)以外は未規定(unspecified)であり可搬性のあるコードを書くならば reinterpret_cast は使うな、が基本でした。つまり、上のような B* → C* あるいは C* → B* についても(続けてやれば元に戻ること以外) reinterpret_cast の結果について言えることはありませんでした。つまり(未定義動作の場合もあるけど)結果が規定されている static_cast と規定されていない reinterpret_cast という位置づけだった訳です。

C++03 以前において例えば char* を unsigned char* にキャストする(規格上)可搬性のある方法は、void* を経由して static_cast する方法でした。

char *pc;
// unsigned char* upc = static_cast<unsigned char*>(pc); // COMPILE ERROR
unsigned char* upc = static_cast<unsigned char*>(static_cast<void*>(pc));

void* への変換は指す位置が変わらないという規定があります(14882:2003 4.10p2)。一方、void* へ変換して元の型に戻すと同じ場所を指すという規定もあるため(14882:2003 5.2.9p10)、void* からのキャストも指す位置は変わらないことになります。一方、以下のコードもコンパイルは通りますが、

char *pc;
unsigned char* upc_bad1 = (unsigned char*)pc;
unsigned char* upc_bad2 = reinterpret_cast<unsigned char*>(pc);

この C-style キャストは(static_cast ではキャストできない変換なので)前述の通り reinterpret_cast として解釈されます。これも前述の通り reinterpret_cast では結果が保証されないためこのコードは可搬性がありません(pc と upc_bad* で同じ位置を指している保証がない)。

というのが、C++03 以前の話。C++11 では reinterpret_cast の規定が変わりました(14882:2011 5.2.10p7)。

An object pointer can be explicitly converted to an object pointer of a different type. When a prvalue v of type “pointer to T1” is converted to the type “pointer to cv T2”, the result is static_cast<cv T2*>(static_cast<cv void*>(v)) if both T1 and T2 are standard-layout types (3.9) and the alignment requirements of T2 are no stricter than those of T1, or if either type is void. Converting a prvalue of type “pointer to T1” to the type “pointer to T2” (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value. The result of any other such pointer conversion is unspecified.

太字下線部分が大体追加された規定で、standard-layout type へのポインタ間の場合、void* を経由する static_cast 2回と等価、つまり↑で可搬性がある方法としていたものになります。standard layout type は規格の範囲で(パディング等はあるけど)メモリレイアウトが決まる型のことです。char, unsigned char については standard-layout type なので、C-style キャストの場合も含めて↑で可搬性がないとしていたコードが可搬性があることになりました。世の人々も指す位置が変わらないと思って reinterpret_cast を使ってるし実装も他に選択肢がなくまず間違いなくそうなってるし、という理由で規定が変更されています(DR658)。もともとコードの見た目でキャストの位置づけが分かるように、という意図で C++ キャストが分けられていたはずなのですが、まぁ現実には勝てない、というところなのでしょうか。

さて、これを B*, C* の例に適用すると、クラスについては、メンバ・基本クラスに非 standard-layout class がないこと、メンバに参照なし、仮想関数・仮想基本クラスなし、継承階層中非静的メンバをもつクラスは自分自身を含めて高々一つ、が standard layout class となるため、B は standard-layout type ですが、C は standard-layout type ではありません。結果、reinterpret_cast についてはやっぱり何も言えない、ということになります。実際には C++03, C++11 いずれの実装であっても指す位置が変わらない、というのが普通の実装でしょう(指す位置を変える積極的な理由がない)。ということを示そうとしたのが http://ccf.myhome.cx:5000/result/12 です。いずれの処理系においても static_cast では指す位置が変わる場合があり、reinterpret_cast では指す位置が変わっていません。なおこのコードでは reinterpret_cast によるポインタと整数との変換をしています。これ自体も(整数のサイズが十分であれば)一周回れば元に戻る、以外はどのような変換が実施されるかは処理系依存です(14882:2003, 14882:2011 5.2.10p5)(そしてアドレスをそのまま整数値とする実装が多いでしょうし、このコードは少なくとも変換が単射であることを期待したコードですので厳密には可搬性はありません)。個人的にはこのポインタと整数の相互変換が C++03 以前で reinterpret_cast を使うべき唯一のケースだと思っています。

さて、「キャストの復習」と題して、static_cast、reinterpret_cast によるポインタの変換について、C++11 での変更を含めてお送りしました。今後 C++ コミュニティにおいて reinterpret_cast の位置づけがどのようになるのか(一部の異なるポインタ型の変換について正当な手段とされるのか、あくまでも処理系依存や未規定なキャストについてのみ使うべきとされるのか)、興味深いところではあります。

まとめ

C++ Advent Calendar 2012 8日目として、「C++ Compiler Farm の紹介」と「キャストの復習」をお送りしました。C++ Advent Calendar 2012 明日の担当は @Flast_ROさんです。お楽しみに→【にゃははー】