C
Linux
X11
日本語入力
input-method

XIM(X Input Method)日本語入力の流れ

2018年3月 作成

「X Window Systemの日本語入力はどうなっているんだろう?」
そう考えた筆者が、日本語入力の流れを調べてみました。
 C言語、X11プログラミングに関する文章です。
 使用OS:ubuntu16.04

日本語入力に対応していないプログラム

まず初めにキーボードから文字を入力するプログラムを作ります。

example01.c
#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で表示される単純なものです。

日本語入力に対応したプログラム

上記のプログラムを改良して、日本語入力ができるようにしたのが、このプログラムです。

example02.c
#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などを指しています)

日本語入力の大まかな流れ

図.gif

図のように、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サンプルを作成しました。