clangでC言語をコンパイル・フォーマット・静的解析


`2023/08/15` clang-tidyの説明のtypoを修正

# やること

- dockerコンテナ上にclangを使った環境を作成
- `Hello world`プログラムの作成とコンパイル、実行
- clang-formatを用いたコードの整形
- clang-tidyを用いた静的解析

# clangとは

クランと読みます。
CやC++向けのコンパイラフロントエンドです。
バックエンドのLLVMと合わせて、C言語で書かれたプログラムから実行ファイルを生成します。

[公式ページ](https://clang.llvm.org)
[Clang(wikipedia)](https://ja.wikipedia.org/wiki/Clang)

# 環境を作る

Dockerはすでにインストール済みの前提で説明します。 
まだの人はこちらを参照。
https://docs.docker.jp/engine/installation/

## docker containerの作成

以下のコマンドを実行して開発用コンテナを作成し接続します。

```bash
# 適当な開発用ディレクトリに移動
cd "path/to/working/dir"
# ベースイメージはなんでもいい。今回はdevianのslim版
docker pull debian:bookworm-slim
# コンテナを作成
docker run -dit -v $(pwd):/work -w /work --name clang-dev debian:bookworm-slim
# コンテナに入る
docker exec -it clang-dev bash
```
コンテナに入ったらパッケージをインストールします。
必要なのはこれ。

- clang
    - コンパイル時に使います
- clang-format
    - フォーマッタを使って整形します
- clang-tidy
    - 静的解析に使います

```bash 
apt update
apt install -y clang clang-format clang-tidy
```
最後にインストールできているかを確認するためにバージョン確認しましょう。
```bash 
clang -v
clang-format --version
clang-tidy --version
```

以上で環境構築は完了です。

# Hello worldの実行

作業ディレクトリに以下のファイルを作成します。
hello_worldしていないのは内緒。

```hello_world.c
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>

int main(void) {
int i;
char input;
printf("数字を入力してください\n>> ");
input=getchar();

if(isdigit(input)){
i=input-'0';
}

printf("入力されたのは %d です。\n", i);
return 0;
}
```
コンテナ内のコマンドラインから以下のコマンドでコンパイルを実施します。
```bash 
clang hello_world.c -o hello_world.out
```
`hello_world.out`というファイルができればOK。
最後にこの`hello_world.out`を実行します。
```bash 
./hello_world.out

# 動作例
# 数字を入力してください
# >> 1
#入力されたのは 1 です。
```

以上でコンパイルと実行は完了です。　

# clang-formatを用いたコードの整形

ここで先ほど使用したコードをもう一度見て見ましょう。

```hello_world.c
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>

int main(void) {
int i;
char input;
printf("数字を入力してください\n>> ");
input=getchar();

if(isdigit(input)){
i=input-'0';
}

printf("入力されたのは %d です。\n", i);
return 0;
}
```
インデントもされていないし、ちょっと見づらいですね。
ここで`clang-format`の出番です。
次のコマンドを実行して、もう一度hello_world.cを開いてみます。

```bash
clang-format -i hello_world.c 
```
```hello_world.c
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  int i;
  char input;
  printf("数字を入力してください\n>> ");
  input = getchar();

  if (isdigit(input)) {
    i = input - '0';
  }

  printf("入力されたのは %d です。\n", i);
  return 0;
}
```
このように適切にインデントされ、見やすくなったと思います。
`.clang-format`ファイルに設定を記述すると`{}`の位置やタブの文字数などを好きに設定することができます。
参考：[clang-formatをイイ感じに設定する](https://yasuharu519.hatenablog.com/entry/2015/12/13/210825)

# clang-tidyを用いた静的解析

上のコード、ちゃんと動いたと思うのですがバグが混入されています。気づきましたか？
入力に数字以外を入れると、初期化されていない変数`i`を出力しようとするので表示がバグります。　

```bash
./hello_world.out 
# 数字を入力してください
# >> a
# 入力されたのは -1401456832です。
```
こういう問題を未然に防ぐために、静的解析ツールが役立ちます。
以下のように実行します。 
```bash 
clang-tidy hello_world.c
```
```
clang-tidy hello_world.c
Error while trying to load a compilation database:
Could not auto-detect compilation database for file "hello_world.c"
No compilation database found in /work or any parent directory
fixed-compilation-database: Error while opening fixed database: No such file or directory
json-compilation-database: Error while opening JSON database: No such file or directory
Running without flags.
1 warning generated.
/work/hello_world.c:15:3: warning: 2nd function call argument is an uninitialized value [clang-analyzer-core.CallAndMessage]
  printf("入力されたのは %d です。\n", i);
  ^                                    ~
/work/hello_world.c:6:3: note: 'i' declared without an initial value
  int i;
  ^~~~~
/work/hello_world.c:11:7: note: Assuming the condition is false
  if (isdigit(input)) {
      ^
/usr/include/ctype.h:192:21: note: expanded from macro 'isdigit'
# define isdigit(c)     __isctype((c), _ISdigit)
                        ^~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/ctype.h:89:4: note: expanded from macro '__isctype'
  ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/work/hello_world.c:11:3: note: Taking false branch
  if (isdigit(input)) {
  ^
/work/hello_world.c:15:3: note: 2nd function call argument is an uninitialized value
  printf("入力されたのは %d です。\n", i);
  ^                                    ~
```

warningが残っていても動きはしますが、バグを引き起こす可能性があるので基本的には消しておきましょう。
　
以上。