HeavyBommer

プログラミング関連記事

概要

 ここではプログラミング関連のTipsやサンプルコードなどを中心にした記事を不定期で掲載していきます。
仕事柄色々な環境/言語を使いますので結構広範囲に渡る話題が提供できると思います。

記事一覧



 



2002/02/25 XMLパーサの使い方

 今回はXMLパーサの使い方についてです。
まず簡単に背景などを説明します。
 XMLとはHTMLに似た感じでタグを使って文書を構造化するためのテキストフォーマットです。
HTMLと根本的に異なるのはHTMLが「見栄え」に関するタグを有していてタグは固定されているのに対し、XMLは単に文書を「構造化」するだけが目的で、タグは勝手に定めて良いと言うことになっています。
詳しくは検索サイトなんかで検索していただければ懇切丁寧に解説されているサイトがあるのでそう言ったサイトを見て貰うとして、このXMLの特徴はデータベース的な用法として想定されていると言うことだけ頭に入れてください。
タグで囲ったりしてツリー構造を表現できますのでほとんどのデータファイルはこれで置き換えることが可能です。
こういった背景から最近はデータファイルとしてXMLフォーマットを扱うプログラムは増えてきています。
私も仕事でXMLフォーマットのデータファイルを扱う必要が出てきたのでXMLパーサを調査しました。
 ここで新たに出てきた“XMLパーサ”ですが、これはXMLを処理するためのライブラリと考えてOKです。
このパーサは色々ありますが、パーサを制御するためのAPI仕様としてDOMとSAXという二種類の仕様が主に使われています。
DOMはW3Cが標準として定めているもので一旦XMLファイルをメモリ上に読み込んで、ツリー構造を生成するのでランダムアクセスに向いています。
それからXMLファイルの読込だけでなく書き込みや修正などにも対応しています。
それに対し、SAXはイベント駆動式のAPIで、XMLファイルを解析しながら適宜コールバックルーチンを呼び出すという形態です。
DOMと違って一度にメモリへ展開する訳ではないのでランダムアクセスできませんがシーケンシャルな処理で十分な場合とか必要なデータが全体からの割合として少ない場合などは効率的です。
もちろん先に読み込んで解析するわけではないので速度的にも有利ですね。
こちらは標準化はされていませんが、デファクトスタンダードとなっているので事実上の標準と見なしても良いでしょう。
 で、両者のどちらを選択すべきかという話ですが、XML自体をデータベース的に使用するようなプログラムとかXMLファイルの書き込み機能が必要なプログラムではDOM、読込のみでシーケンシャルな処理で十分なプログラムや既に内部的に使用するデータ構造が出来上がっていてXMLを読み込んだ結果をそう言った内部データに変換するだけで良い場合(既存プログラムを改良してXMLに対応させる場合など)などはSAXが向いていると言えるでしょう。
と言うわけで、今回は後者にあたるのでSAXを選択しました。
 あとはパーサの選定なんですが、フリーであってUNIXでもWindowsでも問題なく動き、C++からの使用が簡単なものという基準から探したところApacheXerces-C++がもっとも要求に近かったのでこれを選定しました。
 ちなみに、XMLパーサはJAVAからの使用が多いようでC++用のライブラリは少なく、更にC++から使用する場合の情報などは皆無に近い(英語の情報ならあるんでしょうがね)ので、結構C++ユーザにとっては敷居が高いのですが、Xercesで基本的なことをやるだけなら思った以上に簡単でした。
詳しいことはダウンロードページにサンプルコードを置いたのでそれを見てください。
多分このサンプルを見るだけで大体使い方は解ると思いますが、基本的にはサンプルのCHandlerのようにHandlerBaseを継承したハンドラクラスを作って、HandlerBaseで純粋仮想関数として定義されている各ハンドラモジュールを実装してあげれば良いだけです。
あとは初期化やエラー処理程度ですね。
中身としてやることは、基本的にStartElement()/EndElement()でタグの開始/終了時に必要な処理を行う程度で一般的なシステムでは問題なくデータを読み込むことが出来るでしょう。
 ちなみに、サンプルコードはXercesにくっついてくるサンプルのSAXcountを参考に作りました。
添付のサンプルも良いのですが、コメントがほとんどないですし(もちろんあっても英語ですし)なんか私的にはコードも解りづらいのでちょっと苦労しましたが、まあ基本は簡単です。
あとはドキュメントをざっと見て(もちろん英語ですがそれほど複雑な機能があるわけでもないので辞書があればそんなに苦労はしません。WindowsAPIのリファレンスよりは大分読みやすいですね)クラスの関係などを理解しておけば色々応用も利くでしょう。

2002/03/04追記 サンプルコードに一部間違いがあったので修正しておきました。
修正点はXMLString::transcode()で変換した文字列領域を解放するdeleteを配列型に変えたのとpWk(前記関数の戻り値取得用の変数)をconst char*からchar*に変更したという2点です。
前者は単純に私のミスなんですが、後者はSolarisのCCコンパイラでは問題なかったのでわざわざconst修飾したんですがVC++6.0のコンパイラ(cl.exe)ではconst char*はdelete出来ないと訴えるので修正した次第です。(まあ規格に忠実なのがどちらなのかは知りませんし調べてもいませんが)



2002/02/08 スレッド関連の小技

 WindowsでもUNIXでもスレッド関連は複雑で解りづらい所でしょう。
そのなかでもC++では「メンバ関数がスレッドのエントリポイントとして使えない」のは結構痛いところです。
ですがちょっと小細工をすればこの辺は解決可能です。
 まず、スレッドのエントリポイントはstaticメンバ関数にすることです。
以下のように宣言します。


class CTest
{
public:
    CTest();
    virtual ~CTest();
    static void ThreadEntry(void* pParam);  // ←ここです!
    void NormalFunc();
};

こうするとThreadEntry関数はthisコール(自身のクラスオブジェクトポインタであるthisポインタを暗黙的に受領する)ような形態ではなくなりますので、オブジェクトを構築しなくてもコールできるようになります。
つまり以下のようなコールが可能になるわけです。


void func()
{
    CTest cTest;

    cTest.NormalFunc();         // これはごく一般的なメンバ関数コールです
    CTest::ThreadEntry(this);   // ←ここです!
    cTest.ThreadEntry(this);    // もちろんこのようにコールすることも出来ます
    CTest::NormalFunc();        // ちなみにこれはコンパイルエラーになります
}

Windowsの場合はこれだけでスレッドのエントリポイントとしてbiginthreadとかに渡すことが可能です。
 ですが、UNIXの場合pthread_create(POSIX準拠のスレッドライブラリでスレッドを構築する関数)は型として「extern "C" void*(*)(void*)」を要求します。
つまり、C言語形式で型宣言された関数しか受け付けない(まあワーニングが出るだけで実際には直接staticメンバ関数を渡しても動くでしょうが)のです。
この辺は非常に面倒です。
なぜかというと、グローバル関数ならextern "C"を付けてあげることでC言語形式にリンケージ指定することが可能なのですが、メンバ関数にはextern "C"が使えないからです。
もちろんextern "C"は宣言などにしか適用できないので、単純にキャストで回避することも不可能です。
 しかし、こちらの問題にも回避策はあります。
それはtypedefを使うことです。
以下のようにextern "C"付きでtypedefするとそのtypedefされたシンボルはC言語リンケージ指定までを含みます。


extern "C"
{
    typedef void* (*ThreadFunc_t)(void*);
}
      ・
      ・
      ・
    sts = pthread_create(&tid,NULL,(ThreadFunc_t)CTest::ThreadEntry,this);

これはThreadFunc_tという型名に「extern "C" void*(*)(void*)」(戻り値がvoid*で引数がvoid*一つのCリンケージ指定関数へのポインタ)という意味を持たせます。
ですので、最後の行のようにstaticメンバ関数をこの型名でキャストして渡してあげればワーニングも出ることなく、スレッドのエントリポイントにメンバ関数が使えます。

 これで一件落着!と言いたいところですが、まだ肝心の部分が残っていますね。
これだけではグローバル関数をクラスの中に入れただけに過ぎません。
結局、メンバ関数の利点はthisポインタをつかってメンバ変数にアクセスできるところにあるわけですから、メンバ変数にアクセスできないんじゃ意味がありません。
そこで、staticメンバ変数はあくまでスレッドのエントリポイントに特化してしまいます。
で、実際にスレッドの実効処理を行うのは別のメンバ関数に任せるのです。
そのためには(既に上のサンプルでもやっているように)エントリポイントにthisポインタを渡してあげれば良いのです。
つまり以下のような感じになります。


void CTest::ThreadEntry(void* pParam)
{
    CTest* pTest = (CTest*)pParam;
    pTest->NormalFunc();
}

これはスレッドがメモリ領域を共有している為に出来る技で、マルチプロセスでは当然出来ません。
また、一つのオブジェクト内で複数のスレッドが同時に走ることになりますから資源(主にメンバ変数)がスレッドセーフとなるように排他制御しないと思わぬ結果になります。
スレッド間の排他制御はWindowsならCreticalSection、UNIXならMutexが最も簡単です。
きちんとメンバ変数が外部から隠蔽され、アクセス手段がメンバ関数のコールのみとなっていればこれはそれほど面倒なことではありません。
 ちなみに、スレッドに複数のパラメータを渡したいときはthisを含んだポインタ配列を作って、その先頭アドレスを渡してあげれば大丈夫です。


    void*   paParam[3];
    int     a;
    char    str[10];
      ・
      ・
      ・
    paParam[0] = (void*)this;
    paParam[1] = (void*)a;  // ※sizeof(int)<=sizeof(void*)でないとダメですよ
    paParam[2] = (void*)str;
    sts = pthread_create(&tid,NULL,(ThreadFunc_t)CTest::ThreadEntry,paParam);
      ・
      ・
      ・
void CTest::ThreadEntry(void* pParam)
{
    void**  paParam = (void**)pParam;
    CTest*  pTest = (CTest*)paParam[0];
    int     a = (int)paParam[1];
    char*   str = (char*)paParam[2];
    pTest->NormalFunc(a,str);
}

この例ではパラメータの数を固定にしていますが、可変になってしまうようなら配列の先頭(もしくは先頭はthisにしてその次に)パラメータ数を格納するようにすればいいでしょう。



2001/07/12 UDPブロードキャストで同一ポートのモニタ

 以前ブロードキャスト/マルチキャストで「同一端末内で複数のプロセスが同じポートをモニタすることが出来ないので受信用のプロセスを1端末につき1つだけ立ち上げてIPC等で各プロセスに配布しないとダメ」と書きましたが、ソケットオプションの設定で回避する方法がありました。
具体的には以下のようにsetsockopt()でSO_REUSEADDRを設定すればOKです。


// ソケット生成
iSock = socket(PF_INET,SOCK_DGRAM,0);
// Nagleアルゴリズムを無効に設定
iWk = (int)true;
iSts = ::setsockopt(iSock,IPPROTO_TCP,TCP_NODELAY,(const char *)&iWk,sizeof(iWk));
// 同一ポートをbind出来るように設定
iWk = (int)true;
iSts = ::setsockopt(iSock,SOL_SOCKET,SO_REUSEADDR,(const char *)&iWk,sizeof(iWk));
// bind実行
iSts = ::bind(iSock,(const sockaddr*)&sSockAddr,sizeof(sSockAddr));

 ちなみにTCPでこれを設定するとTIME_WAIT状態のポートにバインドすることが出来るようになります。(Windowsでは元々大丈夫なのでこれをやる必要があるのはUNIX系です)
これをやると回線上に万が一送信元送信先ともに全く同じパケットが残っている場合に障害が起きる可能性がありますが、まず起こり得ないので特に問題は無いと思います。
 それから、上記の例でNagleアルゴリズム云々とありますがこれはWindows独特のもので、デフォルトだと「小さいパケットはそのまま出さずに一定期間溜めてある程度大きなまとまりにしてから送信する」という機能となっているため、それをキャンセルする設定です。
 注釈:Nagleアルゴリズムについてですが、別にWindows独自の物というわけではないようです。
UNIXでこのオプションを設定しても大丈夫です。
ただし、こういったアルゴリズムが実装されているかどうかやこの指定でキャンセルできるかどうかは解りません。(OSによるでしょう)



2001/07/06 開設以前のあれこれ

 開設以前のことと言っても全て書いていたらかなり長くなってしまうので、適当にピックアップして書きます。

inserted by FC2 system