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
来在混杂
中找到所有信息
.
除了更小
对象和可执行
文件大小,大多数用户不会注意到程序
变化,新混杂
对,如链接器或调试器
等外部工具,却是重大
变化.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现