标题起得比较模糊,是因为我才疏学浅,不知道怎么去描述这个事情
探究的兴趣来源于 V8 的代码:
它定义了一段我看不懂的宏
[code-0]
#define BYTECODE_LIST(V) \
/* Extended width operands */ \
V(Wide, AccumulatorUse::kNone) \
V(ExtraWide, AccumulatorUse::kNone) \
\
/* Debug Breakpoints - one for each possible size of unscaled bytecodes */ \
/* and one for each operand widening prefix bytecode */ \
V(DebugBreakWide, AccumulatorUse::kReadWrite) \
V(DebugBreakExtraWide, AccumulatorUse::kReadWrite) \
V(DebugBreak0, AccumulatorUse::kReadWrite) \
V(DebugBreak1, AccumulatorUse::kReadWrite, OperandType::kReg) \
V(DebugBreak2, AccumulatorUse::kReadWrite, OperandType::kReg, \
OperandType::kReg) \
V(DebugBreak3, AccumulatorUse::kReadWrite, OperandType::kReg,
//假设它只有这么长
后来我看到这个宏定义的作用是:
对于每一行 V(...)
,用里面 ...
的内容作为 V
的参数,生成一个表达式。例如:
#define FCK(...) fck(...);
BYTECODE_LIST(FCK)
那么我们相当于得到了这样一组调用:
fck(Wide, AccumulatorUse::kNone);
fck(ExtraWide, AccumulatorUse::kNone);
fck(DebugBreakWide, AccumulatorUse::kReadWrite);
这里我们 FCK
宏的定义可以更骚一点,它可以不是函数调用,也可以是别的,比如定义一个类,各位玩的比我好。
有了这个用例,我们就可以做更巧妙的事情。我们可以把一组类型信息(或者常量信息,等等)填入到 [code-0] 这样的‘宏列表’(这个名字是我自己起的,下面还会继续用到)中,让它成为编译器可以读取的信息,从而让我们把这些长串的信息进行自动化的管理,并且不耗费运行时间。当然,我不知道 C/C++ 有没有更加直白的方法在编译器可以做到这一点,接下来是我自己的一个例子,就是用宏列表批量产生类的定义。
下面是一个我模仿 bytecode.h 写的一段“宏列表”,它的内容不重要,重要在于格式(和规模),需要注意的是
OperandType::Symbol*
, OperandType::Index*
是类型。
namespace IR
{
#define BYTECODE_LIST(C)\
/* visit attribute arg1 of arg0 and load it to accumulator */\
C(LoadAttributeToAcc, AccumulatorUse::Write, OperandType::Symbol*, OperandType::Index*)\
/* call function arg0 with pushed parameters, with return value overwriting Acc */\
C(CallFunc, AccumulatorUse::Write, OperandType::Index*)\
/* push function parameter */\
C(PushParamImm64, AccumulatorUse::None, OperandType::Imm64*)\
C(PushParamSymbol, AccumulatorUse::None, OperandType::Symbol*)\
C(PushParamAcc, AccumulatorUse::Read)\
/* jump to arg0 if Acc is true */\
C(JumpOnTrue, AccumulatorUse::Read, OperandType::Label*)\
/* jump to arg1 if Acc is false */\
C(JumpOnFalse, AccumulatorUse::Write, OperandType::Label*)\
/* logical or with Acc */\
C(Or, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(OrImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* logical and with Acc */\
C(And, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(AndImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* logical equal with Acc */\
C(Equal, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(EqualImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* logical not-equal with Acc */\
C(NotEqual, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(NotEqualImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* signed GE with Acc */\
C(GreaterEqual, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(GreaterEqualImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* signed LE with Acc */\
C(LessEqual, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(LessEqualImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* signed GT with Acc */\
C(GreaterThan, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(GreaterThanImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* signed LT with Acc */\
C(LessThan, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(LessThanImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* add with Acc */\
C(Add, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(AddImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* subtract with Acc */\
C(Sub, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(SubImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* multiply with Acc */\
C(Mul, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(MulImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* divide with Acc */\
C(Div, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(DivImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* mod with Acc */\
C(Mod, AccumulatorUse::ReadWrite, OperandType::Symbol*)\
C(ModImm64, AccumulatorUse::ReadWrite, OperandType::Imm64*)\
/* incremenmt Acc */\
C(Inc, AccumulatorUse::ReadWrite)\
/* decrement Acc */\
C(Decre, AccumulatorUse::ReadWrite)\
/* negate Acc, that is to multiply with -1 */\
C(Neg, AccumulatorUse::ReadWrite)\
}
接下来我用这样的宏就可以批量生成名字为 C{Name}
(比如CInc, CDecre) 这样的类,并且利用了对应的类型信息:
namespace IR
{
namespace BytecodeClass
{
#define CLASS_DEF(name, acc_use, ...)\
class C##name {\
public:\
void init(__VA_ARGS__);\
void generate_code();\
const char * get_name() {\
return #name;\
}\
IR::AccumulatorUse::Type get_acc_use() {\
return acc_use;\
}\
};
BYTECODE_LIST(CLASS_DEF)
#undef CLASS_DEF
}
}
但是这样显然有一个问题:我们在实现的时候,没有办法一次把所有类的 init
, generate_code
都实现了,怎么办?(如果不实现,编译时就会有未定义的错误)
先在宏里面批量定义一个定义,然后再重新定义?C++的编译器不支持重定义这样的操作,哪怕是编译期可以确定。利用函数指针是一条路,但是有运行时开销,这和我们初衷不符,但也不是不行。
后来我发现,我只能发现一条路:将需要定义的方法定义为虚方法,然后实现的时候逐一继承这些虚类,这样既可以利用过程信息,又可以利用我们定义好的宏的内存信息。于是我们把这个宏改一下,需要暂时不定义的地方改成 virtual
,并类名也做了修改:
namespace IR
{
namespace BytecodeClass
{
#define CLASS_DEF(name, acc_use, ...)\
class Virtual##name {\
public:\
virtual void init(__VA_ARGS__) = 0;\
virtual void generate_code() = 0;\
const char * get_name() {\
return #name;\
}\
IR::AccumulatorUse::Type get_acc_use() {\
return acc_use;\
}\
};
BYTECODE_LIST(CLASS_DEF)
#undef CLASS_DEF
}
}
这样就很爽了,我们实现的时候,就继承一个虚类出来,然后还可以添加成员属性,好不快活,并且还能得到自动的类型信息!
智能提示里的语法信息来自
C(LoadAttributeToAcc, AccumulatorUse::Write, OperandType::Symbol*, OperandType::Index*)\
从信息流动来回顾一下过程,并且验证一下理解:
1.首先这段信息在“宏列表中被定义”’
2. CLASS_DEF
中,我们通过变长宏参数列表 (name, acc_use, ...)
把 ...
这段,也就是 OperandType::Symbol*, OperandType::Index*
定义了虚函数 init
的参数
3. LoadAttributeToAcc
继承了宏定义产生的类 VirtualLoadAttributeToAcc
它得到了父类虚函数 init
的参数信息。
不得不说 V8 的工程师是玩的溜。C++ 的预处理器也很硬核。