ここではプログラミング関連のTipsやサンプルコードなどを中心にした記事を不定期で掲載していきます。
仕事柄色々な環境/言語を使いますので結構広範囲に渡る話題が提供できると思います。
2002/02/25 XMLパーサの使い方
ApacheのXMLパーサXerces-C++を使ってXMLファイルを処理するための基礎です。
ちなみに、SAXについてのみです。サンプルコードもあります。
2002/02/08 スレッド関連の小技
C++でメンバ関数をスレッドのエントリポイントにする為のテクニックです。
2001/07/12 UDPブロードキャストで同一ポートのモニタ
UDPブロードキャスト/マルチキャストで同一ポートを複数プロセスがモニタする方法です。
2001/07/06 開設以前のあれこれ
開設以前の出来事等について簡単にまとめてあります。
今回は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++からの使用が簡単なものという基準から探したところApacheのXerces-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出来ないと訴えるので修正した次第です。(まあ規格に忠実なのがどちらなのかは知りませんし調べてもいませんが)
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にしてその次に)パラメータ数を格納するようにすればいいでしょう。
以前ブロードキャスト/マルチキャストで「同一端末内で複数のプロセスが同じポートをモニタすることが出来ないので受信用のプロセスを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によるでしょう)
開設以前のことと言っても全て書いていたらかなり長くなってしまうので、適当にピックアップして書きます。
プログラム開発
私はプログラムを作ると言うことはテレビや洗濯機などのHWを作るのとは違い、どちらかというと小説などの文章を記述することに似ていると思います。
小説などの文章では、ある程度万人が読みやすい/理解しやすいという書き方などはありますし、最低限伝えたいことを表現する要素を入れ込む(例えば「リンゴは赤」を表現するなら最低限「リンゴ」と「赤」という単語を入れる必要はある)必要はありますがその表現にはかなり多様性を持たせることが出来ます。
プログラムもこれに似ていて同じ機能を実装するにも千差万別の記述方法が存在しますし、スタイルなどについてもある程度万人にとって理解しやすい構造というのはありますが、細かいところまでどうするのが良いというのは一概には言えない物があります。
HW設計であれば同じ機能なら大体似たような機構なり回路なりになるでしょう(もちろんある程度は多様性があるでしょうがプログラムほどではない)がね。
で、プログラム開発においては仕事としてやる場合大抵は多数の人間が関わるプロジェクトで設計書を作ってインタフェースを決めてコーディングして試験して・・・と段階的にやっていくのが通常の方法ですが、プログラムを小説などと同類に考えるとこれは些かおかしいことですよね。
小説であれば普通は多人数で1つの作品を書くなんてことはありませんし、設計などすることもないでしょう。
普通は書き手が自分の中で作品の大筋を考えて、直接文章を書きながら大筋に肉付けしていって、読み返して更正したり手直ししたりってかんじですね。
プログラムもこれに近い形で作るのが理想的だと思います。
つまり、一人の人間が自分の中でそのプログラムの機能から大まかな構造を定めてそれに肉付けしながらコーディングしていって出来たら動かしながらおかしいところや使い勝手/性能などが悪いところを直していくと。
まあFWなど個人的に作っているようなソフトならほぼこの形でしょうが、仕事であってもこれが理想だと思います。
ただ、仕事でやるには納期が決まっていて更に規模が大きいため一人でこつこつというのは不可能に近いですし、多人数が関わってくるなら設計書なども必要になってきます。
ですが、現在一般的に行われているようなウォーターフローモデル(上流工程から下流工程に段階的に進んでいく方法)は明らかに効率が悪いと思います。
かといってスパイラルモデル(作って評価して作り直してを繰り返しながら徐々に完成品に近づけていく方法)ではスケジュール管理が困難でさらにユーザを巻き込むと要望が限りなく膨らんでいつまでたっても終わらないと言う状況になります。
ではどうすればよいかですが、私はまず外部仕様(ユーザから見たプログラムの機能)を仮決定→それに基づいて1人の人間がスケルトン(ユーザインタフェース部分の大枠と内部的構造の大枠)を作る→それをもとにユーザを巻き込んで外部仕様を本決定→スケルトンを一人で手直し→スケルトンを元に内部仕様を確定→内部的な機能の詳細を複数の人間で(極力機能単位で縦分割が良いでしょう)実装→できあがった物を簡単に試験→ソースを元に設計書を作成(以降のメンテナンス用として)→作成者以外の第三者が外部仕様を元に試験すると言う手順が良いかと思います。
これならば基本的にフレームワークは1人の人間が作るので全体として統制の採れたプログラムになるでしょうし、明確な内部設計を先にやらなくても構造がぐちゃぐちゃになりにくくまた出戻り(下流工程で上流工程での決定に不都合が起きて再度上流工程に戻る)も臨機応変に対応できます。
ただ、この欠点はスケルトンを作る人間に高い能力が求められることですね。
でも、最近はGUIはほとんどお絵かきレベルでスケルトンを作れますからまあスーパーマンじゃなくてもきちんと自分の頭の中で全体構造を把握しながらプログラミングできる人ならそれほど問題ないかと思います。
秀逸なフリーウェアを個人で作れるような人なら全く問題ないでしょう。
ちなみにスケルトンの作成は特にですが、可読性の高いコードを書くことはかなり重要です。
可読性を常に意識していればスパゲッティーにもなりにくいですし。
VxWorks(リアルタイム環境)
以前仕事でとある試験装置に使うボードで動かす為のプログラムを作ることになり、これはVxWorksというUNIX似のリアルタイム環境(一応POSIX準拠)で開発することになりました。
基本的には普通にUNIXのプログラムをCで作るのと同じなんですが、大きく異なるところはリアルタイムなのでOSが存在しないと言うことです。
まあ一応HWをサポートするカーネルがあって、ユーザプログラムはカーネルと一体化して動くという感じです。
さらにプロセスという概念がなく、タスクは全てスレッドですからデータ交換はグローバル変数などを使って容易に出来ますし、タスクの動作順序などもコントロールしようと思えば簡単にコントロールできます。
まあこの辺の特徴は複数人で開発しているとシンボル名がバッティングして思わぬトラブルになる可能性があるとか(コーティングと担当分担のやり方で充分回避できますがモジュールを流用するような場合は結構注意が必要)タスクの実行順序制御を誤るとデッドロックするとか欠点にもなり得ますがね。
こつとしてはこういった特徴をきちんと把握して、それにあったプログラミングをすると言うことでしょうね。
自端末のIPアドレス取得
意外とこれって難しいんですね。
getsockname()で取得する場合、TCPでlocalhost以外のホストと繋がっている状況でないときちんと取得できないみたいですし、そのものズバリの関数なんかはないですし。
で、他の方法としてはgethostname()で自端末のホスト名を所得してそこからgethostbyname()で取得するって方法です。
でもこれにも問題があります。
まず、自端末に複数のNICがあったら取得したIPアドレスがそのまま使えるかどうかは解りませんし、マルチプラットフォームを考えるとgethostname()はSystemV系のUNIXではサポートされていない可能性があります。
SytemVUNIXの方はuname()で代替できますが、複数のNICではやはりなかなか難しいと言わざるを得ませんね。
特に、ブロードキャストメッセージを受信したときに自分自身が出した物を捨てるというような処理の時は要注意です。
このような場合IPアドレスを使うより各プロセスで一意のIDを割り当ててそれでフィルタした方が良いかもしれませんね。
複数のプログラムにメッセージを渡す場合
この場合ブロードキャスト/マルチキャストを使うのが一般的でしょうが、一つ問題があります。
1端末に1つしかプログラムが動かない状況なら(まあデーモン系ならこうなるので良いんですがね)問題ないんですが、そうでない場合単純に作ってしまうと同一ポートを同一端末上の複数のプロセスがオープンすることになってしまってあとから立ち上がったプログラムはbind()でエラーになってしまいます。
私も色々考えたり試したりしたんですが、結局こういう場合各端末でブロードキャストを受信するためのプロセスを1つ立ち上げて、そのプロセスからIPC等でデータを必要なプロセスに分配するしかないようです。
面倒ですね。
ちなみにマルチキャストですが、PC-98シリーズのWindowsNT4.0はマルチキャスト使えないみたいです。(多分他のWindowsでも同じだと思う)
もしかしたらNICによるのかも知れませんが、同時期のAT互換機はOKですからOS自体が対応していないような気がします。
参考:マルチキャストを受信するには以下のようにしてマルチキャストメンバに登録してから普通にrecvfrom()すればOKです。
ip_mreq imr;
imr.imr_multiaddr.s_addr = inet_addr("225.0.0.1");
imr.imr_interface.s_addr = INADDR_ANY;
int sts = setsockopt(sfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,(const char*)&imr,sizeof(imr));
送信は普通にマルチキャストアドレス(上記の例では225.0.0.1)へsendto()すればOKです。