键歇(6)

前些日子,我入手了一台 13 英寸的 MacBook Pro。今天的《键歇》,就来聊聊我是怎么用这货的。

先说说这货的大致使用范围。

  • 堆码。作为一名开发者,这当然是第一要务。
  • 办公。处理邮件与撰写文档。
  • 日常。影音、上网与简单的娱乐。

这样看来,似乎一台普通的 Windows 笔记本也并无不可;不过,我选择这货的理由也是十分充分的,因为在这个移动应用市场空前火爆的时代,我是十分有意去分上一杯羹的。于是乎,同时使用 OS X 与 Windows 便成了唯一的选择。

对于双线作战的需求而言,无非是真机双系统,或者虚拟机的一带一。于我而言,我并非一个喜欢折腾的人,故无论黑苹果抑或 Boot Camp 皆非我所好;与之相比,虚拟机则有着与生俱来的灵活切换能力,大内存和 SSD 又最大限度地消弭了效率和 I/O 上的鸿沟,因此,我更偏爱于虚拟机的解决方案。

接下来,问题来了:Windows 和 OS X,这两者用谁虚拟谁更好呢?

在回答这个问题之前,我觉得有必要陈述一下自己的立场:即使是买了苹果的笔记本,我也认为 Windows 是最好的桌面 OS。而我之所以会选择用 OS X 来虚拟 Windows,恰恰正是为了保护我最为倚重的 Windows。

我记得在 Windows 一枝独秀的全盛期,流传过这么一个段子:

过去以会装系统为荣,现在以不装系统为荣。

这话之所以会成为段子,就是因为给 Windows 重装系统的成本太高太高了——先要准备一张 Windows 系统盘(或是烧录在 U 盘中的 ISO)来安装并激活 Windows 系统本身,然后是那些装机必备的应用、办公软件,另外还有开发者不可或缺的 Visual Studio 和 MSDN……完全无异于一场浩大的工程。

在这一点上,OS X 则有着明显的几点优势:

  1. 无需准备系统盘。按住 Command + R 开机启动恢复模式,只要能连上网络,就能够直接安装系统。
  2. 装好系统后,从 App Store 重新下载各应用。由于 OS X 并非实际意义上的主用 OS,因此这一步并不会有太大的成本。
  3. 把之前备份的 Windows 虚拟机映像拷贝过来,Windows 系统就复活了。

当然,系统也没那么容易就损坏。因此,我相应地还有着用 OS X 的另一个理由,也就是洁癖——那种作为开发者、互联网从业者的洁癖。

对于互联网客户端而言,要是不在自己的安装包里挟点儿私货的话估计都不好意思跟人打招呼,而那种桌面上时不时蹦出个新图标的情况简直是家常便饭。除此之外,还有什么起服务啊、插驱动啦……最要命的是,即使它们这么流氓,我还是离不开它们,因为或是功能或是粘性,它们总有无可替代的理由。

有了虚拟机的帮助,我就可以放手对付它们了:我的做法是,把这些东西一股脑儿塞到一个虚拟的 Windows XP 系统中。什么某雷啊,某度云管家啊……统统进到这个虚拟机里来,什么时候需要用了,就什么时候把这个系统打开让它们放放风。如果有一天这个系统被搞乱了,就利用虚拟机的快照功能恢复到最初的干净系统,让它重新做人。

那么问题又来了:Windows 上不是也可以虚拟出 Windows XP 吗?又何必一定求诸 OS X 来做呢?

答案是,除了上面那些货之外,我还要对付一些必用的、粘性过强的货,比如某讯 QQ 和某里旺旺。对比下没有 App Store 严苛审查的 Windows,这几个货无不是起服务、插驱动的弄潮好手,我没说错吧?

现在回头看看,我居然会为了上面这些原因来入手价格不菲的 MacBook Pro,可见像我这样的用户都被逼成什么样儿了……

(图片来自网络,图中剧名为《欢喜来逗阵》)

Categories: 键歇, 随感录 | Tags: | 1 Comment

键歇(5)

在我看来,PASCAL 的语法着实是严谨死板,就像个不近人情、认死理儿的私塾老先生;但是,大概是在十来年前(2002、2003 的样子),我还真就喜欢过那么一阵子的 Delphi。

如果你猜我是因为 RAD 而喜欢 Delphi 的,那么你错了。其实,我的着眼点很简单——在 Object PASCAL 中,“字符串”这种东西被设定为一个基本的数据类型,完全不需要像 C 那样来依靠字符数组的形式还得一个字符一个字符地精打细算着用。换句话说,那时的我,其实是在逃避 C/C++ 之中最难掌握的知识点——指针、动态内存的分配与调度。

直到今天,我仍然很清楚地记得,那时的我踌躇满志地要写一个读写注册表的工具。但是,Win32 API 的 RegQueryValueEx 却让我望而止步了。究其原因,要查询的键值数据大小是不可预测的,因此使用这货必须遵循类似这样的步骤:先进行一次探测性的调用,在这次调用中使用 lpcbData 参数来接收数据的大小,然后再分配适当大小的内存并进行真实的调用来获取数据。这些步骤的大致代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HKEY hKey;
 
// Open key ...
 
DWORD dwSize;
RegQueryValueEx(hKey, _T("SomeKey"), nullptr, nullptr, nullptr, &dwSize); // ERROR_MORE_DATA
 
PBYTE data = new BYTE[dwSize];
RegQueryValueEx(hKey, _T("SomeKey"), nullptr, nullptr, data, &dwSize);
 
// Use data ...
 
delete[] data;
 
// Close key ...

与之相比,VCL 的 TRegistry 类则显然是贴心多了,一个封装好 ReadString 函数就能搞定:

function ReadString(const Name: string): string;

当然,所谓封装,只不过是把代码根据需要藏起来罢了;至于实现封装用的是 Object PASCAL 还是 C++,那就没有太大的区别了,反正该有的步骤一样都不能缺。最终,随着对内存和指针驾驭能力的提高,我还是回到了 C/C++ 的阵营里,并为自己封装出了适用于 std::basic_string 的 RegKey 类。

故实讲完,那么引出本回《键歇》的话题——有关获取不确定大小的数据的那些事儿。

就像查询注册表键值数据一样,我们在很多场景下获取数据的时候都会遇到这样的问题:取数据之前并不知道目标数据有多大,需要先靠某种方式得知数据大小并为之分配空间,然后再进行获取。Win32 之中,这样的例子比比皆是,除了上述的 RegQueryValueEx,还有 WideCharToMultiByte、MultiByteToWideChar 等等。——当然,最为典型的例子则莫过于 GetWindowTextLength 和 GetWindowText 这一对儿拍档了。考虑如下代码(ATL 中 CWindow::GetWindowText 的实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CWindow
{
public:
    // ...
    int GetWindowText(_Inout_ CSimpleString& strText) const // 1
    {
        int nLength;
        LPTSTR pszText;
 
        nLength = GetWindowTextLength(); // 2
        pszText = strText.GetBuffer(nLength + 1); // 3
        nLength = GetWindowText(pszText, nLength + 1); // 4
        strText.ReleaseBuffer(nLength);
 
        return nLength; // 5
    }
};

如你所见,明晰的函数命名已经甚为赅备地描述了整个过程中所有步骤;但是,我还是决定用中文来总结一下:

  1. 希望获取数据的一方(调用方)向持有数据的一方(被调用方)发起一个调用。
  2. 某方查询目标数据的大小。
  3. 某方根据目标数据的大小来分配相应的内存。
  4. 被调用方向目标内存复制数据。
  5. 执行的领空(context)回到调用方,整个的调用过程结束。

细心的你一定发现了,第 2~3 步我使用了“某”这个不确定的代词——是的,因为对于这两步而言,哪一方调用都是可以的。那么,问题来了:这两个“某处”究竟由哪一方调用才是更优的策略呢?

还是以 CWindow::GetWindowText 为例吧。通常,我们是这样写码的:

CWindow wnd;
// ...
CString strText;
wnd.GetWindowText(strText);

如果严格按照字面概念来对号入座的话,我们自己的代码是调用方,CWindow::GetWindowText 是被调用方;但是,以实际执行体(HMODULE)为单位的概念来讲,我们自己的代码和 CWindow::GetWindowText(它会被编译器安排到我们自己的代码领空中)都是调用方,而 Win32 API GetWindowTextLength 和 GetWindowText 所在的 user32.dll 模块才是真正的被调用方。在明确了这个事实之后,那个隐藏至深的隐患便不难察觉了——如果有另一个线程在调用线程执行到 GetWindowTextLength 和 GetWindowText 之间时改变了窗口的文本(增加了这个字符串的长度),那么调用线程所分配的内存则必然不敷其用,从而导致返回的窗口文本将会是一个被截断的结果。虽然这个场景有些极端,甚或可以说是吹毛求疵,但是,你无权就此而否认这个问题的存在。

再回头看刚才“某方”的问题。于 Win32 API 而言,我当然无能为力;但是,如果我有权来实现被调用方的话,我会使上面的步骤成为下面这个样子:

  1. 调用方向被调用方发起调用。
  2. 被调用方将数据加锁。
  3. 被调用方获取数据大小。
  4. 被调用方分配内存。
  5. 被调用方复制数据。
  6. 被调用方解锁。
  7. 返还执行领空。

如果用 C 代码来描述,可能是类似这个样子:

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
// Callee Code
 
typedef void* (*Allocator)(size_t cb);
 
void* GetSomeData(Allocator alloc)
{
    static const char data[] = "Hello, World!";
 
    void *buf;
    size_t len;
 
    Lock();
    len = strlen(data);
    buf = alloc(len);
    memcpy(buf, data, len);
    Unlock();
 
    return buf;
}
 
// Caller Code
 
void *data = GetSomeData(malloc);
// Use data ...
free(data);

我习惯于把这里面的 Allocator 称作“内存分配器”。借助于这个名字显得比较高大上的东西,被调用方可以自由地按照需要来为调用方分配适当的内存,其好处有二:

  • 简化调用方的代码。
  • 保证整个调用的原子性。

读到这里,也许会有人给出另一个看似简化了的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Callee Code
 
void* GetSomeData(void)
{
    static const char data[] = "Hello, World!";
 
    void *buf;
    size_t len;
 
    Lock();
    len = strlen(data);
    buf = malloc(len);
    memcpy(buf, data, len);
    Unlock();
 
    return buf;
}
 
// Caller Code
 
void *data = GetSomeData();
// Use data ...
free(data);

当然,他的理由肯定会很充分:为什么不直接用 malloc 分配内存,而非要再引入一个“分配器”的概念呢?看我这么做多简洁,而且亲测有效哒!

条条大路通罗马,春风不度玉门关。虽然你那里貌似是没问题的,但你不能保证你在所有情况下都是没问题哒。简而言之,如果你是自己写的代码自己用,我顶多给你俩字儿的评语——“丑陋”;但是,如果你打算把 GetSomeData 这样的函数导出给第三方使用(比如作为软件插件的 API)的话,那么我就会毫不客气地给你的评语再加上俩字儿了,这俩字儿是“愚蠢”。

究其原因,C Runtime Library 虽然号称“标准库”,但这个“标准”其实只是一个松散的、处于“规格”一级的东西,它只规定了 CRT 中必须有些什么名字的函数,这些函数必须长成什么样子;至于这些函数的实现内容和方式,这个“标准”则并未做出任何的要求。这就意味着,对于某些涉及系统资源申请/释放功能的函数而言,跨越不同的 CRT 版本掺杂使用就有可能出乱子。想象一下,你的软件是用 Visual C++ 开发的,而某个插件开发者用的是 Dev-C++……如果说哪天你收到了一封来自他的骂街邮件,我一点儿都不会感到奇怪。

我不否认,有了内存分配器的参与,代码看起来确实不够简洁。不过,如果用 C++ 来实现的话,有了语法糖的帮助,也能得到一个还算不错、近乎优雅的解决方案。

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
// Callee Code
 
class Allocator
{
public:
    virtual void* Alloc(size_t cb) = 0;
};
 
void GetSomeData(Allocator *alloc)
{
    static const char data[] = "Hello, World!";
 
    Lock();
 
    size_t len = strlen(data);
    void *buf = alloc->Alloc(len);
    memcpy(buf, data, len);
 
    Unlock();
}
 
// Caller Code
 
class MyString : public Allocator, public std::string
{
public:
    virtual void* Alloc(size_t cb)
    {
        resize(cb);
        return const_cast<char *>(c_str());
    }
};
 
MyString str;
GetSomeData(&str);

这里插一句,此分配器其实就是 COM 架构中 IMalloc 接口的路数,只不过我根据需要将其裁剪了。即使现今 .NET 大行其道,但我仍然认为 COM 的设计思想是无有出其右者的。

最后再说说语法糖。在刚才的例子中,语法糖帮我们隐藏了一些冗繁的代码,从而使得调用代码看起来更加整洁悦目。比如,像 MultiByteToWideChar 这样需要遵循“二段式”调用步骤的函数,我的封装代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template < typename T, typename N = size_t, class R = std::vector<T> >
class DuoCall
{
public:
    static R Call(std::function<N(T *, N)> callback)
    {
        N size = callback(nullptr, 0);
 
        std::auto_ptr<T> buf(new T[size]);
        callback(buf.get(), size);
 
        return R(buf.get(), size);
    }
};
 
std::wstring ws = DuoCall<WCHAR, int, std::wstring>::Call([](PWSTR buf, int size) {
        return MultiByteToWideChar(CP_ACP, 0, "Hello, World!", -1, buf, size);
});

如你所见,正是 C++ 11 中的 Lambda 表达式赋予了我们这种可能,将本来要书写两次的 MultiByteToWideChar 减成了一次。

当然,这么写仍然是比较繁琐的。事实上,这个 DuoCall 类更适合被用于当成一个基类,从其派生出各个子类来进行特定的“二段调用”处理。考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MB2WC : private DuoCall < WCHAR, int, std::wstring >
{
public:
    MB2WC(UINT cp = CP_ACP) : m_cp(cp) { /* Nothing */ }
public:
    std::wstring Convert(PCSTR src)
    {
        return Call([=](PWSTR buf, int size) {
            return MultiByteToWideChar(m_cp, 0, src, -1, buf, size);
        });
    }
private:
    UINT m_cp;
};
 
MB2WC conv;
std::wstring ws = conv.Convert("Hello, World!");
Categories: 技术的角落, 键歇 | Tags: , , | 7 Comments

夜独酌

孤坐斟夜酒,
啯饮乱麻愁。
纵得星月伴,
安解心幽忧?

Categories: 随感录 | Tags: , | Leave a comment

2014 – 2015,一年又一年

又是一年总结时。照例,先总结下自己先前做的计划,以及完成程度。

推进某个秘密站点的开发。

这一条已经完成,也就是前几天我发布的 悦读·观止

学学 LISP。

这一条没分配出多少时间,解释器也只是开了个头。

增加博客的发表。

第一个季度确实在努力写了,也就是 键歇 系列。但是之后就转移了重心,所以没再过多发表,只是誊写了部分原创微博。

那么接下来是对 2015 的计划。

  • 继续维护 悦读·观止,将以前读过的书的书评补到站里。
  • 继续学习 LISP,完成一个最简易的解释器。
Categories: 随感录 | Tags: | Leave a comment

悦读·观止

好了,我觉得这个东西可以拿出来见人了。

我在 年初的计划 中提到,要推进某个秘密站点的开发。这个站点,就是今天我要发布的这个名叫“悦读·观止”的站点:

http://yuedu.xyz

自 2013 年起,我开始大幅提升自己的阅读量。也就在这期间,我逐渐萌生了做这个站的想法。最初,我只是希望能在第一时间获得 Kindle 商店的特价信息;随着开发的逐步推进,我开始将自己读后的感想与书评归集在这个站上,也就是目前这第一版的悦读·观止了。

顾名思义,我希望您读得开心,所谓“悦读”;也希望以此来帮您节省选择书籍的时间,所谓“观止”。更多的信息,可以移步观看 此站的“关于”页

Enjoy it!

Categories: 原创下载 | 2 Comments