Bazel C++ 构建系列文档(五):多目标与多包项目
--
1. 项目模块化设计
当 C++ 项目逐渐增大时,合理的模块划分变得至关重要。Bazel 的包(Package)机制天然支持模块化设计。
1.1 模块化设计原则
原则一:高内聚、低耦合
- 每个包负责一个明确的功能领域
- 包之间的依赖尽量少且单向
原则二:清晰的接口边界
- 通过 hdrs 暴露公共 API
- 通过 visibility 控制可见范围
- 避免暴露实现细节
原则三:依赖方向明确
- 依赖关系形成有向无环图(DAG)
- 上层模块依赖下层模块,不反向依赖
- 避免循环依赖
1.2 典型的 C++ 项目分层架构
项目目录结构 依赖方向
───────────── ──────────
my-project/
├── app/ ← 应用层(最上层)
│ ├── BUILD ↑
│ └── main.cc |
├── service/ ← 服务层
│ ├── BUILD ↑
│ ├── user_service.cc |
│ └── order_service.cc |
├── core/ ← 核心层
│ ├── BUILD ↑
│ ├── database.cc |
│ └── config.cc |
├── base/ ← 基础层(最底层)
│ ├── BUILD
│ ├── logging.cc
│ └── string_utils.cc
└── tests/ ← 测试层(横向依赖各层)
├── BUILD
└── ...
依赖关系:app → service → core → base
2. 实战:构建一个多模块项目
2.1 项目结构
calculator/
├── WORKSPACE
├── .bazelversion
├── .bazelrc
├── BUILD
├── base/
│ ├── BUILD
│ ├── error.h
│ ├── error.cc
│ └── types.h
├── engine/
│ ├── BUILD
│ ├── calculator.h
│ ├── calculator.cc
│ ├── parser.h
│ ├── parser.cc
│ ├── evaluator.h
│ └── evaluator.cc
├── cli/
│ ├── BUILD
│ └── main.cc
└── tests/
├── BUILD
├── calculator_test.cc
├── parser_test.cc
└── evaluator_test.cc
2.2 base 模块
base/types.h:
cpp
#ifndef BASE_TYPES_H_
#define BASE_TYPES_H_
#include <string>
#include <variant>
namespace calc {
using Value = std::variant<double, int, std::string>;
enum class ValueType {
kNumber,
kString,
};
} // namespace calc
#endif // BASE_TYPES_H_
base/error.h:
cpp
#ifndef BASE_ERROR_H_
#define BASE_ERROR_H_
#include <string>
namespace calc {
class Error {
public:
explicit Error(std::string message) : message_(std::move(message)) {}
const std::string& Message() const { return message_; }
private:
std::string message_;
};
} // namespace calc
#endif // BASE_ERROR_H_
base/error.cc:
cpp
#include "base/error.h"
namespace calc {
// Error 实现可以在这里扩展
} // namespace calc
base/BUILD:
python
# base/BUILD
package(default_visibility = ["//visibility:public"])
cc_library(
name = "types",
hdrs = ["types.h"],
# 纯头文件库,无 srcs
)
cc_library(
name = "error",
srcs = ["error.cc"],
hdrs = ["error.h"],
)
2.3 engine 模块
engine/parser.h:
cpp
#ifndef ENGINE_PARSER_H_
#define ENGINE_PARSER_H_
#include <string>
#include <vector>
#include "base/types.h"
#include "base/error.h"
namespace calc {
struct Token {
std::string value;
ValueType type;
};
class Parser {
public:
ErrorOr<std::vector<Token>> Parse(const std::string& expression);
};
template<typename T>
using ErrorOr = std::variant<T, Error>;
} // namespace calc
#endif // ENGINE_PARSER_H_
engine/parser.cc:
cpp
#include "engine/parser.h"
#include <sstream>
namespace calc {
ErrorOr<std::vector<Token>> Parser::Parse(const std::string& expression) {
std::vector<Token> tokens;
std::istringstream iss(expression);
std::string token;
while (iss >> token) {
tokens.push_back({token, ValueType::kNumber});
}
return tokens;
}
} // namespace calc
engine/evaluator.h:
cpp
#ifndef ENGINE_EVALUATOR_H_
#define ENGINE_EVALUATOR_H_
#include <vector>
#include "base/types.h"
#include "base/error.h"
namespace calc {
struct Token; // 前向声明
class Evaluator {
public:
ErrorOr<double> Evaluate(const std::vector<Token>& tokens);
};
} // namespace calc
#endif // ENGINE_EVALUATOR_H_
engine/evaluator.cc:
cpp
#include "engine/evaluator.h"
#include "engine/parser.h"
#include <cmath>
#include <stack>
namespace calc {
ErrorOr<double> Evaluator::Evaluate(const std::vector<Token>& tokens) {
// 简化的表达式求值实现
double result = 0.0;
for (const auto& token : tokens) {
try {
result += std::stod(token.value);
} catch (...) {
return Error("Invalid number: " + token.value);
}
}
return result;
}
} // namespace calc
engine/calculator.h:
cpp
#ifndef ENGINE_CALCULATOR_H_
#define ENGINE_CALCULATOR_H_
#include <string>
#include "base/error.h"
namespace calc {
class Calculator {
public:
ErrorOr<double> Compute(const std::string& expression);
};
} // namespace calc
#endif // ENGINE_CALCULATOR_H_
engine/calculator.cc:
cpp
#include "engine/calculator.h"
#include "engine/parser.h"
#include "engine/evaluator.h"
namespace calc {
ErrorOr<double> Calculator::Compute(const std::string& expression) {
Parser parser;
auto tokens_result = parser.Parse(expression);
if (std::holds_alternative<Error>(tokens_result)) {
return std::get<Error>(tokens_result);
}
Evaluator evaluator;
return evaluator.Evaluate(std::get<std::vector<Token>>(tokens_result));
}
} // namespace calc
engine/BUILD:
python
# engine/BUILD
package(default_visibility = ["//visibility:public"])
cc_library(
name = "parser",
srcs = ["parser.cc"],
hdrs = ["parser.h"],
deps = [
"//base:types",
"//base:error",
],
)
cc_library(
name = "evaluator",
srcs = ["evaluator.cc"],
hdrs = ["evaluator.h"],
deps = [
"//base:types",
"//base:error",
":parser", # 同包依赖
],
)
cc_library(
name = "calculator",
srcs = ["calculator.cc"],
hdrs = ["calculator.h"],
deps = [
"//base:error",
":parser",
":evaluator",
],
)
2.4 cli 模块
cli/main.cc:
cpp
#include <iostream>
#include <string>
#include "engine/calculator.h"
#include "base/error.h"
int main(int argc, char* argv[]) {
calc::Calculator calc;
if (argc > 1) {
// 从命令行参数计算
std::string expr;
for (int i = 1; i < argc; ++i) {
if (i > 1) expr += " ";
expr += argv[i];
}
auto result = calc.Compute(expr);
if (std::holds_alternative<calc::Error>(result)) {
std::cerr << "Error: " << std::get<calc::Error>(result).Message() << std::endl;
return 1;
}
std::cout << std::get<double>(result) << std::endl;
} else {
// 交互模式
std::cout << "Calculator (type 'quit' to exit)" << std::endl;
std::string line;
while (std::getline(std::cin, line)) {
if (line == "quit") break;
auto result = calc.Compute(line);
if (std::holds_alternative<calc::Error>(result)) {
std::cerr << "Error: " << std::get<calc::Error>(result).Message() << std::endl;
} else {
std::cout << "= " << std::get<double>(result) << std::endl;
}
}
}
return 0;
}
cli/BUILD:
python
# cli/BUILD
cc_binary(
name = "calculator",
srcs = ["main.cc"],
deps = [
"//engine:calculator",
"//base:error",
],
)
2.5 测试模块
tests/parser_test.cc:
cpp
#include "engine/parser.h"
#include "gtest/gtest.h"
namespace calc {
namespace {
TEST(ParserTest, ParsesSimpleExpression) {
Parser parser;
auto result = parser.Parse("1 + 2");
ASSERT_TRUE(std::holds_alternative<std::vector<Token>>(result));
auto tokens = std::get<std::vector<Token>>(result);
EXPECT_EQ(tokens.size(), 3u);
}
} // namespace
} // namespace calc
tests/BUILD:
python
# tests/BUILD
cc_test(
name = "parser_test",
srcs = ["parser_test.cc"],
deps = [
"//engine:parser",
"@com_google_googletest//:gtest_main",
],
)
# 可以使用 test_suite 聚合测试
test_suite(
name = "all_tests",
tests = [
":parser_test",
# 更多测试...
],
)
3. 聚合目标与文件组
3.1 filegroup --- 文件集合
python
# base/BUILD
filegroup(
name = "all_headers",
srcs = glob(["**/*.h"]),
visibility = ["//visibility:public"],
)
filegroup(
name = "all_sources",
srcs = glob(["**/*.cc", "**/*.h"]),
visibility = ["//visibility:public"],
)
3.2 alias --- 目标别名
python
# BUILD(根目录)
alias(
name = "calc",
actual = "//cli:calculator",
visibility = ["//visibility:public"],
)
alias(
name = "all_tests",
actual = "//tests:all_tests",
visibility = ["//visibility:public"],
)
使用别名简化命令:
bash
# 不使用别名
bazel build //cli:calculator
# 使用别名
bazel build //:calc
3.3 test_suite --- 测试套件
python
# tests/BUILD
cc_test(name = "parser_test", ...)
cc_test(name = "evaluator_test", ...)
cc_test(name = "calculator_test", ...)
test_suite(
name = "unit_tests",
tests = [
":parser_test",
":evaluator_test",
],
)
test_suite(
name = "integration_tests",
tests = [
":calculator_test",
],
)
test_suite(
name = "all_tests",
tests = [
":unit_tests",
":integration_tests",
],
)
bash
# 运行所有单元测试
bazel test //tests:unit_tests
# 运行所有测试
bazel test //tests:all_tests
4. 循环依赖检测与解决
4.1 什么是循环依赖
A → B → C → A ← 循环依赖,Bazel 会报错
Bazel 严格要求依赖图为 DAG(有向无环图),不允许循环依赖。
4.2 常见循环依赖场景
python
# ❌ 循环依赖示例
# engine/BUILD
cc_library(
name = "parser",
deps = [":evaluator"], # parser 依赖 evaluator
)
cc_library(
name = "evaluator",
deps = [":parser"], # evaluator 依赖 parser → 循环!
)
4.3 解决方案
方案一:提取公共部分
python
# ✅ 提取公共部分到单独的目标
cc_library(
name = "ast", # 新建抽象语法树库
hdrs = ["ast.h"],
)
cc_library(
name = "parser",
deps = [":ast"], # 都依赖 ast,互不依赖
)
cc_library(
name = "evaluator",
deps = [":ast"],
)
方案二:使用接口/回调解耦
cpp
// engine/evaluator.h
class Evaluator {
public:
using ParseFn = std::function<ErrorOr<std::vector<Token>>(const std::string&)>;
void SetParser(ParseFn parse_fn); // 通过回调注入解析能力
private:
ParseFn parse_fn_;
};
方案三:使用 implementation_deps
python
cc_library(
name = "parser",
hdrs = ["parser.h"],
srcs = ["parser.cc"],
deps = [":ast"],
)
cc_library(
name = "evaluator",
hdrs = ["evaluator.h"],
srcs = ["evaluator.cc"],
deps = [":ast"],
implementation_deps = [":parser"], # 实现依赖,不传播给依赖者
)
5. 大型项目结构模式
5.1 Monorepo 模式
company-monorepo/
├── WORKSPACE
├── MODULE.bazel
├── libs/
│ ├── common/
│ │ └── BUILD
│ ├── network/
│ │ └── BUILD
│ └── database/
│ └── BUILD
├── services/
│ ├── auth_service/
│ │ └── BUILD
│ └── api_service/
│ └── BUILD
└── tools/
├── codegen/
│ └── BUILD
└── linter/
└── BUILD
5.2 子项目模式
workspace/
├── WORKSPACE
├── project_a/
│ ├── BUILD
│ └── ...
├── project_b/
│ ├── BUILD
│ └── ...
└── shared/
├── BUILD
└── ...
5.3 分层依赖规则
python
# 使用 visibility 强制执行分层架构
# base 层:不依赖任何其他内部模块
cc_library(
name = "logging",
visibility = ["//visibility:public"], # 所有层都可以使用
)
# service 层:只依赖 base 层
cc_library(
name = "user_service",
deps = ["//base:logging"],
visibility = ["//service:__pkg__", "//app:__pkg__"], # 只暴露给 app 层
)
# app 层:可以依赖 service 和 base 层
cc_binary(
name = "server",
deps = ["//service:user_service", "//base:logging"],
)
6. 构建性能优化
6.1 减少不必要的依赖
python
# ❌ 不好的做法:把所有东西放在一个巨大的库中
cc_library(
name = "everything",
srcs = glob(["**/*.cc"]),
hdrs = glob(["**/*.h"]),
)
# ✅ 好的做法:按功能拆分为小库
cc_library(name = "string_utils", srcs = ["string_utils.cc"], hdrs = ["string_utils.h"])
cc_library(name = "file_utils", srcs = ["file_utils.cc"], hdrs = ["file_utils.h"])
cc_library(name = "math_utils", srcs = ["math_utils.cc"], hdrs = ["math_utils.h"])
6.2 使用 implementation_deps 减少依赖传播
python
cc_library(
name = "public_api",
hdrs = ["api.h"], # 公共头文件只暴露最小接口
srcs = ["api.cc"],
deps = [
"//base:types", # 公共依赖,传播给使用者
],
implementation_deps = [
"//core:internal_impl", # 实现依赖,不传播
"//third_party:json", # 第三方库,不传播给使用者
],
)
好处:
- 减少使用者的编译依赖
- 修改内部实现不会触发依赖者重新编译
- 更清晰的 API 边界
6.3 避免过度使用 glob
python
# ⚠️ glob 会延迟求值,可能影响增量构建的准确性
srcs = glob(["**/*.cc"]) # 包含所有子目录的 .cc 文件
# ✅ 明确列出文件,构建更可预测
srcs = ["parser.cc", "evaluator.cc", "calculator.cc"]
7. 小结
本篇介绍了多目标与多包项目的组织方式:
- ✅ 模块化设计原则与分层架构
- ✅ 多模块项目实战(calculator 项目)
- ✅ 聚合目标(filegroup、alias、test_suite)
- ✅ 循环依赖的检测与解决方案
- ✅ 大型项目结构模式(Monorepo、子项目)
- ✅ 构建性能优化技巧