LoginSignup
4
4

More than 5 years have passed since last update.

D 言語でデバイスを制御するクロスプラットフォームなラッパライブラリを作る技

Posted at

高尚かつ詐欺的なタイトルですけど、D 言語くんに免じてお許しを。
D 言語くん says ごめんね!

今、eject コマンドを使わずに、D 言語で CD ドライブを開閉したりするプログラムを書いています。そういう低レベル寄りな処理は D で書いたことがないわけですが、まずは Windows、そして Linux 向けに書いてみました。そして DMD の公式バイナリがある FreeBSD でも書いてみたのです。宗教上の理由で OS X はちょっと。

そんな俺が d-ejector(仮称)を書くにあたって使うことになった、普通の遊びで使うような高レベルプログラムではなじみの薄いであろう D 言語の機能を書き連ねます。

バインディングと移植

標準の低レベルライブラリである DRuntime ですが、core.sys 以下にプラットフォーム特有のヘッダを移植した連中が集っており、これらを import することで D 言語から ioctl システムコールなど、マニアックな関数を呼び出したりできます。奴らは DRuntime のコード側で extern(C) あるいは extern(Windows) などされており、コンパイラやリンカによってよしなに処理されるので、D ユーザは気軽に呼び出すことができます。もちろん C++ な奴らも呼び出せますし 2.069 からは Objective-C な奴らも呼び出せるようになったようです。マングリング関係のバグをちょくちょく見る気がしますが、見なかったことにしようという魂の選択をします。

とりあえず Windows 版の今後に期待

Windows に関して、現時点での DMD 2.069 系にはしょぼい数のライブラリしか含まれておらず、キーパーソンたる DeviceIoControl すら呼び出せないザマですが、次期メジャーリリースには dsource-bindings から Windows API に関するものがごっそりと移ってくる予定なので、別途それらを用意する必要がなくなりそうです。自分はその DUB パッケージ版たる windows-headers を使っています。

手動 htod

そんな Windows はもちろん、FreeBSD でも Linux でも、デバイス操作に使いたいようなマニアックなヘッダたちは DRuntime にありません。そのため、C のヘッダから #define された連中、構造体を必要に応じて部分移植しました。

#define ディレクティブとマクロ

テクニックとしては、Windows 用ヘッダのマネをして、次のような感じでマクロをテンプレートで置き換えています。FreeBSD の sys/ioccom.h の一部を変換した例です。

sys/ioccom.h
#define IOCPARM_SHIFT   13
#define IOCPARM_MASK    ((1 << IOCPARM_SHIFT) - 1) 
#define IOC_VOID    0x20000000 
#define _IOC(inout,group,num,len)   ((unsigned long) \
    ((inout) | (((len) & IOCPARM_MASK) << 16) | ((group) << 8) | (num)))
#define _IO(g,n)    _IOC(IOC_VOID,  (g), (n), 0)
ejector.d
enum IOCPARM_SHIFT= 13;
enum IOCPARM_MASK = (1 << IOCPARM_SHIFT) - 1;
enum IOC_VOID = 0x20000000;
enum _IOC(uint inout_, uint group, uint num, uint len) =
    uint(inout_ | ((len & IOCPARM_MASK) << 16) | (group << 8) | num);
enum _IO(uint g, uint n) = _IOC!(IOC_VOID, g, n, 0);

このような enum な eponymous テンプレートは、コンパイル時処理が好きな自分はよく使うので自然に感じられます。また、D 版の簡潔さを見ると、C プリプロセッサの闇を感じます。

構造体

普通の構造体であれば普通に書き直します。

ビットフィールド

普通でない構造体としてビットフィールドが出てきたので、std.bitmanip.bitfields を使いました。Windows の FEATURE_HEADER 構造体の例です。

ntddmmc.h
typedef struct _FEATURE_HEADER {
  UCHAR FeatureCode[2];
  UCHAR Current  :1;
  UCHAR Persistent  :1;
  UCHAR Version  :4;
  UCHAR Reserved0  :2;
  UCHAR AdditionalLength;
} FEATURE_HEADER, *PFEATURE_HEADER;
ejector.d
struct FEATURE_HEADER
{
    UCHAR[2] FeatureCode;
    import std.bitmanip : bitfields;
    mixin(bitfields!(
        UCHAR, "Current", 1,
        UCHAR, "Persistent", 1,
        UCHAR, "Version" , 4,
        UCHAR, "Reserved0", 2
    ));
    UCHAR AdditionalLength;
}

機械的な置き換えで済みました。使わないので、ポインタ型の alias は作っていません。

フレキシブル配列メンバ

ここまでは容易な変換でしたが、C99 のフレキシブル配列メンバ (flexible array member) では悩みました。そもそも C にこんなのがあったんですかーと言いたかったわけですが。例として Windows の GET_CONFIGURATION_HEADER 構造体を見てみます。

ntddmmc.h
typedef struct _GET_CONFIGURATION_HEADER {
  UCHAR DataLength[4];
  UCHAR Reserved[2];
  UCHAR CurrentProfile[2];
  UCHAR Data[]; // flexible array member
} GET_CONFIGURATION_HEADER, *PGET_CONFIGURATION_HEADER;

この構造体の末尾にある Data メンバがそれなのですが、この例では GET_CONFIGURATION_HEADER のサイズは UCHAR が 8 個分で 8 バイト(隙間なし)なのであって、つまり Data メンバの分のサイズはゼロという変な構造体です。使用時にはこの構造体のサイズに加えて Data 配列の分として使いたいサイズを含めて確保したメモリ領域のポインタを PGET_CONFIGURATION_HEADER にキャストしてうまいことやるのだと思います。

やりたいのは、そんなメモリ領域を C 側の関数に渡し、Data を含む各メンバに値を入れてもらうというようなことです。また、Data に入れてもらうデータ構造は別の構造体で定義されています。一例としてそれは FEATURE_DATA_REMOVABLE_MEDIUM 構造体があり、その場合、Datasizeof(FEATURE_DATA_REMOVABLE_MEDIUM) バイト分、つまり 8 要素の UCHAR 配列であってほしいということです。その構造体です。

ntddmmc.h
typedef struct _FEATURE_DATA_REMOVABLE_MEDIUM {
  FEATURE_HEADER Header;
  UCHAR          Lockable  :1;
  UCHAR          Reserved1  :1;
  UCHAR          DefaultToPrevent  :1;
  UCHAR          Eject  :1;
  UCHAR          Reserved2  :1;
  UCHAR          LoadingMechanism  :3;
  UCHAR          Reserved3[3];
} FEATURE_DATA_REMOVABLE_MEDIUM, *PFEATURE_DATA_REMOVABLE_MEDIUM;

D ではこうです。

ejector.d
struct FEATURE_DATA_REMOVABLE_MEDIUM
{
    FEATURE_HEADER Header;
    import std.bitmanip : bitfields;
    mixin(bitfields!(
        UCHAR, "Lockable", 1,
        UCHAR, "Reserved1", 1,
        UCHAR, "DefaultToPrevent", 1,
        UCHAR, "Eject", 1,
        UCHAR, "Reserved2", 1,
        UCHAR, "LoadingMechanism", 3
    ));
    UCHAR[3] Reserved3;
}

では、最初に挙げた GET_CONFIGURATION_HEADER 構造体を D で書くとどうなるのかということです。アイディアその 1。

idea1.d
struct GET_CONFIGURATION_HEADER(T) /* if (T は適切な型) */
{
    UCHAR[4] DataLength;
    UCHAR[2] Reserved;
    UCHAR[2] CurrentProfile;
    T Data; // サイズは T.sizeof
}

//// 使用時 ////

GET_CONFIGURATION_HEADER!FEATURE_DATA_REMOVABLE_MEDIUM s1;

// 適当な C の関数に渡して書き込んでもらう
SomeWriter(&s1, s1.sizeof); // 先頭のポインタと書き込みサイズを指定

writeln(s1.Data);

テンプレートを使って、D っぽくスマートに書けています。構造体テンプレートをインスタンス化した時点でサイズが決まり、フレキシブル配列メンバとかいう謎の存在すら感じさせません。

そしてアイディア 2。

idea2.d
struct GET_CONFIGURATION_HEADER
{
    UCHAR[4] DataLength;
    UCHAR[2] Reserved;
    UCHAR[2] CurrentProfile;
    UCHAR[0] Data; // サイズ 0 の静的配列でサイズは 0
}

//// 使用時 ////

import std.conv : emplace;
auto p = new void[GET_CONFIGURATION_HEADER.sizeof +
    FEATURE_DATA_REMOVABLE_MEDIUM.sizeof]; // Data 分を含めて確保
auto ps2 = emplace!GET_CONFIGURATION_HEADER(p); // 配列全体
auto pdata = emplace!FEATURE_DATA_REMOVABLE_MEDIUM(
    p[GET_CONFIGURATION_HEADER.sizeof .. $]); // 配列後半

// 書き込んでもらう
SomeWriter(ps2, p.length); // ps2 はポインタ

writeln(*pdata); // pdata はポインタ

// Data メンバとして触れるのは面倒
// writeln(*(cast(FEATURE_DATA_REMOVABLE_MEDIUM*)ps2.Data.ptr));

見た目がややこしいです。std.conv.emplace をトリッキーな感じで使ってみました。適当な SomeWriter 関数に渡すのはバッファ全体で、そのバッファ後半を FEATURE_DATA_REMOVABLE_MEDIUM 構造体として扱うというイメージです。Data メンバのサイズがゼロなのを悪用して Data メンバ相当の別の構造体を直結していますが、実際に直接 Data メンバとして後半を触るのは無理で、pdata から触ることになります。そのために emplacepdata を作ったのでした。

この emplace による方法は malloc して構造体ポインタにキャストするイメージであり、C 側の意図を汲んだものという雰囲気を漂わせつつも、これはこれで D らしい方法だと思います。どっちでも意図したとおりに動けばいいのですが、わかりやすそうな前者を採用した後に、可読性の低い後者に転向しました。元々は生の ubyte 配列を使っていましたが。

ただ、どちらのアイディアも、アラインメントを気にしないといけない、という取扱注意ポイントがありますが、今使いたい構造体たちはうまく動くことが期待される構造なので気にしないでおきます。C99 の仕様は知りませんが、一般に C のフレキシブル配列メンバを含む構造体を D ではどう表現できるかという観点から考えたとき、こういうアプローチもあるよという提案にすぎません。

それでもどうにもならないデータ構造

FreeBSD で、トレイが開閉できるか、トレイが開いているかということを調べるために、CAM とかいうのを使っています。そのために使う ccb という共用体はいろいろな構造体や共用体が非常にややこしく入れ子になっており、ロマンティックが止まらないため、手動で移植するのは断念しました。

そこで C に逃げました。C でラッパ関数を書いて D から呼び出すのです。しかしながら、どうも納得がいきません。

妥協の一手が天から降ってきました。外側の ccb 共用体のサイズと、その内側の使いたいメンバのオフセットがわかれば D の ubyte 配列でなんとでもなるのではないか、と。構造体のメンバにアクセスするかわりに配列の当該オフセットの要素にアクセスするのです。C の関数に渡すときはキャストでごまかせるのです。C 側で定義されている構造体について、D 側ではそのメンバには触れずにポインタ型だけが必要な場合(C 関数のプロトタイプ宣言とか)は、不透明構造体 (opaque struct) とかいうのを使い、本体なしで struct S; のように宣言しておくだけで S* 型が扱えるので便利です。

こうなれば C のコードは必要なサイズやオフセットを enum HOGE = XXX; という D のコード片の形式で出力するだけのものでよく、DUB にはその C コードをコンパイルさせて実行させて出力をファイルに書き出させて、D ではそのファイルを文字列インポートし、さらに文字列ミックスインすればよくなるわけです。そして C のラッパ関数相当のものを D で書き直して対処しました。

そんなこんなで、ioctlDeviceIoControl に渡すべきものを D 側に用意しました。

参考になりそうなページ

バージョン条件コンパイル

若さのあまり、1 つのモジュールに 3 つのプラットフォーム用の実装をぶち込んでしまった以上、ターゲット環境に応じてコンパイルされてほしい部分が異なってきます。その切り分けのために version(Windows) などのバージョン条件を濫用しています。

一般に version(Posix) な部分は Linux でも FreeBSD でもコンパイルされるのですが、このライブラリでは非対応の OS X や Solaris といった Posix 環境ではコンパイルされてほしくありません。それゆえ、version(linux || FreeBSD) などと書ければありがたいのですが、そんな構文はありません。

そこで、バージョン指定です。version = hogehoge; と書いておけば、hogehoge というバージョン識別子が使えるようになります。

つまり、次のようにしておけば、version(linux || FreeBSD) の代用として扱えます。

// 準備
version(linux)
{
    version = Ejector_Posix;
}
version(FreeBSD)
{
    version = Ejector_Posix;
}

// 使用
version(Ejector_Posix)
{
    // Linux, FreeBSD 共用

    version(linux)
    {
        // Linux 専用
    }
}

version の多用でコードの可読性は落ちる気がしますが、DRY 優先と割り切りました。今は version(Ejector_Posix) な実装と version(Windows) な実装とは同一ファイル内にあって、それぞれをはっきり分けてはありますが、将来的には DUB の機能をうまく使って別ファイルにするなど、きれいにしておきたいところです。

GitHub のページと、参考までに実装メモを置いておきます。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4