FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RustでRISC-V命令セットシミュレータを作ろう (17. マクロの導入)

f:id:msyksphinz:20190224185310p:plain:w400

Rustで作る自作命令セットシミュレータの続き。アトミック命令をすべて実装したのはいいが、同じような記述を大量に並べてしまっておりどうも気持ちが悪い。

  • AMOADD.Wの実装
    fn execute_amoadd_w  (&mut self, inst: InstT)
    {
        let rs1 = Self::get_rs1_addr(inst);
        let rs2 = Self::get_rs2_addr(inst);
        let rd = Self::get_rd_addr(inst);

        let rs1_data_64 = self.read_reg(rs1);
        let rs2_data_64 = self.read_reg(rs2);

        let mem_addr = self.uext_xlen(rs1_data_64) as Addr64T;
        let rs2_data = Self::extend_sign(rs2_data_64, 31);

        match self.read_bus_word(mem_addr) {
            Ok(mem_data_64) => {
                let mem_data = Self::extend_sign(mem_data_64, 31);
                let ret = mem_data.wrapping_add(rs2_data);
                self.write_bus_word(mem_addr, ret as Xlen64T);
                self.write_reg(rd, mem_data);
            },
            Err(_result) => {},
        }
    }
  • AMOXOR.Wの実装
    fn execute_amoxor_w  (&mut self, inst: InstT)
    {
        let rs1 = Self::get_rs1_addr(inst);
        let rs2 = Self::get_rs2_addr(inst);
        let rd = Self::get_rd_addr(inst);

        let rs1_data_64 = self.read_reg(rs1);
        let rs2_data_64 = self.read_reg(rs2);

        let mem_addr = self.uext_xlen(rs1_data_64) as Addr64T;
        let rs2_data = Self::extend_sign(rs2_data_64, 31);

        match self.read_bus_word(mem_addr) {
            Ok(mem_data_64) => {
                let mem_data = Self::extend_sign(mem_data_64, 31);
                let ret = mem_data ^ rs2_data;
                self.write_bus_word(mem_addr, ret as Xlen64T);
                self.write_reg(rd, (mem_data as XlenT) as Xlen64T);
            },
            Err(_result) => {},
        }
    }

同じような記述を大量に並べてしまっている。これはメンテナンス性が良くないので、どうにかして修正していきたい。このためにはRustのマクロが使えそうだ。勉強してみる。

Rustのマクロは、C++のマクロとその機能は似ているが、より高機能だということだ。型や文法に対して異なるマクロをマッチさせて上手く当てはめることができる。以下の資料を読みながら勉強した。

qiita.com

今回は、レジスタ読み出しのためのビットフィールドの切り出しからレジスタリードまでの一連の流れをマクロに変換する。これで多少はコード量が短くなるはずだ。

        // 以下のコード群を1つのマクロ`get_rs1_rs2()`に置き換えたい。
        let rs1 = Self::get_rs1_addr(inst);
        let rs2 = Self::get_rs2_addr(inst);

        let rs1_data_64 = self.read_reg(rs1);
        let rs2_data_64 = self.read_reg(rs2);

チュートリアルを読みながらマクロを書いていく。まずはこんな感じにした。

macro_rules! get_rs1_rs2 {
    ($inst:expr) => {
        {
            let rs1 = Self::get_rs1_addr($inst);
            let rs2 = Self::get_rs2_addr($inst);
            let rs1_data_64 = self.read_reg(rs1);
            let rs2_data_64 = self.read_reg(rs2);
            (rs1_data_64, rs2_data_64)
        }
    }
}

呼び出し側は以下のように変換した。

        let (rs1_data_64, rs2_data_64) = get_rs1_rs2!(inst);

しかし、これだけでは上手く行かないようだ。どうもselfの参照ができないらしい。なるほど、C++のマクロと違って、そのまま文字列を置き換えるわけではないのだな。という訳で以下の資料を読みながらselfもマクロに渡すようにする。

stackoverflow.com

macro_rules! get_rs1_rs2 {
    ($self:ident, $inst:expr) => {
        {
            let rs1 = Self::get_rs1_addr($inst);
            let rs2 = Self::get_rs2_addr($inst);
            let rs1_data_64 = $self.read_reg(rs1);
            let rs2_data_64 = $self.read_reg(rs2);
            (rs1_data_64, rs2_data_64)
        }
    }
}
        let (rs1_data_64, rs2_data_64) = get_rs1_rs2!(self, inst);

これでコンパイルすることができた。テストとしても問題ないようだ。

しかしRustのマクロは評判通り便利だな。確かにコード量を上手く削減することができそうだ。