環境
この記事は以下の環境で動いています。
項目 | 値 |
---|---|
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
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("hello\n");
return 0;
}
c++コマンドでビルド
c++のソースファイルをビルドする基本はc++コマンドです。以下のcmakeでも最終的には内部的にc++コマンドを実行しています。
cd build_lecture/src/exec_sample1/
c++ main.cpp -o exec_sample1
./exec_sample1
今回のc++コマンドでは(1)プリプロセッサ~(4)リンカまでの処理を1発で行っています。exec_sample1
という実行ファイルが作成されます。
cmakeでビルド
純粋なcmakeだけで先ほどのソースをビルドします。
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での例で使います。
cd build_lecture/
mkdir build
cd build/
cmake ..
make
./exec_sample1
colconでビルド
cmakeのプロジェクトはそのままcolconで使えるので上記の「cmakeでビルド」のまま実行可能です。
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
#include <stdio.h>
void print_data(void);
#include <build_lecture/exec_sample2/printer.hpp>
void print_data(void) {
printf("execute print_data\n");
}
#include <build_lecture/exec_sample2/printer.hpp>
int main(int argc, char *argv[])
{
print_data();
return 0;
}
c++コマンドでビルド
複数のソースファイルからなるコードについては、cppファイルを1つずつビルドしてオブジェクトファイルを作成して、それらを最後にリンクします。
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が未定義のシンボルです。- シンボル名は関数名の前後に引数や返り値に対応する文字が追加されたものになります。これを名前修飾と呼びます。(ちなみに名前修飾のルールはコンパイラごとに違います)
-
$ nm main.o
0000000000000000 T main
U _Z10print_datav
$ nm printer.o
U puts
0000000000000000 T _Z10print_datav
- 3行目で(4)リンカの作業をしています。
- この時に実装が不十分にならないように必要なだけのオブジェクトファイルを取り込む必要があります。
- オフジェクトファイルが足りないと以下のように未定義シンボルが残るエラーが出ます。
$ 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_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}
)
cd build_lecture/
mkdir build
cd build/
cmake ..
make
./exec_sample1
colconでビルド
前の例と同じようにcmakeの例をそのままcolconでビルドできます。
cd ros2_ws
colcon build
ros2 run build_lecture exec_sample2