FreeCAD 中的 FastSignals:基于 C++17 的高性能信号槽库及其从 Boost.Signals2 的迁移实践
【免费下载链接】FreeCADOfficial source code of FreeCAD, a free and opensource multiplatform 3D parametric modeler.项目地址: https://gitcode.com/GitHub_Trending/fr/FreeCAD
导读
FastSignals 是 FreeCAD 项目内置(src/3rdParty/FastSignals)的一套纯 C++17 实现的信号与槽(signals and slots)库,其公开 API 与 Boost.Signals2 兼容,可作为后者的即插即用替代品。本文将围绕该库的设计动机、核心 API、弱引用绑定(weak this)用法,以及从 Boost.Signals2 大规模迁移的完整步骤展开,并结合 FreeCAD 源码(如 src/App/Application.h 中大量fastsignals::signal成员)展示真实应用形态。读完后,你将掌握如何在 FreeCAD 中直接使用该库,并能独立完成存量代码的平滑迁移。
一、FastSignals 是什么
FastSignals 是一个与 Boost.Signals2 API 兼容的 C++17 信号/槽实现,由 iSpring 团队开源并以 MIT 协议发布(见 LICENSE)。正如 README.md 所述,它拥有三项核心承诺:
- 作为 Boost.Signals2 的 drop-in 替代品:使用相同的 API,迁移成本低;
- 更优的性能与更紧凑的二进制代码:这是其存在的根本理由;
- 大部分操作线程安全:包括并发的连接(connect)、断开(disconnect)与发射(emit)。
它由紧凑的纯 C++17 代码实现,库结构集中在 libfastsignals 目录下,头文件位于libfastsignals/include/fastsignals/,实现文件位于libfastsignals/src/。
FreeCAD 在 3rdParty 构建体系中通过 src/3rdParty/CMakeLists.txt 中的add_subdirectory(FastSignals)引入该库,并在 App 核心层大量使用:以 src/App/Application.h 为例,文档对象的生命周期事件(新建、删除、重命名、保存、撤销/重做、属性变更等)均由fastsignals::signal承载,例如signalNewDocument、signalDeletedObject、signalBeforeChangeObject、signalChangedObject。这说明 FastSignals 并非孤立的三方代码,而是 FreeCAD 文档核心事件机制的实际基石。
二、为什么选择 FastSignals
why-fastsignals.md 明确列出了该库的取舍。
优势
- 比 Boost.Signals2 更快;
- 二进制代码更紧凑;
- 与 Boost.Signals2 相同的 API。
局限与差异
- 仅支持 C++17 兼容编译器:Visual Studio 2017、现代 Clang、现代 GCC;
- 缺少少量 Boost.Signals2 中极少使用的特性:
- 无法通过
signal::connect_extended在槽内获得 connection; - 不支持
slot::track的关联对象跟踪——请改用bind_weak(详见 bind_weak.md); - 没有
shared_connection_block类的临时信号阻塞功能(注意:源码层面实际提供了shared_connection_block,见下文分析); - 由于没有
disconnect(slot)重载,无法按槽等价断开; - 任何其他 API 差异都被视为 bug,欢迎上报。
- 无法通过
基准测试结果
文档指出tests/libfastsignals_bench目录包含一个简单基准测试,对比 Boost.Signals2 与 libfastsignals 在信号被高频发射且分别挂载 0、1、8 个活跃连接时的性能,三种场景下 libfastsignals 快 3~6 倍。原文档给出了如下示例结果(相对耗时,数值越大代表相对 Boost 越快):
*** Results: measure emit_boost emit_fastsignals emit_boost/0 1.00 3.00 emit_boost/1 1.00 5.76 emit_boost/8 1.00 3.70 ***需要说明的是,当前仓库中未包含该基准测试目录,上述为原文档记录的数据;如需复现,应结合自身编译环境实测。
源码层面的性能原理
从实现看,FastSignals 的性能优势有其结构基础:signal_impl.h 使用std::vector存储槽函数与 ID,配合自旋锁spin_mutex保护并发访问;signal.h 的operator()直接通过invoke<...>遍历槽并调用,而invoke内部使用if constexpr在编译期区分 void 与非 void 返回路径(见 signal_impl.h),非 void 时才构造 combiner 收集结果。这种“不为不使用的东西付费”的设计正是文档所强调的:唯一的例外是——你始终要为多线程支持付费(每次调用都涉及自旋锁)。
三、快速上手:基础示例
simple-examples.md 提供了两个可直接编译运行的最小示例。
3.1 signal 与 connection
// Creates signal and connects 1 slot, calls 2 times, disconnects, calls again. // Outputs: // 13 // 17 #include "libfastsignals/signal.h" using namespace fastsignals; int main() { signal<void(int)> valueChanged; connection conn; conn = valueChanged.connect([](int value) { cout << value << endl; }); valueChanged(13); valueChanged(17); conn.disconnect(); valueChanged(42); }要点:connect返回connection对象,调用disconnect()后槽不再被调用,因此最后一次valueChanged(42)无任何输出。
3.2 scoped_connection 的 RAII 断开
// Creates signal and connects 1 slot, calls 2 times, calls again after scoped_connection destroyed. // - note: scoped_connection closes connection in destructor // Outputs: // 13 // 17 #include "libfastsignals/signal.h" using namespace fastsignals; int main() { signal<void(int)> valueChanged; { scoped_connection conn; conn = valueChanged.connect([](int value) { cout << value << endl; }); valueChanged(13); valueChanged(17); } valueChanged(42); }与connection不同,scoped_connection在析构时自动断开连接(RAII 语义),因此离开作用域后valueChanged(42)同样不会触发任何槽。
3.3 源码中的连接类型体系
connection.h 完整定义了连接相关的类层次:
connection:管理信号-槽连接生命周期,connected()/disconnect()操作线程安全(可在其他线程发射期间断开);该类本身不线程安全,不能跨线程共享同一连接对象;advanced_connection:支持临时阻塞回调执行,内部持有advanced_connection_impl(其m_blockCounter为std::atomic<int>);shared_connection_block:用于阻塞 advanced connection 的回调执行;scoped_connection/advanced_scoped_connection:析构时自动断开,可移动不可拷贝,scoped_connection提供release()释放连接。
注意一个细节:why-fastsignals.md声称 FastSignals 没有shared_connection_block,但从当前仓库源码看,connection.h 已提供该类的完整实现,且 signal.h 提供connect(slot, advanced_tag)重载来配合阻塞功能(仅限返回void的槽)。因此迁移时若用到临时阻塞,可参考该实现。
四、核心 API 深入:signal 类模板
signal.h 是库的门面,值得逐项理解:
template <class Signature, template <class T> class Combiner = optional_last_value> class signal;- Signature:形如
Return(Arguments...)的函数签名,例如void(int)、void(const Document&, bool)(对应 FreeCAD 中signalNewDocument的声明方式); - Combiner:结果合并器,默认
optional_last_value。其行为见 combiners.h:非 void 时把所有槽返回值缩减为“最后一个槽的返回值”,以std::optional<T>呈现;void特化则直接以void为result_type。
关键成员:
| 成员 | 说明 |
|---|---|
connection connect(slot_type slot) | 订阅槽到信号,返回连接对象;每次调用信号(作为函数对象)时所有槽被依次调用 |
advanced_connection connect(slot_type slot, advanced_tag) | 带阻塞能力的连接,static_assert限定仅用于返回void的槽 |
void disconnect_all_slots() | 断开所有槽 |
std::size_t num_slots() | 返回已挂载槽数量 |
bool empty() | 无任何槽时返回 true |
result_type operator()(args...) | 触发信号发射,调用所有槽 |
operator slot_type() | 允许信号自身作为另一信号的槽(信号链) |
此外,signal禁止拷贝构造/拷贝赋值,但支持移动;std::swap通过 ADL 找到的自由函数swap实现(signal.h)。内部实现上,signal持有std::shared_ptr<detail::signal_impl>,因此移动语义安全且拷贝被有意禁止。
五、bind_weak:弱引用绑定的正确姿势
bind_weak.md 是 FastSignals 替代 Boost.Signals2track()/track_foreign()的官方方案。
5.1 基本用法
- 使用
fastsignals::bind_weak替代std::bind,确保绑定对象已销毁时方法不会被调用; - 第一个参数为指向 T 类成员函数的指针,第二个参数为
shared_ptr<T>或weak_ptr<T>; - 示例:
bind_weak(&Document::save(), document, std::placeholders::_1),其中document为weak_ptr<Document>或shared_ptr<Document>。
5.2 Weak this 惯用法
bind_weak实现了经典的 “weak this” 惯用法,用于避免异步/多线程程序中的悬垂指针与非法内存访问。原始手写形式如下:
struct Entity : std::enable_shared_from_this<Entity> { int value = 42; void print() { std::cout << "print called, num = " << value << std::endl; } std::function<void()> print_later() { // ! weak this idiom here ! auto weak_this = weak_from_this(); return [weak_this] { if (auto shared_this = weak_this.lock()) { shared_this->print(); } }; } }; int main() { auto entity = std::make_shared<Entity>(); auto print = entity->print_later(); // Prints OK. print(); // Prints nothing - last shared_ptr to the Entity destroyed, so `weak_this.lock()` will return nullptr. entity = nullptr; print(); }5.3 用 bind_weak 规避接收者生命周期问题
将Entity::print()连接到信号后,在Entity销毁前后各发射一次:销毁后bind_weak无法lock()到对象,槽不会被调用,也不会发生内存访问违例——第二次event()静默无事。
#include <fastsignals/signal.h> #include <fastsignals/bind_weak.h> #include <iostream> using VoidSignal = fastsignals::signal<void()>; using VoidSlot = VoidSignal::slot_type; struct Entity : std::enable_shared_from_this<Entity> { int value = 42; VoidSlot get_print_slot() { // Here fastsignals::bind_weak() used instead of std::bind. return fastsignals::bind_weak(&Entity::print, weak_from_this()); } void print() { std::cout << "print called, num = " << value << std::endl; } }; int main() { VoidSignal event; auto entity = std::make_shared<Entity>(); event.connect(entity->get_print_slot()); // Here slot called - it prints `slot called, num = 42` event(); entity = nullptr; // Here nothing happens - no exception, no slot call. event(); }5.4 底层实现
bind_weak.h 的实现以detail::weak_binder为核心:它保存成员函数指针与std::weak_ptr<ClassType>,重载operator()时先m_pObject.lock(),成功才调用成员函数,失败则返回ReturnType()的默认构造值。对外提供四个重载,覆盖 const/非 const 成员函数 ×shared_ptr/weak_ptr四种组合,最终通过std::bind包装返回可调用对象。
六、从 Boost.Signals2 迁移的完整指南
migration-from-boost-signals2.md 面向“大型代码库从 Boost.Signals2 迁移到 FastSignals”的场景,给出了处理已知迁移问题的正确路径。迁移中你大概率会遇到两类情况:
- 代码直接使用
boost::signals2::命名空间与<boost/signals2.hpp>头文件; - 代码间接依赖了
<boost/signals2.hpp>隐式包含的第三方头文件。
迁移动机回顾
- FastSignals不是 header-only,二进制代码更紧凑;
- 使用 C++17 可变参数模板、
constexpr if等现代元编程技术,编译更快、二进制更紧凑; - 对多数代码库可能更快——不为不使用的东西付费(多线程支持除外)。
Step 1:创建别名头文件
建议新建一个统一头文件,把命名空间与类型别名映射到 FastSignals,后续代码只需改动 include 路径即可。例如:
// signals.h #pragma once #include <fastsignals/signal.h> #include <fastsignals/bind_weak.h> namespace boost { namespace signals2 { // 将 Boost 命名空间映射到 fastsignals // using fastsignals::signal; ... } }Step 2:重建并修复编译错误
2.1 补全缺失的 include
Boost.Signals2 是 header-only 库,会隐式包含大量 STL/Boost 头;FastSignals 不会。例如:
#include <boost/signals2.hpp> // Also includes std::map, boost::variant, boost::optional, etc. // Compiled OK even without `#include <map>`! std::map CreateMyMap();迁移后这类代码必须显式包含对应头文件。下表给出了常见的缺失符号与应补的头文件:
| Class | Header |
|---|---|
| std::map | #include <map> |
| boost::variant | #include <boost/variant/variant.hpp> |
| boost::optional | #include <boost/optional/optional.hpp> |
| boost::scoped_ptr | #include <boost/scoped_ptr.hpp> |
| boost::noncopyable | #include <boost/noncopyable.hpp> |
| boost::bind | #include <boost/bind.hpp> |
| boost::function | #include <boost/function.hpp> |
如果只是想快速编译通过,可以在你的signals.h头中一次性补上这些 include;但官方明确警告:不推荐在 FastSignals 中引入这些额外头文件(见原文档代码注释)。
2.2 移除 void 信号槽中的多余 return
Boost.Signals2 允许如下代码(无警告),但 FastSignals 要求signal<void(...)>的槽不能返回非 void 值:
boost::signals2::signal<void()> event; event.connect([] { return true; });修复方式:直接删除槽中的return,或用 lambda 包装槽并忽略其返回值。
2.3 用 bind_weak 替换 track() / track_foreign()
Boost.Signals2 可通过track(...)/track_foreign(...)跟踪关联对象的生命周期。原文档给出了典型迁移对照:左侧是 Boost 写法,右侧是 FastSignals 写法。
Boost.Signals2 写法:
#include <boost/signals2.hpp> #include <iostream> #include <memory> using VoidSignal = boost::signals2::signal<void()>; using VoidSlot = VoidSignal::slot_type; struct Entity : std::enable_shared_from_this<Entity> { int value = 42; VoidSlot get_print_slot() { // Here track() tracks object itself. return VoidSlot(std::bind(&Entity::print, this)).track_foreign(shared_from_this()); } void print() { std::cout << "print called, num = " << value << std::endl; } }; int main() { VoidSignal event; auto entity = std::make_shared<Entity>(); event.connect(entity->get_print_slot()); // Here slot called - it prints `print called, num = 42` event(); entity = nullptr; // This call does nothing. event(); }FastSignals 写法:
#include <fastsignals/bind_weak.h> #include <iostream> using VoidSignal = fastsignals::signal<void()>; using VoidSlot = VoidSignal::slot_type; struct Entity : std::enable_shared_from_this<Entity> { int value = 42; VoidSlot get_print_slot() { // Here fastsignals::bind_weak() used instead of std::bind. return fastsignals::bind_weak(&Entity::print, weak_from_this()); } void print() { std::cout << "print called, num = " << value << std::endl; } }; int main() { VoidSignal event; auto entity = std::make_shared<Entity>(); event.connect(entity->get_print_slot()); // Here slot called - it prints `slot called, num = 42` event(); entity = nullptr; // Here nothing happens - no exception, no slot call. event(); }两者行为一致:对象销毁后再次发射信号均“无任何事发生”。迁移时把std::bind(&Class::method, this)替换为fastsignals::bind_weak(&Class::method, weak_from_this())即可。
2.4 结果合并器(Result Combiners)的差异
原文档在此处仅给出章节标题。结合 combiners.h 可推断:FastSignals 默认且主要提供optional_last_value合并器——收集所有槽的返回值但只保留最后一个,并以std::optional包装(void 特化直接返回 void)。这与 Boost.Signals2 的optional_last_value语义一致;如果你的代码依赖其他 combiner(如first_value、last_value等),迁移时需要自行实现对应模板或调整逻辑。
Step 3:运行测试
完成代码修改后,运行你现有的全部自动化测试(单元、集成、系统、压力、基准、UI 测试)。按文档说法,大概率不会报错;若发现问题,请向 FastSignals 项目上报 issue。
七、测试与构建
FastSignals 自带完整的测试体系(见 tests 目录):
- 单元测试
libfastsignals_unit_tests:覆盖signal_tests.cpp、Function_tests.cpp、bind_weak_tests.cpp,使用的测试框架是仓库内置的 Catch2(catch.hpp); - 压力测试
libfastsignals_stress_tests:signal_stress_tests.cpp用于验证并发场景下的线程安全行为,与文档“并发 connect/disconnect/emit 线程安全”的声明相互印证。
CMake 层面,FastSignals/CMakeLists.txt 定义库目标,cmake/functions.cmake 提供辅助构建函数。在 FreeCAD 中,它经由 src/3rdParty/CMakeLists.txt 被加入构建,业务代码只需#include <fastsignals/signal.h>即可使用(如 src/App/Application.h 所示)。
八、FreeCAD 中的实战形态
FreeCAD 在 App 核心层集中使用 FastSignals 实现文档与对象的事件总线。以 src/App/Application.h 为例:
fastsignals::signal<void (const Document&, bool)> signalNewDocument; fastsignals::signal<void (const Document&)> signalDeleteDocument; fastsignals::signal<void ()> signalDeletedDocument; fastsignals::signal<void (const Document&)> signalRelabelDocument; fastsignals::signal<void (const Document&)> signalRenameDocument; fastsignals::signal<void (const Document&)> signalActiveDocument; fastsignals::signal<void (const Document&)> signalSaveDocument; fastsignals::signal<void (const Document&, const std::string&)> signalStartSaveDocument; fastsignals::signal<void (const Document&)> signalUndoDocument; fastsignals::signal<void ()> signalUndo; fastsignals::signal<void (const Document&)> signalRedoDocument; fastsignals::signal<void ()> signalRedo; fastsignals::signal<void (const Document&, const App::Property&)> signalBeforeChangeDocument; fastsignals::signal<void (const Document&, const App::Property&)> signalChangedDocument; fastsignals::signal<void (const App::DocumentObject&)> signalNewObject; fastsignals::signal<void (const App::DocumentObject&)> signalDeletedObject; fastsignals::signal<void (const App::DocumentObject&, const App::Property&)> signalBeforeChangeObject; fastsignals::signal<void (const App::DocumentObject&, const App::Property&)> signalChangedObject;这些信号覆盖了文档从创建、打开、保存、撤销/重做,到对象与属性变更的完整生命周期,供 Gui 层及其他模块订阅。此外,src/App/Application.cpp 中DocOpenGuard以fastsignals::signal<void ()>&引用信号,展示了在 guard 类中按引用持有信号并用于 RAII 通知的典型模式。
这种用法与本文第三节的基础示例完全同构:signal<void (Args...)>声明 →connect订阅 → 通过operator()发射。理解 FastSignals 的核心 API,就等于理解了 FreeCAD 核心层事件机制的钥匙。
九、小结
FastSignals 以纯 C++17、紧凑实现、Boost.Signals2 兼容 API 和 3~6 倍的发射性能提升(按原文档基准数据),成为 FreeCAD 事件系统的底层信号库。迁移路径清晰:创建别名头 → 重建修错(补 include、清理 void 槽 return、bind_weak替换track)→ 全量回归测试。若你的 C++17 项目正受 Boost.Signals2 性能与二进制体积困扰,直接在本仓库的 src/3rdParty/FastSignals 中阅读源码、测试与 docs 文档,即可快速落地。
【免费下载链接】FreeCADOfficial source code of FreeCAD, a free and opensource multiplatform 3D parametric modeler.项目地址: https://gitcode.com/GitHub_Trending/fr/FreeCAD
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考