d的新混杂名
D的新混杂名
混杂名
D采取独立的编译模型:先编译D源码为目标文件,再用链接器绑定目标文件,来构建可执行二进制文件.它允许重用预编译的目标文件和库,从而加快构建过程.由于链接器也用于相同编译模型的其他语言,如C/C++或Fortran,混合不同语言的目标文件是很简单的.
在目标文件中,每个函数或全局变量都有个符号名.链接器用这些符号连接同名定义.如,以下为C函数声明的符号:
extern(C) const(char)* find(int ch, const(char)* str);
不告诉链接器函数参数或返回类型信息,因为C语言使用普通函数名发现(find)作为符号名(某些平台会在前面加上_).如果稍后改变参数顺序为
extern(C) const(char)* find(const(char)* str, int ch);
但更新失败,并重新编译所有使用新声明的源文件,链接器再绑定生成目标文件.这里,程序很可能会崩溃,因为会按串指针解释传递给函数的字符,反之亦然.
D和C++通过在符号名中添加更多信息来避免它,即,把定义符号的域,函数参数类型和返回类型编码到符号名中.即使链接器不解释它,如果生成目标文件的定义不匹配,链接也会因未定义符号错误.如,D函数声明:
module test;
extern(D) const(char)* find(int ch, const(char)* str);
具有_D4test4findFiPxaZPxa符号名,_D中表明符号是从D源码生成的前缀,4test4find表示是在测试(test)模块中的查找(find)的"全名".FiPxaZPxa通过连接参数类型的编码来描述有整数参数的函数类型(由i指定)和C风格串指针(Pxa).Z终止函数参数列表,后面接返回类型的编码,再次,Pxa表示C风格串指针.对比:
extern(D) const(char)* find(const(char)* str, int ch);
按_D4test4findFPxaiZPxa编码,使它为参数类型反转的不同符号.该编码确保了类型和域的统一表示,并带更短符号名.该编码叫"混杂名".
注意:外(C)和外(D)是链接属性.如果D中函数没有显式声明链接属性,则默认外(D).
在D中,也会混杂一些函数属性到符号名中,如@安全,不抛,纯和@nogc.理论上,混杂还可覆盖参数名,用定属,甚至合约,但目前认为这太过了.
注意,即使混杂名可检测函数二进制接口中的一些不匹配(如,在寄存器中或栈上如何传递参数),它也不会抓每个错误;如,构,类和其他用户定义类型只按名混杂,因此改变定义,链接器不会注意到就传递了.
编译时,也可用.mangleof属性.曾用来在编译时,反射类型符号.由于__traits提取信息更快更方便,不再用它了,如:
__traits(getLinkage,symbol);
或
__traits(getFunctionAttributes, symbol);
因此,除了调试外不建议使用.mangleof.
在"解混杂器"中,解混杂时,用户可用所有编码信息,但并不总是产生正确的D语法.上面第1个定义解混杂为:
const(char)* test.find(int, const(char)*)
即添加了test模块名到函数名中.
模板符号
上面,find的两个定义,可在D和C++中共存,因此混杂名不仅是,链接时用来检测错误,而且还是表示重载的必要条件.它至少应该包含足够信息来区分同一域标识的不同重载.
考虑到,对每个参数类型,实例化不同函数或变量定义的模板时,就更明显了.在D中,添加模板实例化信息到符号全名中.
以式模板为例,这是懒求值元编程的常见示例:
module expr;
struct Mul(X,Y)
{
X x;
Y y;
}
struct Add(X,Y)
{
X x;
Y y;
}
auto mul(X,Y)(X x, Y y) { return Mul!(X,Y)(x, y); }
auto add(X,Y)(X x, Y y) { return Add!(X,Y)(x, y); }
编译器降级函数模板到同名模板:
template mul(X, Y)
{
auto mul(X x, Y y) { return Mul!(X,Y)(x, y); }
}
模板名是expr.mul!(X,Y).mul全名的一部分,并按Mul!(X,Y)推导自动返回类型.使符号三次引用X和Y类型.用double和float解混杂该模板的实例化混杂名为:
expr.Mul!(double,float) expr.mul!(double,float).mul(double,float)
DMD2.077前版本的混杂,遍历声明的抽象语法树,并在每次命中时发出类型的混杂表示.考虑栈操作:
auto square(X)(X x) { return mul(x, x); }
auto len = square("var");
pragma(msg, len.square.mangleof);
// S4expr66__T3MulTS4expr16__T3MulTAyaTAyaZ3MulTS4expr16__T3MulTAyaTAyaZ3MulZ3Mul
pragma(msg, typeof(len).mangleof.length);
pragma(msg, len.square.mangleof.length);
pragma(msg, len.square.square.mangleof.length);
pragma(msg, len.square.square.square.mangleof.length);
pragma(msg, len.square.square.square.square.mangleof.length);
pragma(msg, len.square.square.square.square.square.mangleof.length);
pragma(msg, len.square.square.square.square.square.square.mangleof.length);
对DMD 2.076早先版本,显示28u, 78u, 179u, 381u, 785u, 1594u, 3212u,指数增长,总之很差.
压缩符号
总之,效果不好.
我介入创建不省略混杂的概念,早期成果很好,于是更想办法来减少符号长度:
1,由于全名总是包含符号的包名和模块名,因此混杂名中经常出现他们.
2,全名很可能来自同一模块或包,所以最好按同一实体编码它们.
2,Phobos的单元测试运行时库,是候选基准测试对象,因为它们包含大量模板符号.当时,在窗口版本的映射文件中找到了127,172个符号.以下是不同的混杂的结果:
后引用 | 最大长度 | 平均长度 |
|---|---|---|
无 | 416133 | 369 |
类型 | 2095 | 157 |
类型+标识 | 1263 | 128 |
类型+标识+全名 | 1114 | 117 |
D混杂在所有平台上都应该相同,但这些符号对特定平台上的链接器有特殊意义.
在DMD中,确定标识和类型的标识相当简单,因为后者根据混杂合并.然而,全名及关联符号却很复杂.即,在core.demangle中mangle函数允许,从串函数参数给定的全名和模板参数给定的类型来构建混杂名.对运行时使用时,实现它要复制编译器的全部混杂机制和内省能力,这是不现实的.因此放弃编码全名.
以下是新混杂的一些细节:
1,现在由Q符加相同标识或类型的原始外观的相对位置来编码后引用.位置以26为基编码,最后一位数字用小写编码,其他数字用大写编码.这样,多数后引用的长度为2或3个字符,4个都是特例.对最后数字使用不同的编码可无需查看下个符,就确定数字尾.避免了歧义.(ItaniumC++ABI混杂组合数字和字母,以36为基编码,但需要_终止符.)
2,按C++混杂计数可编码实体会使混杂名稍短,但需要混杂器保留相应位置的动态列表.当前解混杂器的设计是,只要输出缓冲足够大就不分配.
3,选择相对位置而不是绝对位置,来允许_D前缀,而不必重新编码符号.某些平台还会在前面附加相对位置不可知的下划线.
4,混杂语法有时允许在同一位置为类型或标识,因此即使给定后引用,解混杂器也需要区分它们.因此要查找引用位置来继续解混杂;标识总是以数字开头,而类型总是以字母开头.
5,使用Q来后引用抓取编码类型的最后空闲字母,但在混杂语法中至少定义一个,不应出现在混杂中(即类型标识)的类型,因此,如果有必要,可复活.
如上式模板类型现在可混杂为
pragma(msg, len.square.mangleof);
// S4expr__T3MulTSQo__TQlTAyaTQeZQvTQtZQBb
// ^^ ^^ ^^ ^^ ^^ ^^^ decode to:
// | | | | | |
// | | | | | +- 3Mul
// | | | | +---- S4expr__T3MulTAyaTAyaZ3Mul
// | | | +------- 3Mul
// | | +---------- Aya
// | +----------------- 3Mul
// +---------------------- 4expr
不带后引用,长度为39,而不是78.结果大小为线性增长的23,39,57,76,95,114,133.12个调用平方从207,114个字符缩减到247个,即缩减了800倍.
对上面提到的带后引用标识的混杂,实现mangleFunc,仍较难;虽然全名不应包含类型(如,构模板参数),但混杂名中的标识可再次出现在函数类型中.用内省式设计扩展解混杂器来解决.
使Demangle构为模板.并以提供勾挂的构为模板参数.
struct NoHooks {}
// 支持: 静 极 parseLName(ref Demangle); ...
private struct Demangle(Hooks = NoHooks)
{
Hooks hooks;
// ...
void parseLName()
{
static if(__traits(hasMember, Hooks, "parseLName"))
if (hooks.parseLName(this))
return;
// 普通解码...
}
}
创建用适当的后引用替换复活标识的勾挂
struct RemangleHooks
{
char[] result;
size_t[const(char)[]] idpos;
// ...
bool parseLName(ref Demangler!RemangleHooks d)
{
// 刷新输入至result[]
if (d.front == 'Q')
{
// 再编码回引用
}
else if (auto ppos = currentIdentifier in idpos)
{
// 在*ppos再编码回
}
else
{
idpos[currentIdentifier] = currentPos;
}
return true;
}
}
像以前一样组合全名和类型(core.demangle仍可解码),并用勾挂的解混杂器跑它:
char[] mangleFunc(FuncType)(const(char)[] qualifiedName)
{
const(char)mangledQualifiedName = encodeLNames(qualifiedName);
const(char)mangled = mangledQualifiedName ~ FuncType.mangleof;
auto d = Demangle!RemangleHooks(mangled, null);
d.mute = true; // 无未混杂输出
d.parseMangledName();
return d.hooks.result;
}
新混杂健壮吗?
编码到混杂中的后引用扩展了现有的混杂.不过有歧义.core.demangle拒绝了Phobos单元测试中大约3%的未修改符号,而15%的符号部分解码.
std.traits中的一些实现使用符号混杂来检查编译时属性,如,确定链接.这是用简化的解混杂器完成的.随着后引用的引入,这些除了对简单的符号名外,不再管用.
使用core.mangleFunc是可行的,但会大大降低编译速度,因为解混杂需要CTFE.
幸好,添加了新的__traits来在混杂中找到所有信息.
除了更小对象和可执行文件大小,大多数用户不会注意到程序变化,新混杂对,如链接器或调试器等外部工具,却是重大变化.
浙公网安备 33010602011771号