第一个模块编译:创建简单C++模块、编写BUILD文件、使用bazel build编译、运行测试
说实话,很多刚接触Apollo的朋友,第一关就卡在编译上。我记得自己第一次打开Apollo源码时,看着那一堆BUILD文件,心里直犯嘀咕——这玩意儿到底怎么玩?
别急,今天我们就从零开始,亲手创建一个C++模块,把它编译出来,再跑起来。你想想看,当你在终端看到自己的代码成功运行,那种感觉,嗯,挺爽的。
1. 准备工作:了解Apollo的模块结构
在动手之前,我们先搞清楚一件事:Apollo的模块到底长什么样?
我个人习惯把Apollo的每个模块看作一个独立的小世界。它有自己的代码、配置文件、依赖关系,还有最重要的——BUILD文件。这个BUILD文件就是Bazel的"施工图纸",告诉编译系统该怎么搭这个模块。
一个典型的Apollo模块目录结构是这样的:
my_module/
├── BUILD # Bazel编译规则
├── my_module.h # 头文件
├── my_module.cpp # 源文件
├── main.cpp # 主入口
└── conf/ # 配置文件目录(可选)
说白了,你只需要三个核心文件:头文件、源文件、BUILD文件。主入口文件看情况,如果你要写一个可执行程序,那就需要。
2. 创建你的第一个C++模块
好,我们开始动手。假设我们要写一个简单的计算模块,功能是计算两个数的和。别笑,虽然简单,但麻雀虽小五脏俱全。
先创建目录结构:
mkdir -p /apollo/modules/my_first_module
cd /apollo/modules/my_first_module
然后创建头文件 adder.h:
#ifndef MODULES_MY_FIRST_MODULE_ADDER_H_
#define MODULES_MY_FIRST_MODULE_ADDER_H_
namespace apollo {
namespace my_first_module {
class Adder {
public:
Adder() = default;
~Adder() = default;
// 计算两个整数的和
int Add(int a, int b);
};
} // namespace my_first_module
} // namespace apollo
#endif // MODULES_MY_FIRST_MODULE_ADDER_H_
再创建源文件 adder.cpp:
#include "modules/my_first_module/adder.h"
namespace apollo {
namespace my_first_module {
int Adder::Add(int a, int b) {
return a + b;
}
} // namespace my_first_module
} // namespace apollo
最后创建主入口文件 main.cpp:
#include <iostream>
#include "modules/my_first_module/adder.h"
int main() {
apollo::my_first_module::Adder adder;
int result = adder.Add(3, 5);
std::cout << "3 + 5 = " << result << std::endl;
return 0;
}
apollo::模块名::子模块名。
3. 编写BUILD文件——这才是关键
BUILD文件是Bazel的灵魂。没有它,你的代码就是一堆散落的文本文件。
创建 BUILD 文件:
load("//tools:cpplint.bzl", "cpplint")
package(default_visibility = ["//visibility:public"])
cc_library(
name = "adder",
srcs = ["adder.cpp"],
hdrs = ["adder.h"],
deps = [
"//cyber",
],
)
cc_binary(
name = "my_first_module",
srcs = ["main.cpp"],
deps = [
":adder",
],
)
cpplint()
我们来拆解一下这个BUILD文件:
| 指令 | 作用 | 我的理解 |
|---|---|---|
cc_library |
定义一个C++库 | 相当于把代码打包成"零件" |
cc_binary |
定义一个可执行程序 | 把零件组装成"成品" |
deps |
声明依赖关系 | 告诉Bazel:我需要谁 |
cpplint() |
代码风格检查 | Apollo的代码洁癖,习惯就好 |
deps里的顺序最好按照依赖的层级来排列,从底层到上层。
4. 使用bazel build编译
好了,现在万事俱备,只欠编译。打开终端,执行:
cd /apollo
bazel build //modules/my_first_module:my_first_module
这里解释一下这个命令:
//modules/my_first_module:目标路径,从源码根目录开始:my_first_module:目标名称,对应BUILD文件里的cc_binary的name
如果一切顺利,你会看到类似这样的输出:
INFO: Analyzed target //modules/my_first_module:my_first_module (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //modules/my_first_module:my_first_module up-to-date:
bazel-bin/modules/my_first_module/my_first_module
INFO: Elapsed time: 1.234s, Critical Path: 0.56s
INFO: 2 processes: 2 internal.
INFO: Build completed successfully, 2 total actions
看到"Build completed successfully"了吗?恭喜你,你的第一个模块编译成功了!
deps有没有写全。我曾经因为漏掉了//cyber这个基础依赖,折腾了半小时。记住,Apollo里几乎所有模块都依赖//cyber。
5. 运行测试
编译通过只是第一步,跑起来才是真本事。执行:
bazel-bin/modules/my_first_module/my_first_module
你应该会看到输出:
3 + 5 = 8
嗯,8,没错。虽然简单,但这是你亲手搭建的第一个Apollo模块。
如果你想用Bazel直接运行,也可以用这个命令:
bazel run //modules/my_first_module:my_first_module
效果是一样的,但bazel run会自动处理一些环境变量和路径问题。我个人更推荐用bazel run来测试。
6. 本章知识体系
为了让你更直观地理解整个流程,我画了一张图:
这张图把整个流程串起来了。你从创建代码开始,然后写BUILD文件,接着编译,最后运行。核心就是那个deps依赖管理,它决定了你的模块能不能找到它需要的"零件"。
7. 总结一下
今天你学会了:
- 创建Apollo模块的标准目录结构
- 编写头文件和源文件,遵循命名空间规范
- 编写BUILD文件,理解
cc_library和cc_binary的区别 - 使用
bazel build和bazel run编译运行模块
说实话,这个例子虽然简单,但它包含了Apollo模块开发的所有核心要素。你以后写的任何模块,本质上都是这个流程的延伸和扩展。
嗯,今天就到这里。记住,编译通过了别急着走,跑一下看看结果,那才是你代码真正活过来的时刻。