Bazel C++ 构建系列文档(五):多目标与多包项目

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、子项目)
  • ✅ 构建性能优化技巧
相关推荐
Hello:CodeWorld1 小时前
【C++ 避坑指南】告别缓冲区溢出!全面解析 std::snprintf 的安全美学与核心陷阱
开发语言·c++·安全
凡人叶枫1 小时前
Effective C++ 条款38:通过复合塑模出 has-a 或 \“根据某物实现出\
linux·开发语言·c++·windows
枫叶丹41 小时前
【HarmonyOS 6.0】MDM Kit:PC/2in1设备用户行为限制策略详解
开发语言·华为·harmonyos
weilaieqi11 小时前
微短剧 + 时代到来,短剧内容正在赋能文旅、品牌与数字文化产业
开发语言
云烟成雨TD1 小时前
Agent Scope Java 2.x 系列【11】中间件(Middleware):核心设计
java·人工智能·agent
ytttr8731 小时前
航天器姿态控制 MATLAB 仿真程序
开发语言·matlab
心之伊始1 小时前
Spring AI Chat Memory 实战:用 JDBC 给 Java Agent 加会话记忆
java·spring boot·agent·spring ai·chat memory
charlie1145141911 小时前
嵌入式Linux驱动开发——从轮询到中断
linux·开发语言·驱动开发·嵌入式
凡人叶枫1 小时前
Effective C++ 条款40:明智而审慎地使用多重继承
java·数据库·c++·嵌入式开发·effective c++