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

「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](https://qiita-image-store.s3.amazonaws.com/0/202736/f85c7f90-9af9-2cfa-6b32-db494dac17a8.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サンプルを作成しました。