一个轻量型的 Command-Mapping Macros

某 Bililive 沿用了 Chromium 负责在 UI 层不同模块通讯的 command/handler 的机制,但是因为 Chromium 对 command 机制使用得很节制,因此相关的代码量不多,其处理函数一直是一个大大的 switch...case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void ExecuteCommandWithParams(Bililive* receiver, int command, const CommandParamsDetails& params)
{
switch (command) {
case IDC_ALPHA:
// handler code
break;
case IDC_BRAVO:
// handler code
break;
case IDC_CHARLIE:
// handler code
break;
...
}
}

然而到了 Bililive 这里,因为开发人员水平限制,没有最好模块内外的通讯规划,导致存在大量的 command 通信,而 handler 代码依然是直接原封不动写在 switch 里。于是 ExecuteCommandWithParams() 就包含了上千行的实现,而且由于都在一个函数内,没有语法上的 scope 和清晰的语义划分,维护起来相当痛苦。

一个有效的解决方案是采用类似 MFC Message-Mapping 的方式,将不同的 message handler 分离到单独的函数里。至于参数,因为所有 command 的参数都是一致的,直接简化了处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "base/logging.h"
#define BEGIN_BILILIVE_COMMAND_MAP(cmd_receiver, cmd_id, cmd_params) \
{ \
auto __receiver = cmd_receiver; \
auto __id = cmd_id; \
const auto& __params = cmd_params; \
switch (cmd_id) { \
#define ON_BILILIVE_COMMAND(cmd_id, fn) \
case cmd_id: { \
fn(__receiver, __params); \
} \
break; \
#define ON_BILILIVE_COMMAND_UNHANDLED_ERROR() \
default: { \
NOTREACHED() << "Unhandled command " << __id; \
} \
break; \
#define END_BILILIVE_COMMAND_MAP() \
} \
}

于是处理相关的代码就变成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void OnAlpha(Bililive* receiver, const CommandParamsDetails& params)
{}
void OnBravo(Bililive* receiver, const CommandParamsDetails& params)
{}
void OnCharlie(Bililive* receiver, const CommandParamsDetails& params)
{}
void ExecuteCommandWithParams(Bililive* receiver, int command, const CommandParamsDetails& params)
{
BEGIN_BILILIVE_COMMAND_MAP(receiver, command, params)
ON_BILILIVE_COMMAND(IDC_ALPHA, OnAlpha)
ON_BILILIVE_COMMAND(IDC_BRAVO, OnBravo)
ON_BILILIVE_COMMAND(IDC_CHARLIE, OnCharlie)
ON_BILILIVE_COMMAND_UNHANDLED_ERROR()
END_BILILIVE_COMMAND_MAP()
}

Monthly Read Posts in Sep 2017

The Horror of the Standard Library

一个 allocator 引发的惨案…


CppCon2015: Extreme Type Safety with Opaque Typedefs

类型自带语义,但是 typedef/using 只是类型别名,编译器无法区分。而 opaque typedefs 声称可以通过通过 wrapper 的方式生成具有单独类型语义 typedefs。


HTTP Series

https://www.code-maze.com/http-series-part-1/
https://www.code-maze.com/http-series-part-2/
https://www.code-maze.com/http-series-part-3/
https://www.code-maze.com/http-series-part-4/
https://www.code-maze.com/http-series-part-5/

可以作为不错的 HTTP 协议扫盲


CppCon2015: C++ Requests - Curl for People

受到 python requests 启发的 C++ counterpart implementation。

基于 CURL,但是做了非常细致的封装。

看了一下 demo,感觉是个非常不错 curl wrapper


Android GC 那点事儿

行文杂乱,讲道理还不如看专业的 posts…


How to Return a Smart Pointer AND Use Covariance

为 C++ 中实现涉及 smart pointer 的 covariance 接口提供了一个非常不错而且可用的解决方案。

Fix 使用 GDB 调试 Clang 编译的程序时标准库类型始终显示 incomplete type

现象描述

系统是 Linux mint 18,其实就是 ubuntu 16.04 LTS,所用的 clang 版本是 apt 默认的 3.8;gdb 使用系统自带。

使用 clang 编译出来的二进制,哪怕开启了 -g 生成调试信息,在 gdb 下 std::string 始终显示为 incomplete type;而 primitive 类型的值可以正确显示

并且通过 apt 安装 lldb,使用 lldb 调试能够看到变量的值。

因此基本可以确定问题发生在 gdb 和 clang 这一对上

解决方案

来自路边社的资料:clang 的 linux 版本输出调试信息时,默认去掉了标准库相关的组件信息 (??? MMP)

在编译时追加 -fno-limit-debug-info 标志,就可以保证输出完整的调试信息

安利时刻

为什么使用 clang 编译的原因很简单,ubuntu 16.04 上的 apt 默认的 gcc/g++ 5.4 有 bug,编译我的项目代码一定会出错…对比直接从 ppa test 安装 gcc 6.x/7.x 而言,安装 clang 无疑最省事儿…

另外我发现这个问题的解决方案得益于 vscode-lldb-debugger 这个扩展。

正是这个扩展在输出里通过 warning 告诉我需要用相关的编译开关输出完整调试信息,这个问题才的可以解决。(从侧面说明程序员还是应该关注一下 warning XD)

最后安利一个 gdb gui 前端:Nemiver

不过现在看起来 linux 上最好的 gdb/lldb 前端应该是 vscode -.-

Monthly Read Posts in Aug 2017

How to Feel Progress

一篇讲述如何自我管理的 post

核心内容大概就是:

  • Making progress in meaningful work.
  • Break a large project into several small pieces.

Lightning Talks: Anatomy of A Smart Pointer

适合新手扫盲

话说作者其实在 cppcon 2015 也做了一个类似的 talk,但是没有找到视频,ppt 倒是和14年这个 tech talk 的一致


C++ devirtualization in clang

编译器有时候会根据上下文,自动 inline 某些虚函数调用。

这个 talk 讲的就是 clang 在这方面的一些细节。

不做编译期,对细节并不是很感兴趣。

另外这个小哥应该是个俄罗斯人,口语发音实在是太卧槽了


C++ WAT

和上面那个 tech talk 一个小哥,口语听起来想死…

这个 tech talk 专门介绍了一些 C++ 里的坑(UB),作为茶余饭后的点心稍微过一下就差不多了。

工作里要是有人这么写代码,直接拖出去打死。


TCP TIME-WAIT 笔记 - 概览
TCP Flow Control

第一篇本身就是某些 post 的笔记,有时间可以读读原文

第二篇非常细致的解释了 flow control

实话说,理论性的教科书看了很多时候还是云里雾里的,最后还是需要实际工程的洗礼;要不怎么说人的认知是螺旋式上升的呢


Make a class thread safe C++

这篇文章看起来很长,其实就说了一点:两个原子的函数的复合函数自身并不保证原子……

….好吧,如果比较闲的话可以看看。


Thinking in Parallel

核心观点:thinking in parallel is to think about your data as being made up of small fixed-size subset of data that can be dealt with independently.


Shared Libraries: Understanding Dynamic Loading

这篇 post 比较有意思,算是墙裂推荐。

非常工程的叙述如何在 Linux 上使用 shared library,非常好的扫盲科普文。


How to write documentation that’s actually useful

前篇论证为什么要写文档;举例使用的 truck factor theory 挺好玩的…

后面解释应该怎么写文档

讲道理,我觉得如何写文档还真是一个和具体事情强相关的活,除非你和 MSDN 一样可以找到专门撰写的外包团队


A Brief History of the UUID

前半部分是 UUID 的考古,作者文笔还不错,可以当作阅读材料…

后半部分提作者根据自己业务实现的一个 KSUID,优点是吸收 timestamp,可以 k-sortable ordered

先 mark 一下,没准哪天需要

Controlled Type Injection in C++

之前实现 kbase 的 ENSURE 宏支持如下用法

1
2
3
4
5
// Throw a std::runtime_error when condition is violated.
ENSURE(RAISE, cond)(var).Require();
// Throw a MyException when condition is violated.
ENSURE(RAISE, cond)(var).Require<MyException>();

这几天在重新审视的时候觉得将自定义异常类型依托 Require() 注入是一个有问题的设计:强行耦合了 RAISE 这个行为的具体策略和行为无关的 Require() 函数,在设计语义上给人感觉莫名其妙。

更加糟糕的是,因为 ENSURE 宏依托的 Guarantor 类不是一个 class template,自定义异常类的信息没办法直接保存在类中,导致 Require<MyException>() 的重载实现非常丑陋…

于是我想,如果能将异常类型注入通过一个单独的函数完成,并且保存在类成员中,那上面提到的两个问题都可以得到解决

1
2
3
// Call ThrowIn to configure exception type that will be raised.
// And there is only one Require().
ENSURE(RAISE, cond)(var).ThrowIn<MyException>.Require();

并且由于 ThrowIn() 函数具有 configuration 的语义,哪怕实际操作只是 performed-in-debug-only 的 CHECK,也不会有任何违和感

1
2
// We configured exception type but we don't raise an exception at all.
ENSURE(CHECK, cond)(var).ThrowIn<MyException>.Require();

由于个人极度不希望将 Guarantor 变成一个 class template,因此不能使用常规方法保存这个注入的类型信息,换言之,我们大概需要一种能够将类的类型和他包含的某个成员的类型信息的关联拆开。

然后我就想到了 std::shared_ptr

std::shared_ptr 的神奇之处在于,他的 deleter 不是自己类型的一部分,而且可以在运行期替换,which literally is what we need!

于是我就照猫画虎,写了一个 exception-pump,做类似的事情(不过我只是研究了一下实现逻辑,才没有傻到真的去看 std::shared_ptr 的源代码呢 XD):

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
class ExceptionPump {
public:
virtual ~ExceptionPump() = default;
virtual void Throw(const std::string& what) const = 0;
};
template<typename E>
class ExceptionPumpImpl : public ExceptionPump {
public:
void Throw(const std::string& what) const override
{
throw E(what);
}
};
class Guarantor {
public:
// ...
template<typename E>
Guarantor& ThrowIn()
{
static_assert(std::is_base_of<std::exception, E>::value, "E is not a std::exception");
exception_pump_ = std::make_unique<internal::ExceptionPumpImpl<E>>();
return *this;
}
void Guarantor::DoThrow()
{
if (!exception_pump_) {
exception_pump_ = std::make_unique<internal::ExceptionPumpImpl<EnsureFailure>>();
}
exception_pump_->Throw(exception_desc_.str());
}
private:
std::unique_ptr<internal::ExceptionPump> exception_pump_;
}

exception_pmp_ 仅会在需要是才会创建具体对象实例,因此对仅使用 CHECK 行为的语句来说,这块可以认为是 zero-cost

BTW:在我改完这部分代码之后,我把 RAISE 改名成了 THROW,感觉和上下文更加 consistent 一些

GetEnvironmentVariable, API 设计的反面教材

本次 post 的主角 GetEnvironmentVariableW() 用于获取某个环境变量的值,它的文档可以参见这里,看的时候记得仔细研究一下返回值细节。

这个 API 的设计可以说是一个生动的反面教材。

根据 MSDN,它的返回值如下:

  • 如果成功,则返回保存到缓存中的值的字符数,不包括 terminating null character
  • 如果缓冲区不够,那么就返回需要的缓冲区大小,以字符数计数,且包含 terminating null character
  • 如果函数失败,则返回 0 (具体细节由 GetLastError() 给出)

这里不吐槽在返回值非 0 的前提下,存在两种计数的返回值,我们考虑一种很 rare 的情况:环境变量的值是一个 empty string 时,这个 API 应该返回什么

Windows 上是可以通过 SetEnvironmentVariableW() 将某个环境变量的值设置为空字符串的

1
2
3
4
const wchar_t* env_name = L"KCPath";
const wchar_t* str = L"";
BOOL rv = SetEnvironmentVariableW(env_name, str);
assert(rv);

那么此时用 GetEnvironmentVariableW() 获取值时,会返回 0 (因为不包含 terminating null,字符数为 0)。那么问题来了,如果我事先不知道这是一个空字符串,那么我怎么知道这个返回 0 意味着失败还是空字符串?

答案是没法区分,即使你再进一步调用 GetLastError()

为什么?

因为 GetLastError() 的文档说的很清楚:在大部分 API 失败时, calling thread 的 last error code 会被设置;而有部分(少量)API在成功时,也会设置 last error code。

但是很遗憾,通过实验验证,环境变量相关的 API 不属于后者。

这意味着存在一种可能:首先调用了某个环境变量的API,例如 GetEnvironmentVariableW(),但是他失败了,于是 last error code 被设置了;然后紧接着的代码片段利用 GetEnvironmentVariableW() 获取了某个环境变量的值,而这个值是 empty string,于是 API 返回 0;此时你调用了一下 GetLastError(),他返回了一开始调用 API 错误设置的那个错误值,于是这下你就以为后面的调用失败了….

来上一个 PoC 吧

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
int main()
{
const wchar_t* env_name = L"KCPath";
const wchar_t* str = L"";
// Failed, and last error code will be set.
DWORD size_needed = GetEnvironmentVariableW(env_name, nullptr, 0);
assert(size_needed == 0);
assert(GetLastError() == ERROR_ENVVAR_NOT_FOUND);
// set an empty string.
BOOL rv = SetEnvironmentVariableW(env_name, str);
assert(rv);
// Request buffer size from the API.
// Returned size includes null-terminate character.
size_needed = GetEnvironmentVariableW(env_name, nullptr, 0);
assert(size_needed == 1);
wchar_t buf[MAX_PATH] {0};
DWORD size_read = GetEnvironmentVariableW(env_name, buf, MAX_PATH);
// WAT? is last call failed?
assert(size_read == 0);
auto err = GetLastError();
// Boom!
assert(err == ERROR_ENVVAR_NOT_FOUND);
return 0;
}

这个故事告诉我们,一个接口的返回值语义的清晰和不重叠是多么的重要。

最后:这个故事来自于我一个很意外的单元测试用例;我一开始一度以为问题出在 GetEnvironmentVariableW() 不能正确处理空字符串值,以至于这篇 post 最开始的标题是 the werid behavior of GetEnvironmentVariable;但是在我编写 PoC 的阶段我却发现问题怎么也复现不出来…经过快有半小时的跟踪分析后,我才最终确定真正的 root cause;这也告诉我们一个道理,verification 是多么的重要…

Monthly Read Posts in June 2017

Starting a tech startup with C++

又名,Facebook 开源项目宣传广告。

不过实话说,以 Facebook 写 C++ 那部分代码的恣意姿态,如果团队的平均水平够不到中上的槛,弄不好项目还没发布自己就先挂了。

Andrei Alexendrescu:我仿佛听见有人在背后黑我…


Tips for Productive Debugging with GDB
CppCon 2015: Greg Law “ Give me 15 minutes & I’ll change your view of GDB”

GDB Tips…

第二个是 CppCon 2015 的一个 talk,感觉还凑合吧。不过实话说,15分钟我都能撸一发了,何必浪费时间在这个上…上个靠谱的前端不才应该是正确的姿势么…


CppCOn 2015: Visualize Template Instantiation

这个 talk 的团队基于 Eclipse 做了一个 Cevelop,杀手锏功能就是 able to visualize template instantiation

不过比较蛋疼的是演示关键部分的时候,摄像机没有给对位置…

(希望 JetBrains 早点吧这个功能抄过去…)

PS:这个 speaker 神似我的初中语文老师….


A better date and time C++ library

安利了 Howard Hinnant 写的一个 date lib。

而且据说这个 lib 很有机会进入标准库;现在标准库的 chrono 只是针对 timing 设计而没有对 date 的有力支持确实太蛋疼了


理解基于 TCP 的应用层通信协议

大水文一篇,基本没有一点营养。

不客气地说,现在国内开发者刊物上,大部分内容都是这种没有什么营养、容易误导读者,而且还浪费时间的文章。

挂出来批判一下

就是要你懂 TCP
就是要你懂 TCP | 最经典的TCP性能问题
关于TCP 半连接队列和全连接队列

阿里中间件团队官方博客的三篇 TCP 相关分享。

算是有水平的内容文。

第一篇介绍了一下 TCP 的基础内容;第二篇介绍了 delayed ack 和 nagle’s algorithm;第三篇涉及 sync queue 和 accept queue 的处理问题

TCP Peculiarities for Games, part 1
TCP Peculiarities as Applied to Games, Part II

一个游戏服务端开发大牛的两篇关于游戏服务端 TCP 开发相关的文章。

两篇文章内容不光已经涵盖了前面阿里中间件团队的前两篇内容,还谈到了一些容易被忽略的点,例如 Head-of-Line Blocking。

第二篇还专门针对游戏服务端的业务特点,给了一些建议