Use base::Bind With std::function

base::Bind()base::Callback 可以看作是对标准库 std::bind()std::function 的模拟;因为 chromium 项目早在 C++ 11 正式通过前就已经存在好多年了。

我为直播姬设计新的网络通信基础组件时,接口的回调 handler 通常设计为 std::function 对象,因为它可以“吸收”任何函数对象,大多数情况下这个设计运转的非常良好;但是这里有一个略微棘手的问题: base::Callback 对象无法被 std::function 使用。

问题本质很简单,因为 base::Callback 不是一个 function object,因为它不支持以函数形式调用(没有提供 operator() 的重载),instead,它提供了一个 Run() 函数来执行这个 callback…

虽然我之前吐槽过好多次 chromium 的很多架构设计完全是 Java-Style 的,但是这里并不打算再拎出来批判一次,我们要聚焦的是如何解决这个问题。

解决思路很简单:既然它不是一个 function object,那么我们就给他包一层 function object 的皮就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "base/bind.h"
template<typename>
class CallableCallback;
template<typename R, typename... Args>
class CallableCallback<R(Args...)> {
public:
using callback_t = base::Callback<R(Args...)>;
explicit CallableCallback(callback_t callback)
: callback_(callback)
{}
R operator()(Args... args) const
{
return callback_.Run(args...);
}
private:
callback_t callback_;
};
template<typename Callback>
auto MakeCallable(Callback callback)->CallableCallback<typename Callback::RunType>
{
using Sig = typename Callback::RunType;
return CallableCallback<Sig>(callback);
}

辅助函数 MakeCallable() 可以将一个 base::Callback 对象转换成一个 function object。

一个简单的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int Inc(int i)
{
return ++i;
}
void Foo(const std::string&, std::string, int)
{}
class Test {
public:
Test()
: msg_("test"), weak_ptr_factory_(this)
{}
void Bark(const std::string& data)
{
std::cout << data << ":\t" << msg_ << std::endl;
}
std::string msg_;
base::WeakPtrFactory<Test> weak_ptr_factory_;
};
int main()
{
std::cout << "--- test case 1 ---\n";
std::function<int(int)> fn(MakeCallable(base::Bind(&Inc)));
std::cout << fn(5) << std::endl;
std::cout << "--- test case 2 ---\n";
Test* ptr = new Test();
std::function<void()> mfn(
MakeCallable(base::Bind(&Test::Bark, ptr->weak_ptr_factory_.GetWeakPtr(), "inside")));
mfn();
std::cout << "release test instance\n";
delete ptr;
mfn();
return 0;
}

小巧优雅纯天然

禁止程序多实例并存并且自动激活第一个实例

多实例检测非常常规,在程序启动时直接检查用来标记的内核对象是否存在即可,一般都是使用 Mutex

麻烦的点在于如何激活第一个实例,显示它的主窗口。

某直播姬一开始的想法是利用 Pipe 建立 IPC 通讯连接,然后后续实例通过发送消息,让主实例 activate 自己的窗口。

嗯,看起来没毛病,但是做起来问题很多。

首先,IPC 建立需要时间,通讯需要时间,并且断开 IPC 后主实例(Windows 下)一定得需要重新创建一个 Pipe;更加诡异的是,我在本地编译的 Rlease 版本明明可以工作,利用构建机编译出来的版本,子实例管道连接一直失败….

于是看起来 IPC 是一个很自然地选择,实际上却是一个导致复杂度暴涨的下策。

另一个思路是创建一个 Event 内核对象,后续实例直接通过 signal 这个 event 内核对象来通知主实例。

但是这里实现起来有个麻烦的地方:如果要追求简单化,那么就需要一个单独的线程 wait 在这个内核对象上,负责接收通知,缺点是要浪费掉一个线程;如果不想资源浪费,那么考虑到很多框架都提供了 async i/o 的 wrapper,所以可以利用这个点,你开心的话可以挂到 IOCP 上。

嗯,等等,我们是想做啥来着?!

绕了一圈最后想想还是这样算了:

利用 RegisterWindowMessage() 注册一个自定义消息,主实例在 UI message loop / window message procedure 里监听这个消息;子实例启动后就利用 BroadcastMessage() 或者 PostMessage() 广播一下这个消息。

这样有个额外的好处,主实例收到消息直接是在 UI 线程,省掉一次 post task。

某直播姬最后采用的就是这个方案,为此我加了一个 class 起了个名字叫 SingleInstanceGuarantor,嗯,有点中二的感觉。

最后要注意一点,如果框架封装了 window procedure,将几个活动窗口的 window procedure 聚合到一个 message handler 里的话,可能会收到多条消息,可以利用消息的 timestamp 过滤一下。

Monthly Read Posts in May 2017

http resumable download

虽然之前写直播姬自动更新时实现过续传下载,但是功能规范上并没有太完备;而这篇文章很好的补充了几个断点续传中,严格实现会遇到的几个 key points。

例如:不应当假设资源一定支持续传,要首先使用 request header range 检查目标资源是否支持续传,支持 range 的 http resonse code 是 206。

另外,两次断点下载期间,资源可能发生变化,需要在请求时同时附带上 EtagLast-modified 记录,由服务器确定资源是否变化。

Why Exceptions Should be Exceptional

文章从性能角度阐述了为什么不能将 exception 机制作为 routine control flow。

但是个人认为这个切入角度不好,因为很容易给人一种异常机制开销大的错觉,导致读者之后避开使用异常。

异常处理一直都是一个大麻烦,相比 error code handling 不够直观,没有足够的经验很难控制好,加上某些语言自身特性导致固有复杂度暴涨(例如 C++)。

shared-ptr

文章大部分的内容(除了 aliasing)其实 Effective Modern C++ 里都有…

Magical Captureless Lambdas

核心总结出来就是一句话:Captureless lambdas 能够自动转换为对应的 C-Style 函数指针,而在 MSVC 里,implicit cast 能够自动处理不同的 calling convention

Leaky Closures Captureless Lambdas

An entity that is mentioned or used but is not ODR-used within the lambda body, does not need to be captured in the capture list.

Informally, an object is odr-used if its address is taken, or a reference is bound to it.

Lambdas Callbacks

又名:如何用 capturing lambdas 作为 c-style callbacks

Combining Static and Dynamic Polymorphism with C++ Template Mixins
C++ Mixins - Reuse through inheritance is good when done the right way

Mixins pattern in C++ 快速导读

The Very Real Mess of Virtual Functions

十足标题党

并且个人认为,作者试图解决一个前提错误的问题。

文中以 Init()UnInit() 为例阐述观点,然而,这正好说明了为什么如无必要不应该使用 two-phase initialization

文中试图实现的机制不就是 ctor 和 dtor 所做的么……

而对于其他可能需要子类调用父类虚函数的场景,我认为,non-vitual-interface idiom已经足够了。

BTW:就工程领域而言,可能对 GUI 框架来说,文中的一些套路还是有用的;不过因为个人在这方面没有什么经验,因此持保留意见

修复 Breakpad 不能启用 Full Minidump

发布分支为 chrome-58 的 google-breakpad 存在无法启用 full minidump 的问题,表现症状是,一旦启用 MiniDumpWithFullMemory 标志,则输出的 dump 文件为 0 字节,但是整个 dump 生成流程没有任何其他异常,相关返回值甚至是 true

经过一个上午的跟踪调试,发现问题出现在,breakpad 针对 MiniDumpWithFullMemory 做了特殊处理(会生成一个普通的 minidump 和一个 full dump,后者的文件名多了一个full 后缀),但是这个版本(chrome-58)里却忘了调用生成 full dump 的生成函数…..

解决方法:对 master 和 ) 分支的 CrashGenerationServer::GenerateDumpchrome-58 分支做一个 diff,将缺的代码加回来…

缺失的代码就是下面这段…

1
2
3
4
5
6
7
8
9
// If the client requests a full memory dump, we will write a normal mini
// dump and a full memory dump. Both dump files use the same uuid as file
// name prefix.
if (client.dump_type() & MiniDumpWithFullMemory) {
std::wstring full_dump_path;
if (!dump_generator.GenerateFullDumpFile(&full_dump_path)) {
return false;
}
}

另外,开启了 full minidump 模式之后,完整的 dump 体积会暴增到 200MB 上下,要做好心理准备。

用 FFMpeg 生成视频缩略图

除了可以使用 ffmpeg 压制视频外,还能利用 ffmpeg 生成某个视频的缩略图。

利用命令行:

1
ffmpeg.exe -skip_frame nokey -i "some_video.mp4" -vsync 0 -vframes 9 -c:v mjpeg "output_dir\thumb_%d.jpg"

就可以在 output_dir 这个目录下为视频文件 some_video.mp4 从头开始生成最多不超过9张关键帧缩略图,文件名分别是 thumb_1.jpg ~ thumb_9.jpg

这个默认策略适合长度较短的视频(比如半分钟或一分钟内),对于长视频,很可能跑完9张关键帧缩略图,正片都还没开始…

所以,这个时候的生成策略应该考虑能够尽可能的均匀分布于视频内容。

首先利用这个 post 里提到的做法,用 ffprobe.exe 获得视频的长度,记为 T(单位,秒)。

T 均分为 10(9+1)个片段,取 $ t_i = \frac{T}{10} * i, i = 1, 2, \cdots 9 $,这么做的原因是避免取到开头和结尾。

最后针对每个 $ t_i $,运行 ffmpeg

1
ffmpeg.exe -ss t_i -skip_frame nokey -i "some_video.mp4" -vsync 0 -vframes 1 -c:v mjpeg "output_dir\thumb_i.jpg"

唯一麻烦的地方在于输出文件名序列需要我们自己指定。

Monthly Read Posts in Apr 2017

The Cost of Conditional Moves and Branches

Conditional moves 指令并不一定能提升性能,有时候甚至会导致性能衰减。

Post 里直接援引了 StackOverflow 上的这个案例


Cpu Performance Counters on Windows

The ETW(Event Tracing for Windows) now supports CPU performance counters.


GitHub Restful API References
RESTful API最佳实践
撰写安全合格的REST API
跟着 Github 学习 Restful HTTP API 设计
RESTful API 编写指南

如何编写正确的 Restful APIs 以及,一个优秀的 Restful APIs 的范例


Tech Talk: How to Speed up a Python Program 114,000 times

讲道理这个 tech talk 我跳着看了一部分就放弃了,原因很简单,如果真的需要 talk 里提到的那些方法来克服 python 的性能缺点,我选择使用 C++ 重写…

使用 ffmpeg 压制视频

帮主站重写完投稿工具的上传模块后,Neo 和我说,我们这期版本还是得带上视频压制功能…

这是我第一次知道原来 ffmpeg 还可以压制视频。因为重构的缘故,老版本的代码完全不能用(就算不考虑换上层 UI 框架的事儿,老版本那个代码质量…),所以只能抄一下他们的压制相关的驱动参数,自己从头把功能实现一遍。

花了差不多刚好一周的时间提供了一个底层压制模块后,我发现,因为产品需求的因素,实现这部分功能居然可以很好的做为熟悉一个语言涉及操作层面的 roadmap,整理一下 feature list,算算差不多至少要能够提供:

  • 运行子进程,并且能够通过 IPC 读取并解析子进程的 stdout 和 stderr 数据;通常情况下,这意味着需要熟悉 Windows 的 Pipe
  • 压制首先涉及到利用 ffprobe.exe 检测视频的 meta-info,符合压制条件并且用户确认后利用 meta-info 作为压制的部分输入信息;中间涉及到几个线程间的交互
  • 因为压制功能和视频上传在逻辑上具有先后顺序,并且都需要有专门的 worker thread,因此他们可以共用一个线程池;而支持多任务并行操作,又要求线程池的调度策略要足够“及时”(事实上,这个要求有一部分锅来自 chromium net 的 URLFetcher,which requires 网络操作运行的线程在 URLFetcher context 生命周期内是唯一且固定的,因此会出现一个上传任务只要不结束,就会一直霸占一个线程的情况;另有一部分锅来自 chromium base 的 scoped_reptr,居然没有考虑对外暴露自己 ref-count 信息,无形中加大了实现线程池的困难)

因此在项目提测结束,基本验收通过之后,我想籍由这个功能,重新回忆一下 C# 和 WPF (噫,怎么老是再回忆这个…),所以花了一个周末,就有了 SimpleVideoEncoder

但是在实现过程中我发现,纵使有 async/await,它也不是银弹,也有他覆盖不了的 case;而我对 C#/.NET 的多线程模型实在是完全不懂,导致某些细节完全没法下手(只能说某些case 下,CSP 真是好用到想哭,还好 WPF 的 UI-thread 提供了一个 Dispatcher 支持了对 UI 线程的 CSP;再将需求简化到 Demo 程度后,勉强实现了出来。

上层界面基于 WPF,但是没有走 MVVM。

实话说,经过一年半(距离上次写 EasyKeeper),自己主导过两个不大不小的项目后,对于为什么需要有 MVC/MVP/MVVM 有了一些更实际的想法,并且也能理解有时候过分追求这些反而是一种坑。

考虑到目前应该是没法公开为投稿工具写的那部分代码(压制模块算起来大概有 1K+ lines of code),所以这个 SimpleVideoEncoder 的意义也大大折扣,基本只能作为一个 ffmpeg 压制功能的简单展示…

不过最后还是要赞一下 C# 对 Pipe IPC 的封装,几行代码做了需要几十行 C++ 代码的事情;并且基于 event 的通知机制比起传统的 observer,更能准确的体现 OOP 中对象级别消息通信的内涵。