「X Window Systemの日本語入力はどうなっているんだろう?」
そう考えた筆者が、日本語入力の流れを調べてみました。
C言語、X11プログラミングに関する文章です。
使用OS:ubuntu16.04
日本語入力に対応していないプログラム
まず初めにキーボードから文字を入力するプログラムを作ります。
#include <stdio.h>
#include <locale.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
int main(void)
{
Display* dpy = XOpenDisplay(NULL);
Window win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 100, 100, 0, 0, 0);
XMapRaised(dpy, win);
XSync(dpy, False);
XSelectInput(dpy, win, KeyPressMask | KeyReleaseMask);
XEvent ev = {};
while(1){
XNextEvent(dpy, &ev);
switch(ev.type){
case KeyPress: {
KeySym keysym = NoSymbol;
char text[1024] = {};
XLookupString((XKeyEvent *)&ev, text, sizeof(text) - 1, &keysym, NULL);
printf("Got chars: (%s)\n", text);
if(keysym == XK_Escape){
puts("Exiting because escape was pressed.");
return 0;
}
}
break;
}
}
return 0;
}
コンパイルパラメータは以下の通りです。
gcc -o example01 example01.c -lX11
コンパイルして端末エミュレータ(GNOME端末など)から実行すると、ウインドウに入力した文字が端末エミュレータにprintfで表示される単純なものです。
日本語入力に対応したプログラム
上記のプログラムを改良して、日本語入力ができるようにしたのが、このプログラムです。
#include <stdio.h>
#include <locale.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
int main(void){
#if 1
setlocale(LC_ALL, "");
#endif
Display* dpy = XOpenDisplay(NULL);
Window win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 100, 100, 0, 0, 0);
XMapRaised(dpy, win);
XSync(dpy, False);
#if 1
XSetLocaleModifiers("");
XIM xim = XOpenIM(dpy, 0, 0, 0);
if(!xim){
// fallback to internal input method
XSetLocaleModifiers("@im=none");
xim = XOpenIM(dpy, 0, 0, 0);
}
XIC xic = XCreateIC(xim,
XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
XNClientWindow, win,
XNFocusWindow, win,
NULL);
XSetICFocus(xic);
#endif
XSelectInput(dpy, win, KeyPressMask | KeyReleaseMask);
XEvent ev = {};
while(1){
XNextEvent(dpy, &ev);
#if 1
if(XFilterEvent(&ev, None) == True) continue;
#endif
switch(ev.type){
case KeyPress: {
KeySym keysym = NoSymbol;
char text[1024] = {};
#if 1
Xutf8LookupString(xic, &ev.xkey, text, sizeof(text) - 1, &keysym, NULL);
printf("Got chars: (%s)\n", text);
#else
XLookupString((XKeyEvent *)&ev, text, sizeof(text) - 1, &keysym, NULL);
printf("Got chars: (%s)\n", text);
#endif
// example of responding to a key
if(keysym == XK_Escape){
puts("Exiting because escape was pressed.");
return 0;
}
}
break;
}
}
return 0;
}
コンパイルパラメータは以下の通りです。
gcc -o example02 example02.c -lX11
X11プログラム(例えば、xterm)で日本語が入力できるのあれば、このプログラムでも日本語が入力できます。
(このプログラムの参考URL https://gist.github.com/baines/5a49f1334281b2685af5dcae81a6fa8a)
数個の関数をプログラムに追加しましたが、IMサーバに直接関わる関数は、次の四つです。
XIM xim = XOpenIM() //XIMプロトコルでIMサーバと接続
XIC xic = XCreateIC() //IMサーバとのコンテキスト作成
XSetICFocus() //フォーカスを設定
XFilterEvent() //IMサーバへイベント送信
そして、IMサーバとのキーイベントのやり取りは、次の二つで行います。
XNextEvent() //IMサーバからのイベントを受け取る
XFilterEvent() //IMサーバにイベントを送る
(IMサーバとは、fcitxやibusなどを指しています)
日本語入力の大まかな流れ
図のように、X11プログラムはキーイベントをXNextEvent関数で受け取り、XFilterEvent関数でIMサーバにそのイベントを送るかどうか判断します。
XFilterEvent関数はIMサーバにそのイベントを送る場合、戻り値をTrueで返します。
X11プログラムはIMサーバからのキーイベントもXNextEvent関数を使って受け取ります。このIMサーバからのイベントをXFilterEvent関数に渡しても戻り値がFalseで返り、IMサーバに再度送られることはありません。
(IMサーバとIME間のやり取りは、この文章では触れません)
IMサーバ
もう少し深入りして、IMサーバについて調べてみます。
IMサーバは、IMdkitという機能を使って実装します。下記URLにプログラムコードを見つけましたが、メンテナンスされておらずコンパイルエラーが出ます。
https://www.x.org/releases/unsupported/lib/IMdkit/
さらに探してみると、ibusやfcitxでは独自の修正を加えてIMdkitの機能を使っていることが分かり、下記URLからIMdkitのソースをダウンロードすることができました。
https://github.com/fcitx/fcitx/tree/master/src/frontend/xim/lib
IMサーバのサンプルプログラムは下記URLからダウンロードしました。
https://www.x.org/releases/unsupported/lib/IMdkit/Xi18n_sample/
sampleIM(IMサーバ)のコンパイル
ダウンロードしたプログラムコードのままではコンパイルが通らないため、若干の修正を加えたIMdkitとサンプルIMサーバを下記URLに準備しました。
http://ai56go.ivory.ne.jp/sample/sampleIM.zip
このzipファイルを展開すると、buildディレクトリがあります。
このbuildディレクトリ内に移り、以下のコマンドを打つと、sampleIMプログラムが作られます。
chmod 755 build.sh
chmod 755 build2.sh
./build.sh
./build2.sh
(sampleIM.cのソースコードが古いため、多くのwarningメッセージが出ますが、sampleIMプログラムは作成されます)
sampleIM(IMサーバ)の実行
export XMODIFIERS=@im=sampleIM
./sampleIM &
xterm
と順番に打ってxtermを起動します。
(ヒント、xtermがない場合、代わりに上記で紹介したexample02を使って下さい。GNOME端末などはXIM以外の方法でIMサーバと通信をするのでここでは使えません)
xtermでshift+space
と打ってからキーボードから文字を打つと、
sampleIMを起動した端末エミュレータに入力した文字が表示されます。
実際のIMサーバの場合、このタイミングでIMEと通信をして日本語変換の処理をします。
ctrl+k
と打つと"これは IM からの確定文字列です。"
とxtermに表示されます。
最後にshift+space
を打つことで、普通にxtermに文字が入力できます。
このsampleIMは
shift+space
入力の切り替え
ctrl+k
確定
の機能がありますが、日本語変換は行わないので、確定で返される文字列はいつも同じものになります。
sampleIMを終われせるには、下記のように、psでプロセスIDを調べてkillすることで終わらせます。
ps | grep sampleIM
kill (プロセスID)
sampleIMの解説
このsampleIMは、IMサーバとしての機能が最低限備わっています。日本語変換はIMEの仕事なのでこの文章では触れません。
sampleIMの動作をプログラムの視点から見ていくと、
まず、sampleIM.cの398行に
ims = IMOpenIM(dpy,
とあり、IMサーバの初期化をしています。
このIMOpenIM関数の引数として、401行
IMServerName, imname,
とあります。このimname値は"sampleIM"となっていて、
上記で入力したexport XMODIFIERS=@im=sampleIM
の値と一致していないといけません。
次に、425行に
IMProtocolHandler, MyProtoHandler,
とあり、コールバック関数を指定します(ここではMyProtoHandler関数)。
次に、440行の
XNextEvent(dpy, &event);
で、X11プログラム(xtermなど)からのキーイベントを受け取り、
X11プログラムからのイベントであった場合のみ、441行
if (XFilterEvent(&event, None) == True) continue;
のXFilterEvent関数はMyProtoHandler関数を呼び、戻り値にTrueを戻し、continue;する仕組みになっています。
249行の
MyProtoHandler(ims, call_data)
でIMサーバとしての処理を行います。
もし、sampleIMをもっと充実させるのであれは、このMyProtoHandler関数を充実させることになるでしょう。
IMdkitのAPIは下記URLに記載されています。
https://www.x.org/releases/unsupported/lib/IMdkit/doc/API.text
IMdkitで必要とされる関数は下記のとおりで多くはありません。
XIMS IMOpenIM(Display display,...)
char *IMSetIMValues(XIMS ims,...)
char *IMGetIMValues(XIMS ims,...)
void IMCloseIM(XIMS ims)
int IMPreeditStart(XIMS ims, XPointer im_protocol)
int IMPreeditEnd(XIMS ims, XPointer im_protocol)
void IMForwardEvent(XIMS ims, XPointer im_protocol)
void IMCommitString(XIMS ims, XPointer im_protocol)
int IMCallCallback(XIMS ims, XPointer im_protocol)
最後に
この文章は、XIMを使った日本語入力の流れについて説明しました。
・日本語入力を行うための関数(サンプルプログラム)を紹介。
・イベントのやり取りには、XNextEvent関数、XFilterEvent関数を使う。
・IMサーバはIMdkit機能を使う。
また、オリジナルのIMdkitサンプルではコンパイルエラーが出る状態ですが、エラーがでないようにまとめたIMdkitサンプルを作成しました。