ROS2講座03 colconでの実行ファイルのビルド

# 環境
この記事は以下の環境で動いています。

| 項目 | 値 |
|:-:|:-:|
| CPU | Core i5-8250U |
| Ubuntu | 22.04 |
| ROS2 | Humble |

# 概要
ROS2ではCmakeListに記述をすることで「ビルド」の設定をして、`colcon build`でビルドを行うことが出来ます。
しかし「ビルド」は複数のツールにまたがって行われるためにエラー時の解決や、目的の動作をする記述を調べることは容易ではありません。
ここではこの「ビルド」の設定について順を追って解説します。まず注釈として

* C++パッケージのビルドの設定のみを扱います。pythonパッケージやpure cmakeパッケージの話は扱いません
* ROS2パッケージの話のみを扱います。ROS1パッケージの話は扱いません。 
* Ubuntu上でのビルドのみを扱います。Win/Mac対応の話はしません。

今回は一番基本的な「実行ファイル」のビルドを見ていきます。

# ビルド処理の区分け
ビルド処理は以下の4つに分かれます。

* (1)プリプロセッサ  
`#include`等のプリプロセッサを処理します。
* (2)コンパイラ  
C++のコードをアセンブル言語に変換します。
* (3)アセンブラ  
アセンブル言語を機械語に変換します。これによって1つのcppファイルから1つのオブジェクトファイルが生成されます。
* (4)リンカ
関数呼び出しを買い替えつして複数のオブジェクトファイルを結合します。これによって実行ファイルやライブラリファイルを作成します。

また厳密には「ビルド」に含めませんが生成されたファイルや既存のファイルを再配置する「インストール」のステップがこの後に続きます。


# シンプルな実行ファイルをビルド

以下のようなmain関数で`printf`を実行するだけの例をビルドしてみましょう。


build_lecture/
├ src/
│  └ exec_sample1/
│     └ main.cpp
└ CMakeList.txt

```c++:build_lecture/src/exec_sample1/main.cpp
#include <stdio.h>
int main(int argc, char *argv[])
{
  printf("hello\n");
  return 0;
}
```

## c++コマンドでビルド
c++のソースファイルをビルドする基本はc++コマンドです。以下のcmakeでも最終的には内部的にc++コマンドを実行しています。

```shell
cd build_lecture/src/exec_sample1/
c++ main.cpp -o exec_sample1
./exec_sample1 
```

今回のc++コマンドでは(1)プリプロセッサ~(4)リンカまでの処理を1発で行っています。`exec_sample1`という実行ファイルが作成されます。

## cmakeでビルド

純粋なcmakeだけで先ほどのソースをビルドします。

```cmake:build_lecture/CMakeList.txt
cmake_minimum_required(VERSION 3.8)
project(build_lecture)

add_executable(exec_sample1
  src/exec_sample1/main.cpp)

install(
  TARGETS exec_sample1
  DESTINATION lib/${PROJECT_NAME}
)
```

* `add_executable`で実行ファイルの作成の指示をします。
* installは実行ファイルをフォルダに配置する指示です。今回の記述ではexec_sample1を`lib/${PROJECT_NAME}`に配置することになります。後ほどのcolconでの例で使います。

```shell
cd build_lecture/
mkdir build
cd build/
cmake ..
make
./exec_sample1 
```

## colconでビルド
cmakeのプロジェクトはそのままcolconで使えるので上記の「cmakeでビルド」のまま実行可能です。

```shell
cd ros2_ws
colcon build
ros2 run build_lecture exec_sample1
```

`colcon build`を実行すると`ros2_ws/install/build_lecture/lib/build_lecture/exec_sample1`の実行ファイルが生成・配置されます。`ros2 run`ではこのファイルを実行しています。

# 複数のcppファイルからなる実行ファイルをビルド

build_lecture/
├ include/
│  └ build_lecture/
│     └ exec_sample2/
│         └ printer.hpp
├ src/
│  └ exec_sample2/
│     ├ printer.cpp
│     └ main.cpp
└ CMakeList.txt

```c++:build_lecture/include/build_lecture/exec_sample2/printer.hpp
#include <stdio.h>
void print_data(void);
```

```c++:build_lecture/src/exec_sample2/printer.cpp
#include <build_lecture/exec_sample2/printer.hpp>
void print_data(void) {
  printf("execute print_data\n");
}
```

```c++:build_lecture/src/exec_sample2/main.cpp
#include <build_lecture/exec_sample2/printer.hpp>
int main(int argc, char *argv[])
{
  print_data();
  return 0;
}
```

## c++コマンドでビルド
複数のソースファイルからなるコードについては、cppファイルを1つずつビルドしてオブジェクトファイルを作成して、それらを最後にリンクします。

```shell
c++ -c src/exec_sample2/main.cpp -I include/ 
c++ -c src/exec_sample2/printer.cpp -I include/
c++ main.o printer.o -o main
./main
```

* 1,2行目でそれぞれのソースファイルについて(1)プリプロセッサ~(3)アセンブラの作業をします。これによって`main.o`、`printer.o`の2つの実行ファイルを得ます
    * `-I`で`#include`での探索フォルダの追加の指定をします。
    * 例えば1行目の処理の指定ファイルだけでは、`print_data()`の実装が不十分です。このようにオブジェクトファイルでは実装が不十分でも処理は進みます。
    * それぞれのオブジェクトファイルにどのようなシンボルがあるかは`nm`コマンドで分かります。ざっくり解説するとTが定義済みのシンボル、Uが未定義のシンボルです。
        * シンボル名は関数名の前後に引数や返り値に対応する文字が追加されたものになります。これを名前修飾と呼びます。（ちなみに名前修飾のルールはコンパイラごとに違います）

```shell:シンボル情報の表示
$ nm main.o 
0000000000000000 T main
                 U _Z10print_datav
$ nm printer.o 
                 U puts
0000000000000000 T _Z10print_datav
```


* 3行目で(4)リンカの作業をしています。
    * この時に実装が不十分にならないように必要なだけのオブジェクトファイルを取り込む必要があります。
    * オフジェクトファイルが足りないと以下のように未定義シンボルが残るエラーが出ます。

```shell:未定義シンボルが残る例
$ c++ main.o 
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x14): undefined reference to `print_data()'
collect2: error: ld returned 1 exit status
```

## cmakeでビルド

純粋なcmakeだけで先ほどのソースをビルドします。

```cmake:build_lecture/CMakeList.txt
cmake_minimum_required(VERSION 3.8)
project(build_lecture)

add_executable(exec_sample2
  src/exec_sample2/main.cpp
  src/exec_sample2/printer.cpp
)
target_include_directories(exec_sample2
  PUBLIC include
)

install(
  TARGETS exec_sample2
  DESTINATION lib/${PROJECT_NAME}
)
```

```shell
cd build_lecture/
mkdir build
cd build/
cmake ..
make
./exec_sample1 
```

## colconでビルド
前の例と同じようにcmakeの例をそのままcolconでビルドできます。

```shell
cd ros2_ws
colcon build
ros2 run build_lecture exec_sample2
```


# 参考
* [c++のビルドとは](https://kamino.hatenablog.com/entry/c%2B%2B-principle-of-build-library#sec2)
* [nmコマンド](https://www.ibm.com/docs/ja/aix/7.2?topic=n-nm-command)
* [名前修飾](https://ja.wikipedia.org/wiki/名前修飾)

# 目次ページへのリンク
[ROS2講座の目次へのリンク](https://qiita.com/srs/items/5f0d765343ca78f524ed)