[ C++で開発 ]

ACEプログラミング(その1)

クロスプラットフォームなC++プログラミングを実現するフレームワークがACEです。

まず最初に

最初のプログラムとして、例によってHello Worldを作成します。ACEはGUIについては対象外なので、コンソールプログラムで作成します。

Hello, World

ACEを用いる場合、Cの標準関数(<stdio.h>など)ですらラッピングされているので、printfのラッピングであるACE_OS::printfを呼びます。

// hello_ace.cpp
#include <ace/OS.h>

int main(int argc, char* argv[])
{
    ACE_OS::printf("Hello, ACE World!\n");
    
    return 0;
}

main関数の書式

ACEでは、main関数を裏で置き換えてライブラリの初期化等を行っています。そこで、mainを書くときにいくつか制約があります。次の3つのどれかの書式で書かなくてはなりません。引数の省略や型の違い(第2引数をchar**で受ける等)はNGです。

  1. int main(int argc, char *argv[])
  2. int wmain(int argc, wchar_t *argv[])
  3. int ACE_TMAIN(int argc, ACE_TCHAR *argv[])

2.の書式は今のところWin32のみ提供されています。したがって3.の書式の方が移植性が高いと云えます。

// hello_ace.cpp
#include <ace/OS.h>

int ACE_MAIN(int argc, ACE_TCHAR* argv[])
{
    ACE_OS::printf("Hello, ACE World!\n");
    
    return 0;
}

コンパイル(Linux)

ACEが以下のディレクトリにインストールされている場合

/usr/local/ace
            +--- ace
            |      +--- *.h *.inl
            +--- bin
            +--- lib
                   +--- lib*.so

インクルードパスとライブラリパスを適切に指定すればOKです。

work$ g++ -I/usr/local/ace -L/usr/local/ace/lib -lACE hello_ace.cpp
work$ ./a.out
Hello, ACE World!
work$

コンパイル(VC++)

ACEが以下のディレクトリにインストールされている場合

D:\lib\ace
        +--- ace
        |      +--- *.h *.inl
        +--- bin
        |      +--- *.dll
        +--- lib
               +--- *.lib

インクルードパスとライブラリパスを適切に指定すればOKです。

ACEのDLLを使用
C:\work>cl /DWIN32 /Id:\lib\ace /GX /MD hello_ace.cpp
  /link /LIBPATH:d:\lib\ace\lib ace.lib
    :
C:\work>hello_ace 
Hello, ACE World!
C:\work>
ACEのスタティックリンクライブラリを使用
C:\work>cl /DWIN32 DACE_AS_STATIC_LIBS /Id:\lib\ace /GX /MD hello_ace.cpp
  /link /LIBPATH:d:\lib\ace\lib advapi32.lib aces.lib
    :
C:\work>hello_ace 
Hello, ACE World!
C:\work>

スタティック版でビルドする際は、aces.libの中から呼び出しているシステム・ライブラリをリンク時に指定しなくてはならないようです。また、aces.libをビルドする時にデフォルトでランタイムライブラリはDLLを指定しているので、/MDを指定する必要があります。

MPCによるプロジェクト構築

複数のソースファイルからプログラムを構成するときは、makeやVisual Studio等の統合開発環境といったビルドツールを使用し、Makefileや各統合開発環境用のプロジェクト設定を記述します。

いくつものACE+TAOアプリケーション・プログラムを作っていく際、それぞれ毎に1からプロジェクト設定を記述するのはとても面倒です。特にVisual Studioの場合、プロジェクト1個1個にリンクするライブラリ名やプリプロセッサ定義をGUI上で入力する必要があり、かなりの手間となります。

そこで、ACE+TAOでは、MPC(Makefile, Project, and Workspace Creator)と呼ばれるプロジェクト設定作成ツールを使うと便利です。なお、MPCの詳細は合わせて「MPCの使い方」ページを参照ください。

MPC使用時の環境設定

MPCの設定では、ACEのインクルードファイル、ライブラリファイルの存在するディレクトリを、環境変数ACE_ROOTで示す必要があります。

環境変数名 設定内容(UNIX) 設定内容(Windows)
ACE_ROOT
/usr/local/ace
D:\lib\ace

Hello, Worldプロジェクト設定

まず、以下のMPCのプロジェクト設定ファイルを記述します。

以下は、ACEライブラリをリンクして1つの実行ファイルを構築するプロジェクト設定ファイルです。

hello_ace.mpc
project(hello_ace) : aceexe {
    exename = hello_ace
    Source_Files {
        hello_ace.cpp
    }
}

次に、ビルドツール固有のプロジェクト設定ファイルをMPCツールを使って生成します。

UNIX系

work$ mpc.pl -type make hello_ace.mpc
    :
work$ 
Makefile.hello_aceが生成されます。

VC++ 7.1

work$ mpc.pl -type vc71 hello_ace.mpc
    :
work$ 

VC++ 8.0

work$ mpc.pl -type vc8 hello_ace.mpc
    :
work$ 
hello_ace.vcprojが生成されます。

その他のMPCプロジェクト記述例

共有ライブラリ・ファイル構築プロジェクト

以下は、ACEライブラリをリンクして1つの共有ライブラリ・ファイルを構築するプロジェクト設定ファイルです。

hello_ace_lib.mpc
project(hello_ace_lib) : acelib {
    sharedname = hello_ace
    libout = .
    Source_Files {
        hello_ace_lib.cpp
    }
    Header_Files {
        hello_ace_lib.h
    }
}

liboutを指定しないと、デフォルトの場所$(ACE_ROOT)/libに出力されてしまいます。

ロギング(デバッグプリント)

ACEでは、ロギングのためのフレームワークが用意されています。

#include ace/Log_Msg.h

ロギング用マクロ

ace/Log_Msg.hをインクルードすると、以下のロギング用マクロが利用できます。ログ出力は標準エラー出力に行われます。

ロギング用マクロ定義 内容 op_status コンパイル制御
ACE_ASSERT(test)
assert()とほぼ同様。testが偽ならば、メッセージとファイル名・行番号を出力しアボートする。
ACE_NDEBUG
ACE_HEX_DUMP(
  (level, buffer, size [,text]))
bufferの内容を16進文字列で出力する。textが指定されていたらダンプの直前に表示する。 0
ACE_NOLOGGING
ACE_RETURN(value)
関数からリターンする。 value
ACE_ERROR_RETURN(
  (level, string, ...), value)
stringをlevelで指定したログレベルで記録し、関数からリターンする。 value
ACE_ERROR(
  (level, string, ...))
stringをlevelで指定したログレベルで記録する。 -1
ACE_DEBUG(
  (level, string, ...))
stringをlevelで指定したログレベルで記録する。 0
ACE_ERROR_INIT(value, flags)
ログ機構のオプションフラグをflagsに設定する value
ACE_ERROR_BREAK(
  (level, string, ...))
breakに続いてACE_ERRORを呼び出す。
使用例)for/while文の中でエラーメッセージを表示し抜ける時
ACE_TRACE(string)
ファイル名、行番号、stringを表示する。また、このACE_TRACEを含むブロックから出る時にLeaving 'string'と表示する。
ACE_NTRACE

通常、ACE_NTRACEマクロは1(TRACE出力抑制)になっているので、ACE_TRACEを有効にするには、コンパイル時にACE_NTRACE=0を定義する必要があります。

ACE_DEBUG

ACE_DEBUG()マクロを使用してデバッグプリントを行うことができます。

ACE_DEBUG((LM_DEBUG, "recvBuffer[n-1]=%x\n", recvBuffer[n-1]));

マクロの第一引数はデバッグレベルを示す定数です。以下のように規定されています。

ace/Log_Priority.hで定義されているレベル
シンボル 内容
LM_SHUTDOWN 1
LM_TRACE 2 関数呼び出しのトレースに使用
LM_DEBUG 4 デバッグ時に使用する場合
LM_INFO 8
LM_NOTICE 16 エラーではないが特別な扱いが必要
LM_WARNING 32 警告メッセージ
LM_STARTUP 64 ロギングの初期化
LM_ERROR 128 エラーメッセージ
LM_CRITICAL 256 致命的な状況(ハード障害など)
LM_ALERT 512 至急対処が必要な状況
LM_EMERGENCY 1024 パニック状況。
LM_MAX 1024

マクロの第二引数は、printf書式指定と同様の文字列を指定します。書式指定はロギングに便利な機能がいろいろ揃えられています。

書式 内容 書式 内容
a - プリント後プログラムをアボート R int 10進数
c char 単一文字 S int シグナルの名前
C char* 文字列 s ACE_TCHAR* 文字列、ワイド文字列を含む
i, d int 数値 T - 現在時刻 hh:mm:ss.uuuuuu
I - インデント D - 現在日時
mm/dd/yy hh:mm:ss.uuuuuuuu
e,E,f,F,g,G double 倍精度浮動小数 t - スレッドID
l - 行番号 u int 符号無し10進数
M - メッセージのデバッグレベル w wchar_t 単一ワイド文字
m - errnoに対応するメッセージ W wchar_t* ワイド文字列
N - マクロが書かれたファイル名 x, X int 16進数
n - プログラム名(初期化時に指定要) @ void* 16進でポインタ値
o int 8進数 % N/A '%'
P - プロセスID
p ACE_TCHAR* perrorのようにerrnoに対応した文字列
Q ACE_UINT64 10進数
r void (*)()

コンパイル時に、ACE_NLOGGINGを定義すると、デバッグプリントはすべて抑制されます。

デバッグプリントのレベルを制御するには、ACE_Log_Msgクラスのpriorityメンバ関数を使用します。

  u_long priority_mask = ACE_LOG_MSG->priority_mask(ACE_Log_Msg::PROCESS);
  ACE_CLR_BITS(priority_mask, LM_DEBUG | LM_INFO);
  ACE_LOG_MSG->priority_mask(priority_mask, ACE_Log_Msg::PROCESS);

とすれば、以降LM_DEBUGとLM_INFOを指定したACE_DEBUG等の出力が抑制されます。

ロギングの初期化

int main(int argc, char* argv[]) {
    ACE_LOG_MSG->open(argv[0], ACE_Log_Msg::STDERR);

初期化としてプログラム名とロギングの出力先を設定しています。初期化を呼ばなくてもデフォルトでロギングの出力先が標準エラー出力になりますが、プログラム名は設定されません。

ロギングに関するTips集

タイムスタンプを出力したい

デフォルトのロギングではメッセージにタイムスタンプが付きません。以下の設定で、各ログにタイムスタンプを付与することができます。

    ACE_LOG_MSG->set_flags(ACE_Log_Msg::VERBOSE_LITE);

 VERBOSE_LITEの場合、タイムスタンプとログ優先度が付与されます。以下に例を示します。

Jan 13 22:00:51.682 2008@LM_DEBUG@Using TCP Server port 9999

 VERBOSEにすると、VERBOSE_LITEに加えてさらにホスト名、プロセスIDが追加されます。

ログをファイルに出力したい

デフォルトのロギングは標準エラー出力に吐き出されるので、標準エラー出力をリダイレクトすればファイルに落とすことと同じです。

しかし、画面とファイル、またさらにsyslogなどに出力したいときなどの設定ができます。

ToDo:

コマンドラインの解析

ACE_Get_Optクラスを使うと、コマンドラインオプションの解析が簡潔に記述できます。

クラス名 ACE_Get_Opt
#include ace/Get_Opt.h

簡単な例を以下に述べます。

      書式:sample.exe [-c count] [-n name] [-x]
#include <ace/Get_Opt.h>

int main(int argc, char *argv[])
{
    ACE_Get_Opt get_opts(argc, argv, ACE_TEXT("c:n:x"));
    int opt;
    int count;
    char *name;
    bool isXMode = false;

    while ( (opt=get_opts()) != -1 ) {
       switch (opt) {
       case 'c':
            count = ACE_OS::atoi(get_opts.opt_arg());
            break;
       case 'n':
            name = get_opts.optarg;
            break;
        case 'x':
            isXMode = true;
            break;
        default:
            print_usage();
            return -1;
        }
    }

    :

UDPマルチキャスト送受信

UDPマルチキャストによるネットワーク通信のプログラム・サンプルを作成します。

プロジェクト構成とMPC設定

multicast
   +--- multicast.mwc
   +--- mcast_send
   |       +--- mcast_send.mpc
   |       +--- mcast_send.cpp
   +--- mcast_recv
           +--- mcast_recv.mpc
           +--- mcast_recv.cpp

MPCワークスペース設定(multicast.mwc)

workspace {
        mcast_recv
        mcast_send
}

今回は、2つのプロジェクト(mcast_send, mcast_recv)の2つを束ねるだけのワークスペースなので、上記のような記述になります。

MPCプロジェクト設定(mcast_recv.mpc)

project : aceexe {
}

ACEライブラリを使用する実行ファイルを生成するプロジェクトなので、aceexeプロジェクト設定を継承する記述をしています。それ以外はデフォルトの設定となります。

MPCプロジェクト設定(mcast_send.mpc)

project : aceexe {
}

ACEライブラリを使用する実行ファイルを生成するプロジェクトなので、aceexeプロジェクト設定を継承する記述をしています。それ以外はデフォルトの設定となります。

MPCワークスペースからビルドツール用設定ファイルの生成

Visual Studio .NET 2003 C++ (Visual C++ 7.1)用
D:\work\multicast> mwc.pl -type vc71 multicast.mwc
Using .../lib/tao/bin/MakeProjectCreator/config/MPC.cfg
Generating 'vc71' output using default input
Generation Time: 0s
D:\work\multicast>

この結果、以下のVisual C++ 7.1用ソリューション・プロジェクトファイルが生成されます。

multicast
   +--- multicast.sln
   +--- mcast_send
   |       +--- mcast_send.vcproj
   +--- mcast_recv
           +--- mcast_recv.vcproj

マルチキャスト受信

注)ACE_TEXTの使用によるワイド文字列対応は使用していません。

mcast_recv.cpp

// -*- C++ -*-
#include <ace/OS.h>
#include <ace/Log_Msg.h>
#include <ace/INET_Addr.h>
#include <ace/SOCK_Dgram_Mcast.h>

int main(int argc, char* argv[])
{
    if (argc < 3) {
        ACE_ERROR_RETURN((LM_ERROR, "引数が足りません\n"
                                    "\t書式:%s <マルチキャストアドレス>"
                                    " <ポート番号>\n"
                                    , argv[0]), -1);
    }
    char* mcastAddrText = argv[1];
    u_short mcastPort = ACE_OS::atoi(argv[2]);
    ACE_DEBUG((LM_DEBUG, "[%s]:%d\n", mcastAddrText, mcastPort));

    ACE_INET_Addr mcastAddr(mcastPort, mcastAddrText);

    ACE_SOCK_Dgram_Mcast mcast;
    if (-1 == mcast.join(mcastAddr)) {
        ACE_ERROR_RETURN((LM_ERROR, "%p\n", "join"), -1);
    }

    char buff[64];
    while (true) {
        ACE_OS::memset(buff, 0, sizeof(buff));
        ssize_t recvLen = mcast.recv(buff, sizeof(buff), mcastAddr);
        if (recvLen > 0) {
            ACE_DEBUG((LM_DEBUG, "Mcast data received (%d bytes)\n", recvLen));
        } else if (recvLen == -1) {
            ACE_ERROR((LM_ERROR, "ACE_SOCK_Dgram %p)\n", "recv error"));
        }
        ACE_OS::sleep(1); // [sec]
    }

    ACE_RETURN(0);    
}

アドレス情報(ACE_INET_Addr)の生成

ACE_INTE_Addrクラスのコンストラクタには、いくつかのバリエーションがあります。今回は、以下のポート番号(u_short型)とホスト名(char*)を指定したコンストラクタでACE_INTET_Addrクラスのインスタンスを生成します。

  ACE_INET_Addr (u_short port_number,
                 const char host_name[],
                 int address_family = AF_UNSPEC);

アドレス・ファミリをAF_UNSPECにしているので、ホスト名の文字列に応じてIPv4またはIPv6のアドレス情報が生成されます。

マルチキャスト・ソケット(DGRAM)の生成

コネクションレスのマルチキャスト用ソケット(ACE_SOCK_Dgram_Mcast)クラスを生成し、次に受信するマルチキャストのアドレス・ポートを設定(join)します。

引数なしでマルチキャスト用ソケットを生成します。

    ACE_SOCK_Dgram_Mcast mcast;

先ほど生成したアドレス情報を引数にjoinメンバ関数を呼び出します。内部では、setsockoptコールでマルチキャスト受信の登録を行っています。

    if (-1 == mcast.join(mcastAddr)) {
        ACE_ERROR_RETURN((LM_ERROR, "%p\n", "join"), -1);
    }

データグラムの受信

マルチキャスト・ソケットからデータ受信を行います。

        ssize_t recvLen = mcast.recv(buff, sizeof(buff), mcastAddr);

Windows版では、受信したデータグラムの大きさが上記のrecv引数に渡すバッファの大きさより大きいときは、エラーとなります。

TCPソケット送受信

TCPによるネットワーク通信のプログラム・サンプルを作成します。

TCPサーバの記述

TCPクライアントからの接続を待ち、接続が完了するとデータを受信するサンプルを以下に示します。サーバの挙動には、同時に1つのクライアントからの接続を処理するサーバ、複数の接続を並行して処理するサーバがあります。複数接続の処理には、マルチスレッド、非同期(select)などの手法があります。

単一クライアントの接続を処理するTCPサーバ

まず簡単にACEのソケットプログラミングを見るために、同時に単一のクライアントからの接続だけを処理するシングルスレッドのTCPサーバを作成します。

TCPサーバではポート番号を指定して待ちうけるので、ACE_INET_Addrオブジェクトをポート番号(またはTCPサービス名文字列)を指定して生成します。コンストラクタに引数でポート番号を渡さず、setメンバ関数でポート番号を指定しているのは、戻り値でエラーを取るためです。エラー処理は、後ほど示すTCPサーバ全体コードに記述しています。

    ACE_INET_Addr portToListen;
    portToListen.set(8181);

TCPクライアントからのコネクション要求を待ち受けるために、ACE_SOCK_Acceptorオブジェクトを待ちうけアドレス(上述)およびポート再利用有無を指定して生成します。

    ACE_SOCK_Acceptor acceptor;
    acceptor.open(portToListen, 1);

実際にTCPクライアントからのコネクション要求があるまでブロックして待ち続けるacceptメンバ関数を呼びます。コネクションが確立後、リード・ライト操作をする対象となるストリームは、ACE_SOCK_Streamオブジェクトとなります。acceptメンバ関数の引数に指定します。

        ACE_SOCK_Stream peer;
        acceptor.accept(peer);

コネクション確立後、ACE_SOCK_Streamオブジェクトのrecvメンバ関数を呼んで、TCPクライアントからのデータを受信します。ここでは記述していませんが、TCPクライアントへ送信するときは、同じACE_SOCK_Streamオブジェクトのsendメンバ関数を呼びます。

        char buffer[4096];
        ssize_t bytesReceived = peer.recv(buffer, sizeof(buffer));

以下に、TCPサーバ(単一クライアント版)のコード全体を示します。

#include <ace/INET_Addr.h>
#include <ace/SOCK_Acceptor.h>
#include <ace/Log_Msg.h>
#include <string>
#include <iostream>

void dieWithError(const std::string& message);

int main(int argc, char* argv[])
{
    ACE_INET_Addr portToListen;
    if (portToListen.set(8181) == -1) {
        dieWithError("ACE_INET_Addr::set failed.");
    }
    ACE_SOCK_Acceptor acceptor;
    if (acceptor.open(portToListen, 1) == -1) {
        dieWithError("ACE_SOCK_Acceptor::open failed.");
    }
    for (;;) {
        ACE_SOCK_Stream peer;
        ACE_DEBUG((LM_DEBUG, ACE_TEXT("waiting for remote connection...\n")));
        if (acceptor.accept(peer) == -1) {
            dieWithError("ACE_SOCK_Acceptor::accept failed.");
        }
        ACE_DEBUG((LM_DEBUG, ACE_TEXT("accepted.\n")));
        char buffer[4096];
        ssize_t bytesReceived;
        while ((bytesReceived = peer.recv(buffer, sizeof(buffer))) != -1) {
            if (bytesReceived == 0) { // peer reset connection
                break;
            }
            ACE_HEX_DUMP((LM_DEBUG, buffer, bytesReceived, ACE_TEXT("TCP received")));
        }
        peer.close();
    }
}

void dieWithError(const std::string& message) {
    ACE_ERROR((LM_ERROR, ACE_TEXT("[Error]%p : "), ACE_TEXT(message)));
    ACE_OS::exit(1);
}

ACE_CDR

ブログの記事参照

ToDo: いずれ記載予定

参考文献

ACEプログラミングに関する情報

書籍

Webサイト