FPGA開発日記

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

DSLでビルドツールを自作する (11日目 Rumy-Makeを実際のプロジェクトに適用してみる)

この記事は「Qiita Advent Calendar 2019 DSLで自作ビルドツールを作ろう」の11日目の記事です。

11日目 Rumy-Makeを実際のプロジェクトに適用してみる

ここまで、Rumy-Makeの開発行ってきて基本的な機能は実装できたように思います。しかし、まだ足りないところがありそうな気がする。何となくこれでは物足りないような... モヤモヤするので、実際のプロジェクトに適用することによって問題を把握することにしたいと思います。

Rumy-Makeを適用するプロジェクト : RISC-V命令セットシミュレータSwimmer-RISCV

Rumy-Makeを適用するプロジェクトとしては、私が管理しているRISC-V命令セットシミュレータSwimmer-RISCVをターゲットにしたいと思います。Swimmer-RISCVは完全C++で書かれたRISC-Vシミュレータで、RISC-V向けにビルドされたLinuxを立ち上げることも可能なシミュレータです。ソースコードは数十個のファイルで構成されており、外部ライブラリも複数使用しています。

もともとSwimmer-RISCVはビルドツールとしてCMakeを使用しています。今回は、これをRumy-Makeで置き換えて、どの程度使い物になるのか見てみたいと思います。

  • Swimmer-RISCVはsoftfloatをサブプロジェクトとして使用している。

Swimmer-RISCVはsoftfloatをサブプロジェクトとして含んでいます。こちらも、ビルドにはCMakeを使ってビルドしています。CMakeであればこのプロジェクトをサブプロジェクトとして含むことができるのですが、Rumy-Makeでは複数Makeファイルを使ったサブビルドという概念がまだ実装できていません。したがって、とりあえずはsoftfloatをCMakeでビルドして、ビルド済みのライブライを使ってリンクするところまでを考えます。

  • riscv_cedar (RISC-V ISSのコアシミュレーション部分)はライブラリとして分離している。

Swimmer-RISCVは2段階のビルド構成を取っており、まずはRISC-V命令セットシミュレータのコア部分をライブラリとしてビルドしています。CMakeLists.txtでは、以下のように記述しています。

add_library (riscv_cedar
  ../src/riscv_pe_thread.cpp
  ../src/riscv_syscall.cpp
  ../src/riscv_fds.cpp
...
  ../src/memory_block.cpp
  ../src/mem_body.cpp
  ../src/gdb_env.cpp
)

これをそのままRumy-Makeに置き換えるのですが、Rumyにはadd-libraryのようなC言語のライブラリコンパイルをまとめるだけの高解機能を持っていません。そこで、単純にC++ソースファイルをコンパイルしてオブジェクトファイルを生成し、それをライブラリとしてまとめ上げるとという手順を取ります。以下のようにRumyファイルを記述しました。

obj_lists = [
  "riscv_pe_thread.o",
  "riscv_syscall.o",
  "riscv_fds.o",
...
  "memory_block.o",
  "mem_body.o",
  "gdb_env.o"
]
c_options = compile_options.join(' ').to_s
l_options = link_options.join(' ').to_s

obj_lists.each {|obj|
  make_target obj do
    src = "../src/" + obj.sub(".o", ".cpp")
    depends [src]
    executes ["g++ #{c_options} -c #{src} -o #{obj}"]
  end
}

make_target "libriscv_cedar.a" do
  depends obj_lists
  executes ["ar qc libriscv_cedar.a #{obj_lists.join(' ').to_s}"]
end

ソースファイルのオブジェクトをリストobj_listsで管理し、これらにループを適用してmake_targetを適用していきます。Rubyのループを使えば簡単に複数のターゲットを定義することができます。さらに、ライブラリlibriscv_cedar.aの生成には、依存ファイルとしてすべてのオブジェクトファイルをリストとして指定し、これを実行時にすべて渡します。obj_lists.join(' ').to_sと記述することで、["riscv_pe_thread.o", "riscv_syscall.o", ... "gdb_env.o"]という記述をriscv_pe_thread.o riscv_syscall.o ... gdb_env.oという文字列に置き換えることができ、これをリンクファイルとして指定します。これで、libriscv_cedar.aを作ることができます。

オプションの処理方法ですが、指定したいオプションを以下のようにリストとして管理し、これを文字列として展開してすべてのコンパイルコマンドに渡していきます。

compile_options = []
compile_options = compile_options + ["-DARCH_RISCV"]
compile_options = compile_options + ["-I../vendor/cmdline/"]
compile_options = compile_options + ["-I../vendor/softfloat/SoftFloat-3d/source/include/"]
...
c_options = compile_options.join(' ').to_s
  • バージョン情報表示のためのヘッダファイル生成

Swimmer-RISCVでは、--versionオプションを指定したときに、ビルド日時とgitのリビジョン番号について出力するようにしています。

$ swimmer_riscv --version
// Swimmer-RISCV
//  Version 20190920 Revision acc20b3
//  developed by msyksphinz <msyksphinz.dev@gmail.com>

これを実現しているのは、config.h.inというオリジナルファイルからconfig.hppというファイルを生成し、このヘッダファイルにリビジョン番号とビルド日時を挿入していることによります。

  • config.h.in
#define VERSION "@VERSION@"
#define REVISION "@REVISION@"

上記のヘッダテンプレートに対して、@VERSION@@REVISION@の置き換えを行う訳ですが、CMakeの場合は特殊変数として認識されるのですがRumy-Makeはその機能は存在していないので、単純に外部コマンドを呼び出して対応します。

build_date =    `date +%Y%m%d`.sub("\n", "")
build_version = `git rev-parse --short`.sub("\n", "")

config.hppを生成するために以下の:config_hppターゲットルールを作成しました。

make_target :config_hpp do
  executes ["sed 's/@VERSION@/#{build_date}/g' config.h.in |
             sed 's/@REVISION@/#{build_version}/g' > config.hpp"]
end

これらをまとめ上げて、最後にswimmer_riscvバイナリを生成するためのルールを生成します。swimmer_main.cppのみは上記のconfig.hppの生成が事前に必要なので、ルールを変更しました。

swimmer_obj_lists = [
  "swimmer_util.o",
  "riscv_bfd_env.o",
  "python3_env.o"
]


swimmer_obj_lists.each {|obj|
  make_target obj do
    src = "../src/" + obj.sub(".o", ".cpp")
    depends [src]
    executes ["g++ #{c_options} -c #{src} -o #{obj}"]
  end
}
make_target "swimmer_riscv" do
  global
  depends swimmer_obj_lists + ["swimmer_main.o"] + ["libriscv_cedar.a"]
  link_libs = ["-lbfd", "-lpython3.6m", "-lgmp", "-lgmpxx"].join(' ').to_s
  executes ["g++ #{l_options} #{swimmer_obj_lists.join(' ').to_s} \
            swimmer_main.o \
            libriscv_cedar.a \
            ../vendor/softfloat/build/libsoftfloat.a \
            -o swimmer_riscv \
            #{link_libs}"]
end

最後のswimmer_riscvルールが、バイナリを生成するためのルールです。libriscv_cedera.aと、`これまでに生成したオブジェクトをすべて結合してバイナリを作るルールにしています。

これを実行すると、時間がかかりますが無事にバイナリを生成することができました。とりあえず、ビルドツールとして最低限の仕事ができているようです。

ここまでの試行で、問題となったのは以下です。

  • C++ファイルをコンパイルするのにルールの記述が冗長すぎる。
    • RumyにC++専用の方言を用意して、C++→オブジェクトなどのルールは独自に与えてもよさそうです。
  • 外部のライブラリや、外部のRumyルールを呼び出せる専用の機能が欲しい。
    • softfloatなどの外部ライブラリを呼びに行くことができるルールがあると便利そうです。
  • 並列ビルドによるビルド高速化
    • 現在は単一CPUによるビルドしかサポートしていないので遅いですが、並列ビルドができるようになりたいです。かといって可能な限り並列に実行する構成にするとCPUを使い切ってしまうので、最大CPU数を制限するような方式を導入して並列ビルドを実現したいです。