4. 第一个模块编译

说实话,很多刚接触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的规范来写。我见过不少新手因为命名空间写错,导致编译通过但运行时找不到符号。记住: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的代码洁癖,习惯就好
注意:我曾经在项目中遇到过一个问题——BUILD文件里的依赖写错了顺序,结果编译报了一堆莫名其妙的链接错误。排查了整整两个小时才发现是依赖顺序的问题。所以,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. 本章知识体系

为了让你更直观地理解整个流程,我画了一张图:

Apollo模块编译流程 步骤1:创建代码 .h + .cpp + main.cpp 步骤2:编写BUILD cc_library + cc_binary 步骤3:编译 bazel build 步骤4:运行测试 bazel run 或 直接执行 核心要素 deps依赖管理 常见问题 头文件找不到 → 检查deps | 链接错误 → 检查依赖顺序 | 运行时崩溃 → 检查命名空间

这张图把整个流程串起来了。你从创建代码开始,然后写BUILD文件,接着编译,最后运行。核心就是那个deps依赖管理,它决定了你的模块能不能找到它需要的"零件"。

7. 总结一下

今天你学会了:

  • 创建Apollo模块的标准目录结构
  • 编写头文件和源文件,遵循命名空间规范
  • 编写BUILD文件,理解cc_library和cc_binary的区别
  • 使用bazel build和bazel run编译运行模块

说实话,这个例子虽然简单,但它包含了Apollo模块开发的所有核心要素。你以后写的任何模块,本质上都是这个流程的延伸和扩展。

嗯,今天就到这里。记住,编译通过了别急着走,跑一下看看结果,那才是你代码真正活过来的时刻。