dr2001 发表于 2012-12-13 21:11:59

[代码][交流]基于C预处理器的ProtoThread实现

本帖最后由 dr2001 于 2012-12-15 07:40 编辑

这是一个非常奇怪的代码写法。
代码的核心目的在于:使用基于×标准C×的Switch/Case结构描述ProtoThread/CoRoutine结构时,能够使得Case的值从0开始依序递增,让编译器能够实现switch的跳转表优化。

这是一个毁誉参半的代码描述方法:
1、由于会在代码中插入大量的include,所以,代码的可读性下降,让不了解的人读起来,很感觉奇怪。
2、恰好由于ProtoThread的Stackless的描述方法,但凡插入include的地方,都是潜在发生问题的地方,能够提醒编码者注意。
3、include插入的是×预处理宏和代码×,这个操作本身也是有风险的。
总之,代码中奇怪的地方,显示了它的不安全性;不仅是include本身,对上下文的代码和变量都有影响。比基于宏的实现多了某种提示作用;同时达成了优化的效果。

这个描述值得思考的地方:C语言的预处理器PreProcessor是有计算能力的。
C++的模板是图灵完备的,因此有Boost库来利用模板进行Meta Programming。
进而有人挖掘了C预处理器的处理能力,详情可以参考Boost库的PreProcessor部分。

就我目前的理解来说,C语言预处理器的计算能力仅体现在#if/#elif的值判断上。
当预处理器遇到这两个条件时,会对条件本身进行宏替换,然后×计算×表达式的值,判断True/False。
于是,计算的实质就是进行一系列的真伪判断,定义相应的结果宏,最后进行组合。

计算的核心宏是:ptCounter.ci
#if   (((_ptCounter+1)/0x10)&0xF) == 0x0
#undef_ptNew0x10
#define _ptNew0x10 0
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x1
#undef_ptNew0x10
#define _ptNew0x10 1
... ...
#elif   (((_ptCounter+1)/0x10)&0xF) == 0xF
#undef_ptNew0x10
#define _ptNew0x10 F
#else
#error"ptCounter: UnExpected Error."
#endif

#if   ((_ptCounter+1)&0xF) == 0x0
#undef_ptNew0x01
#define _ptNew0x01 0
#elif   ((_ptCounter+1)&0xF) == 0x1
#undef_ptNew0x01
#define _ptNew0x01 1
... ...
#elif   ((_ptCounter+1)&0xF) == 0xF
#undef_ptNew0x01
#define _ptNew0x01 F
#else
#error"ptCounter: UnExpected Error."
#endif

#undef_ptCounter
#define _ptCounter _ptCTR_Make(_ptNew0x10, _ptNew0x01)
特别注意在什么时候#undef宏。取消定义早了,会导致后续的宏替换出问题。

典型的ProtoThread代码如下:
pt_t    ctrl = {ptStatReset};
void ptFunc1(ptArg, uint8_t msg) {
#   include ptEnter
    if(msg == 0) {
      printf("A");
... ...
    } else if (msg == 2) {
      printf("C");
#       include ptStall
    } else if (msg == 3) {
      printf("D");
#       include ptYield
      printf("E");
... ...
      printf("H");
#       include ptStall
    }
#   include ptLeave
}
这是不带返回值的情况。
带有返回值的情况因为include不能传递参数,所以必须用额外的宏进行定义。具体参考附件中的代码。

附件中的宏支持GCC的Labels as Values和基于标准C的Switch/Case两种实现。会根据__GNUC__宏的预定义情况自动选择对应的宏。
基于标准C的宏实现可以在以下GCC命令行编译通过:
gcc -O2 -std=gnu99 -pedantic -Wall -Wextra ptinc_demo.c
基于GCC的Label as Values的宏,不支持-pedantic参数,会有警告。

如果要观察可以使用:
宏替换:gcc -O2 -std=gnu99 -Wall -Wextra -E -DPREPROC ptinc_demo.c
反汇编:gcc -O2 -std=gnu99 -Wall -Wextra -c ptinc_demo.c 然后objdump即可。


以下是ARM编译的结果片段,基于标准C的(ptinc.h的CFG_PT_FORCE_STD_C = 1),Cortex-M3。Keil MDK也能得到相似的结果。
命令行:
arm-none-eabi-gcc -O2 -std=gnu99 -Wall -Wextra -march=armv7-m -mthumb -c ptinc_demo.c
arm-none-eabi-objdump -d ptinc_demo.o
反汇编结果:
00000000 <ptFunc1>:
   0:   b510            push    {r4, lr}
   2:   7803            ldrb    r3,
   4:   4604            mov   r4, r0
   6:   2b04            cmp   r3, #4
   8:   d808            bhi.n   1c <ptFunc1+0x1c>
>>查表跳转。
   a:   e8df f003       tbb   
   e:   0903            .short0x0903
10:   0a12            .short0x0a12
12:   09            .byte   0x09
13:   00            .byte   0x00
当然,如果Switch数量少,以及行所在位置差异不大等特定情况,编译器也会进行跳转表优化,但依然效率不高。


另外,该宏经过改造,可以用在其它需要静态递增赋值的场合。缺点是一个变量名需要改造一次,不能一劳永逸。



flor 发表于 2012-12-13 23:44:18

这写的代码不容易读啊,,,

Gorgon_Meducer 发表于 2012-12-14 10:57:35

本帖最后由 Gorgon_Meducer 于 2012-12-14 12:43 编辑

我觉得需要解释一下隐藏在
#include ptEnter
#include ptStall
这些宏下面的内容,否则即便给出ptCounter.ci的内容也不容易懂啊……

看完了,这个技术太巧妙了!学会了,哈哈,解决了一个困扰我很久的问题~这样以后我的宏系统会更飘逸的~{:lol:} 我感觉自己正在踏上混乱C语言的不归路……
既然学会了,我就尝试解释一下核心的宏标号计算部分。

很多时候,我们知道宏似乎是有一定计算能力的,比如考虑下面的代码:
//! 在配置文件的某个地方...
#define REG_ITEM_COUNT    (13u)
...
//! 在代码的具体实现之前
//! 假设每一个REG_ITEM消耗2个BIT,所以4个REG_ITEM消耗一个字节,下面计算具体消耗的字节数,不足一个字节的按照一个字节计算
#define REQ_ITEM_COUNT_BYTE   ((((REG_ITEM_COUNT) / 2) + 3) / 4)

//! 所以在代码上我们可以这么写
uint8_t s_chRequests = {0};
上面的代码是没有任何问题,体现了宏自动计算的强大。但这种强大是表面上的,宏其实并没有聪明到让我们可以忘乎所以的地步,比如下
面的场合:

#define __REQ_INIT(__COUNT, __VALUE)      (__VALUE),

uint8_t s_chRequests = {MREPEAT(REQ_ITEM_COUNT_BYTE, __REQ_INIT, 0xFF)};
这里我解释一下,MREPEAT是一个很有用的宏,他的作用是把另外一个宏,比如这里的__REQ_INIT重复指定的次数,比如这里的
REQ_ITEM_COUNT_BYTE次,这里被重复的宏__REQ_INIT需要接受一个参数,也就是这里的0xFF。所以很容易看出,这个代码的
意图就是根据REQ_ITEM_COUNT_BYTE的数量,将所有的字节都初始化为0xFF。
问题出来了,宏MREPEAT实际上只能接受实际的数值,也就是说,这里的REQ_ITEM_COUNT_BYTE必须是一个常数,而不能是任
何表达式,这是宏MREPEAT的内容限制的,不相信,我们可以看看MREPEAT的内容片段:

#define TPASTE2( a, b)                            a##b

/*! \brief Macro repeat.
*
* This macro represents a horizontal repetition construct.
*
* \param countThe number of repetitious calls to macro. Valid values range from 0 to MREPEAT_LIMIT.
* \param macroA binary operation of the form macro(n, data). This macro is expanded by MREPEAT with
*               the current repetition number and the auxiliary data argument.
* \param data   Auxiliary data passed to macro.
*
* \return       <tt>macro(0, data) macro(1, data) ... macro(count - 1, data)</tt>
*/
#define MREPEAT(count, macro, data)         TPASTE2(MREPEAT, count)(macro, data)

#define MREPEAT0(macro, data)
#define MREPEAT1(macro, data)       MREPEAT0(macro, data)   macro(0, data)
#define MREPEAT2(macro, data)       MREPEAT1(macro, data)   macro(1, data)
#define MREPEAT3(macro, data)       MREPEAT2(macro, data)   macro(2, data)
#define MREPEAT4(macro, data)       MREPEAT3(macro, data)   macro(3, data)
...
#define MREPEAT254(macro, data)       MREPEAT253(macro, data)   macro(253, data)
#define MREPEAT255(macro, data)       MREPEAT254(macro, data)   macro(254, data)
#define MREPEAT256(macro, data)       MREPEAT255(macro, data)   macro(255, data)
则上面的代码实际上会被展开为:
uint8_t s_chRequests = {
    TPASTE2(MREPEAT, REQ_ITEM_COUNT_BYTE)(__REQ_INIT, 0xFF)
};
前面的代码编译肯定会报告错误因为宏系统并不会将REQ_ITEM_COUNT_BYTE计算后的值作为常量替换到表达式中,相反,宏系统会保持
原汁原味的一种文字替换,因此,上面的代码实际会被进一步替换为
uint8_t s_chRequests = {
    MREPEAT##REQ_ITEM_COUNT_BYTE(__REQ_INIT, 0xFF)
};
也就是
uint8_t s_chRequests = {
    MREPEAT ((((REG_ITEM_COUNT) / 2) + 3) / 4)(__REQ_INIT, 0xFF)
};
这就是预编译的可能结果,于是到了编译阶段编译器就会把MREPEAT当成一个函数,首先,这个函数显然不存在,于是一个implicit funciton declaration
的warning会首先扔出来,紧接着到了Link阶段,系统发彪了,说找不到函数MREPEAT在xxxx.o……当然,这个过程是我YY的,具体编译器会有不同
的行为,但不管怎么样,宏的计算能力在我们最信任它的时候掉了链子,因为我们期望的结果是这样的:
uint8_t s_chRequests = {
    MREPEAT4(__REQ_INIT, 0xFF)
};然后这个宏会被进一步展开,最终成为4个0xFF。

=============================华丽的分割线========================================
原本到这里,故事就结束了,是的,我死心了,决定把这个作为常识了,放弃了……哎……就这样吧……
然而,今天的楼主有一次震彻了我死灰一般的大叔之心……是的,故事还可以继续!
=============================华丽的分割线========================================

有时候我们会想,要是宏的计算支持变量就好了,这样我们可以在预编译的过程中获得更大的灵活度,之前我们认为这是很难做到的,现在看来这只是一个思路
问题。假设我们通过下面的方法定义了一个宏,并且期望它能起到变量的效果:
#define _ptCounter    0x00
这个宏变量的名字叫做_ptCounter , 并有一个初始值0x00,当然实际上这个初始值可以是任意的单子节数值,接下来,我们要将这个_ptCounter的值拆分成高
4bit和低4bit,并独立定义两个宏分别对应高4bit和低4bit:
比如,这里通过宏_ptNew0x10来表示高4bit
#if   (((_ptCounter+1)/0x10)&0xF) == 0x0
#undef_ptNew0x10
#define _ptNew0x10 0
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x1
#undef_ptNew0x10
#define _ptNew0x10 1
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x2
#undef_ptNew0x10
#define _ptNew0x10 2
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x3
#undef_ptNew0x10
#define _ptNew0x10 3
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x4
#undef_ptNew0x10
#define _ptNew0x10 4
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x5
#undef_ptNew0x10
#define _ptNew0x10 5
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x6
#undef_ptNew0x10
#define _ptNew0x10 6
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x7
#undef_ptNew0x10
#define _ptNew0x10 7
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x8
#undef_ptNew0x10
#define _ptNew0x10 8
#elif   (((_ptCounter+1)/0x10)&0xF) == 0x9
#undef_ptNew0x10
#define _ptNew0x10 9
#elif   (((_ptCounter+1)/0x10)&0xF) == 0xA
#undef_ptNew0x10
#define _ptNew0x10 A
#elif   (((_ptCounter+1)/0x10)&0xF) == 0xB
#undef_ptNew0x10
#define _ptNew0x10 B
#elif   (((_ptCounter+1)/0x10)&0xF) == 0xC
#undef_ptNew0x10
#define _ptNew0x10 C
#elif   (((_ptCounter+1)/0x10)&0xF) == 0xD
#undef_ptNew0x10
#define _ptNew0x10 D
#elif   (((_ptCounter+1)/0x10)&0xF) == 0xE
#undef_ptNew0x10
#define _ptNew0x10 E
#elif   (((_ptCounter+1)/0x10)&0xF) == 0xF
#undef_ptNew0x10
#define _ptNew0x10 F
#else
#error"ptCounter: UnExpected Error."
#endif
同理,这里用宏_ptNew0x01对应低4bit:
#if   ((_ptCounter+1)&0xF) == 0x0
#undef_ptNew0x01
#define _ptNew0x01 0
#elif   ((_ptCounter+1)&0xF) == 0x1
#undef_ptNew0x01
#define _ptNew0x01 1
#elif   ((_ptCounter+1)&0xF) == 0x2
#undef_ptNew0x01
#define _ptNew0x01 2
#elif   ((_ptCounter+1)&0xF) == 0x3
#undef_ptNew0x01
#define _ptNew0x01 3
#elif   ((_ptCounter+1)&0xF) == 0x4
#undef_ptNew0x01
#define _ptNew0x01 4
#elif   ((_ptCounter+1)&0xF) == 0x5
#undef_ptNew0x01
#define _ptNew0x01 5
#elif   ((_ptCounter+1)&0xF) == 0x6
#undef_ptNew0x01
#define _ptNew0x01 6
#elif   ((_ptCounter+1)&0xF) == 0x7
#undef_ptNew0x01
#define _ptNew0x01 7
#elif   ((_ptCounter+1)&0xF) == 0x8
#undef_ptNew0x01
#define _ptNew0x01 8
#elif   ((_ptCounter+1)&0xF) == 0x9
#undef_ptNew0x01
#define _ptNew0x01 9
#elif   ((_ptCounter+1)&0xF) == 0xA
#undef_ptNew0x01
#define _ptNew0x01 A
#elif   ((_ptCounter+1)&0xF) == 0xB
#undef_ptNew0x01
#define _ptNew0x01 B
#elif   ((_ptCounter+1)&0xF) == 0xC
#undef_ptNew0x01
#define _ptNew0x01 C
#elif   ((_ptCounter+1)&0xF) == 0xD
#undef_ptNew0x01
#define _ptNew0x01 D
#elif   ((_ptCounter+1)&0xF) == 0xE
#undef_ptNew0x01
#define _ptNew0x01 E
#elif   ((_ptCounter+1)&0xF) == 0xF
#undef_ptNew0x01
#define _ptNew0x01 F
#else
#error"ptCounter: UnExpected Error."
#endif
到这一步,我们就获得了两个宏_ptNew0x10和_ptNew0x01分别代表一个自己的高4bit和低4bit,接下来,就要借助另外一个宏的帮助将
这两个宏组合起来形成一个新的常数——是的,其实仍然是字面上的常数,但不管怎么样,预编译系统都是看“字面上的意思的”。
#define _ptCTR_Cons(_X, _Y)   0x ## _X ## _Y
#define _ptCTR_Make(_X, _Y)   _ptCTR_Cons(_X, _Y)
这个宏就是将_X和_Y代表的内容组合在一起,比如_ptCTR_Make(A,B)实际上最后组合的结果就是0xAB,有了这个宏的帮助,下面的内容
就很明了:
#undef_ptCounter
#define _ptCounter _ptCTR_Make(_ptNew0x10, _ptNew0x01)
这两行代码意义非凡,尤其是第一句#undef _ptCounter,这是我们站在xx城楼上向预编器庄严宣誓“旧的_ptCounter已经被打倒了!”,
然后第二句说“新的_ptCounter成立了!”——从此_ptCounter由0x00变成了0x01。

旧的桎梏被推翻了,新的秩序正在建立中,可谓百废待兴啊……只听xx城楼下密密麻麻的原代码 哗……哗……哗……可谓举国欢庆啊!

接下来首先需要考虑两个问题,第一宏变量的命名问题,如你所见,前面的红变量叫做_ptCounter,并且下面起到核心支撑作用的条件编译也都
建立在_ptCounter这个名称的基础之上,那如果我们想换一个名字怎么办呢?立马就有人说,那还不简单,再定义一重呗,例如:
//! 这是我们自己定义的宏变量
#define EXAMPLE_MACRO_VAR   0x04

//!在使用前面提到的支持系统之前先做一个替换
#define _ptCounter    EXAMPLE_MACRO_VAR
...

//! 在新的_ptCounter被定义以后,我们也更新EXAMPLE_MACRO_VAR
#undef EXAMPLE_MACRO_VAR
#define EXAMPLE_MACRO_VAR _ptCounter
很好,很强大……但似乎有一个问题……由于.c文件是独立编译的,如果一个.c的某一个代码块里面同时用到了两个宏变量,难么显然
最后大家都会对应到_ptCounter上,这就冲突了。看来这里存在潜在的共享问题。不过不用怕,新世界一穷二白,但不缺乏勤劳善良的程序员
于是有人提出用人海战术——即,通过古老的ctrl+C和ctrl+v法则(其实我想写大x,由于你懂的原因,最后换了一个很不爽的词,因此fuck那个
xx大x)定义一揽子宏变量及其支撑预编译系统,比如_ptCounter00,_ptCounter01...这是利国利民的大事啊,就像xx水坝一样,一次施工,
造福千年——这样我们就可以在同一个.c的同一个代码块里面使用若干个宏变量了——一般不会超过8个吧?就定义8个好了……好奢侈……

又有人提出,一个字节似乎太少了,还应该支持2个字节和4个字节的宏变量,这也是利国利民的大好事,于是大家一致决定制定如下的命名规则
MACRO_CNT_Un_xx
这里n表示位数,xx表示这是第几个变量(为了支持同一个代码块里面多个宏变量),例如
MACRO_CNT_U8_00
MACRO_CNT_U16_01
...
还有人提出,是否可以开发PC工具自动生成所需的支撑系统,放在需要的模块里面,同时还可以解除每次只增加1的限制……P民(Programmer民),
只有P民能爆发出如此的智慧……让敌人暴露在P民战争的汪洋大海中!

dory_m 发表于 2012-12-14 11:17:46

学习,学习!!!{:smile:}

dr2001 发表于 2012-12-14 12:16:43

Gorgon_Meducer 发表于 2012-12-14 10:57 static/image/common/back.gif
我觉得需要解释一下隐藏在
#include ptEnter
#include ptStall


莫非……你不会……递归Include宏吧。。。那就翻了。

因为CPP是顺序处理,应该没什么问题,回头我来尝试尝试,这个有趣味了。
这个的细节和CPP自身的限制还需要在思考思考。
已经开始有新收获了。哈哈。

Gorgon_Meducer 发表于 2012-12-14 12:18:50

dr2001 发表于 2012-12-14 12:16 static/image/common/back.gif
莫非……你不会……递归Include宏吧。。。那就翻了。

因为CPP是顺序处理,应该没什么问题,回头我来尝试 ...

你一个苹果,我一个苹果,交换以后还是两个苹果(有没有虫就不知道了)。
你一个点子,我一个点子,交换以后就是更多的点子{:lol:}

smset 发表于 2012-12-16 12:03:05

{:biggrin:} 看第一眼被打晕,等回过神继续看。

smset 发表于 2012-12-16 12:12:23

Gorgon_Meducer 发表于 2012-12-14 10:57 static/image/common/back.gif
我觉得需要解释一下隐藏在
#include ptEnter
#include ptStall


表述能力太强大了。

dds 发表于 2012-12-16 12:24:21

不错,支持总结

onepower 发表于 2012-12-16 14:01:01

晕菜~~~~~~~为了那一点点的效率,把代码搞成这样!!!!
简直就是得不偿失; 用宏来实现这样的计算, 本来就是 宏设计者想避免的东西;

ifree64 发表于 2012-12-16 14:59:44

本帖最后由 ifree64 于 2012-12-16 15:21 编辑

这就是传说中的大牛用代码写代码。

楼主的核心思路是,利用include、if、elif,重定义一个宏的值,使得每包含一次,这个宏的值就自动加1,将ProtoThread中利用__LINE__定义的状态变为用自动递增的宏从0开始按照自然顺序递增的顺序;
以使得编译器生成的switch代码可以利用查找表优化。

既然c++的模板也具有元编程的能力,可以用模板来实现类似的效果吗?

dr2001 发表于 2012-12-16 17:07:20

C预处理器能达成的东西,C++的预处理器加上模板展开一定可以达成并且实现的更好。

但是我对C++的预处理器和模板展开的流程还没什么深入研究,具体如何实现,能让代码多么优雅现在还是未知数。Boost库应该可以作为某种参考。

Gorgon_Meducer 发表于 2012-12-16 18:54:01

onepower 发表于 2012-12-16 14:01 static/image/common/back.gif
晕菜~~~~~~~为了那一点点的效率,把代码搞成这样!!!!
简直就是得不偿失; 用宏来实现这样的计算, 本来就是 宏 ...

工具而已,如果适用这个工具的小群体都能用好这个工具,那么不过就是个工具,犯不着为他那么纠结,
用就好了。它既不会改变代码的逻辑也不会改变你的编码思想,不是么?

newselect 发表于 2012-12-17 12:11:23

#define _ptCTR_Cons(_X, _Y)   0x ## _X ## _Y
这句解决我多时的困扰

hongyancl 发表于 2013-1-19 16:08:42

c语言的define用法

minier 发表于 2013-1-19 19:33:13

学习、学习!受用了

qufuta 发表于 2013-1-31 10:39:26

也许是我的水平太低了,没有理解为什么要这样使用宏,能不能解释下?

stely 发表于 2013-1-31 10:44:41

mark 傻孩子的精品讲解

cumtcmeeczm 发表于 2013-2-20 21:10:48

傻孩子的精品讲解

Hz01800475 发表于 2013-3-12 14:30:48

不会用啊。

mcuprogram 发表于 2013-3-12 14:42:27

mark!                        

myxiaonia 发表于 2013-5-12 20:42:36

你们都是牛人吾辈还得隐匿200年啊

Xplain 发表于 2013-5-12 22:12:28

学习可以参考下,但是要我需要去维护这样的代码,我会骂人的,

add00 发表于 2014-9-17 14:27:34

的确很取巧,但不要让我来维护这类代码~~

qq11qqviki 发表于 2014-9-17 22:06:18

可以参考下,

shian0551 发表于 2014-9-19 20:47:55

傻孩子的精品讲解

blueagle2012 发表于 2014-9-19 22:35:10

强大的宏,学习了

ZBCC2530 发表于 2014-9-22 17:35:53

MARK

以后再来看

DepravedLucien 发表于 2014-9-24 14:10:00

mark学习

xurenhui 发表于 2014-9-29 22:07:38

Gorgon_Meducer 发表于 2012-12-14 10:57
我觉得需要解释一下隐藏在
#include ptEnter
#include ptStall


兄弟无私奉献,好好学习天天向上

xCamel 发表于 2014-10-1 16:03:45

高大上的东西

liyang121316 发表于 2015-1-4 22:53:24

学无止境!

374184600 发表于 2015-6-24 16:05:23

好资料,看3遍都不过瘾。

yangzhong316 发表于 2017-6-23 12:58:21

刚开始了解。

Gorgon_Meducer 发表于 2017-10-22 02:13:40

发现ARM Compiler 5好像并不支持

#include ptEnter

这样的语法……也就是说MDK和DS-5都没法这样玩……

dr2001 发表于 2017-10-23 10:10:02

Gorgon_Meducer 发表于 2017-10-22 02:13
发现ARM Compiler 5好像并不支持

#include ptEnter


5是Clang还是6是?或者选项问题?

C99-N1256 6.10.2-4和6.10.2-8说明include的目标文件的宏替换是符合标准的。

Gorgon_Meducer 发表于 2017-10-24 23:00:11

dr2001 发表于 2017-10-23 10:10
5是Clang还是6是?或者选项问题?

C99-N1256 6.10.2-4和6.10.2-8说明include的目标文件的宏替换是符合标 ...

ARM Compiler 5
C99 和 C90 我都尝试过了
编译报错。坑死我了
页: [1]
查看完整版本: [代码][交流]基于C预处理器的ProtoThread实现