D言語をC言語にする利点
「特集 D言語組み込みプログラミング入門」1では、clangを使って直接ARM向けのバイナリを出力するという方法を用いました。
ここでお話しするのは、D言語から一度C言語のソースコードにトランスパイル2することによって、どんなマイコンでもC言語のコンパイラさえあればバイナリが得られるようにするというものです。
用意するもの
使用するものは以下の通りです。
-
LDC3
LLVMベースのD言語コンパイラ。
これをつかって、*.ll
または*.bc
っていうファイルを作ります。 -
llvm-cbe4
LLVM C言語バックエンド。
*.ll
や*.bc
から*.cbe.c
というC言語ソースファイルを生成します。 - C言語のコンパイラやリンカ、オブジェクトコンバータなど
マイコンメーカ提供のコンパイラなど。上記ツイートの例ではgccを例として挙げています。
*.cbe.c
からバイナリ(*.exe
,*.elf
,*.bin
,*.s
,*.mot
,*.hex
など)を得ます。
本稿後半においては、RL78用のビルド環境としてCS+(CC-RL)5によるビルド例を紹介します。
llvm-cbeのビルド
Windowsユーザーの私はこの作業に非常に苦労しました。Ubuntu(WSL)ではあっさりビルドできました。Linux使ってる人はllvm-cbeの公式ページ4にビルド方法が載っているので、そのままやればいいと思います。
Windowsで私がビルドに成功したパターンは2パターンあって、MinGWを使う方法と、Visual Studio(cl.exe)6を使った方法です。ここではVisual Studioを使った方法を紹介します。
Visual Studio BuildTools 2019
Visual Studioといっても、フル機能をインストールする必要はありません。コマンドラインからビルドを行うので、BuildToolsで十分です。
Visual Studio BuildTools 2019は、以下のURLからインストーラをダウンロードしてインストールします。
https://aka.ms/vs/16/release/vs_buildtools.exe
上記URLは、Dockerとかのコンテナにインストールする際に使うものだそうです。7
CMake
CMakeの公式ページから8Windows版のCMakeをダウンロードしてきて、ビルドの際にはパスを通します。
また、ビルドの際にはコマンドラインも使えますが、今回はcmake-guiでビルド時の設定を行います。
Ninja
Ninjaは公式ページ9からWindows版をダウンロードして、ビルドの際にはパスを通します。
ソースコードの修正
Cloneしたままの状態ではコンパイルエラーが出てしまいます。(2019/7/7時点)
Visual Studioでは、 alloca()
関数を使う場合は #include <malloc.h>
が必要なようです。
Issueで報告があります10ので、そのうち修正されるかもしれません。
#if defined(_MSC_VER)
#include <malloc.h>
#endif
ビルド
コマンドラインでビルドを行います。
Visual Studio以外の必要なものは C:\app
以下にインストールし、ビルドは C:\work\llvm-cbe
以下で行うものとして説明します。
"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\Tools\VsDevCmd.bat" -arch=x64
PATH=C:\app\CMake\bin;C:\app\Ninja;%PATH%
cd /D C:\work\llvm-cbe
mkdir build
git clone git clone https://github.com/llvm-mirror/llvm
cd llvm
git checkout release_70
cd projects
git clone git clone https://github.com/JuliaComputing/llvm-cbe
cd ../../build
cmake-gui
これで、CMakeの設定をGUIから行います。コマンドラインでも必要なものをすべて指定すればできますが、引数がとても多くなりますので、GUIから行った方がラクです。
CMakeの設定ができたらあとはビルドするだけです。先程のコマンドプロンプトを引き続き使用して以下を入力します。
ninja llvm-cbe
MinGWを使った場合より使用するRAMもコンパイル後のバイナリサイズも小さいので、オプションやビルド後の処理等は特に必要ありませんでした。
トランスパイル
以下の工程でD言語のソースコードをC言語に変換します。
D言語のソースコードを、-output-ll -betterC
でコンパイルします。これで*.ll
が得られます。
*.bc
が欲しい場合は-output-bc -betterC
でコンパイルします。*.ll
でも*.bc
でも内部の表現方法(前者はテキスト、後者はバイナリ)が違うだけで、根本は同じです。
以下のコードは、wait500ms
, outputLED
という関数がそれぞれC言語によって記載されている前提で、それを使ってメイン関数でLチカさせるプログラムです。
module app;
extern (C) void wait500ms();
extern (C) void outputLED(ubyte cmd);
extern (C) void d_main() {
while (1) {
outputLED(1);
wait500ms();
outputLED(0);
wait500ms();
}
}
*.ll
ファイルを作成します。
ldc2 -output-ll -betterC source/app.d -o app.ll
得られた*.ll
, *.bc
をllvm-cbeで*.cbe.c
にトランスパイルします。
llvm-cbe app.ll -o app.cbe.c
コンパイルとビルド
今回は、ルネサスのCS+5を使ってRL78マイコン(RL78/G11 型式R5F1058AALA)11でのビルドを試してみます。RL78マイコンはARM以外のLLVMがバックエンドを持っていないアーキテクチャですので、C言語へのトランスパイルの効果が活きる環境といえます。
llvm-cbeで吐き出した*.cbe.c
のファイルをRL78用コンパイラのCC-RLでコンパイルします。
評価ボードとして秋月電子通商より販売されている「RL78/G11 スティック型評価ボード」12を使用しました。
C言語部分の実装
前述のD言語のコードでは、wait500ms
, outputLED
の関数がないため、それぞれC言語で記載します。
最近のルネサスのマイコンでは、GUIによるコードジェネレーターが充実しているので、生成されたC言語コードの適切な箇所に関数を記載していきます。
タイマ設定
/** Renesas' maker provided function */
static void __near r_tau0_channel0_interrupt(void) {
/* Start user code.
Do not edit comment generated here */
wk_cnt_tim500ms++;
if (wk_cnt_tim500ms > 1000) {
wk_cnt_tim500ms = 0;
wk_flg_tim500ms = 1;
}
/* End user code.
Do not edit comment generated here */
}
/* Start user code for adding.
Do not edit comment generated here */
void wait500ms(void) {
while (wk_flg_tim500ms == 0) {
R_WDT_Restart();
}
wk_flg_tim500ms = 0;
}
/* End user code.
Do not edit comment generated here */
ポート設定
/* Start user code for adding.
Do not edit comment generated here */
void outputLED(uint8_t cmd) {
P5_bit.no6 = !cmd;
}
/* End user code.
Do not edit comment generated here */
メイン関数の呼び出し
/** Renesas' maker provided function */
void main(void) {
R_MAIN_UserInit();
/* Start user code.
Do not edit comment generated here */
d_main();
/* End user code.
Do not edit comment generated here */
}
/** Renesas' maker provided function */
static void R_MAIN_UserInit(void) {
/* Start user code.
Do not edit comment generated here */
R_TAU0_Channel0_Start();
EI();
/* End user code.
Do not edit comment generated here */
}
こんな感じにタイマ設定、ポート設定、メイン関数の箇所に関数を定義しました。
これだけでビルドできれば良いのですが、CS+(CC-RL)だとこれだけではビルドすることができませんでした。生成される*.cbe.c
には、#define __noreturn __attribute__((noreturn))
というものが定義されるようなのですが、CS+(CC-RL)ではこれを解釈することができないようです。したがって、CS+でincludeされるstdint.hなどのヘッダファイルに以下のようにこれを無視するようdefineを付け加えます。
#define __attribute__(x)
これですべての準備が整いました。
先ほど用意した app.cbe.c
ファイルをビルド対象に指定して、ビルドを行います。CS+ですと、F7キーを押下(またはメニューやツールバーからビルドを選択)することでビルドできます。
警告はいくつか出ますが、ビルド成功し、このままICEなどのデバッガで接続したり、プログラムライタでバイナリをマイコンへダウンロード(書き込み)することでプログラムが動作し、LEDが点滅します。
DUBを使う
基本的な原理としてはここまでに解説した内容で終わりなのですが、D言語を使っているとビルドに便利なのがdubです。今回においても、 app.cbe.c
を生成するところまではdubで行うことができます。以下に設定例を記載します。
{
"authors": [ "SHOO" ],
"copyright": "Copyright © 2019, SHOO",
"description": "A minimal D application.",
"license": "public domain",
"name": "rl78_test",
"targetName": "app.ll",
"buildTypes": {
"release": {
"buildOptions": [
"releaseMode", "inline",
"optimize", "noBoundsCheck", "betterC"],
"dflags-ldc": ["--output-ll", "-conf="]
}
},
"postBuildCommands-windows": [
"llvm-cbe app.ll.exe -o d_llvm_cbe/app.cbe.c"
]
}
Windowsの場合、dubによって*.ll.exe
とexe拡張子が付け加えられてしまいますが、そのままllvm-cbeに渡すことができるようです。
また、dubでビルドする際には、以下のように --compiler=ldc2
を指定するようにします。
dub build -b=release --compiler=ldc2
上記のようにdubで設定を行った場合、source以下のすべてのファイルが一つのapp.cbe.c
にまとめられます。
この上で、再度C言語環境での(今回の例ではCS+で)ビルドを行うことで、バイナリを得ることができます。
最後に
今回のプロジェクトはGitHubのリポジトリ13にまとめてあります。
llvm-cbeをビルドし、パスを通した状態でdubでC言語のコード生成を行ったあとに、同梱のCS+用プロジェクトファイル(*.mtpj
)によってビルドできるようにしておきました。
ぜひご参考になさってください。