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的两个定义,可在DC++中共存,因此混杂名不仅是,链接时用来检测错误,而且还是表示重载的必要条件.它至少应该包含足够信息来区分同一域标识的不同重载.

考虑到,对每个参数类型,实例化不同函数或变量定义的模板时,就更明显了.在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个符号.以下是不同的混杂的结果:

后引用最大长度平均长度
416133369
类型2095157
类型+标识1263128
类型+标识+全名1114117

D混杂在所有平台上都应该相同,但这些符号对特定平台上的链接器有特殊意义.

DMD中,确定标识和类型标识相当简单,因为后者根据混杂合并.然而,全名及关联符号却很复杂.即,在core.demanglemangle函数允许,从串函数参数给定的全名模板参数给定的类型来构建混杂名.对运行时使用时,实现它要复制编译器的全部混杂机制和内省能力,这是不现实的.因此放弃编码全名.

以下是新混杂的一些细节:
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来在混杂中找到所有信息.

除了更小对象和可执行文件大小,大多数用户不会注意到程序变化,新混杂对,如链接器或调试器等外部工具,却是重大变化.

posted @   zjh6  阅读(21)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示