Email: Takayama Fumihiko <tekezo@pqrs.org>

キーボードリマッパーから見る Mac OS X のカーネル拡張の作り方

Mac OS X のカーネル拡張の作り方

はじめに

このドキュメントではキー配列の変更を行なう KeyRemap4MacBook の開発を通して、カーネル拡張の作り方の一端を説明します。

参考となる文献

Mac OS X の場合、ドキュメントが充実しているので非常に開発が容易です。 あたりを切り口に読み進んでいくと、とりあえずのカーネル拡張は簡単に作れるかと思います。

カーネル拡張を作るにあたってのフロー

カーネル拡張を作成・公開するには以下のステップがあります。
  1. カーネル拡張本体の開発
  2. (必要があれば) ローダスクリプト等のユーティリティの作成
  3. パッケージング
  4. 公開

開発環境

Xcode が入っているシステムであれば、スグにカーネル拡張を作り始めることが出来ます。

ちなみに Xcode のプロジェクトをコマンドラインからビルドするには xcodebuild コマンドを使います。
開発環境に因りますが、最初に Xcode でハリボテを作ったら後はコマンドラインから操作したほうが楽です。
% xcodebuild clean
% xcodebuild build
またパッケージングには PackageMaker を使いますが、コレもコマンドラインから使えます。 以下コマンドでヘルプが出ますので、適切にオプションを指定して使いましょう。
% /Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker --help

カーネル拡張を作るにあたっての注意点

  • C++ 言語を使う場合 STL とか使えません。 mem_fun_ref とかは諦めましょう。
  • コードにバグがあると、まずクラッシュします。 カーネル拡張の種類によっては Darwin/x86 とかを仮想マシンに用意して作業したほうが楽だと思います。 仮想マシンが無理な場合には、テスト用ハードウェアを別途用意したほうが良いでしょう。

キーボードリマッパーを作るにあたって

方向性

単純なキーのリマップであれば XML 形式の設定ファイルで行なうことが可能です。 (Technical Note TN2056: Installable Keyboard Layouts)

が、例えば Enter キーをコマンドキーにするような Modifier キーの追加・修正には XML 形式の方法は使えません。
もっとハードウェア因りの部分で生のキーコードを捉える必要が出てきます。

どうするかというと、カーネルの IOHIDSystem のレイヤをフックして、キーコードを変更させます。 (Intercepting Keyboard Events)

Hello I/O Kit

IOHIDSystem をフックするためには I/O Kit の Kernel Extension を作成する必要があります。

最初にハリボテを作成する為に良質なドキュメントが Apple から提供されており、 Kernel Extension Concepts: Hello I/O Kit: Creating a Device Driver With Xcode を参考にすると即座にカーネル拡張の雛型が出来ます。

なお、Info.plist の設定はスクラッチから登録するのはメンドイので、 KeyRemap4MacBook の kext/Info.plist をコピーして編集すると楽かと思います。

この段階で

load.sh や unload.sh といった開発用スクリプトを作ったり、 xcodebuild でのビルドとか IOLog を用いたデバグなどの基本的な環境整備を行っておきます。

開発

フックをかける

IOHIDSystem 内でリマップを行う為にフックをかける場所はいくつか候補があるのですが、 ここでは以下の理由から IOHIKeyboard の _KeyboardEventAction にフックを仕込む方向性で考えます。
  • 処理が比較的簡潔になる。
  • アプリケーションレイヤに近いレイヤでの処理になる。
  • 他のソフトウェアでの実績がある。

ソースコード

IOHIDSystem のソースコードは IOHIDFamily に含まれており、 _KeyboardEventAction にフックを仕込むには IOHIKeyboard.cpp、 IOHIKeyboard.h をあたります。

// IOHIKeyboard.h

class IOHIKeyboard : public IOHIDevice
{
...
    OSObject *                 _keyboardEventTarget;
    KeyboardEventAction        _keyboardEventAction;
    OSObject *                 _keyboardSpecialEventTarget;
    KeyboardSpecialEventAction _keyboardSpecialEventAction;
    OSObject *                 _updateEventFlagsTarget;
    UpdateEventFlagsAction     _updateEventFlagsAction;
...
}
IOHIKeyboard の中で _keyboardEventAction は関数ポインタとして宣言されており、 IOHIKeyboard::open の引数として渡される KeyboardEventCallback 型の関数へのポインタを保持しています。
// IOHIKeyboard.cpp

bool IOHIKeyboard::open(...
                        KeyboardEventCallback        keCallback,
                        ...)
{
    ...
    _keyboardEventAction        = (KeyboardEventAction)keCallback;
    ...
}
_keyboardEventAction が実際に呼ばれる際には _keyboardEvent 関数内部にて KeyboardEventCallback 型にキャストされます。
// IOHIKeyboard.cpp

void IOHIKeyboard::_keyboardEvent(...)
{
    KeyboardEventCallback       keCallback;
    keCallback = (KeyboardEventCallback)self->_keyboardEventAction;

    if ( !keCallback )
        return;

    (*keCallback)(...);
}
実際にフックをかけるには、この _keyboardEventAction 変数を独自の関数へのポインタにすることで行います。

_keyboardEventAction 変数を書き替える

IOHIKeyboard のインスタンスは接続されているキーボード毎に生成されていますので、 それらをスキャンして _keyboardEventAction を書き替える必要があります。

その際に接続されているキーボードを取得するには以下のような方法があります。
  • IOHIDSystem の getProviderIterator で取得する。
  • Notification Methods を利用する。
getProviderIterator を利用する場合、新しいキーボードが接続される毎にスキャンを行う必要がありますので、 ここでは Notification Methods を使います。
ドキュメントは I/O Kit Device Driver Design Guidelines: Notifications and Driver Messaging が参考になります。

ここで接続されたキーボードを見て、IOHIDKeyboard だった場合には _keyboardEventAction 関数を自前のものに差し替えます。
KeyRemap4MacBook で該当する処理は以下になります。
// KeyRemap4MacBook.cpp

bool
org_pqrs_driver_KeyRemap4MacBook::start(IOService *provider)
{
  keyboardNotifier = addNotification(gIOMatchedNotification,
                                     serviceMatching("IOHIKeyboard"),
                                     ((IOServiceNotificationHandler)&(org_pqrs_driver_KeyRemap4MacBook::notifier_hookKeyboard)),
                                     this, NULL, 0);
}

bool
org_pqrs_driver_KeyRemap4MacBook::notifier_hookKeyboard(org_pqrs_driver_KeyRemap4MacBook *self, void *ref, IOService *newService)
{
  IOLog("KeyRemap4MacBook::notifier_hookKeyboard\n");

  IOHIKeyboard *kbd = OSDynamicCast(IOHIKeyboard, newService);
  return replaceKeyboardEvent(kbd);
}

bool
org_pqrs_driver_KeyRemap4MacBook::replaceKeyboardEvent(IOHIKeyboard *kbd)
{
  if (kbd) {
    const char *name = kbd->getName();
    IOLog("KeyRemap4MacBook::replaceKeyboardEvent name = %s\n", name);

    if (strcmp(name, "IOHIDKeyboard") == 0) {
      HookedKeyboard *p = new_hookedKeyboard();
      if (p) {
        IOLog("KeyRemap4MacBook::replaceKeyboardEvent 0x%x (%x)\n", kbd, kbd->_keyboardEventAction);
        p->kbd = kbd;
        p->origEventAction = kbd->_keyboardEventAction;

        kbd->_keyboardEventAction = reinterpret_cast<KeyboardEventAction>(org_pqrs_driver_KeyRemap4MacBook::keyboardEventCallBack);
      }
    }
    return true;
  }
  return false;
}
こうすることで、自前の関数である org_pqrs_driver_KeyRemap4MacBook::keyboardEventCallBack 関数が呼ばれるようになります。

自前の keyboardEventCallBack では何をしているかというと、キーコードを差し替えてからオリジナルの _keyboardEventAction を呼んでいます。
これでキー配列を自由に操れることが可能になりました。
// KeyRemap4MacBook.cpp

void
org_pqrs_driver_KeyRemap4MacBook::keyboardEventCallBack(OSObject *target,
                                                        unsigned eventType,
                                                        unsigned flags,
                                                        unsigned key,
                                                        unsigned charCode,
                                                        unsigned charSet,
                                                        unsigned origCharCode,
                                                        unsigned origCharSet,
                                                        unsigned keyboardType,
                                                        bool repeat,
                                                        AbsoluteTime ts,
                                                        OSObject *sender,
                                                        void *refcon)
{
  IOHIKeyboard *kbd = OSDynamicCast(IOHIKeyboard, sender);
  if (kbd) {
    HookedKeyboard *p = search_hookedKeyboard(kbd);
    if (p) {
      remap(&eventType, &flags, &key, &charCode, &charSet,
            &origCharCode, &origCharSet, &keyboardType);

      p->origEventAction(target, eventType, flags, key, charCode,
                         charSet, origCharCode, origCharSet, keyboardType, repeat, ts);
    }
  }
}

トリック

この一連の _keyboardEventAction の差し替えですが、ひとつトリックがあります。

通常、 _keyboardEventAction は KeyboardEventAction 型として宣言されています。
// IOHIKeyboard.h

typedef void (*KeyboardEventAction)(       OSObject * target,
                    /* eventFlags  */      unsigned   eventType,
                    /* flags */            unsigned   flags,
                    /* keyCode */          unsigned   key,
                    /* charCode */         unsigned   charCode,
                    /* charSet */          unsigned   charSet,
                    /* originalCharCode */ unsigned   origCharCode,
                    /* originalCharSet */  unsigned   origCharSet,
                    /* keyboardType */     unsigned   keyboardType,
                    /* repeat */           bool       repeat,
                    /* atTime */           AbsoluteTime ts);
ところが、この状態だと実際に _keyboardEventAction を呼んだ IOHIKeyboard インスタンスが誰なのかわかりません。
それはつまり、差し替え前のオリジナルの _keyboardEventAction 関数の情報が取得できないということになります。

通常の C++ を使う場合には関数アダプタを使って Notification Methods の登録をしておけば、 インスタンスと紐付いた状態で呼ばれるので問題がないのですが、カーネル拡張ではその手法は使えません。

で、どうするかということなのですが以下のような回避策があります。
  1. とりあえず、最初に見つかった IOHIKeyboard インスタンスを信じて使う。
  2. keyboardEventGated によろしく頼む。
  3. _keyboardEventAction は実際には KeyboardEventCallback 型として処理されていることを利用する。
どれも一長一短があるのですが、 KeyRemap4MacBook では 3 番目の方法で対処しています。
この場合 IOHIDSystem の実装に依存してしまうので、 将来的に KeyboardEventCallback 型として処理されなくなったときに問題になるので注意が必要です。

KeyboardEventCallback 型として処理する方法ですが、 sender に IOHIKeyboard のインスタンスへのポインタが含まれているので、 sender を使ってオリジナルの関数を引っぱる形になります。
// IOHIKeyboard.h

typedef void (*KeyboardEventCallback)(
                    /* target */           OSObject * target,
                    /* eventFlags  */      unsigned   eventType,
                    /* flags */            unsigned   flags,
                    /* keyCode */          unsigned   key,
                    /* charCode */         unsigned   charCode,
                    /* charSet */          unsigned   charSet,
                    /* originalCharCode */ unsigned   origCharCode,
                    /* originalCharSet */  unsigned   origCharSet,
                    /* keyboardType */     unsigned   keyboardType,
                    /* repeat */           bool       repeat,
                    /* atTime */           AbsoluteTime ts,
                    /* sender */           OSObject * sender,
                    /* refcon */           void *     refcon);

開発 (sysctl まわり)

sysctl による設定を可能にする

カーネル拡張の場合、設定変更には sysctl を使うと簡単です。
詳しいドキュメントは Kernel Programming Guide: BSD sysctl API になります。

準備

sysctl を使うには、カーネル拡張の Info.plist で com.apple.kpi.bsd を指定する必要があります。
    <key>OSBundleLibraries</key>
    <dict>
      ...
      <key>com.apple.kpi.bsd</key>
      <string>8.0.0</string>
      ...
    </dict>
  </dict>
また、上記のドキュメントにありますが CFLAGS や CPLUSPLUSFLAGS に -no-cpp-precomp を指定する必要があります。

コードに組み込む

単純な値の取得、保存を行う sysctl を組み込むには以下の手順になります。
  • SYSCTL_INT や SYSCTL_STRING で宣言を行う。
  • sysctl_register_oid で登録 & sysctl_unregister_oid で登録解除を行う。
まずは SYSCTL_INT などのマクロで宣言を行います。
// KeyRemap4MacBook.cpp

SYSCTL_DECL(_keyremap4macbook);
SYSCTL_NODE(, OID_AUTO, keyremap4macbook, CTLFLAG_RW, 0, "KeyRemap4MacBook");

SYSCTL_INT(_keyremap4macbook, OID_AUTO, return2option, CTLFLAG_RW,
            &(org_pqrs_driver_KeyRemap4MacBook::config_return2option),
           0,
           "Remap 'Return Key' as 'Command Key'");

SYSCTL_INT(_keyremap4macbook, OID_AUTO, shift2escape, CTLTYPE_INT|CTLFLAG_RW,
           &(org_pqrs_driver_KeyRemap4MacBook::config_shift2escape),
           0,
           "Remap 'ShiftR Key' as 'Escape Key'");

SYSCTL_INT(_keyremap4macbook, OID_AUTO, shift2escape, CTLTYPE_INT|CTLFLAG_RW,
           &(org_pqrs_driver_KeyRemap4MacBook::config_shift2escape),
           0,
           "Remap 'ShiftR Key' as 'Escape Key'");

SYSCTL_INT(_keyremap4macbook, OID_AUTO, debug, CTLTYPE_INT|CTLFLAG_RW,
           &(org_pqrs_driver_KeyRemap4MacBook::config_debug),
           0,
           "Output Debug Messages");

SYSCTL_STRING(_keyremap4macbook, OID_AUTO, version, CTLFLAG_RD,
              org_pqrs_driver_KeyRemap4MacBook::config_version,
              0,
              "Output Version");
この後、カーネル拡張の init() と free() にて sysctl_register_oid, sysctl_unregister_oid を呼びます。
// KeyRemap4MacBook.cpp

bool
org_pqrs_driver_KeyRemap4MacBook::init(OSDictionary *dict)
{
  ...
  sysctl_register_oid(&sysctl__keyremap4macbook);
  sysctl_register_oid(&sysctl__keyremap4macbook_return2option);
  sysctl_register_oid(&sysctl__keyremap4macbook_shift2escape);
  sysctl_register_oid(&sysctl__keyremap4macbook_debug);
  sysctl_register_oid(&sysctl__keyremap4macbook_version);
  ...
}

void
org_pqrs_driver_KeyRemap4MacBook::free(void)
{
  ...
  sysctl_unregister_oid(&sysctl__keyremap4macbook);
  sysctl_unregister_oid(&sysctl__keyremap4macbook_return2option);
  sysctl_unregister_oid(&sysctl__keyremap4macbook_shift2escape);
  sysctl_unregister_oid(&sysctl__keyremap4macbook_debug);
  sysctl_unregister_oid(&sysctl__keyremap4macbook_version);
  ...
}
こうすると config_return2option といった変数で設定値を読むことが可能になります。

パッケージング

はじめに

カーネル拡張の場合.app をドラッグしてインストールというわけにいかないので、 パッケージングをする必要があります。

今の Mac OS X では統一的なアンインストールの機構が無いので、 出来るだけ消去しやすいように一箇所にファイルをまとめるとか、アンインストール用のスクリプトを付属させるとかの工夫をしましょう。

パッケージング

パッケージングについては Kernel Extension Concepts: Packaging Your KEXT for Distribution and Installation が詳しいです。

上記ドキュメントを見ながら最初は PackageMaker の GUI でパッケージングを行ってみて、 それからはコマンドラインで作っていくと良いかと思います。

コマンドラインでパッケージングを行う場合、Info.plist と Description.plist が追加で必須となります。 GUI で作ったパッケージの中からコピーしてきたり、 KeyRemap4MacBook からコピーして編集すると楽かと思います。

Comments for This Page.
Date: 2006-10-08 00:00 (JST)