起動するまでの長い道のり D言語編(3) 標準ライブラリ格闘の巻(1)

 D言語を使って(あれを「使って」と言って良いなら……)、シリアル・ポートを叩くことに成功した。
 しかし今のコードはカッコ悪い。IOを直に叩いているだけで、printfでHello,World!するのとあまり変わらない。これから先シリアル・ポートを叩く機会もたくさんあるだろうから、シリアル・ポート操作用の処理をまとめておきたい。今回はそれにチャレンジし、壁にぶちあたる(笑)。

今回のソース

シリアル・ポート・クラスの作成

 さて、シリアル・ポートの処理をまとめる。しかもオブジェクト指向らしくクラスでラップする。ポート番号をメンバ変数に持ち、初期化はコンストラクタ・入出力や待機処理はメンバ関数で行わせる。定数は全部enumで定義する。
 で、大体以下のようになった。

module outlandish.os.serial;

import std.stdint;
import outlandish.os.io;

/// COMポート。
enum Port {
    COM1 = 0x3f8,
    COM2 = 0x2f8,
}

/// ボーレート。
enum Baudrate : uint16_t {
    BPS_115_2K = 0x0001,
    BPS_57_6K = 0x0002,
    BPS_38_4K = 0x0003,
    BPS_19_2K = 0x0006,
    BPS_9600 = 0x000c,
    BPS_4800 = 0x0018,
    BPS_2400 = 0x0030,
    BPS_1200 = 0x0060,
    BPS_600 = 0x00c0,
    BPS_300 = 0x0180,
}

/// 割り込みマスク。
enum InterruptEnable : uint8_t {
    RX_DATA = 0b0001,
    TX_DATA = 0b0010,
    RX_LINE = 0b0100,
    MODEM   = 0b1000,
    NONE    = 0b0000,
}

/// ライン・ステータス。
enum LineStatus : uint8_t {
    RX_ERROR            = 0b1000_0000,
    COMPLETE_TRANSMIT   = 0b0100_0000,
    EMPTY_REGISTER      = 0b0010_0000,
    RECEIVE_BREAK       = 0b0001_0000,
    FRAMING_ERROR       = 0b0000_1000,
    PARITY_ERROR        = 0b0000_0100,
    RECEIVE_OVERFLOW    = 0b0000_0010,
    RECEIVE_DATA        = 0b0000_0001,
}

/// モデム・ステータス。
enum ModemStatus : uint8_t {
    DCD_ASSERT  = 0b1000_0000,
    RI_ASSERT   = 0b0100_0000,
    DSR_ASSERT  = 0b0010_0000,
    CTS_ASSERT  = 0b0001_0000,
    CHANGE_DCD  = 0b0000_1000,
    RI_NEGATE   = 0b0000_0100,
    CHANGE_DSR  = 0b0000_0010,
    CHANGE_CTS  = 0b0000_0001,
}

/// ライン・コントロール。
enum LineControl : uint8_t {
    ACCESS_BAUDRATE = 0b1000_0000,
    BREAK           = 0b0100_0000,
    FIXED_PARITY    = 0b0010_0000,
    EVEN_PARITY     = 0b0001_0000,
    ENABLE_PARITY   = 0b0000_1000,
    STOP_BITS_2     = 0b0000_0100,
    DATA_BITS_8     = 0b0000_0011,
}

/// モデム・コントロール。
enum ModemControl : uint8_t {
    LOOP_BACK           = 0b0001_0000,
    INTERRUPT_ENABLE    = 0b0000_1000,
    LOOP_BACK2          = 0b0000_0100,
    ASSERT_RTS          = 0b0000_0010,
    ASSERT_DTS          = 0b0000_0001,
}

/// シリアル・ポート操作関数。
class SerialPort {
    
    /// ポート・ボーレート・ラインコントロール・割り込みマスクを指定して初期化する。
    this(Port p,
         Baudrate bps,
         LineControl line = LineControl.DATA_BITS_8,
         uint8_t modem = ModemControl.INTERRUPT_ENABLE
             | ModemControl.ASSERT_RTS
             | ModemControl.ASSERT_DTS,
         InterruptEnable itr = InterruptEnable.NONE) {
        port_ = p;
                
        // ボーレート設定。
        outp(ioPort(Register.LINE_CONTROL), LineControl.ACCESS_BAUDRATE);
        outp(ioPort(Register.BAUDRATE_HIGH), cast(uint8_t)(bps >> 8u));
        outp(ioPort(Register.BAUDRATE_LOW), cast(uint8_t)(bps & 0x00ff));
        
        // ライン・コントロール設定。
        outp(ioPort(Register.LINE_CONTROL), line);
        
        // モデム・コントロール設定。
        outp(ioPort(Register.MODEM_CONTROL), modem);
        
        // 割り込みマスク設定。
        outp(ioPort(Register.INTERRUPT_ENABLE), itr);
    }
    
    /// データの出力。
    void put(uint8_t val) {outp(ioPort(Register.TRANSMIT_DATA), val);}
    
    /// データの入力。
    uint8_t get() {return inp(ioPort(Register.RECEIVE_DATA));}
    
    /// ライン・ステータス。
    uint8_t lineStatus() {return inp(ioPort(Register.LINE_STATUS));}
    
    /// モデム・ステータス。
    uint8_t modemStatus() {return inp(ioPort(Register.MODEM_STATUS));}
    
    /// 割り込みID。
    uint8_t interruptId() {return inp(ioPort(Register.INTERRUPT_ID));}
    
    /// 転送が終了したかどうか。
    bool isCompleteTransmit() {
        return (lineStatus() & LineStatus.COMPLETE_TRANSMIT) != 0;
    }
    
private:
    
    /// 各レジスタ。
    enum Register : uint16_t {
        RECEIVE_DATA = 0x0,     /// 受信データ。
        TRANSMIT_DATA = 0x0,    /// 送信データ。
        BAUDRATE_LOW = 0x0,     /// ボーレート下位。
        BAUDRATE_HIGH = 0x1,    /// ボーレート上位。
        INTERRUPT_ENABLE = 0x1, /// 割り込みマスク。
        INTERRUPT_ID = 0x2,     /// 割り込みID。
        FIFO_CONTROL = 0x2,     /// FIFOコントロール。
        LINE_CONTROL = 0x3,     /// ライン・コントロール。
        MODEM_CONTROL = 0x4,    /// モデム・コントロール。
        LINE_STATUS = 0x5,      /// ライン・ステータス。
        MODEM_STATUS = 0x6,     /// モデム・ステータス。
        SCRATCH = 0x7,          /// スクラッチ・パッド。
    }
    
    /// IOポート番号を得る。
    uint16_t ioPort(Register f) {return cast(uint16_t)(port_ + f);}
    
    /// ポート番号。
    Port port_;
}

 で、startupの中で呼び出す。

module outlandish.os.startup;

import outlandish.os.serial;

/// 起動直後の処理。
extern(C) void startup() {
    const COM1 = 0x300;
    
    // ボーレートを19200bpsに設定。
    scope serial = new SerialPort(Port.COM1, Baudrate.BPS_19_2K);
    
    foreach(c; "Hello,World!\r\n") {
        serial.put(cast(ubyte) c);
    }
    
    // 止まる。
    asm {hlt;}
}

 ポート番号の値を0x3f8まで定義するようにしたのは、ほさんのアドバイスのおかげだ。ありがとうございます。
 で、意気揚々とmakeしてみる。ちゃんとコンパイルが通って……ぎゃー!!

 なんだこの膨大な数のリンクエラーは!
 そう。これが今まで散々匂わせてきた標準ライブラリの壁そのものだ……。
 D言語は(C++とかJavaもだけど)、言語自身の機能が標準ライブラリに依存していたりする。逆に言えば、標準ライブラリがないと言語の機能が使えない。単純な関数や計算式ならば標準ライブラリを使わずにビルドできるが、クラスや動的メモリ確保(newとdelete)を使ったりする場合はどうしても標準ライブラリが必要になる。
 が、標準ライブラリは手元にない。そもそもWindowsLinuxといった既存のプラットフォームではない、まったく新しいプラットフォームにD言語を移植しようとしているのだ。我々がやろうとしていることは実はそういうことなのだ……。
 で、どうするか。作る。なければ作ればいい。標準ライブラリの機能を、一生懸命一つずつ移植していくのだ!
 さあやろう。みんな俺について来い!

戦略を練る

 とはいえ標準ライブラリ丸ごとなんてやっぱりハードルが高い。そもそもあまり評判のよくないPhobosを移植するなんて……(ヲイ)。なので、ここではとりあえずクラスが使えたりするくらいの最小限の実装に留める。
 標準ライブラリに依存している言語機能は、まとめると以下の通りだ。

  • 動的型情報(RTTI)
  • 動的メモリ割り当て(GC・new・delete。D言語では動的配列操作も含む)
  • 連想配列
  • 例外処理

 このうち後半3つはOSの実装にあまり必要ないと思う。まさかカーネルGCなんて動かさないし、連想配列だって多分使わないし、例外処理なんて重いエラー処理手法も使わないに越したことはない。だから、下の3つは使わないことで解決させる
 で、実は試してみて分かったことだが、動的型情報についてはクラスや構造体やenumを定義した時点で勝手に作られてしまう。ので、これはある意味必須だ。ただ動的型情報の実装の仕方をうまく工夫すると、下3つを使わないままで利用できる。
 ちなみに、上のリストの用語が分からない人はぐぐろう

次回予告

 実はSourceForgeに上がっているソース・コードは既に標準ライブラリ移植済みだったりする……。まあともかく次回は辛く厳しい標準ライブラリ移植物語を書く。面倒な人はソースを取得するだけで済ませよう。