一次好玩的扯淡——在C++中实现Objective-C的categories

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:一次好玩的扯淡——在C++中实现Objective-C的categories

说明啊说明:本人纯属小菜,对Objective-C神马的完全的不懂,这篇文章纯属扯淡,各位大大路过,欢迎各种指教~

今天一个同事对我说,他花了两个晚上的时间,啃完了Objective-C。我表示非常震惊,这种速度,应该是为了避免跟不上工作进度,穿越回来的时候选早了几天。于是接着我们就围着Objective-C侃了起来。

他说他看到Objective-C里面的一些特性非常好玩,于是说给我听,比如:
这是一个很神奇的特性,虽然我完全没有看过Objective-C,但是听大致的描述是这个意思:只要我们拥有某个类的头文件和其对应的静态链接库,那么我们就可以对这个类进行扩展,比如,给这个类加一个成员函数。

这种用法,对于基本只在C++里面转圈的我来说,确实也是第一次听到,觉得相当新鲜,于是就开始想了:C++里面是不是也能做到这件事情呢?
让我们来开始尝试吧!给一个名为Test的类添加名为Func1的成员函数。

静态链接下的类扩展

我们知道,静态链接生成的.lib文件,实际上可以看成是一个.o文件的集合。而编译器在链接的时候,实际本质上只是在尝试连接名称匹配的Symbol
那么这样看起来加一个成员函数似乎就变成一件很容易的事情了。

我们首先来建两个工程:

  • SimpleStaticLib:Static Library,用于生成我们要扩展的静态链接库。
  • ExpandStaticLib:Console类型的Exe,用于实验类扩展的结果。他依赖于SimpleStaticLib工程。

最终样子如下:
expand-static-lib-sln:
expand-static-lib-sln

然后给SimpleStaticLib添加一个简单的Test类:

test.h

#pragma once

class Test
{
public:
	Test();
	~Test();

	void Func0();

private:
	int a;
};

test.cpp

#include "stdafx.h"
#include "test.h"
#include <stdio.h>

Test::Test()
: a(1)
{

}

Test::~Test()
{

}

void Test::Func0()
{
	printf_s("Test::Func0! a = %d\n", a);
}

在ExpandStaticLib中里面,我们按照如下方法给Test类添加一个Func1的函数,代码如下:

main.cpp

#include "stdafx.h"
#include <stdio.h>

class Test
{
public:
	Test();
	~Test();

	void Func0();
	void Func1();

private:
	int a;
};

void Test::Func1()
{
	printf_s("Test::Func1! a = %d\n", a);
}

int main()
{
	Test o;
	o.Func0();
	o.Func1();

	return 0;
}

编译,运行,我们可以发现,成功了!
expand-static-lib-result:
expand-static-lib-result

原因分析

刚刚我们也提到,Link的本质是链接同名的符号,那么这些符号在真正编译器里面是个什么样子呢?我们来一探究竟。

在Windows上,我们可以使用dumpbin来查看Obj和Lib文件中的具体内容。

dumpbin /SYMBOLS SimpleStaticLib.lib

在SimpleStatictest.lib中,我们可以找到Test类的成员函数的符号。
simple-static-lib-symbols:
simple-static-lib-symbols

在ExpandStaticLib工程的Debug目录下,我们可以找到main.obj,在里面我们可以找到对Test::Func1的引用。
main-symbols-1:
main-symbols-1

我们再来看对Test其他函数的引用。
main-symbols-2:
main-symbols-2

我们可以看出来,这两个符号是不同的,因为Test::Func0的实现是在main.o的外部实现的,所以就没有后面的Section信息,而Test::Test则相反,是一个内部实现,在后续的Section中就可以看到其最后编译出的代码长度。
在链接的时候,链接器会加载所有的.lib和.obj文件,然后在其中查找各自的符号和其对应的实现,最后将他们链接在一起并最终输出成可执行文件。
当然,我们也可以使用link命令简单的来试着生成最后的可执行文件。

link SimpleStaticLib.lib ../ExpandStaticLib/Debug/main.obj

动态链接下的类扩展

那如果我们使用动态链接还可以实现这种效果吗?二话不说来试一试吧。
首先,再建立两个工程:

  • SimpleDynamicLib:和SimpleStaticLib一样,只是是一个DLL的工程。
  • ExpandDynamicLib:和ExpandStaticLib一样,只是依赖于SimpleDynamicLib。

最后样子如下:
expand-dynamic-lib-sln:
expand-dynamic-lib-sln

然后,我们将Test的代码复制到SimpleDynamicLib工程中,将Main的代码复制到ExpandDynamicLib工程中。
接着,将两个工程中的Test类都加入__declspec(dllimport),如下:

class __declspec(dllimport) Test

最后,编译,执行。结果还是通过了!这里为了区分和上面的结果,我在输出中加入了”Dynamic”的文字。
expand-dynamic-lib-result:
expand-dynamic-lib-result

为什么呢?其实原因大家肯定都能猜个大概了。
在dll中导出类的实现,实际上是将类的成员函数全部变成导出函数,在生成的lib中实际上只保存了类的大小信息和成员函数的符号定义。
我们在depends中可以看到SimpleDynamicLib.dll的导出函数和导出变量。
simple-dynamic-lib-exports:
simple-dynamic-lib-exports

所以,在链接的时候,链接器根据同名符号进行链接,就依然是成功的了。

成员变量的扩展

最后,我们在来想想,我们能否对Test类中间的成员变量进行扩展呢?我们来修改一下我们的程序,给Test类加入一个int型变量b。
刚刚我们看到在动态链接和静态链接的时候,链接器做的事情本质上基本是一样的,所以为了方便观察和描述原理,我们这里修改动态链接的工程:ExpandDynamicLib中的Test类。

class __declspec(dllimport) Test
{
public:
	Test();
	~Test();

	void Func0();
	void Func1();

private:
	int a;
	int b;
};

void Test::Func1()
{
	b = 2;
	printf_s("Dynamic Test::Func1! a = %d, b = %d\n", a, b);
}

编译,运行,可以看到,这次也正常执行了!那这次实际上到底发生了什么事情呢?
expand-dynamic-lib-var-result:
expand-dynamic-lib-var-result

我们上面说到链接器的工作原理实际上是链接相同名称的符号,换句话说,就是他并不会修改除了符号信息之外的代码,在编译的过程中,编译出来的最终代码主要是由编译器来生成的。我们刚刚也看到了SimpleDynamicLib的导出函数,其中我们并没有发现这样的一个导出函数:申请类的空间。通过查看汇编代码,我们可以从中看到C++是怎样来处理的。
为了更好的看到C++的行为,在这里请使用Release来编译工程,并且禁用堆栈检查,避免他们对最后汇编代码的影响。
expand-dynamic-lib-var-rt:
expand-dynamic-lib-var-rt

我们可以看到,C++实际上是在ExpandDynamicLib.exe中申请了8个字节的内存,然后对其调用在SimpleDynamicLib.dll中的Test类的构造函数。
既然原理是这样,那么我们就可以想到扩展类成员变量的缺陷了:

  • 扩展的类成员变量无法进行正常的构造。
  • 如果我们在已有的类成员之前添加类成员,而不是添加到类成员的末尾,那么可能会引起对象构造/析构混乱。
  • 扩展类成员时,千万不可以删除原有的类成员,否则会造成构造/析构错误,操作对象以外的内存,造成更多的问题。
  • 无法扩展除了基本类型以外的类成员,因为这些对象是需要在构造函数中加入其成员变量的构造函数的调用的,而这些函数已经被编译进了动态/静态链接库中,无法修改了。
  • 多重继承和虚继承会让对象的内存结构变得更加复杂,所以在进行类成员扩展时,需要对类的内存分布有绝对的把握,否则很容易出问题。

总结

经过一番折腾,我们可以大致了解到一些C++编译器的一些工作原理,但是这个技术本身我们在平时写代码的时候最好不要使用。
如果需要增强其他人提供的Lib,其实还有很多的方式可供我们选择,在《重构——改善既有代码的设计》中,我们就可以找到两种较为简单的方式:

  • Introduce Foreign Method:引入外加函数
  • Introduce Local Extension:引入本地扩展

当然一切都是TradeOff,具体用什么方法来增强外部的代码,在平时写代码时,只要符合工程的需要就行。

近期杂感

最近感觉很忙碌,不知道为什么人生仿佛失去了一些目标,进公司快三年了,中年危机正式来袭,到底如何才能更好的提高自己呢?

有些事情总是让人觉得很难调和,比如:一旦工作了,就没有那么多时间来学习了,可是不学习又怎么能做好工作呢?一个让人无语的问题。
我没有聪颖的天资,所以也只能花更多的时间在学习上,少睡一点,多看一点,也许会好一点,但是总觉得不是那么个办法。

人生总是有那么多遗憾,还好MM总是能理解我,无论我是不是周末不理她,或者是平时晚回家,回到家她都能让我感到温暖。

有些事情一旦开始就停不下来了,给自己鼓鼓劲,加油。

Posted in 02 My Soul Perigee. Tags: . 一条评论 »

充电什么的最不能停止了

在经过了一年的折腾之后,发现有太多东西是在是没有学好了,去年买了很多书,也都还没有看。今年是要好好充电了,很多事情都是心有余而力不足,人生没有目标什么的,迷茫什么的,实在太讨厌了。恩,列下比较重要又还没有好好看完的书单,争取在今年把他们都解决掉。

《Windows核心编程》
《Windows内核原理与实现》
《STL源码分析》
《软件调试》

恩,加油!

Posted in 01 My Soul Apogee. Tags: . 没有评论 »

小可爱啊小可爱

昨天打完篮球回来,mm居然用ipad给我画了一幅画当作5月20号的礼物,哈哈。。小可爱啊小可爱,你能不这么可爱么?
不过为什么说画的是我?这到底哪里像我了?

20110522-093629.jpg

Posted in 02 My Soul Perigee. Tags: . 一条评论 »

iPad入手!

感谢MM,帮我买了一个iPad2,了却了我一大心愿,开心(^_^)
玩了一天,感觉确实不错,屏幕很大,做很多事情都很舒服,比如写博客,发右键,看电子书。
先测试一下发博客,WordPress的应用在iPad上用起来感觉还挺舒服的,自带的输入法使用起来也很不错,整句输入貌似准确率还可以啊。不过遗憾的是暂时还没有破解,神马东西都要钱。。(T_T)

20110518-133926.jpg

Posted in 01 My Soul Apogee. Tags: . 4 条评论 »

又要上班了

春节还真是快,一下子就又结束了,回到家,本来打算看看《Windows核心编程》给自己好好充充电,结果也没能如愿,摆弄了下博客,就又要回去上班了,囧啊。

算上原来在深圳的半年,跟着这个项目已经有一年的时间了,期间经历了很多,这里真的要感谢很多人对我的帮助,让我这个Windows小白也能在这个项目上发挥自己的作用。马上又要开始上班了,但愿新年我能表现的更好。

兔年要做机灵的小兔子,不能做成了一个2!

Posted in 01 My Soul Apogee. Tags: . 4 条评论 »

终于回家了

过年了,经过了令人崩溃的春运,终于回到了家。
老妈又老了一点,给她的笔记本还是没有装着Ubuntu,不过她已经受不了Linux了,命令行这种东西对她来说还是太难接受了。
不过正好,我就用上了久违了Linux。装上个Chrome,架起GAE,然后开始看各种小网站,哈哈。
湖南果然很冷,而且关键是没有暖气,不过幸运的是遍地都是火炉,不然真的是要被冻成小冰块,哦不,是大冰坨。
再过两天就要回老家了,之间有5天左右应该是与网络隔绝了,初5重新回来,哈哈。到时候,在和朋友们一起搞起~~

话说,最近有神马魔兽的任务关好玩么?RPG神马的最给力了。

Posted in 01 My Soul Apogee. Tags: . 没有评论 »

下歌的地方又少了一个

Verycd又被捅了一刀,音乐部分被阉割掉了,找歌的地方又少了一个。。。FT。。。

Posted in 01 My Soul Apogee. Tags: . 没有评论 »

一年没有用Linux了

好不容易重新通网,在GR上闲逛的时候,发现Ubuntu上的软件又越来越多了,GTK3也貌似快出来了,Ubuntu10.10也要支持多点触摸了,再加上Linux runs everywhere的特点,前途果然是大大的有啊。

话说大学毕业已经一年了,Linux也是已经一年没有用过了,挺怀念大学在寝室通宵敲代码的时光,那么的有激情。当代码变成职业的时候,反而有点不想敲代码了,有点时间就想玩游戏,看动漫什么的。真够颓啊。

再过几天就要去北京了,但愿能有一个新的开始。

Posted in 01 My Soul Apogee. Tags: . 2 条评论 »

断网了,这下真的要断网了

马上就要在业余时间和网络说拜拜了,因为马上去要北京了,电脑什么的也都装箱了,再过几天就统统的寄出去了,什么上网啊,博客啊,瞬间都变成浮云了。。。。9月初重新复活,这段期间,就当我轮回去了吧。。

北京的朋友们,到时候一起聚聚折腾一把吧。。

PS: 感谢小柔同学帮我在北京找好房子,让我过去可以直接住。。

Posted in 01 My Soul Apogee. Tags: . 一条评论 »