Wt Web Toolkit による C++ Web プログラミング

プーシキン全集 Web コンコーダンス・プログラムの Web インタフェース部分は Wt C++ Web Toolkit(以下 Wt)を使って拵えた。この C++ ライブラリは,Widget(画面部品オブジェクト)ベースで Web 画面を構成しつつ,ビジネスロジックも C++ で書いて行くスタイルを想定したものである。今日はコンコーダンス・プログラムで覚えた Wt のサワリ(奥が深いのでサワリに過ぎない)についてメモを残しておく。

最近では,Web のサーブレットを Java で作成し,ビジネスロジック(昔からある基幹システムは多く C/C++ で書かれている)とは Java JNI や CORBA,RPC,SOAP で接続し,画面周りは JSP ないしは HTML + JavaScript ベースで準備するのが一般的のように思われる。こうすることで,Web デザインとビジネスロジックを分割して,前者を Web デザイナ,後者をプログラマが担当するというふうに,それぞれのプロフェショナルに作業分担を行うのがやりやすい,というメリットがある。私の担当システムでもこのやり方が主流である。

この潮流からすると,Wt のデザインパターンは一見,時代に逆行しているように私には思われた。でも実際にこれでプログラムを書いてみると,確かに HTML ほど画面構築は簡単ではないが,ヘタにデザイナに弄らせるとまずい Web フォームのような基本インタフェースを含め,コンテンツ HTML の枠組みマークアップ部分をシステム屋・プログラマに任せつつ,「見た目」のデザイン自体はスタイルシートとしてデザイナに任せ,説明文など文字コンテンツについては外部 XML の編集という形で運用者・(プログラマでない)プロジェクト要員に分担させることができる,と納得した。Wt は,総じて,極めてよく考えられたプラットフォームであることがわかったのである。いまや Web デザインは HTML ではなく CSS でこそ実現する時代なのである。

既存ソフトウェア資産は C/C++ が多いし,Java プログラマよりも C/C++ プログラマのほうが圧倒的に人材が豊富なので,プロジェクトの運営もしやすいと私は思う。Wt なら C++ であらゆる Web コードが実現でき,比較的高速に動作するし,C/C++ の膨大な既存資産との連携に関しても,Java よりもずっと悩みが少ない。しかも,画面 HTML,JavaScript をまったく書かずに Ajax 非同期通信の高速 Web サーバ・アプリケーションを構築できる。また,システムの作り・ビジネスロジックを完全に隠蔽することができる(どんなスタイルシートを使っているのかすらユーザにはわからないし,リンクすら見えずロボットで画像を根こそぎパクられることも回避できる。システムが生成した画面の HTML から見えるのはシステムが動的に生成・送信した,わけのわからない JavaScript ばかりである)。Gdb シンボリック・デバッガを用いて Web アプリをデバッグできるところも私には魅力である。「何でいまさら Web アプリを C++ で?」という方はぜひお試しいただきたいと思う。

Wt ライブラリのインストールについてはブログ記事 Wt C++ Web Toolkit にしるしたので,そちらを参照。ただし,その記事には FastCGI について記述がなかったので,もし Wt で作成した Web サーバプログラムを FastCGI で動作させたいのであれば,Wt インストールに先立って,FastCGI のデベロッパライブラリと Apache モジュールをシステムに組込んでおかなければならない。これらは FastCGI のサイトのダウンロードページから,それぞれ,fcgi-current.tar.gzmod_fastcgi-current.tar.gz を取得して,インストールする。./configure; gmake; sudo gmake install で導入はしごく簡単である。その上で Wt を make すると,FastCGI 用プログラムを作成するための Wt ライブラリ libwtfcgi.so(Mac OS X の場合は libwtfcgi.dylib)がコンパイルされるはずである。

Wt ベースのコンコーダンス Web アプリは以下のようなものである。ただし,Wt に関係の薄いコンコーダンス・ビジネスロジック部分は省略してある。

/* -*- coding: utf-8; mode: c++; -*-
 * Concordance to A. S. Pushkin's Works
 * $Id: insomnia.txt 222 2014-03-27 11:23:12Z isao $
 * Copyright (C) 2012, isao yasuda
 */
 
#include <Wt/WApplication>
#include <Wt/WContainerWidget>
#include <Wt/WEnvironment>
#include <Wt/WLineEdit>
#include <Wt/WGroupBox>
#include <Wt/WButtonGroup>
#include <Wt/WRadioButton>
#include <Wt/WPushButton>
#include <Wt/WCheckBox>
#include <Wt/WText>
#include <Wt/WBreak>
#include <turglem/lemmatizer.hpp>
#include <turglem/russian/charset_adapters.hpp>
#include <boost/tokenizer.hpp>
#include <boost/locale.hpp>
#include "ShmCorpusWordTree.hpp"
 
using namespace Wt;
using namespace boost::interprocess;
typedef std::pair<std::string, std::string> string_type;
 
/* コンコーダンス用グローバル変数,関数の記述。省略 */
 
// コンコーダンスクラス
class Concordance : public WApplication
{
    WLineEdit*    expEdit;
    WButtonGroup* wbg0;
    WButtonGroup* wbg1;
    WGroupBox*    gbox;
    WCheckBox    *g0, *g1, *g2, *g3, *g4, *g5, *g6, *g7,
        *g8, *g9, *g10, *g11, *g12; // 00-11 + all
    WText* wmesg;
    WText* pmesg;
    WText* cmesg;
    void clearvse();
    void clearother();
    void generate();
    void errorsend(const std::string emsg);
public:
    Concordance(const WEnvironment& env);
};
 
Concordance::Concordance(const WEnvironment& env)
    : WApplication(env)
{
    // メッセージファイル message.xml の指定
    messageResourceBundle().use(appRoot() + "message");
    // タイトル
    setTitle(L"Динамический Конкорданс к сочинениям А. С. Пушкина");
    // スタイルシート
    useStyleSheet("style.css");
    // message.xml から header を出力
    root()->addWidget(new WText(WString::tr("header"), XHTMLText));
    root()->addWidget(new WText(WString::tr("subheader"), XHTMLText));
    root()->addWidget(new WBreak());
 
    // 入力プロンプト(初期値付き))
    root()->addWidget(new WText(L"Выражения ", XHTMLText));
    // expEdit = new WLineEdit(L"^П[ЕЁ]СТР.*", root());
    // 入力エリア: Enter key で submit 
    expEdit = new WLineEdit(root());
    expEdit->setEmptyText
        (L"Вводите Слова или Регулярные выражения, напр.: п[её]стр.*");
    expEdit->setTextSize(70);
    // expEdit->setFocus(); // focus より説明テキスト
    expEdit->enterPressed().connect(this, &Concordance::generate);
    // submit ボタン
    WPushButton *button = new WPushButton(L"поиск", root());
    button->setMargin(10, Left);
    // ジャンル一覧へのリンク(message.xml)
    root()->addWidget(new WText(WString::tr("link"), XHTMLText));
    root()->addWidget(new WBreak());
 
    // 見出語形/出現形選択ラジオボタン
    WGroupBox *gbox0 = new WGroupBox(L"Форма Базы данных", root());
    gbox0->setId("gbx0");
    wbg0 = new WButtonGroup(gbox0);
    WRadioButton *rb0;
    rb0 = new WRadioButton(L"Lemmatized (форма заглавных слов)", gbox0);
    wbg0->addButton(rb0, 0);
    rb0 = new WRadioButton(L"Non Lemmatized (форма в тексте)", gbox0);
    wbg0->addButton(rb0, 1);
    wbg0->setCheckedButton(wbg0->button(0)); // Lemmatized
 
    // 入力を見出語変換(見出語形の場合のみ)
    WGroupBox *gbox1 = new WGroupBox
        (L"Лемматизация входа", root());
    gbox1->setId("gbx1");
    wbg1 = new WButtonGroup(gbox1);
    WRadioButton *rb1;
    rb1 = new WRadioButton(L"ON (только в случае Lemmatized)", gbox1);
    wbg1->addButton(rb1, 0);
    rb1 = new WRadioButton(L"OFF", gbox1);
    wbg1->addButton(rb1, 1);
    wbg1->setCheckedButton(wbg1->button(0)); // YES
 
    // ジャンル選択チェックボックス
    gbox = new WGroupBox(L"Выбор жанров", root());
    gbox->setId("gbx");
    g12 = new WCheckBox(L"Все", gbox);
    g0  = new WCheckBox(L"Стихи", gbox);
    g1  = new WCheckBox(L"Поэмы", gbox);
    g2  = new WCheckBox(L"Сказки", gbox);
    // その他チェックボックス設定は省略
    // ジャンルデフォルト: すべて
    g12->setChecked(true);
    // チェック時イベント処理登録
    g0->checked().connect(this,  &Concordance::clearvse);
    g1->checked().connect(this,  &Concordance::clearvse);
    g2->checked().connect(this,  &Concordance::clearvse);
    // その他チェックボックス設定は省略
    g12->checked().connect(this, &Concordance::clearother);
 
    // ラベル表示,ユーザ入力内容表示エリア
    wmesg = new WText(root());
    pmesg = new WText(root());
    root()->addWidget(new WBreak());
 
    // コンコーダンス出力エリア
    cmesg = new WText(root());
    root()->addWidget(new WBreak());
 
    // フッタを message.xml から出力
    root()->addWidget(new WText(WString::tr("footer"), XHTMLText));
    root()->addWidget(new WBreak());
 
    // 入力内容で submit
    button->clicked().connect(this, &Concordance::generate);
}
 
// エラーメッセージ出力
void Concordance::errorsend(const std::string emsg)
{
    WString s(emsg, UTF8);
    s = "<p class=\"err\">" + s + "</p>";
    cmesg->setTextFormat(XHTMLUnsafeText);
    cmesg->setText(s);
}
 
// オール以外クリア
void Concordance::clearother()
{
    // オール指定がチェックされていたらほかをクリア
    if (g12->isChecked()) {
        g0->setChecked(false); g1->setChecked(false); g2->setChecked(false);
        // その他設定省略
    }
}
 
// オール指定クリア
void Concordance::clearvse()
{
    // オール指定以外がチェックされていたらオール指定をクリア
    if (g0->isChecked())  g12->setChecked(false);
    if (g1->isChecked())  g12->setChecked(false);
    // その他設定省略
}
 
// コンコーダンス生成
void Concordance::generate()
{
    const WEnvironment& env = WApplication::instance()->environment();
    // ユーザ入力単語式: WT::WString を std::string に toUTF() で変換
    std::string ldata = (expEdit->text()).toUTF8(); // 元入力
    std::string userip = "IP: " + env.clientAddress() + " Req: " + ldata;
    logging(userip.c_str(), 0);
 
    // 大文字変換
    std::string udata = boost::locale::to_upper(ldata);
 
    // 実行 Word Tree の選択,Lemmatizer 実行可否判断
    offset_ptr<tree> word_tree; // 実行 Word Tree
    WString opts(ldata, UTF8);
    opts = L" (Ваш вход: " + opts;
    // Lem Tree on なら見出語形ツリー選択
    if (wbg0->checkedId() == 0) {
        word_tree = word_tree_l; // select Lemmatized 
        opts += L"; DB: Lemmatized; Лемматизация входа: ON)";
        // Lemmatizing on なら Lemmatizer で入力を見出語変換する
        if (wbg1->checkedId() == 0) {
            lemmatizer(udata);   // 見出語変換
        }
    } else {
        // off なら出現形ツリー選択
        word_tree = word_tree_a; // select Non Lemmatized
        opts += L"; DB: Non Lemmatized; Лемматизация входа: OFF)";
    }
 
    // ユーザ入力をトークナイザで分解し,式を vector にセット
    std::vector<std::string> wds;
    exp_tokenizer(udata, wds);
    if (wds.empty()) {
        errorsend("No expressions specified.");
        return;
    }
 
    // 対象ジャンルのセット: global int vector にセット
    // - g0 から順にチェックを確認し,true なら push_back
    // - 全選択がチェックされている場合は vector を空にする
    // - ひとつも選択がない場合は全選択となる
    genrev.clear();
    if (g0->isChecked())  genrev.push_back(0);
    if (g1->isChecked())  genrev.push_back(1);
    if (g2->isChecked())  genrev.push_back(2);
    // 中略
    if (g12->isChecked()) genrev.clear();
    if (genrev.empty())
        grall = true;
    else
        grall = false;
 
    // ラベルの出力
    wmesg->setTextFormat(XHTMLText);
    wmesg->setText(L"<div class=\"blk\"></div><br />Выражения: ");
 
    // ユーザ入力内容の表示
    // 変換済ユーザ入力
    WString wdata(udata, UTF8);
    wdata += opts;
    pmesg->setTextFormat(PlainText);
    pmesg->setStyleClass("exp");
    pmesg->setText(wdata);
 
    /* 以下にビジネスロジック cmesg に出力コンコーダンスを格納する。省略 */
 
    // コンコーダンスの出力
    cmesg->setTextFormat(XHTMLUnsafeText);
    cmesg->setText(conc);
}
 
// サービスオブジェクトの生成
WApplication *createApplication(const WEnvironment& env)
{
    return new Concordance(env);
}
 
// main: 各種 Web パラメータとともにサービスオブジェクトを生成する
int main(int argc, char** argv)
{
    /* Web サービスの初期処理を行う。省略 */
    // サービス開始
    return WRun(argc, argv, &createApplication);
}

このように,Concordance クラスとそのクラスメソッドとして,画面,ビジネスロジックを記述して行く。WApplication クラス継承として Concordance クラスを生成し,Wt::WServer::WRun() メソッドでサービスを開始する形態である。

プーシキン・コンコーダンス画面では,テキストボックス,送信ボタン,チェックボックス,ラジオボタンをコンコーダンス・ビジネスロジック動作条件の設定のために用いている。これらの画面要素を Concordance クラスのコンストラクタのなかで Wt Widget オブジェクトとして定義している。 画面の「地」は,WApplication クラスのメソッドである root() によってオブジェクトのポインタが返されるようになっていて,これに紐付けて各種 Widget を new 演算子で生成して行く。

上例では,チェックボックスやラジオボタンを WGroupBoxWButtomGroup といった,ボタンやチェックボックスを纏める Widget のなかに入れており,この場合は root() にバインドされたこれらグループに紐付けてボタン等の Widget を生成する。 root() に付随するすべての画面オブジェクトは,プログラムが終了する時点で再帰的に解放されるので,プログラマがポインタを管理し delete を発行する必要はない。提供 Widget,インクルードすべきヘッダ等,その使い方の詳細は Wt ドキュメントWidget Gallery デモプログラムを参照のこと。

テキストデータは Wt::WString クラスオブジェクトとして扱う必要がある。Wt::WString(char* value, UTF8) のように初期化すれば,std::stringchar* の UTF-8 文字列からも,Wt::WString オブジェクトが得られる。UTF8 という指定は,UTF-8 エンコードデータとして Wt::WString に格納する旨の指示である。直接 UTF-8 文字列を格納したい場合は Wt::WString(L"Выражения") のようにワイド文字定数として書けばよい。ソースコード中の文字列のエンコーディングの扱いはコンパイラ依存ということになっているが,最近の GNU C/C++ Version 4 では UTF-8 として扱うはずである。もしそうでないコンパイラを使っていたり,移植性を重視するなら,\u十六進数 記法によってユニバーサル文字定数を書けばよい。

useStyleSheet("スタイルシートファイル名") でスタイルシートの指定ができる。Widget オブジェクトに対し,setClass()setId() メソッドで class や id の HTML 要素を付加でき,「見た目」のコントールを,指定したスタイルシートで定義することが可能である。

上例の messageResourceBundle().use(appRoot() + "message"); 行は,外部 XML ファイルリソースの指定である。この場合 message.xml という XML ファイル(名称は任意)を指定している。このファイルに,

<?xml version="1.0" encoding="UTF-8" ?>
<!-- 
message.xml: コンコーダンス用メッセージファイル
-->
<messages>
  <message id="header">
    <div id="header">
      <h1>Конкорданс к тексту А. С. Пушкина</h1>
    </div>
  </message>
  <!-- 後略 -->
</messages>

と記述しておくと,Wt::WString::tr("header") によって,message.xml ファイルの header という ID 属性をもつ message タグのテキストが Wt::WString オブジェクトとして与えられ,これを Web 画面に挿入することができる。こうして,画面のテキストコンテンツを別ファイルで管理でき,文言修正のためにプログラム改修しなければならない不都合を回避できる。これらスタイルシートや XML はプログラム動作中でも,修正がすぐ反映される。また,画面ソースなどを通してユーザから見えることはないので,コンテンツのその他の文言や画像リンクなどをユーザから隠蔽することが可能である。ただし,message.xml ファイルに XML / XHTML 文法エラーがあるとページレイアウトが破壊されるので,validation 機能のある XML エディタ(Emacs など)で確認した上で適用したほうがよい。

Wt ではアプリケーションを動作させる環境として二種類をサポートしている。ひとつは Wthttpd という独自のスタンドアロン Web Socket サーバとして,いまひとつは Apache2 等の Web サーバから起動する FastCGI のアプリケーションとして動作させることができる。この際,ソースコードはまったく同じであり,これらの違いはリンケージするとき,libwthttplibwtfcgi のどちらを使うかに依存する。プーシキン・コンコーダンスの場合は FastCGI で動作させているので,コンパイル/リンクは以下のとおりである。

% g++ -g -I/usr/local/include -L/usr/local/lib \
  -boost_random -lboost_signals -lboost_system -lboost_thread \
  -boost_filesystem -lboost_program_opions -lboost_date_time \
  -lboost_regex -lboost_locale \
  -licudata -licui18n -licuuc \
  -lwtfcgi -lwt \
  -o concordance concordance.cpp \
  -lturglem -lturglem-russian -lMAFSA

-lwtfcgi -lwt のライブラリ・リンケージ指定が FastCGI 運用の条件である。Boost の各種ライブラリも Wt に必要である(すべてというわけではない)。ICU Unicode ライブラリはコンコーダンスのロシア語正規表現で使うもの。最後の三つのライブラリは Lemmatizer である。

FastCGI を Apache から使えるようにするには,Apache の httpd.conf に,モジュールを追加するだけでよい。モジュールのパスはインストールしたところを指すようにする。

LoadModule fastcgi_module  libexec/apache22/mod_fastcgi.so

Wt Web アプリを FastCGI として動かすには,以下のような設定を外部ファイル(fastcgi.conf)に記述し,Apache が読込む構成ファイルディレクトリに格納する。Alias の指定は必須ではないが,サイトのドキュメントルートからアクセスできるマッピングをしておかなくては,実質利用できない。

<IfModule mod_fastcgi.c>
    Alias /pushkin/lemmatized/ /home/isao/src/pushkin/concordance/
    FastCgiConfig -idle-timeout 600 -maxClassProcesses 20 -maxProcesses 60 \
        -killInterval 1200 -autoUpdate \
        -initial-env CONCORDANCE_CONFIG=beatrice.conf \
        -initial-env WT_AP_ROOT=/etc/wt
    FastCgiServer /home/isao/src/pushkin/concordance/concordance
</IfModule>

上のコンコーダンス・プログラム例でいえば,/home/isao/src/pushkin/concordance/ ディレクトリ(アプリケーションディレクトリ)下にロードモジュール,messeage.xmlstyle.css などのリソースを格納しておく。あと,Wt Widget がデフォルトで参照するスタイルシートや画像ファイルが Wt パッケージの resources ディレクトリにあるので,これをアプリケーションディレクトリ内にシンボリックリンクしておくとよい。

FastCgiConfig 行は必要に応じて指定すればよい。-initial-env パラメータで,アプリに渡す環境変数を設定することができる。通常の Apache 環境変数設定手段である SetEnv ディレクティブは FastCGI では使えないので注意する必要がある。その他のパラメータについては,FastCGI ドキュメント Module mod_fastcgi を参照。

Apache をリスタートすればアプリが動くはずである。Apache エラーログ httpd-error.log に FastCGI 関連の起動メッセージが出力されるので,正しく動きはじめたかがわかる。ここで Permission denied のようなエラーが出て異常終了しているなら,/var/run/wt ディレクトリに Web httpd のユーザ(www, _www など)による書込権限が付与されているか確認するとよい。

FastCGI ではなく Wthttpd で動かすのなら,リンクライブラリを -lwthttp -lwt とすればよい。そして,以下のようにプログラムを起動する。

% concordance (プログラムロードモジュール名) --docroot . \
  --http-address 0.0.0.0 --http-port 8080 &

無事起動できれば,http://host:8080/ URL でブラウザからアクセスできるようになる。

Wt では /etc/wt/wt-config.xml にサーバ動作設定を記述するようになっている。wthttpd の引数指定ともども,詳細は Wt ドキュメントを参照いただきたい。Wt には SVG,PDF を生成する関数なども用意されており,HTML5 にも追従している。これまで C++ Web アプリを CGI のとろいインタフェースにしか結びつけられなかったシステム設計者には,Wt は驚きのライブラリである。