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。

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

利用 Job 内核对象实现父进程关闭时自动结束所有子进程

投稿工具的压制功能一直来都有一个问题:如果主进程被强行结束了(例如利用任务管理器),那么创建的 ffmpeg 压制进程仍然会继续运行。

因为 ffmpeg 是以二进制的方式部署的,因此不存在修改它的代码,自己和主进程建立 IPC 监控的方式。

至于采用远线程注入的方式来强行 HACK,我一直对这种无视客观规律的霸道方式都不太感冒,毕竟我们又不是做安全软件。

所以我想到了曾经在 Windows 核心编程中看到的一个方法:使用 Job 内核对象。

核心方法总结起来就是一句话:将 ffmpeg 压制进程加入到一个 Job 对象中,利用 Job 对象的 Kill-On-Close 特性,在 Job 内核对象被释放时,Job 内的所有进程都会被系统结束。

于是我写了一个 demo snippet,解决了几个坑之后验证了我的想法。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <conio.h>
#include <cassert>
#include <iostream>
#include <string>
#include <Windows.h>
bool LaunchAndWait(const std::wstring& cmdline, HANDLE job)
{
wchar_t buf[255] {0};
wcscpy_s(buf, cmdline.c_str());
STARTUPINFO startup {0};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process_information {nullptr};
if (!CreateProcessW(nullptr,
buf,
nullptr,
nullptr,
FALSE,
CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB, // important
nullptr,
nullptr,
&startup,
&process_information)) {
auto err = GetLastError();
std::cerr << "Failed to create process: " << err;
return false;
}
if (!AssignProcessToJobObject(job, process_information.hProcess)) {
std::cerr << "Failed to assign process to job: " << GetLastError();
return false;
}
ResumeThread(process_information.hThread);
CloseHandle(process_information.hProcess);
CloseHandle(process_information.hThread);
return true;
}
int main()
{
BOOL is_in_job;
IsProcessInJob(GetCurrentProcess(), nullptr, &is_in_job);
std::cout << "Is the main process in job: " << is_in_job << std::endl;
std::cout << "Wait for constructing job object!\n";
HANDLE job = CreateJobObjectW(nullptr, nullptr);
if (!job) {
auto err = GetLastError();
std::cerr << "Failed to create job: " << err << std::endl;
return 0;
}
// Enforce limits.
JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_limits;
memset(&job_limits, 0, sizeof(job_limits));
job_limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &job_limits, sizeof(job_limits))) {
auto err = GetLastError();
std::cerr << "Failed to set job information: " << err << std::endl;
}
std::cout << "Job object created! Launch test subjects...\n";
bool r = LaunchAndWait(LR"(C:\Windows\notepad.exe)", job);
assert(r);
_getch();
return 0;
}

最终的产品代码思路是完全一样的,当然代码不可能像上面这样随意。

这里有两个比较隐蔽的坑(MSDN 上不一定能看到)需要注意一下:

第一个是创建进程时最好带上 CREATE_BREAKAWAY_FROM_JOB,因为 Job 属性存在自动继承性:如果一个进程属于一个 Job,那么他创建的子进程会自动关联到这个 Job。

explorer 和 VS 都是存在于 Job 中的,因此如果在这两个环境下运行程序, IsProcessInJob() 会返回 true

第二个坑特别隐蔽:我一开始的 test subject 是 calc.exe,在 Windows 10 上这是一个 UWP 程序,然而无论我怎么做,calc 进程都活得好好的,一点不像是被约束的样子。。。甚至所有 API 的调用都并没有返回错误。

而当我将 test subject 换成 Win32 程序 notepad.exe 之后,功能就可以正常跑了….

我猜想这大概和 UWP 程序的一些安全特性有关,不过因为本身对安全并没有偏执的热爱,所以在这个点上就没有继续深入了。

Send Specific Cookie in URLFetcher

使用 URLFetcher 作为网络请求的基础组件有一个优势:如果 server 的 response header 指明了 set-cookie,那么 URLFetcher 在发出下一个请求时会自动往 request header 中设置 cookie。

这为使用某些需要在请求时附带 cookie 的 API 提供了便利;但是同时也在某一些场合下导致了一些问题。

如果你需要为某个 API 请求的 request header 中携带自定义的 cookie 数据,例如 cookie: buvid=xxxx,那么,即使你在发送请求前通过 AddExtraRequestHeader() 添加了 cookie 头,你也会惊奇地发现,出去的请求并没有携带你设置的 cookie,而是只有 URLFetcher 自己设置的,服务端提供的 cookie 值。

不要问为什么这个时候要用 cookie 传递这个信息,而不是某个独有的 request header,亦或者 query-string;你永远不知道和你对接的人是一个怎样的智障;毕竟,没有谁会真的去读 RFC。

自定义 cookie 丢失的原因大概如下:

  1. 你首先设置了 cookie,然后发出请求
  2. URLFetcher 在内部实际进行网络请求前,会设置服务端的 cookie 值(因为之前的请求存在 set-cookie
  3. 因为 RFC 规定,一个 request header 中, cookie header line 有且仅有一个,也就是说
1
2
cookie: buvid=xxx
cookie: sig=xxx

是不合法的;虽然可以通过

1
cookie: buvid=xxx; sig=xxx

来传递多个value,但是这要求 URLFetcher 在添加 cookie 时要能够 aware 到这点;很可惜,根据情况来看这点不成立

  1. 于是 URLFetcher 粗暴的替换了你设置的 cookie 值

扫了一遍代码之后,我想到一个 workaround,实践证明确实可以:

发请求前设置 URLFetcher 的 load flag,加上 LOAD_DO_NOT_SEND_COOKIE,阻止 URLFetcher 自己设置 cookie 的行为,完全由自己控制请求的 cookie。

话说用 URLFetcher 被坑了好多次(算上 chromium 其他的 lib,次数可以翻翻),还好每次都能够凭借“优秀工程师的第六感”蒙中问题的 root cause,进而化险为夷。我突然感觉自己理解了什么叫 文章本天成,妙笔偶得之

Bypass Proxy in URLFetcher

net::URLFetcher 默认情况下会使用系统代理,对于针对应用于浏览器而设计的网络组件来说,这是合情合理的;并且这也方便了测试对于网络接口的调试,因为只需要打开 Fiddler 或者 Charles,就可以看到应用发出去的 HTTP 请求。

但是有时候我们又希望默认情况下不开启代理支持,比如:在我用 net::URLFetcher 重写某直播姬的网络通信组件后,出现了不少傻逼用户因为不知道自己系统上为什么会各种乱七八糟的本地代理而导致无法登陆。

这个时候产品的策略就应该改为:默认不使用代理,如果有特殊需要,通过命令行参数启动。

因为产品项目用的 chromium 的代码比较老(估计是 2x 的),这个版本的 net::URLFetcher 只要是使用 URLRequestContextBuilder 创建的 URLRequestContext,那么就一定会使用系统代理,这是写死在代码里的…

而且 URLRequestContextBuilder 也没有提供额外的途径让我们修改;而一旦创建完 URLRequestContext,此时再设置新的 ProxyService 是没有用的…

我觉得这是一个设计上的失误,不知道新版本的代码有没有针对这块做了什么改动。

万幸,URLFetcher 对象可以通过 SetLoadFlags(),使用标志 LOAD_BYPASS_PROXY 来强迫当前 fetcher 对象绕过代理。

于是设计就变成:如果没有外部设置,fetcher 在发出请求前设置 LOAD_BYPASS_PROXY

Afterthoughts

在研究代码过程中我发现 URLRequestContext 理论上是可以使用自己定义的子类的;builder 创建的是内部实现的一个 BasicURLRequestContext。chromium 在这里的设计颇有点机制策略分离的意味。

不过因为这个问题发现在新版本即将提测前,因此就将这个想法列为后续的 TODO 了。

Monthly Read Posts in June 2017

一种头像缓存策略

本来还以为有什么惊天地的策略…这不是很普通的策略嘛…

Move Safety

这篇 post 引入了一个 move-safety 的概念;和 exception safety 类似,move safety 描述了一个实例被移动之后的 post-condition invariant.

Why do we need move safety? Because std::move() creates artificial temporaries, and we might want to use them again after the move operation.

4-Level Move Safety

  • No move guarantee: copy only
  • Strong move safety: moved object is valid and well-defined; like std::unique_ptr
  • Basic move safety: moved object is valid, but its state is unspecified; like std::string, due to possible SSO. It is what the standard library guarantees for all types unless otherwise specified.
  • No move safety: the state is invalid, and you can only call its destructor, or assign it a new value.

Stack and Heap

这篇 post 挺有意思。

作者观点是,无论 heap 还是 stack 来描述一个 C++ object 的 storage location 都是不准确的,因为标准并未规定 storage location 具体是什么,而规定了所谓的 storage duration:Storage duration defines the minimum potential lifetime of the storage that contains the object.

4 standard-defined storage duration:

  • Static storage duration
  • Automatic storage duration
  • Thread storage duration
  • Dynamic storage duration

Mix-in Based Programming in C++
Paper: Mixed based programming in C++

第一篇是介绍 mix-in 的 post,第二篇是在 mix-in 基础上提出一种 mix-in layer 的 paper。

mix-in 是一个挺有意思的 patter,但是因为没有自身语言支持,在 C++ 里的实现有时候会遇到比较 tricky 的问题。

第一篇 post 就提到了一个核心问题:如何有效地解决 mix-in classes 的 construction problem。但是讲真,post 里给出的 solution 有点诡异过头了;如果现实中的工程问题需要这样的一个解决方案,弃用 mix-in 弄不好是一个更靠谱的做法。

至于第二篇 paper,其实核心所谓的 mix-in layer,概括起来就是

1
2
3
4
5
6
7
8
9
10
template <class Next >
class NUMBER : public Next {
public:
class Workspace : public Next::Workspace {
// Workspace role members
};
class Vertex : public Next::Vertex {
// Vertex role members
};
};

paper 中还提到了对工程中使用 mix-in 的一些建议和 recommended practice,看看也还不错。

Connection Management in Chromium

Chromium 官方研发写的 post。

提到了他们试图解决 connection latency & parallelism 的问题。

于是就有这么几个点:

Handshakes (including TCP handshake & SSL handshake) are costly. Therefore, a better connection management is on demand.

Optimizations on transport layer (TCP or SPDY)

.etc

Apply tuple to function efficiently

Yet another implementation of apply-on-tuple