本文档是Bitcoin Core的开发者笔记,涵盖了代码编写风格、开发技巧、调试方法、锁定/互斥锁的使用、线程、忽略IDE/编辑器文件、开发指南等多个方面。同时,该文档还提供了关于通用Bitcoin Core、钱包、通用C++、C++数据结构、字符串和格式化、脚本、源代码组织、GUI、子树、升级LevelDB、脚本化差异、发布说明、RPC接口指南和内部接口指南等开发的指导方针。
<!-- markdown-toc start --> 目录
<!-- markdown-toc end -->
在代码库的历史中,使用了各种编码风格, 结果不是很一致。但是,我们现在正尝试趋同于 一种单一的风格,如下所示。在编写补丁时,倾向于新的 风格,而不是尝试模仿周围的风格,除了仅移动的 提交。
不要仅提交用于修改现有代码风格的补丁。
缩进和空格规则 在 src/.clang-format 中指定。你可以使用提供的 clang-format-diff 脚本 工具在提交前自动清理补丁。
public
/protected
/private
或 namespace
没有缩进。( this )
。if
,for
和 while
后有一个空格。if
只有一个语句的 then
子句,它可以
与 if
在同一行,没有括号。在其他任何情况下,
都需要括号,并且 then
和 else
子句必须
正确缩进在新的一行。符号命名约定。这些在新的代码中是首选的,但不是 在这样做需要对现有代码进行大量更改时,这是必需的。
变量(包括函数参数)和命名空间名称均为小写,并且可以使用 _
分隔单词 (snake_case)。
类成员变量具有 m_
前缀。
全局变量具有 g_
前缀。
常量名称全部为大写,并使用 _
分隔单词。
枚举器常量可以是 snake_case
、PascalCase
或 ALL_CAPS
。
这是一个比 C++ Core
Guidelines
更宽松的策略,
它建议使用 snake_case
。请使用看起来合适的。
类名、函数名和方法名是 UpperCamelCase
(PascalCase)。不要在类名前加上 C
。有关此约定
的例外情况,请参见 内部接口命名风格。
测试套件命名约定:文件 src/test/foo_tests.cpp
中的 Boost 测试套件
应命名为 foo_tests
。测试套件名称必须是唯一的。
其他
++i
优先于 i++
。nullptr
优先于 NULL
或 (void*)0
。static_assert
优先于 assert
。一般来说; 编译时检查优先于运行时检查。int(x)
或 int{x}
,而不是 (int) x
。
当在更复杂的类型之间转换时,使用 static_cast
。
根据需要使用 reinterpret_cast
和 const_cast
。列表初始化 ({})
。
例如 int x{0};
而不是 int x = 0;
或 int x(0);
NOLINTNEXTLINE(misc-no-recursion)
来抑制检查。对于函数调用,应显式指定命名空间,除非此类函数已在其内部声明。 否则,可能会触发 参数依赖查找,也称为 ADL, 这会使代码更难以维护和推理:
##include <filesystem>
namespace fs {
class path : public std::filesystem::path
{
};
// 目的是禁止此函数。
bool exists(const fs::path& p) = delete;
} // namespace fs
int main()
{
//fs::path p; // 错误
std::filesystem::path p; // 已编译
exists(p); // ADL 用于非限定名称查找
}
块样式示例:
int g_count{0};
namespace foo {
class Class
{
std::string m_name;
public:
bool Function(const std::string& s, int n)
{
// 总结此代码段所做的事情的注释
for (int i = 0; i < n; ++i) {
int total_sum{0};
// 当出现问题时,提前返回
if (!Something()) return false;
...
if (SomethingElse(i)) {
total_sum += ComputeSomething(g_count);
} else {
DoSomething(m_name, total_sum);
}
}
// 成功返回通常在最后
return true;
}
}
} // namespace foo
在对函数参数进行排序时,首先放置输入参数,然后是任何 输入/输出参数,然后是任何输出参数。
理由:API 一致性。
优先直接返回值,而不是使用输入/输出或输出参数。使用
std::optional
在返回值的帮助下。
理由:不易出错(无需假设输出在失败时初始化为哪个值),更易于读取,并且通常具有相同或更好的 性能。
通常,使用 std::optional
表示可选的按值输入(以及
而不是神奇的默认值,如果没有真正的默认值)。非可选的
输入参数通常应为值或常量引用,而非可选的输入/输出和输出参数通常应为引用,因为它们不能为 null。
传递命名参数时,请使用 clang-tidy 可以理解的格式。否则,参数名称无法通过 clang-tidy 验证。
例如:
void function(Addrman& addrman, bool clear);
int main()
{
function(g_addrman, /*clear=*/false);
}
要在 Ubuntu/Debian 上运行 clang-tidy,请安装依赖项:
apt install clang-tidy clang
将 clang 配置为编译器:
cmake -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
该输出会消除来自外部依赖项的错误。
要在所有源文件上运行 clang-tidy:
( cd ./src/ && run-clang-tidy -p ../build -j $(nproc) )
要在已更改的源代码行上运行 clang-tidy:
git diff | ( cd ./src/ && clang-tidy-diff -p2 -path ../build -j $(nproc) )
请参考 /test/functional/README.md#style-guidelines。
Bitcoin Core 使用 Doxygen 来生成其官方文档。
对函数、方法和字段使用 Doxygen 兼容的注释块。
例如,要描述一个函数,请使用:
/**
* ... 描述 ...
*
* @param[in] arg1 输入描述...
* @param[in] arg2 输入描述...
* @param[out] arg3 输出描述...
* @return 返回案例...
* @throws 错误类型和案例...
* @pre 函数的前提条件...
* @post 函数的后置条件...
*/
bool function(int arg1, const char *arg2, std::string& arg3)
完整的 @xxx
命令列表可以在 https://www.doxygen.nl/manual/commands.html 中找到。
由于 Doxygen 通过分隔符(在本例中为 /**
和 */
)识别注释,因此你
不需要 提供任何命令来使注释有效;只需提供描述文本即可。
要描述一个类,请在类定义上方使用相同的构造:
/**
* 警报用于通知旧版本,如果它们变得过于过时并且
* 需要升级。该消息显示在状态栏中。
* @see GetWarnings()
*/
class CAlert
要描述成员或变量,请使用:
//! 成员前的描述
int var;
或
int var; //!< 成员后的描述
也可以:
///
/// ... 描述 ...
///
bool function2(int arg1, const char *arg2)
Doxygen 不会拾取:
//
// ... 描述 ...
//
Doxygen 也不会拾取:
/*
* ... 描述 ...
*/
Doxygen 拾取的完整注释语法列表可以在 https://www.doxygen.nl/manual/docblocks.html 中找到, 但首选上述样式。
建议:
避免在函数描述中复制类型和输入/输出信息。
使用反引号 (`) 在函数和 参数描述中引用
argument` 名称。
引用 Doxygen 已知的函数时,不需要反引号; 它会自动为这些函数构建超链接。有关完整信息,请参见 https://www.doxygen.nl/manual/autolink.html。
避免链接到外部文档;链接可能会断开。
Javadoc 和所有有效的 Doxygen 注释都从 Doxygen 源代码
预览中删除(STRIP_CODE_COMMENTS = YES
在 Doxyfile.in 中)。如果
你希望保留注释,则必须改用 //
或 /* */
。
假设构建目录名为 build
,
可以使用 cmake --build build --target docs
生成文档。
结果文件将位于 build/doc/doxygen/html
中;
打开该目录中的 index.html
以查看主页。
在构建 docs
目标之前,你需要安装以下依赖项:
Linux:sudo apt install doxygen graphviz
MacOS:brew install doxygen graphviz
当通过运行 cmake -B build
使用默认构建配置时,
-DCMAKE_BUILD_TYPE
设置为 RelWithDebInfo
。此选项会添加调试符号,
但也会执行一些编译器优化,这些优化可能会使调试变得更加棘手,
因为代码可能不直接对应于源代码。
如果你需要专门为调试而构建,请将 -DCMAKE_BUILD_TYPE
设置为
Debug
(即 -DCMAKE_BUILD_TYPE=Debug
)。你始终可以使用 ccmake build
检查现有构建的 cmake 构建选项。
如果你启用了 ccache,则绝对路径将从调试信息中删除
使用 -fdebug-prefix-map
和 -fmacro-prefix-map
选项(如果编译器支持)。如果你在编译后移动二进制文件、从项目根目录以外的目录进行调试或使用 IDE,这可能会破坏源文件检测。IDE 仅支持用于调试的绝对路径(例如,它不会在断点处停止)。
有几种可能的解决方法:
对于 gdb
,请创建或附加到 .gdbinit
文件:
set substitute-path ./src /path/to/project/root/src
对于 lldb
,请创建或附加到 .lldbinit
文件:
settings set target.source-map ./src /path/to/project/root/src
添加到 ./src
目录的符号链接:
ln -s /path/to/project/root/src src
使用 debugedit
修改二进制文件中的调试信息。
如果你的 IDE 有一个选项,请将你的断点更改为仅使用文件名。
debug.log
如果代码的行为异常,请查看数据目录中的 debug.log
文件;
错误和调试消息会写入该文件。
可以使用 -debug
和 -loglevel
启用启动时的调试日志记录
配置选项,并通过 logging
切换正在运行的 bitcoind
RPC。例如,使用 -debug
或 -debug=1
启动 bitcoind 将打开
所有日志类别,-loglevel=trace
将打开所有日志严重性级别。
Qt 代码将 qDebug()
输出路由到 debug.log
,类别为 "qt":运行 -debug=qt
来查看它。
如果你正在测试需要在 Internet 上运行的多机代码,你可以使用 -signet
或 -testnet4
配置选项来使用测试网络上的“play bitcoins”进行测试。
如果你正在测试可以在一台机器上运行的东西,请使用 -regtest
选项运行。在回归测试模式下,可以按需创建区块;
请参阅 test/functional/ 以获取在 -regtest
模式下运行的测试。
Bitcoin Core 是一个多线程应用程序,死锁或其他多线程错误可能很难找到。-DCMAKE_BUILD_TYPE=Debug
构建选项将 -DDEBUG_LOCKORDER
添加到编译器标志。这将插入运行时检查,以跟踪持有哪些锁,并在 debug.log
文件中检测到不一致时添加警告。
定义 DEBUG_LOCKCONTENTION
会将“lock”日志记录类别添加到日志记录
RPC,启用后,会将每个锁争用的位置和持续时间记录到 debug.log
文件中。
-DCMAKE_BUILD_TYPE=Debug
构建选项将 -DDEBUG_LOCKCONTENTION
添加到
编译器标志。你也可以通过将 -DDEBUG_LOCKCONTENTION
添加到你的 CPPFLAGS 来手动启用它,
即 -DAPPEND_CPPFLAGS="-DDEBUG_LOCKCONTENTION"
。
然后,你可以使用 -debug=lock
配置选项在 bitcoind 启动时启用锁争用日志记录,
或使用 bitcoin-cli logging '["lock"]'
在运行时启用锁争用日志记录。
可以使用 bitcoin-cli logging [] '["lock"]'
再次将其关闭。
实用程序文件 src/util/check.h
提供了帮助程序来防止编码和
内部逻辑错误。它们绝不能用于验证用户、网络或任何其他输入。
assert
或 Assert
应用于记录假设,当任何
违规意味着继续执行程序是不安全的。
代码始终在启用断言的情况下编译。
CHECK_NONFATAL
应该用于可恢复的内部逻辑错误。在
失败时,它将抛出一个异常,该异常可以被捕获以从错误中恢复。
Assume
应该用于记录假设,当程序执行可以
即使假设被违反,也可以安全地继续。在调试版本中,它
的行为类似于 Assert
/assert
,以通知开发人员和测试人员有关
非致命错误。在生产环境中,它不会发出警告或记录任何内容,但
表达式始终会被评估。但是,如果编译器可以证明
Assume
内的表达式是无副作用的,则它可能会优化调用,
从而跳过其在生产环境中的评估。这使得以较低成本的方式
对代码进行显式声明,从而有助于审查。
Valgrind 是一种用于内存调试、内存泄漏检测和
性能分析的编程工具。该 repo 包含一个 Valgrind 抑制文件
(valgrind.supp
),
其中包括我们依赖项中已知的 Valgrind 警告,这些警告无法在树内修复。示例用法:
$ valgrind --suppressions=contrib/valgrind.supp build/bin/test_bitcoin
$ valgrind --suppressions=contrib/valgrind.supp --leak-check=full \
--show-leak-kinds=all build/bin/test_bitcoin --log_level=test_suite
$ valgrind -v --leak-check=full build/bin/bitcoind -printtoconsole
$ ./build/test/functional/test_runner.py --valgrind
LCOV 可用于生成基于 ctest
执行的测试覆盖率报告。
必须在系统上安装 LCOV(例如 Debian/Ubuntu 上的 lcov
软件包)。
要在测试运行期间启用 LCOV 报告生成:
cmake -B build -DCMAKE_BUILD_TYPE=Coverage
cmake --build build
cmake -P build/Coverage.cmake
## 现在可以在 `./build/test_bitcoin.coverage/index.html` 访问覆盖率报告,
## 该报告涵盖单元测试,以及 `./build/total.coverage/index.html`,该报告涵盖
## 单元测试和功能测试。
可以使用 LCOV_OPTS
指定其他 LCOV 选项,但可能依赖于
LCOV 的版本。例如,当使用 LCOV 2.x
时,可以通过设置 LCOV_OPTS="--rc branch_coverage=1"
来启用分支覆盖率:
cmake -DLCOV_OPTS="--rc branch_coverage=1" -P build/Coverage.cmake
要启用测试并行性:
cmake -DJOBS=$(nproc) -P build/Coverage.cmake
以下内容为单元测试和功能测试生成覆盖率报告。
使用以下标志配置构建:
考虑使用
rm -rf build
以干净状态构建
## MacOS 可能需要 `-DCMAKE_C_COMPILER="$(brew --prefix llvm)/bin/clang" -DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++"`
cmake -B build -DCMAKE_C_COMPILER="clang" \
-DCMAKE_CXX_COMPILER="clang++" \
-DAPPEND_CFLAGS="-fprofile-instr-generate -fcoverage-mapping" \
-DAPPEND_CXXFLAGS="-fprofile-instr-generate -fcoverage-mapping" \
-DAPPEND_LDFLAGS="-fprofile-instr-generate -fcoverage-mapping"
cmake --build build # 在此处附加 "-j N",以便进行 N 个并行作业。
基于 ctest
和功能测试执行生成原始配置文件数据:
## 创建用于存储原始配置文件数据的目录
mkdir -p build/raw_profile_data
## 运行测试以生成配置文件
LLVM_PROFILE_FILE="$(pwd)/build/raw_profile_data/%m_%p.profraw" ctest --test-dir build # 在此处附加 "-j N",以便进行 N 个并行作业。
LLVM_PROFILE_FILE="$(pwd)/build/raw_profile_data/%m_%p.profraw" build/test/functional/test_runner.py # 在此处附加 "-j N",以便进行 N 个并行作业
## 将所有原始配置文件数据合并到单个文件中
find build/raw_profile_data -name "*.profraw" | xargs llvm-profdata merge -o build/coverage.profdata
注意: 可以安全地忽略“计数器不匹配”警告,但可以通过更新到 Clang 19 来解决此问题。 出现此警告是由于版本不匹配,但这不会影响覆盖率报告的生成。
生成覆盖率报告:
llvm-cov show \
--object=build/bin/test_bitcoin \
--object=build/bin/bitcoind \
-Xdemangler=llvm-cxxfilt \
--instr-profile=build/coverage.profdata \
--ignore-filename-regex="src/crc32c/|src/leveldb/|src/minisketch/|src/secp256k1/|src/test/" \
--format=html \
--show-instantiation-summary \
--show-line-counts-or-regions \
--show-expansions \
--output-dir=build/coverage_report \
--project-title="Bitcoin Core 覆盖率报告"
注意: 可以安全地忽略“函数具有不匹配的数据”警告,尽管存在此警告,但仍会正确生成覆盖率报告。 出现此警告是由于在为共享库执行合并流程期间创建的 profdata 不匹配。
可以在 build/coverage_report/index.html
中访问生成的覆盖率报告。
cmake -B build \
-DCMAKE_C_COMPILER="clang" \
-DCMAKE_CXX_COMPILER="clang++" \
-DCMAKE_C_FLAGS="-fprofile-instr-generate -fcoverage-mapping" \
-DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping" \
-DBUILD_FOR_FUZZING=ON
cmake --build build # 在此处附加 "-j N",以便进行 N 个并行作业。
使用一个或多个目标运行 fuzz 测试
## 对于使用所选目标运行的单个目标
LLVM_PROFILE_FILE="$(pwd)/build/raw_profile_data/txorphan.profraw" ./build/test/fuzz/test_runner.py ../qa-assets/fuzz_corpora txorphan
## 如果运行多个目标
LLVM_PROFILE_FILE="$(pwd)/build/raw_profile_data/%m_%p.profraw" ./build/test/fuzz/test_runner.py ../qa-assets/fuzz_corpora
## 合并配置文件
llvm-profdata merge build/raw_profile_data/*.profraw -o build/coverage.profdata
生成报告:
llvm-cov show \
--object=build/bin/fuzz \
-Xdemangler=llvm-cxxfilt \
--instr-profile=build/coverage.profdata \
--ignore-filename-regex="src/crc32c/|src/leveldb/|src/minisketch/|src/secp256k1/|src/test/" \
--format=html \
--show-instantiation-summary \
--show-line-counts-or-regions \
--show-expansions \
--output-dir=build/coverage_report \
--project-title="Bitcoin Core Fuzz 覆盖率报告"
可以在 build/coverage_report/index.html
中访问生成的覆盖率报告。
性能分析是准确了解代码中花费时间的好方法。在
Linux 平台上进行性能分析的一种工具称为
perf
,并且已集成到
功能测试框架中。Perf 可以观察正在运行的进程并采样
(以某种频率)其执行位置。
Perf 的安装取决于你运行的内核版本;请参阅 此线程 以获取具体说明。
可能需要设置某些内核参数,perf 才能检查 正在运行的进程的堆栈。
$ sudo sysctl -w kernel.perf_event_paranoid=-1
$ sudo sysctl -w kernel.kptr_restrict=0
确保你了解安全 权衡 设置这些内核 参数。
要对正在运行的 bitcoind 进程进行 60 秒的性能分析,你可以使用 perf 记录
的调用,如下所示:
$ perf record \
-g --call-graph dwarf --per-thread -F 140 \
-p `pgrep bitcoind` -- sleep 60
然后,你可以通过运行以下命令来分析结果:
perf report --stdio | c++filt | less
或使用图形工具,例如 Hotspot。
有关如何在测试中调用 perf 的信息,请参阅功能测试文档。
可以使用启用各种 "sanitizers" 来编译 Bitcoin Core,这些 "sanitizers" 添加了有关内存安全、线程竞争条件或未定义行为等问题的检测。这由 -DSANITIZERS
cmake 构建标志控制,该标志应是以逗号分隔的要启用的 sanitizer 列表。sanitizer 列表应与编译器中支持的 -fsanitize=
选项对应。这些 Sanitizers 具有运行时开销,因此它们在测试更改或生成调试构建时最有用。
一些示例:
## 同时启用地址 Sanitizer 和未定义行为 Sanitizer
cmake -B build -DSANITIZERS=address,undefined
## 启用线程 Sanitizer
cmake -B build -DSANITIZERS=thread
如果使用 GCC 进行编译,则通常需要安装相应的 “san” 库才能实际使用这些标志进行编译,例如 libasan 用于地址 Sanitizer,libtsan 用于线程 Sanitizer,libubsan 用于未定义 Sanitizer。如果缺少所需的库,则在测试 Sanitizer 标志时,构建将因链接器错误而失败。
测试套件应在 thread
和 undefined
Sanitizer 中干净地通过。你
可能需要使用抑制文件,请参阅 test/sanitizer_suppressions
。可以使用它们,如下所示:
export LSAN_OPTIONS="suppressions=$(pwd)/test/sanitizer_suppressions/lsan"
export TSAN_OPTIONS="suppressions=$(pwd)/test/sanitizer_suppressions/tsan:halt_on_error=1:second_deadlock_stack=1"
export UBSAN_OPTIONS="suppressions=$(pwd)/test/sanitizer_suppressions/ubsan:print_stacktrace=1:halt_on_error=1:report_error_type=1"
有关更多示例以及有关任何其他选项的更多信息,请参阅 CI 配置和上游文档。
并非所有 Sanitizer 选项都可以同时启用,例如,尝试构建
-DSANITIZERS=address,thread
将在构建中失败,因为
这些 Sanitizer 互不兼容。有关
这些选项以及编译器支持的 Sanitizer 的更多信息,请参阅编译器手册。
其他资源:
代码是多线程的,并使用互斥锁以及
LOCK
和 TRY_LOCK
宏来保护数据结构。
由于不一致的锁定顺序(线程 1 锁定 cs_main
,然后锁定 cs_wallet
,
而线程 2 以相反的顺序锁定它们:结果,死锁,
因为每个线程都在等待另一个线程释放其锁)的死锁是一个问题。使用
-DDEBUG_LOCKORDER
编译(或使用 -DCMAKE_BUILD_TYPE=Debug
)可以获取 debug.log
文件中报告的锁定顺序不一致。
重新设计核心代码,以便在各种组件之间具有更好定义的接口
是一个目标,任何必要的锁定都由组件完成
(例如,请参阅自包含的 FillableSigningProvider
类及其 cs_KeyStore
锁)。
主线程 (bitcoind
)
:从 bitcoind.cpp
中的 main()
启动。负责启动和
关闭应用程序。
Init load (b-initload
)
:执行各种加载任务,这些任务是 init 的一部分,但不应阻止节点启动:外部区块导入、
重建索引、重建链状态、主链激活、生成索引后台同步线程和内存池加载。
CCheckQueue::Loop (b-scriptch.x
)
:区块中交易的并行脚本验证线程。
ThreadHTTP (b-http
)
:Libevent 线程,用于侦听 RPC 和 REST 连接。
HTTP 工作线程(b-httpworker.x
)
:用于服务 RPC 和 REST 请求的线程。- 索引器线程 (b-txindex
等)
: 每个索引器一个线程。
SchedulerThread (b-scheduler
)
: 执行异步后台任务,例如转储钱包内容、转储 addrman 和运行异步 validationinterface 回调。
TorControlThread (b-torcontrol
)
: 用于 Tor 连接的 Libevent 线程。
网络线程:
ThreadMessageHandler (b-msghand
)
: 应用层消息处理(发送和接收)。几乎所有 net_processing
和验证逻辑都在此线程上运行。
ThreadDNSAddressSeed (b-dnsseed
)
: 从 DNS 加载对等点的地址。
ThreadMapPort (b-mapport
)
: 通用即插即用启动/关闭。
ThreadSocketHandler (b-net
)
: 在端口 8333 上与对等点发送/接收数据。
ThreadOpenAddedConnections (b-addcon
)
: 打开到添加节点的网络连接。
ThreadOpenConnections (b-opencon
)
: 启动与对等点的新连接。
ThreadI2PAcceptIncoming (b-i2paccept
)
: 通过 I2P SAM 代理监听并接受传入的 I2P 连接。
一些与风格无关的开发者建议,以及比特币核心代码审查者应注意的事项。
新功能应首先在 RPC 上公开,然后才能在 GUI 中使用。
宏 LogInfo
、LogDebug
、LogTrace
、LogWarning
和 LogError
可用于记录消息。它们应按如下方式使用:
LogDebug(BCLog::CATEGORY, fmt, params...)
是你希望
在大多数情况下使用的,它应该用于对调试有用的日志消息,并且可以在生产
系统(具有足够的可用存储空间)上合理地启用。如果程序以-debug=category
或-debug=1
启动,则将记录它们。
LogInfo(fmt, params...)
应该很少使用,例如,对于启动
消息或不频繁且重要的事件,例如找到新的区块头或建立新的出站连接。这些日志消息
是无条件的,因此必须小心,以免攻击者使用它们来填满存储空间。请注意,LogPrintf(fmt, params...)
是
LogInfo
的已弃用别名。
LogError(fmt, params...)
应该代替 LogInfo
用于
需要节点(或子系统)完全关闭的严重问题(例如,存储空间不足)。
LogWarning(fmt, params...)
应该代替 LogInfo
用于
节点管理员应解决的严重问题,但这些问题不足以保证关闭
节点(例如,系统时间
似乎不正确,未知的软分叉似乎已激活)。
LogTrace(BCLog::CATEGORY, fmt, params...)
应该代替
LogDebug
用于在生产系统上不可用的日志消息,例如,由于在正常使用中过于嘈杂,或者处理起来过于耗费资源。如果启动
选项-debug=category -loglevel=category:trace
或 -debug=1 -loglevel=trace
被选中,这些将被记录。
请注意,只有在启用日志类别时,才会对 LogDebug
和 LogTrace
的格式字符串和参数进行评估,因此你必须
小心避免这些表达式中的副作用。
对于通用的 C++ 指南,你可以参考 C++ 核心指南。
常见的误解在这些章节中得到澄清:
在 C++ 核心指南 中传递(非)基本类型。
如果你使用 .h
,你必须链接 .cpp
。
.h
移动函数到 .cpp
不应导致构建错误。尽可能使用 RAII(资源获取即初始化)范例。例如,通过使用 unique_ptr
进行函数中的分配。
从不使用 std::map []
语法从 map 中读取,而是使用 .find()
。
[]
会执行插入(默认元素)。这过去导致了内存泄漏以及竞争条件(期望读-读行为)。使用 []
适合于写入 map。不要将一个数据结构的迭代器与另一个数据结构的迭代器进行比较(即使是同一类型)。
注意越界的向量访问。&vch[vch.size()]
是非法的,包括空向量的 &vch[0]
。改用 vch.data()
和 vch.data() + vch.size()
。
向量边界检查仅在调试模式下启用。不要依赖它。
初始化定义它们的所有非静态类成员。 如果由于充分的理由跳过此步骤(即,关键路径上的优化),请添加有关此的显式注释。
class A
{
uint32_t m_count{0};
}
默认情况下,声明构造函数 explicit
。
使用显式签名或无符号 char
,甚至更好的 uint8_t
和 int8_t
。除非要传递给第三方 API,否则不要使用裸 char
。此类型可以是带符号或无符号的,具体取决于架构,这可能导致互操作性问题或危险情况,例如越界数组访问。
优先选择显式构造,而不是依赖于“神奇”C++ 行为的隐式构造。
当函数可以对任何类似范围的容器进行操作时,使用 std::span
作为函数参数。
Foo(const vector<int>&)
相比,如果调用者碰巧将输入存储在另一种类型的容器中,这避免了(可能很昂贵的)转换为 vector 的需要。但是,请注意span.h中记录的陷阱。void Foo(std::span<const int> data);
std::vector<int> vec{1,2,3};
Foo(vec);
尽可能优先选择 enum class
(作用域枚举)而不是 enum
(传统枚举)。
int
,以及由于枚举器导出到周围作用域而导致的的名称冲突。关于枚举的 switch
语句示例:
enum class Tabs {
info,
console,
network_graph,
peers
};
int GetInt(Tabs tab)
{
switch (tab) {
case Tabs::info: return 0;
case Tabs::console: return 1;
case Tabs::network_graph: return 2;
case Tabs::peers: return 3;
} // no default case, so the compiler can warn about missing cases
assert(false);
}
理由: 该注释记录了跳过 default:
标签,并且它符合 clang-format
规则。该断言防止在某些编译器上触发 -Wreturn-type
警告。
使用 std::string
,避免使用 C 字符串操作函数。
\0
字符带来的意外。此外,某些 C 字符串操作
倾向于根据平台甚至用户区域设置而采取不同的操作。对于 strprintf
、LogInfo
、LogDebug
等,格式化字符不需要算术类型的大小说明符(hh、h、l、ll、j、z、t、L)。
谨慎使用 .c_str()
。其唯一有效的用途是将 C++ 字符串传递给采用 NULL 结尾的 C 函数。
不要在使用大小数组时(因此也与 .size()
一起)使用它。改用 .data()
获取指向原始数据的指针。
理由:尽管从 C++11 开始保证这是安全的,但 .data()
更好地传达了意图。
不要在使用 .c_str()
向tfm::format
,strprintf
,LogInfo
,LogDebug
等传递字符串时,使用.c_str()
。
理由:这是多余的。Tinyformat 可以处理字符串。
不要使用 .c_str()
转换为QString
。请改用QString::fromStdString()
。
理由:Qt 具有用于从/向 C++ 转换其字符串类型的内置功能。无需自己滚动。
在确实调用.c_str()
的情况下,你可能还需要检查该字符串是否不包含嵌入的“\0”字符,因为它会(必然)截断该字符串。这可能用于从日志记录中隐藏部分字符串或规避检查。如果字符串的使用对此敏感,请注意首先检查字符串中是否包含嵌入的 NULL 字符,如果存在任何字符,则拒绝该字符串。
尽管默认情况下未启用遮蔽警告 (-Wshadow
)(它可以防止使用具有相同名称的不同变量引起的问题),
请命名变量,以使它们的名称不会遮蔽源代码中定义的变量。
当使用嵌套循环时,不要将内部循环变量命名为与外部循环中相同的名称,等等。
Clang lifetimebound
属性
可用于告知编译器生命周期绑定到一个对象,并且如果该对象具有比临时对象的无效使用更短的生命周期,则可能会看到编译时警告。你可以通过添加定义在 src/attributes.h
中的 LIFETIMEBOUND
注解来使用该属性;请在代码库中搜索示例。
优先使用 Mutex
类型而不是 RecursiveMutex
类型。
一致地使用 Clang 线程安全分析 注释 ,以获取关于代码中潜在的竞争条件或死锁的编译时警告。
在与定义分开声明的函数中,线程安全注释应仅添加到函数声明中。定义上的注释可能导致调用站点之间出现误报(缺少编译失败)。
优先使用类中的锁,而不是全局锁,并且这些锁是类的内部锁(私有或受保护的),而不是公共锁。
将函数声明中的注释与函数定义中的运行时断言相结合(如果 LOCK()
在其之后是无条件调用的,则可以省略 AssertLockNotHeld()
,因为 LOCK()
在内部执行与 AssertLockNotHeld()
相同的检查,对于非递归互斥锁):
// txmempool.h
class CTxMemPool
{
public:
...
mutable RecursiveMutex cs;
...
void UpdateTransactionsFromBlock(...) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, cs);
...
}
// txmempool.cpp
void CTxMemPool::UpdateTransactionsFromBlock(...)
{
AssertLockHeld(::cs_main);
AssertLockHeld(cs);
...
}
// validation.h
class Chainstate
{
protected:
...
Mutex m_chainstate_mutex;
...
public:
...
bool ActivateBestChain(
BlockValidationState& state,
std::shared_ptr<const CBlock> pblock = nullptr)
EXCLUSIVE_LOCKS_REQUIRED(!m_chainstate_mutex)
LOCKS_EXCLUDED(::cs_main);
...
bool PreciousBlock(BlockValidationState& state, CBlockIndex* pindex)
EXCLUSIVE_LOCKS_REQUIRED(!m_chainstate_mutex)
LOCKS_EXCLUDED(::cs_main);
...
}
// validation.cpp
bool Chainstate::PreciousBlock(BlockValidationState& state, CBlockIndex* pindex)
{
AssertLockNotHeld(m_chainstate_mutex);
AssertLockNotHeld(::cs_main);
{
LOCK(cs_main);
...
}
return ActivateBestChain(state, std::shared_ptr<const CBlock>());
}
使用 -DDEBUG_LOCKORDER
构建和运行测试,以验证是否未引入潜在的死锁。默认情况下,在使用 -DCMAKE_BUILD_TYPE=Debug
构建时会定义此选项。
当使用 LOCK
/TRY_LOCK
时,请注意锁存在于当前范围的上下文中,因此请用大括号将语句和需要锁的代码括起来。
正确:
{
TRY_LOCK(cs_vNodes, lockNodes);
...
}
错误:
TRY_LOCK(cs_vNodes, lockNodes);
{
...
}
尽可能使用 Python 或 Rust 编写脚本,而不是 bash。
实现代码应放入 .cpp
文件,而不是 .h
文件,除非由于模板使用或由于内联导致的性能至关重要。
每个 .cpp
和 .h
文件都应 #include
它直接使用的每个头文件,这些文件中的类、函数或其他定义,即使这些头文件已通过其他头文件间接包含。
不要将任何内容导入全局命名空间 (using namespace ...
)。使用完全指定的类型,例如 std::string
。
使用注释终止命名空间 (// namespace mynamespace
)。注释应与关闭命名空间的大括号位于同一行,例如
namespace mynamespace {
...
} // namespace mynamespace
namespace {
...
} // namespace
请勿在模型代码(类 *Model
)中显示或操作对话框。
避免在 GUI 线程中添加缓慢或阻塞的代码。特别地,不要添加新的 interfaces::Node
和 interfaces::Wallet
方法调用,即使它们现在可能很快,以防它们将来被更改为锁定或跨进程通信。
首选将 GUI 线程中的工作卸载到工作线程(参见控制台代码中的 RPCExecutor
作为示例)或采取其他步骤(参见 https://doc.qt.io/archives/qq/qq27-responsive-guis.html)以保持 GUI 的响应性。
存储库的几个部分是在其他地方维护的软件的子树。
通常,这些由 Bitcoin Core 的活跃开发者维护,在这种情况下,更改应直接向上游进行,而无需直接针对该项目进行 PR。它们将在下一个子树合并中合并回来。
其他的是与我们的项目没有紧密联系的外部项目。对这些项目的更改也应发送到上游,但错误修复也可能谨慎地针对 Bitcoin Core 子树进行 PR,以便可以Swift集成它们。外观上的更改应发送到上游。
test/lint/git-subtree-check.sh
中有一个工具(说明)以检查子树目录与其上游存储库的一致性。
该工具说明还包括由 Bitcoin Core 管理的子树列表。
少数外部管理的子树的最终上游是:
src/leveldb
src/crc32c
升级 LevelDB 时必须格外小心。本节解释了你必须注意的问题。
在大多数配置中,我们使用 max_open_files
的默认 LevelDB 值,在撰写本文时该值为 1000。如果 LevelDB 实际上使用了如此多的文件描述符,则会导致 Bitcoin 的 select()
循环出现问题,因为它可能导致创建新的套接字,其中 fd 值 >= 1024。因此,在 64 位 Unix 系统上,我们依赖于内部 LevelDB 优化,该优化使用 mmap()
+ close()
打开表文件,而无需实际保留对表文件描述符的引用。如果要升级 LevelDB,则必须对更改进行健全性检查,以确保此假设仍然有效。
除了查看 env_posix.cc
中的上游更改外,你还可以使用 lsof
来检查这一点。例如,在 Linux 上,此命令将显示打开的 .ldb
文件计数:
$ lsof -p $(pidof bitcoind) |\
awk 'BEGIN { fd=0; mem=0; } /ldb$/ { if ($4 == "mem") mem++; else fd++ } END { printf "mem = %s, fd = %s\n", mem, fd}'
mem = 119, fd = 0
mem
值显示了映射了多少文件,fd
值显示了这些文件正在使用的文件描述符的数量。你应该检查 fd
是否是一个小数字(通常在 64 位主机上为 0)。
有关更多详细信息,请参见 dbwrapper.cc
中 SetMaxOpenFiles()
函数中的注释。
LevelDB 更改可能会无意中更改节点之间的共识兼容性。这发生在 Bitcoin 0.8 中(当时首次引入 LevelDB)。升级 LevelDB 时,你应该查看上游更改,以检查影响共识兼容性的问题。
例如,如果 LevelDB 有一个 bug,该 bug 意外地阻止在边缘情况下返回密钥,并且该 bug 已在上游修复,则该 bug“修复”将是不兼容的共识更改。在这种情况下,正确的行为是在将更新应用于 Bitcoin 的 LevelDB 副本之前,先还原上游修复。通常,你应该警惕任何影响从 LevelDB 查询返回的数据的上游更改。
对于可以使用 bash 脚本轻松自动化的重新格式化和重构提交,我们使用脚本化差异提交。bash 脚本包含在提交消息中,我们的 CI 作业检查脚本的结果是否与提交相同。这有助于审查者,因为他们可以验证脚本是否完全按照应该执行的操作进行。它也有助于修改(因为只需在新主提交上重新运行相同的脚本)。
要创建脚本化差异:
scripted-diff:
开始提交消息(然后在同一行上描述差异)-BEGIN VERIFY SCRIPT-
-END VERIFY SCRIPT-
脚本化差异由工具 test/lint/commit-script-check.sh
验证。该工具的默认行为是,在提供提交时,验证从开始到所述提交的所有脚本化差异。在内部,该工具将提供的第一个参数传递给 git rev-list --reverse
,以确定要验证脚本差异的提交,忽略不符合上述提交消息格式的提交。
对于开发,验证范围 A..B
内的所有脚本化差异可能会更方便,例如:
test/lint/commit-script-check.sh origin/master..HEAD
如果需要在多个文件中替换,请优先使用 git ls-files
而不是 find
或 globbing,并使用 git grep
而不是 grep
,以避免更改未受到版本控制的文件。
对于高效的替换脚本,请将选择范围缩小到可能需要修改的文件,因此例如,不要 blanket git ls-files src | xargs sed -i s/apple/orange/
,而是使用 git grep -l apple src | xargs sed -i s/apple/orange/
。
此外,最好使文件选择范围尽可能具体 - 例如,仅在期望替换的目录中进行替换 - 因为这降低了通过重新运行脚本修改提交来引入意外更改的风险。
脚本化差异的一些很好的例子:
scripted-diff: 将 InitInterfaces 重命名为 NodeContext 使用优雅的脚本来替换所有源文件中多个术语的出现。
scripted-diff: 删除 g_connman, g_banman 全局变量 替换特定源文件列表中的特定术语。
scripted-diff: 将 fprintf 替换为 tfm::format 进行全局替换,但排除某些目录。
要查找存储库中以前的所有脚本化差异用法,请执行以下操作:
git log --grep="-BEGIN VERIFY SCRIPT-"
应为以下任何 PR 编写发行说明:
发行说明应添加到 PR 特定的发行说明文件中,位于 /doc/release-notes-<PR number>.md
,以避免多个 PR 之间的冲突。所有 release-notes*
文件都会在发布之前合并到一个 release-notes-<version>.md
文件中。
一些关于引入和审查新 RPC 接口的指南:
方法命名:使用连续的小写名称,例如 getrawtransaction
和 submitblock
。
参数和字段命名:请考虑 API 中是否已存在用于所讨论对象类型的命名样式或拼写约定(例如,blockhash
),如果是,请尝试使用它。如果没有,请使用蛇形命名法 fee_delta
(而不是,例如,feedelta
或驼峰命名法 feeDelta
)。
使用 JSON 解析器进行解析,除非绝对必要,否则不要手动从参数中解析整数或字符串。
理由:在调用者和被调用者站点都引入了手动字符串操作代码,这很容易出错,并且很容易出错,例如转义错误。JSON 已经支持嵌套数据结构,无需重新发明轮子。
例外:AmountFromValue 可以将金额解析为字符串。引入此功能是因为许多 JSON 解析器和格式化程序都将十进制数字硬编码为浮点值处理,从而导致潜在的精度损失。这对于货币价值是不可接受的。在输入或输出货币价值时,始终使用 AmountFromValue
和 ValueFromAmount
。唯一的例外是 prioritisetransaction
和 getblocktemplate
,因为它们的接口在 BIP22 中已指定为“按原样”。
缺少参数和'null' 应该被视为相同:作为默认值。如果没有默认值,两种情况都应以相同的方式失败。遵循此指南的最简单方法是使用 params[x].isNull()
而不是 params.size() <= x
检测未指定的参数。params[x].isNull()
如果参数为 null 或缺失则返回 true,而 params.size() <= x
如果缺失则返回 true,如果为 null 则返回 false。
尽量不要基于参数类型重载方法。例如,不要让 getblock(true)
和 getblock("hash")
执行不同的操作。
理由:这无法与 bitcoin-cli
一起使用,并且可能会让用户感到意外。
例外:由于历史原因,一些 RPC 调用可以同时接受 int
和 bool
,最值得注意的是,当布尔值切换为多值时。在这种情况下,始终 将 false 映射到 0,将 true 映射到 1。
对于新的 RPC 方法,如果实现 verbosity
参数,请使用整数详细程度而不是布尔值。禁止使用布尔详细程度(参见 util.h 中的 ParseVerbosity()
)。
将每个非字符串 RPC 参数 (method, idx, name)
添加到 rpc/client.cpp
中的表 vRPCConvertParams
。
bitcoin-cli
和 GUI 调试控制台使用此表来确定如何将纯文本命令行转换为 JSON。如果类型不匹配,则该方法可能无法从那里使用。RPC 方法必须是钱包方法或非钱包方法。不要引入基于钱包存在与否而在行为上有所不同的新方法。
尝试使 RPC 响应成为 JSON 对象。
钱包 RPC 调用 BlockUntilSyncedToCurrentChain,以保持与
getblockchaininfo
在调用执行之前的状态一致。行为不依赖于当前链状态的钱包
RPC 可以省略此调用。
使用无效的 bech32 地址(例如,在常量数组 EXAMPLE_ADDRESS
中)用于 RPCExamples
帮助文档。
在文档中描述 UNIX 纪元时间或时间戳时,使用 UNIX_EPOCH_TIME
常量。
在将路径转换为 JSON 字符串时,请使用 fs::path::u8string()
/fs::path::utf8string()
和 fs::u8path()
函数,而不是 fs::PathToString
和 fs::PathFromString
一些修改现有 RPC 接口的指南:
最好避免以向后不兼容的方式更改 RPC,但在这种情况下,添加关联的 -deprecatedrpc=
选项以在弃用期间保留以前的 RPC 行为。向后不兼容的更改包括:数据类型更改(例如,从 {"warnings":""}
到 {"warnings":[]}
、更改值从字符串到数字等)、值的逻辑含义更改或键名更改(例如,{"warning":""}
到 {"warnings":""}
)。通常认为向对象添加键是向后兼容的。包括一个版本说明,该说明将用户引用到 RPC 帮助中,以了解功能弃用和重新启用先前行为的详细信息。RPC 帮助示例。
deprecatedrpc
的实现为下游应用程序提供了迁移的宽限期。版本说明提供给下游用户的通知。在代码库中旨在相互独立的各个部分(节点、钱包、GUI)之间的内部接口,定义在 src/interfaces/
中。其中定义的主要接口类有 interfaces::Chain
,钱包使用它来访问节点的最新链状态;interfaces::Node
,GUI 使用它来控制节点;interfaces::Wallet
,GUI 使用它来控制单个钱包;以及 interfaces::Mining
,RPC 使用它来生成区块模板。还有一些更专业的接口类型,如 interfaces::Handler
和 interfaces::ChainClient
,它们在各种接口方法之间传递。
接口类的编写风格很特殊,这样节点、钱包和 GUI 代码就不需要在同一个进程中运行,并且类声明可以更轻松地与支持进程间通信的工具和库一起使用:
接口类应该是抽象的,并且具有纯虚函数。这允许多个实现继承自同一个接口类,特别是允许一个实现在本地进程中执行功能,而其他实现可以将调用转发到远程进程。
接口方法定义应该包装现有功能,而不是实现新功能。任何实质性的新节点或钱包功能都应该在 src/node/
或 src/wallet/
中实现,并且仅在 src/interfaces/
中公开,而不是在那里实现,这样它才能更模块化,并且可以被单元测试访问。
接口方法参数和返回类型应该要么是可序列化的,要么是其他接口类。接口方法不应该传递对无法序列化或从另一个进程访问的对象的引用。
示例:
// 好:接受字符串参数并返回接口类指针
virtual unique_ptr<interfaces::Wallet> loadWallet(std::string filename) = 0;
// 坏:返回无法从另一个进程使用的 CWallet 引用
virtual CWallet& loadWallet(std::string filename) = 0;
// 好:接受和返回基本类型
virtual bool findBlock(const uint256& hash, int& out_height, int64_t& out_time) = 0;
// 坏:返回指向链接列表中无法被其他进程访问的内部节点的指针
virtual const CBlockIndex* findBlock(const uint256& hash) = 0;
// 好:接受普通的回调类型并返回接口指针
using TipChangedFn = std::function<void(int block_height, int64_t block_time)>;
virtual std::unique_ptr<interfaces::Handler> handleTipChanged(TipChangedFn fn) = 0;
// 坏:返回特定于本地进程的 boost 连接
using TipChangedFn = std::function<void(int block_height, int64_t block_time)>;
virtual boost::signals2::scoped_connection connectTipChanged(TipChangedFn fn) = 0;
接口方法不应该被重载。
理由:一致性和对代码生成工具的友好性。
示例:
// 好:方法名称是唯一的
virtual bool disconnectByAddress(const CNetAddr& net_addr) = 0;
virtual bool disconnectById(NodeId id) = 0;
// 坏:方法按类型重载
virtual bool disconnect(const CNetAddr& net_addr) = 0;
virtual bool disconnect(NodeId id) = 0;
接口方法名应该是 lowerCamelCase
,而独立函数名应该是 UpperCamelCase
。
理由:一致性和对代码生成工具的友好性。
示例:
// 好:lowerCamelCase 方法名
virtual void blockConnected(const CBlock& block, int height) = 0;
// 坏:大写类方法
virtual void BlockConnected(const CBlock& block, int height) = 0;
// 好:UpperCamelCase 独立函数名
std::unique_ptr<Node> MakeNode(LocalInit& init);
// 坏:小写独立函数
std::unique_ptr<Node> makeNode(LocalInit& init);
注意:最后一个约定在 src/interfaces/
之外通常不遵循,尽管之前在 #14635 中讨论过。
- 原文链接: github.com/bitcoin/bitco...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!