mlterm のインプットメソッドプラグインについて

#はじめに
　Linuxや*BSD上で日本語入力を行うに当たっては，インプットメソッドフレームワークとして、uimやscim，ibus，fcitx など様々な選択肢が存在します。
　mltermでは，XIMを経由せず，これらと直接やりとりして日本語等の入力ができるようになっており，インプットメソッドごとに，必要の都度，動的にロードするモジュールとして実装しています。　また，wnn や canna といったかな漢字変換サーバに直接アクセスするモジュールも提供しています。
　本稿では，この仕組み(インプットメソッドプラグインと呼んでいます。)やモジュールの作り方などをまとめてみました。
　なお，インプットメソッドプラグインの基盤及び多くのモジュールは，http://twitter.com/seiichi さんが実装してくださったものです。ありがとうございます。

#とりあえず使ってみる
　$ ./configure ...
　...
　Input Methods: XIM kbd uim m17nlib ibus fcitx scim canna wnn skk
　(XIM 以外がインプットメソッドプラグイン)
　...
　$ make
　$ sudo make install

　3.6.1以降では，特にconfigureにオプションを指定しなくても，インプットメソッドのライブラリを探し，ビルドするモジュールがconfigureの最後に表示されます。
　インストールが完了すれば，~/.mlterm/mainに
　input_method = <インプットメソッド>
と設定するか
  $ mlterm --im <インプットメソッド>
  のように --imオプションを指定してmltermを起動してください。
  (Framebuffer上でuim又はibusを使用する場合は，--imオプションにインプットメソッド名だけなく，"uim:mozc"のようにかな変換エンジンも併せて指定してください。)
　各インプットメソッドの開始キー(Control+spaceや半角全角キー)を押下することで，日本語入力を行うことができます。~/.mlterm/keyに
Control+space=IM_HOTKEY
のようにショートカットキーを指定することも可能です。
　なお，モジュールのロードに失敗した場合は，~/.mlterm/msg.log に，
<インプットメソッド>: Could not load.
というログが出力されます。

　以下は，実際に動作している様子を映したスクリーンショットです。

　Ubuntu 15.10 上で mlterm (libvte互換ライブラリ) + ibus
![1.png](https://qiita-image-store.s3.amazonaws.com/0/14112/e7e1763b-4401-4430-f0fd-1010442b8fa4.png)

　同 mlterm (libvte互換ライブラリ) + fcitx
![2.png](https://qiita-image-store.s3.amazonaws.com/0/14112/076da0c6-7228-7159-a2ad-00fc178d8b6b.png)

　同 mlterm (libvte互換ライブラリ) + uim (anthy)
![5.png](https://qiita-image-store.s3.amazonaws.com/0/14112/69eabb9b-1eb4-131b-ac5b-16098c42c046.png)

　同 mlterm-fb + ibus (mozc)
![4.png](https://qiita-image-store.s3.amazonaws.com/0/14112/7f037548-d63f-aa83-b5c5-7de482b17b1b.png)

　Windows 8 / Cygwin 上で mlterm + wnn
![3.png](https://qiita-image-store.s3.amazonaws.com/0/14112/99934fbf-9dde-5bc3-cf8d-490bb83f42f5.png)

#対応プラットフォーム
　上記のスクリーンショットのとおり，インプットメソッドプラグインは，Xlib版だけでなく，Framebuffer版やWin32 GDI版のmltermでも利用できます。また，3.7.1からはMacOSX/Cocoa版でも一部利用可能となっています。(Android版のmltermでは未対応(システム標準のインプットメソッドのみサポート))
　各インプットメソッド用のモジュールは，基本的に，特定のプラットフォームに依存しないため，ソースコードはほぼ同一です(ibusやfcitxモジュールには，#ifdef USE_FRAMEBUFFERでの切り分けがありますが，Xlib版では変換候補ウィンドウの表示をインプットメソッドに任せる一方，Framebuffer版ではmlterm自身で描画する必要があることによるものです。)。

mlterm-3.7.1での対応状況

|インプットメソッド| Xlib | Framebuffer |  Win32 GDI | MacOSX Cocoa |
|:---------------:|:----:|:-----------:|:----------:|:------------:|
|kbd              |○     |○            |○           |○             |
|uim              |○     |○            |(not tested)|×             |
|m17nlib          |○     |○            |(not tested)|×             |
|ibus             |○     |○            |(not tested)|×             |
|fcitx            |○     |○            |(not tested)|×             |
|scim             |○     |×            |(not tested)|×             |
|canna            |○     |○            |(not tested)|(not tested)  |
|wnn              |○     |○            |○(cygwin)   |(not tested)  |
|skk              |○     |○            |○           |○             |

![1.png](https://qiita-image-store.s3.amazonaws.com/0/14112/900a019a-8f9e-f03e-1b13-7a5b0068b61f.png)

#モジュールの作り方
　mlterm-x.x.x/inputmethod/以下に新しいディレクトリを掘って，uimなど既存のソースコードをコピペし，必要な修正を行っていく方法が簡単です。
　また，configure.in にも，対応しようとするインプットメソッド のライブラリの検出を追加する必要があります。

　モジュールの実装における基本的な事項は，次のとおりです。

1. x_im_tを拡張しインプットメソッド独自の情報を保持する構造体を定義
　typedef struct im_xxx {
　　x_im_t im;
　　/\* private members \*/
　} im_xxx_t;
<br>

2. mlconfigがモジュールの情報を入手するための関数を実装
　im_info_t\* im_xxx_get_info(char\* locale, char\* encoding);
<br>
3. コンストラクタとなる関数を実装
　x_im_t\* im_xxx_new(u_int64_t magic, ml_char_encoding_t encoding,
　　　　x_im_export_sym_t\* syms, char\* engine, u_int mod_ignore_mask) {

　　　/\* **ABI互換性のない古いモジュールがロードされていないかチェック
　　　 \* IM_API_COMPAT_CHECK_MAGIC は mlterm-x.x.x/xwindow/x_im.h に定義** \*/
　　　if (magic != IM_API_COMPAT_CHECK_MAGIC) {
　　　　return NULL;
　　　}

　　　/\* **インプットメソッドの初期化等** \*/

　　　/\* **im_xxx_t オブジェクトの生成** \*/
　　　xxx = calloc(1, sizeof(im_xxx_t));

　　　/\* **mlterm 本体からのイベントを受け取るコールバック関数を登録** \*/
　　　xxx->im.delete = delete;
　　　xxx->im.key_event = key_event;
　　　xxx->im_switch_mode = switch_mode;
　　　xxx->im.is_active = is_active;
　　　xxx->im.focused = focused;
　　　xxx->im.unfocused = unfocused;

　　　/\* **その他 im_xxx_t の private members を設定** \*/

　　　return xxx;
　}<br>
4. mlterm 本体からのイベントを受け取るコールバック関数(mlterm-x.x.x/xwindow/x_im.h)を実装
　**x_im_t オブジェクトの破棄**
　int (\*delete)(x_im_t\*);
　**キーイベント。mlterm本体内でキーイベントの処理を継続しない場合は 0 を返す**
　int (\*key_event)(x_im_t\*, u_char, KeySym, XKeyEvent\*);
　**インプットメソッドの有効・無効の切り替え**
　int (\*switch_mode)(x_im_t\*);
　**インプットメソッドが有効かどうか**
　int (\*is_active)(x_im_t\*);
　**フォーカスイベント**
　void (\*focused)(x_im_t\*);
　void (\*unfocused)(x_im_t\*);<br>
5. インプットメソッドからプリエディット文字列の編集や変換候補の表示に関するイベントを受け取り，x_im_event_listener_t や x_im_export_syms_t を使って(プラットフォーム依存の描画APIを使用せず)，mlterm の画面にプリエディット文字列や変換候補ウィンドウを表示
**・x_im_event_listener_t**
　現在入力中の画面(x_screen_t)とやりとりする関数群。第一引数に x_im_event_listener_t::self を渡して呼び出す。
　　**現在のカーソル位置のXY座標値を取得**
　　int (\*get_spot)(void\*, ml_char_t\*, int, int\*, int\*);
　　**行の高さを取得**
　　u_int (\*get_line_height)(void\*);
　　**縦書き中かどうか**
　　int (\*is_vertical)(void\*);
　　**プリエディット文字列の描画※**
　　int (\*draw_preedit_str)(void\*, ml_char_t\*, u_int, int);
　　**変換エンジンが変わったことをmlterm本体に通知**
　　void (\*im_changed)(void\*, char\*);
　　**xmodifier mappingと照合し装飾キーの押下状態を取得**
　　int (\*compare_key_state_with_modmap)(void\*, u_int, int\*, int\*, int\*, int\*, int\*, int\*, int\*, int\*);
　　**確定した文字をptyに書き込む**
　　void (\*write_to_term)(void\*, u_char\*, size_t);
　　**x_im_export_syms_t::ml_convert_to_internal_chの第2引数に使用**
　　ml_unicode_policy_t (\*get_unicode_policy)(void\*);

**・x_im_export_syms**
　mlterm 本体が持つユーティリティ関数群
　　**ml_char_t(プリエディット文字列)の初期化・破棄※**
　　int (\*ml_str_init)(ml_char_t\*, u_int);
　　int (\*ml_str_delete)(ml_char_t\*, u_int);
　　**ml_char_tに文字(又は結合文字)をセット※**
　　int (\*ml_char_set)(ml_char_t\*, u_int32_t, mkf_charset_t cs, int, int, ml_color_t, ml_color_t, int, int, int, int, int);
　　ml_char_t\* (\*ml_char_combine)(ml_char_t\*, u_int32_t, mkf_charset_t, int, int, ml_color_t, ml_color_t, int, int, int, int, int);
　　**ml_char_encoding_tとエンコーディング名の変換**
　　char\* (\*ml_get_char_encoding_name)(ml_char_encoding_t);
　　ml_char_encoding_t (\*ml_get_char_encoding)(const char\*);
　　**指定したエンコーディングのパーサ・コンバータを生成※**
　　mkf_parser_t\* (\*ml_parser_new)(ml_char_encoding_t);
　　mkf_conv_t\* (\*ml_conv_new)(ml_char_encoding_t);
　　**only_use_unicode_font等のオプションを反映※**
　　int (\*ml_convert_to_internal_ch)(mkf_char_t\*, ml_unicode_policy_t, mkf_charset_t);
　　**変換候補ウィンドウを生成。表示する文字列は x_im_candidate_screen_t::set でセット**
　　x_im_candidate_screen_t\*  (\*x_im_candidate_screen_new)(x_display_t \*, x_font_manager_t\*, x_color_manager_t\*, int, int, ml_unicode_policy_t, u_int, int, int);
　　**ステータスウィンドウを生成。表示する文字列は x_im_status_screen_t::set でセット**
　　x_im_status_screen_t\* (\*x_im_status_screen_new)(x_display_t\*, x_font_manager_t\*, x_color_manager_t\*, int, u_int, int, int);
　　**mlterm本体のイベントループにコールバック関数を登録・解除**
　　int (\*x_event_source_add_fd)(int, void (\*handler)(void));
　　int (\*x_event_source_remove_fd)(int);

※ プリエディット文字列の表示は，米印の関数を使って以下のように行います。
　インプットメソッドから受け取った文字列をmkf_parser_t::set_str & next_charでmkf_char_tに変換し，ml_convert_to_internal_chした後，ml_char_tにセットして，draw_preedit_str で描画

#まとめ
　Linuxや*BSD界隈におけるインプットメソッドは新旧の入れ替わりの激しいため，gtk+やQtといったツールキットに依存せずにソフトウェアを作る場合，全部XIM任せにするか，新しいインプットメソッドが出る都度，APIを調べ，自分で対応するしかありません。
　mlterm の場合，http://twitter.com/seiichi さんにより，(10年以上前から)インプットメソッドプラグインという枠組が整理されていたため，最近のインプットメソッド(ibusやfcitx)への対応も比較的容易に進められました。
　また，当初，インプットメソッドプラグインは，Xlibのみで動作していましたが，Xlibに依存しないように設計されていたため，FramebufferやWin32 GDIへの移植も簡単にできました。
　プラットフォームやライブラリに依存する部分と非依存部分の切り分けを意識しておけば，後々楽ができるということかもしれません。
　なお，もういい加減面倒なので，ibusやfcitxを置き換える新しいインプットメソッドが出てこないことを祈っています。