最近在给bochs加一个调试模块,不得不用C++,写到半截,积累了不少工具性的(比如stack,heap)代码,心想不如改成C写,以后也有能重复用。
大概每个c程序员都有这种经历:某时某刻,发现用用oop操作某个数据结构特别合适,像push(stack_t *stack,unsigned x)还能忍受,但go_tail(stringparser_t *parser)就显得别扭了。使用parser::go_tail()逻辑上顺些。
因此我写了一个工具包(叫strufunc),用以在c语言下使用"::"和"=>"(指针访问形式,下文只提::)。用strufunc有什么效果呢,举个例子。现在需要处理字符串,我们要写一个stringparser结构体,以及围绕它的函数,通常会这样写:
/**-----------stringparser.h----------*/
typedef struct stringparser{
char *workfield;
char *current;
}stringparser_t;
/**-----------stringparser.c----------*/
#include"stringparser.h"
void stringparser_init(strparser_t *parser, char *workfield){
parser->workfield = workfield;
parser->current = workfield;
}
void stringparser_go_tail(strparser_t *parser){
parser->current += (strlen(parser->workfield - 1);
}
/**-------------test.c-----------*/
void main(void){
stringparser_t parser;
stringparser_init(&parser, "abcdef");
stringparser_go_tail(&parser);
}
上面是C语言里常见的情景,看得到test.c里go_tail那一行很别扭。
有了strufunc的话,stringparser.c和stringparser.h仍照原样写,但在test.c里可以写成:
/**-------------test.c-----------*/
void main(void){
stringparser_t parser;
stringparser_init(&parser, "abcdef");
parser::go_tail(); /**只这一句变了*/
}
注意初始化函数,也就是stringparser_init,不能写成"parser::init("abcdef")。在它之后,就都可以了。如果有一个指向parser的指针p,还可以用"=>"操作符,p=>go_tail()。
strufunc怎么工作的呢,它控制整个编译分4步走。我们用xx.c指代源文件。
1,update_std
扫描指定的目录,一旦发现相同stem(例子里的stem是“stringparser”)的头文件,就进一步分析xx.h是否declare了名为“xx”的结构体,以及xx.c里是否define了“xx_init”函数。若都符合,strufunc判定程序员想用"::"来调用该模块的函数,于是对xx.c和xx.h做相应的处理,并将处理结果放到xx.cstd和xx.hstd文件里。所谓”相应的处理“,就是往结构体内添加函数指针。strufunc在这一步上做了优化,将函数指针搜集到一个专门的静态结构体内,只在xx结构体内安排一个指向它的指针,这样的好处是结构体的空间开销和init的开销都降为O(1)。
例子中的处理结果是:
----------------stringparser.hstd-------------
typedef struct stringparser{
char *workfield;
char *current;
struct stringparser_func_hub *__hub;
}stringparser_t;
struct stringparser_func_hub{
void (*init)(stringparser_t *parser, char *workfield);
void (*go_tail)(stringparser_t *parser);
};
--------------stringparser.cstd-----------------
#include"stringparser.h"
static struct stringparser_func_hub __hub;
void stringparser_init(stringparser_t *parser, char *workfield){
parser->__hub = &__hub;
parser->workfield = workfield;
parser->current = workfield;
}
void stringparser_go_tail(stringparser_t *parser){
parser->current += (strlen(parser->workfield) - 1);
}
static struct stringparser_func_hub __hub={
stringparser_init,
stringparser_go_tail
};
第一步还没完,如果发现xx.c不符合上述条件,strufunc还会分析它是否含有"::"操作符,如果有,替换之,替换结果也放到xx.cstd里。例子里test.c的go_tail那一行
parser::go_tail();
被替换成:
parser.__hub->go_tail(&(parser));
替换模式是:xx::method(...); =======>xx.__hub->method(&(xx)...)
从这里也能看出,你写程序定义接口时,接口的第一个参数必须是要操作的结构体指针。
上面的替换过程没有做词法分析,只是穷举各种语境,且只能尽量“穷”。这也是strfunc的硬伤。
如果xx.c不包含“::”操作符,那strufunc认为它是普通c文件,放过它。
2,std_front
扫描指定目录,如果发现文件xx在同路径下有对应的文件xxstd,交换之。例子中的t.c就和t.cstd交换,等等。
3,compile
执行常规的编译。
4,std_retire
重复step2做的事情,这样一来,源文件层的面貌就恢复了。我把这一步叫做“std层隐退”。
上面4步由sftool和makefile合作完成。所以要想用strufunc这个工具,必须用makefile来编译文件,且需要4步:
make update_std
make std_front
make compile
make std_retire
为了好用,我把它们写成了shell script,名字叫makeoo。
注意,std_front和std_retire一旦失败,源文件就被损坏,这也是日后要加强的地方。上例中makeoo运行输出:
------------------------------------------------------------
wws@wws-desktop:~/lab/test/cextend/strufunc/demo$ ./makeoo
../strufunc t.c -I./
../strufunc stringparser.c -I./
../strufunc common.c -I./
../sftool --exchg ./
gcc -x c -c -o t.oo t.c -I./ -Wall -std=c99 -fno-builtin -Wno-implicit-function-declaration
t.c:3:6: warning: return type of ‘main’ is not ‘int’ [-Wmain]
gcc -x c -c -o stringparser.oo stringparser.c -I./ -Wall -std=c99 -fno-builtin -Wno-implicit-function-declaration
gcc -x c -c -o common.oo common.c -I./ -Wall -std=c99 -fno-builtin -Wno-implicit-function-declaration
../sftool --exchg ./
0 0 0 0
留意最后4个0,对应4个make的返回值。
makeoo之后,再make link,就得到可执行文件了。不要在当前目录的makefile里编译其它目录的c文件。
因为要转到硬件上几个月,这个strufunc先撂在这儿,源码加demo一起传上来(没怎么整理,源码像垃圾堆一样,你可能要折腾一阵子才能弄好),欢迎有兴趣的朋友也用它,这样完善的才快。
buglist:
1, "::"和"=>"只能以功能性的代码出现,不能出现在注释里,也不要写printf("==>>");之类的代码。
2, makefile里的Ihpath必须含"-I./"。
PS:其实给大家展示的算是第二个版本,第一个版本在设计上是试图用strufunc命令代替gcc命令,很好用,而且对源码修改是轻量级的,不会在xx.c和xx.h里添加函数指针等等。第二版本的源码本身就用了"::"操作符,因此必须用strufunc1编译。它的原理是这样的,还用例子说明:
void push(stack_t *stack, unsigned x){}
调用上面的函数,平常是这样:push(&stack, 123);
用strufunc1的话,可以这样写:stack::push(123)。
strufunc1只简单的把stack::push(123)替换成push(&stack,123),仅此而已。这样没有任何额外的内存和时间开销。但它不支持同名函数,像比我先在有一个foostack.c,一个stack.c,两个C文件里都要写push接口,那strufunc1就没辙了。第二个版本虽然会让结构体的体积增加4byte,并且每次函数调用都增加两次内存访问,但一般用到"::"操作符时,结构体本身都挺重。再说写它的目的也不是为了让C进化到OOP,只是不想在特别该用OOP时束手无策。
将.txt改成.tar.gz