Displaying posts written in

3月 2009

Erlang/OTP R13A の Unicode サポート

先日に引き続き、Erlang/OTP R13A の Unicode サポートについて調査していく。

リストとバイナリの混成

iodata() と同様のコンセプトの Unicode 対応版として、*chardata() というデータ型が導入される。

chardata()
UTF-8 でエンコードされたバイナリと、妥当な Unicode コードポイントからなるリストの混成
latin1_chardata()
latin1(iso-8859-1) でエンコードされたバイナリと、latin1 文字コードからなるリストの混成。iodata() と直接の互換性がある
external_chardata()
UTF-8 以外の Unicode 系エンコーディング(UTF-16/UTF-32)でエンコードされたバイナリと、妥当な Unicode コードポイントからなるリストの混成

このうち標準となるのが chardata() であり、Erlang/OTP 内部ではこの表現で統一するよう推奨されている。latin1_chardata() と external_chardata() は(少なくとも現時点では)後方互換性と外部システムとの連携のために用意されたものだと言える。実際 latin1_chardata() の定義におけるリストの制限は露骨に後方互換性を意識したものであることを示しているし、external_chardata() の定義はかなりテキトーだ(バイナリのエンコーディングに制限がないので、複数のエンコーディングが混在しても定義に適う。もちろんそれでは使い物にならないはずだが)。

unicode モジュール

*chardata() をフラットなリスト/バイナリに変換するための関数が unicode モジュールに用意される。詳細はマニュアルを参照。基本となるのは次の2つの関数である。

characters_to_list(Data, InEncoding)
Data を InEncoding でデコードして、Unicode コードポイントのリストに変換する
characters_to_binary(Data, InEncoding, OutEncoding)
Data を InEncoding でデコードして、OutEncoding でエンコードしたバイナリを返す

InEncoding に latin1 を指定した場合、Data は latin1_chardata() でなければならない。すなわちリスト内に含まれる整数は 255 以下に制限される。

例1:リストで表現された”日本語”という文字列を UTF-8 バイナリに変換

unicode:characters_to_binary([26085,26412,35486], utf8, utf8).
%% -> <<230,151,165,230,156,172,232,170,158>>

例2:UTF-8 でエンコードされた”日本語”という文字列を UTF-16LE に変換

unicode:characters_to_binary(
	<<230,151,165,230,156,172,232,170,158>>,
	utf8,
	{utf16,little}).
%% -> <<229,101,44,103,158,138>>

例3:”日本語”という文字列のリスト表現と UTF-16LE バイナリ表現を連結してフラットなリストに変換

unicode:characters_to_list(
	[[26085,26412,35486],<<229,101,44,103,158,138>>],
	{utf16,little}).
%% -> [26085,26412,35486,26085,26412,35486]

続く。

visitors でリモートホストの集計

高速・軽量なアクセス解析ツール visitors およびその日本語化バージョン。素晴らしいツールなのだが、デフォルトではアクセス元ドメインの集計が全て「numeric IP」になってしまうのが難点だった。

そこで日本語化バージョンを元に、まずはIPアドレスごとに集計を行うよう修正した(パッチダウンロード)。

diff -ur visitors_0.7.orig/visitors.c visitors_0.7/visitors.c
--- visitors_0.7.orig/visitors.c	2007-08-20 19:11:54.000000000 +0900
+++ visitors_0.7/visitors.c	2009-03-28 17:38:59.000000000 +0900
@@ -1607,13 +1607,9 @@
 	char *tld;
 	int res;
 
-	if (vi_is_numeric_address(hostname)) {
-		tld = "numeric IP";
-	} else {
-		tld = strrchr(hostname, '.');
-		if (!tld) return 0;
-		tld++;
-	}
+	tld = hostname;
+	if (!tld) return 0;
+
 	res = vi_counter_incr(&vih->tld, tld);
 	if (res == 0) return 1;
 	return 0;

あとは出力時に逆引きを行えば良い。本当は visitors.c 内で行うように修正すべきなんだろうが、よく分からないのでPerlのワンライナーで変換することにした(下例ではIPアドレスっぽいテキストを全て変換してしまうので、もう少し真面目に書いた方が良いとは思うが……)。

visitors -A -m 45 -o html access_log | perl -pe \
's%(\d{1,3}\.){3}\d{1,3}%gethostbyaddr(pack("C4", split(/\./, $&)), 2) || $&%e' \
> out.html

元々 visitors のこの機能は、トップレベルドメインを集計して国別のアクセス傾向を知るためのものなので、リモートホストの集計に用いるのは邪道かも知れない。しかし実用上はこの方が役に立つ機会は多いと思う。

この改造にあたっては以下のページを参考にした:

こちらではまずアクセスログに記録されたIPアドレスを全て逆引きする方法を紹介されているが、ログが何十万行もある場合は逆引きに時間がかかり過ぎる……。この用途に限って言えば、IPアドレスで集計して上位のアドレスだけ逆引きするという方針で十分だろう。

Erlang/OTP R13A を Ubuntu にインストール

最新の Erlang/OTP R13A を Ubuntu 8.10 にインストール。せっかくだから wxErlang も動くようにしよう!と欲を出したせいでひどく苦労した。普通にインストールするだけなら

./configure --prefix=/usr/local/otp_r13a
make
sudo make install

で良いはず(既存のバージョンとは別のディレクトリにインストールしている)。

wxErlang を有効にするには……正確な方法はよく分からない。いろいろ試行錯誤した揚げ句、何が最低限必要だったのか分からなくなってしまった。最初は apt に標準で用意されている wxWidgets 関連パッケージをインストールしまくったのだが、configure で「wxWidgets don’t have gl support」とか「wxWidgets don’t have wxStyledTextControl (stc.h)」とかのエラーが出てしまい、最終的にはwxErlang のページに従ってまず wxWidgets をソースからインストールして、それから以下のようにコンパイル&インストールした。

./configure --enable-wx=/usr/local \
	    --enable-smp-support \
	    --prefix=/usr/local/otp_r13a
make
sudo make install

これでも configure の最後で先述のエラーは出るのだが(wxWidgets のコンパイル時に OpenGL サポートは追加しているし、stc.h も存在するのに)、そのまま make && make install したらうまくインストールできた。もしかしたら apt の標準パッケージでもできていたのかも知れない。

あと SMT を有効にしておかないと「WX ERROR: SMT emulator required」となるので、–enable-smp-support は必須(デフォルトで有効になる?)。あと erl の起動時にも -smp オプションが必要。

/usr/loca/otp_r13a/bin/erl -smp

1章 「wxErlang」 – INAの日記を参考に最小限のサンプルを動かして、とりあえずウィンドウが表示されるところまでは確認した。長い道のりだった……。

Erlang/OTP R13Aリリース

3月17日に Erlang/OTP の次期バージョン R13A がリリースされた。これはβリリースであり、正式版となる R13B は4月29日にリリースされるとのこと。

このリリースのハイライトとして、トップページでは以下の内容が紹介されている:

  • SMP/マルチコア環境における大幅なパフォーマンス改善
  • Unicode サポートの追加
  • WxErlang(GUIライブラリ WxWidgets に対するバインディング)の追加
  • リリース管理ツール RelTools の追加

マルチバイト圏の人間としては、何と言っても Unicode サポートの追加が嬉しい。仕様は EEP10 に基づいているとのことなので、一通り目を通してみた。

Erlang/OTP R13A の Unicode サポート

Erlang において「文字列」を表現するには、リストによる方法とバイナリによる方法の二通りがあるが、R13A 以降はそれぞれ次のように変化する。

リスト
R12B までは 0 ~ 255 の整数が iso-8859-1 の1文字を表すと見なされ、0 ~ 255 の範囲の整数のみを含むリストが「文字列」として扱われていた。R13A 以降はこれが 0 ~ 16#10fff の範囲に拡張され、整数1つは Unicode のコードポイントと見なされるようになる。
Unicode のコードポイントにおける 0 ~ 255 の範囲は iso-8859-1 の文字コードと互換性があるので、
R13A 以降の「文字列」は、R12B 以前の「文字列」の上位互換だと言うことができる。
バイナリ
R12B までは8ビットが1区切りとされ、それぞれが iso-8859-1 の1文字を表すと見なされていた。しかしR13A 以降、バイナリで文字列を表現する場合は UTF-8 でエンコードするのが標準となる。ただし iso-8859-1 他のエンコーディングで扱う方法もある。

要するに、抽象的な概念としての「文字」と、具体的な表現としての「バイナリ」が厳密に区別されるようになった、ということになる。今までの Erlang ではこの区別が曖昧なところがあった。iso-8859-1 の範囲で考える限り、バイト列=文字列と見なして問題なかったからだ。しかし今後は、文字を文字として扱いたいときはリストを用いて操作を行い、外部システムやファイルとのやり取りで具体的なビット表現が必要なときはバイナリとして扱う、という区別が必要になる。

抽象概念としての文字は一意な存在なので、リストを用いた文字列の表現はただ一通りに定まる。”日本語”という文字列を表すリストは
[26085,26412,35486] しかあり得ない。しかしこれをバイナリとして表現する方法は幾通りもあり得る。UTF-8 で表現すれば <<230,151,165,230,156,172,232,170,158>> となり、UTF-16ビッグエンディアンで表現すれば <<101,229,103,44,138,158>> となる。

これから実際にインストールして、動作を確かめてみたい。

OpenX のインストール(日本語対応)

オープンソースの広告配信サーバ OpenX。バージョン2.6からは日本語リソースファイルもデフォルトで含まれているが、若干問題が残っている。

以下、OpenX 2.6.4 を $OPENX_PATH に www.example.com というドメインでインストールした場合の説明。MySQLは 5.0 系を使用。

データベースの文字コード設定

インストール完了後、管理画面からデータを追加する前に設定ファイルを修正する。修正すべきファイルは
$OPENX_PATH/var/www.example.com.conf.php
このファイルの中の clientCharset を utf8 に変更する。デフォルトでは多分 latin1 になっているはず。

[databaseCharset]
checkComplete=1
clientCharset=utf8

この設定をしておかないと「一見正常に動いているがDB内部では文字列が壊れている」という状態になる。

Excelの文字化け修正

デフォルトではレポート画面で出力できるExcelファイルが文字化けする。これは OpenX が内部で使用しているPEARパッケージ Spreadsheet_Excel_Writer の問題。バグレポートからパッチを落としてきて適用する。

cd $OPENX_PATH
patch -p0 < excel-all-in-one-patch.diff

ちなみにこのパッチは私が投稿したものだが、フォーラムで紹介されていた複数のパッチを一つにまとめただけで、修正の内容は理解していない。また mbstring が必須となっているため、このまま本体に取り込まれる可能性はまずないと思われる。

タプルを使用した関数呼び出し

「Erlang dict」で検索すると最初に引っかかるページ「Erlang クエックブック」。このページにおいてdictの使い方は次のように説明されている:

D1 = dict:new().                            % 作成
D2 = D1:store(key1, value1).                % キーと値のペアを入れる

新しく作った辞書を「モジュール名として」指定して、dict モジュールの関数を呼び出している。まるでオブジェクト指向におけるメソッド呼び出しのような奇妙な記法だが、確かにこれで動作する。

なぜだろう?

いろいろと調べた結果、これは dict に特有の話ではなく、モジュール名としてタプルを指定した場合の特殊な動作だということが分かった(dict モジュールは個々の辞書をタプルとして表現している)。Mod:Fun(Arg1,..ArgN) の評価を疑似コードで書くと次のようになる:

{Mod,Fun,Args} when is_tuple(Mod), is_atom(element(1, Mod)) ->
	erlang:apply(element(1, Mod), Fun, Args ++ [Mod])

次のように動作を確かめることができる。

erl> M1 = {erlang, 1, 2, 3}.
{erlang,1,2,3}
erl> M1:tuple_to_list().
[erlang,1,2,3]

問題は、この動作が公式なドキュメントのどこにも書いていないということだ。少なくとも私は見つけられなかった。

これとよく似た例として、{Module, FunctionName} という形式のタプルが Module:FunctionName として評価されることはいくつかのドキュメントで触れられており、「非推奨」だとされている。よってこれも既に使われなくなった、古い形式の呼び出し方だということだろうか……?

CentOSで最新のPHP 5.2を使用する

サードパーティのリポジトリを利用することで、2009年3月20日現在最新のphp-5.2.9をyumでインストールすることができる。リポジトリ管理者の素晴らしい仕事には頭が下がるばかりだ…。

注意:この方法でインストールされる php-mysql は MySQL 5.1.30 の libmysqlclient とリンクされる。

English : Repository Configuration – Les RPM de Remiに従ってEPELとLes RPM de Remiのリポジトリを追加。CentOS 5の場合は

wget http://download.fedora.redhat.com/pub/epel/5/i386/epel-release-5-2.noarch.rpm
wget http://rpms.famillecollet.com/el5.i386/remi-release-5-6.el5.remi.noarch.rpm
rpm -Uvh remi-release-5*.rpm epel-release-5*.rpm

php-mysql をインストールする場合は、予め remi-test リポジトリから mysqlclient16 パッケージをインストールしておく。

sudo yum --enablerepo=remi-test install mysqlclient16

自分が必要とするパッケージをインストールする(パッケージによってはEPEL内のパッケージに依存することがある)。

sudo yum --enablerepo=epel,remi install php php-cli php-mysql php-gd

インストール後はリポジトリ管理者のブログLes RPM de Remiを常にチェックしておくことをおすすめする。PHPの新しいバージョンが出た場合、今までは数日中には対応されてきた。しかしアップデートに伴って、時々パッケージの構成が変わったりライブラリのバージョンが変わったりして、すんなりアップデートできなくなることもあった。そのあたりは自己責任で。

トライグラム反復子

『プログラミングErlang』15章本文では

  1. 単語ごとに分割する
  2. 単語の両端に空白を付加する
  3. 3文字ずつ取り出す

という3段階の手順になっている。これを1段階に、すなわち先頭から順番に3文字ずつ抜き出していくアルゴリズムに変更してみた。

-define(NL, $\r,$\n).
 
% 先頭に改行を付加して処理開始
my_scan_trigrams([?NL|_]=L, F, A) -> each_trigram(L, F, A);
my_scan_trigrams(L, F, A) -> each_trigram([?NL|L], F, A).
 
each_trigram(L, F, A) ->
    case L of
	%% 終端の処理
	[?NL]      -> A;
	[?NL, X]   -> F([$\s, X, $\s], A);
	[X,   Y]   -> F([X,   Y, $\s], A);
	%% 単語の先頭
	[?NL, X, ?NL|T] -> each_trigram([?NL |T], F, F([$\s, X, $\s], A));
	[?NL, X, Y  |T] -> each_trigram([X, Y|T], F, F([$\s, X, Y  ], A));
	%% 単語の途中
	[X,   Y, ?NL|T] -> each_trigram([?NL |T], F, F([X,   Y, $\s], A));
	[X,   Y, Z  |T] -> each_trigram([Y, Z|T], F, F([X,   Y, Z  ], A))
    end.

微々たる差ではあるが、少しだけ速い。

Counting by my_scan_trigrams - No of trigrams=3357707 time/trigram=0.46476926068891655
Counting by scan_word_list - No of trigrams=3357707 time/trigram=0.5411714006016606

raw モードで開いたファイルの後始末

先日の続き。

file:open/2 に raw オプションを指定した場合の IoDevice の実体はポート+αだった。ということは、ポートに対するBIFを使えば、プロセス終了時の挙動も確かめられるはずである。

次のようなコードで確認してみた(rpc/2 の定義は省略)。

-module(raw_test).
-compile(export_all).
 
start() ->
    Pid = spawn(fun test/0),
    io:format("The process which opened the file is ~p.~n", [Pid]),
    Port = rpc(Pid, get),
    io:format("The opened port is ~p.~n~n", [Port]),
    io:format("is_process_alive: ~p~n", [erlang:is_process_alive(Pid)]),
    io:format("port_info: ~p~n~n", [erlang:port_info(Port)]),
    rpc(Pid, close),
    io:format("is_process_alive: ~p~n", [erlang:is_process_alive(Pid)]),
    io:format("port_info: ~p~n", [erlang:port_info(Port)]).
 
test() ->
    %% IoDeviceのUndocumentedな内部構造に依存
    {ok, {_,_,{Port,_}}} = file:open("raw_test.erl", [read, raw]),
    test(Port).
 
test(Port) ->
    receive
	{From, get} ->
	    From ! {self(), Port},
	    test(Port);
	{From, close} ->
	    From ! {self(), close}
    end.

実行結果は次のようになった。

The process which opened the file is <0.60.0>.
The opened port is #Port<0.1695>.

is_process_alive: true
port_info: [{name,"efile"},
            {links,[<0.60.0>]},
            {id,1695},
            {connected,<0.60.0>},
            {input,9},
            {output,18}]

is_process_alive: false
port_info: undefined

ファイルを開いたプロセスが生きているときにはきちんとポートの情報が取れる。links や connected に pid が入っていることも確認できる。プロセスが終了するとポート情報は undefined となり、ポートが閉じられたことが分かる。

IoDevice の内部実装についてはともかく、「ファイルを開いたプロセスが終了したとき、自動的にファイルが閉じられる」という挙動については信用しても良さそうだ。例えば『プログラミングErlang』14章のSHOUTcastサーバではクライアントがTCP接続を閉じたときにコントローラプロセスがクラッシュするという仕様になっているが、このときにもきちんとファイルは close される、ということになる。

開いたファイルの後始末

erl -man file より引用:

IoDevice is really the pid of the process which handles the file. This process is linked to the process which originally opened the file. If any process to which the IoDevice is linked terminates, the file will be closed and the process itself will be terminated. An IoDevice returned from this call can be used as an argument to the IO functions (see io(3)).

IoDevice の実体はファイルを操作するプロセスの Pid であり、ファイルを open したプロセスとリンクされている。そしてそのプロセスが終了したとき、ファイルが自動的に close されるようになっている。サーバを書く上では実に頼もしい仕様だ。

ところで file:open/2 で指定できるオプションには raw というものがある。同じく erl -man file によれば、このオプションを指定するとアクセスが高速化される反面、次のような制限が加わる:

  • io モジュールの関数を使えなくなる
  • ファイルを open したプロセスからしかアクセスできなくなる
  • リモートErlangファイルサーバ(これが何なのかよく分からないが)を使用することができなくなる

「ファイルを open したプロセスが終了しても、自動的にファイルが閉じられなくなる」とは書いていないから、冒頭に引用した部分の説明は raw オプションを指定した場合でも基本的に有効、と判断して良いのだろうか。

erlシェルで見る限り、raw を指定した場合の IoDevice の実体はポート+αらしい。ポートもまた Erlang が提供する抽象のひとつだから、やはりプロセス終了時には自動的に後片づけしてくれる、と思いたくなる。

erl> {ok, Io2} = file:open("m1.erl", [read, raw]).
{ok,{file_descriptor,prim_file,{#Port<0.420>,9}}}

実際もし自動的にファイルが close されないとなると、この IoDevice にアクセスできるのは open した当のプロセスだけなのだから、実質的に close する手段が無くなってしまう。