原文
符号表示
概述
符号是本地语言功能
的最终表示
.在编译器或语言级别,不准确表示
它们可能会导致链接失败
.这些问题可能是令人沮丧
的重要来源,甚至可能导致人们认为无法实现方法
.
此DIP
的目的是解决和纠正
各种跨平台和目标
的常见共享库链接错误
.
理由
对不熟悉链接器
的人来说,理解和解决
这些错误可能令人生畏,一般会导致寻求帮助.
仅限于静态库
和独立可执行文件
使用语言时,D
中的很大一部分符号
的表示问题仍模糊不清.但是,此配置并不能满足所有用户的不同需求和偏好
.
需要更灵活
的如涉及
共享库的二进制配置
.
把插件整合
到D
中,为其他语言
创建D插件
或开发可替换
二进制文件
等用例,都说明
了涉及D的共享库
有益的场景.为了确保D
的一致性和易用性
,增强语言的符号表示
方法至关重要
.
主要焦点
应放在模块
级别.默认,了解模块
是在当前二进制文件
的内部还是外部
,是解决许多链接器问题
的关键.这些基本知识
为进一步改进和修改
奠定了基础.
前期工作
2016
年,BenjaminThaut
在DConf
上发表了题为"D
的导入出业务"的演讲,提出了改进D语言
特别是导出和共享库功能
的提案.
虽然该DIP
并非直接源自Thaut
的提议,且具体实现细节
不同,但它在导出
的作用上得出了类似
结论.
即,它认识到,为了在D
中有效利用注解
,导出
不应仅按可见性限定器
使用.
与Thaut
方法的一个显著区别
是,他建议设置dllimport
开关为"all"
,来指示符号是在DllImport
模式.
然而,根据D社区
在随后几年
中在共享库
方面的丰富经验,很明显,该方法经常导致链接器错误
.因此,需要一个更细致,更细粒度
方法来有效解决
这些问题.
描述
变更基石
本节概述
了未来更新
的关键更改
,以确保无误编译
.
1,外部导入路径开关:
引入
了新的extI
编译器标志
.它的功能类似I
开关,按当前正在编译
的二进制文件
的外部
,指定模块
.
最好,构建
管理器可自动执行
此过程,对共享库
关联的模块,用依赖关系的知识
用-extI
替换-I
.
2,二进制外模块:
(通过-extI
开关)按外部
标识的模块
,对所有非模板化域
,有个隐式的extern
属性.
3,导入符号:
要按DllImport
模式导入
符号,需要同时有export
和extern
注解.此时,是否存在函数体
不重要.
以下各节,考虑模板
在与共享库
,D
接口生成器,不同的导出注解
和内联
链接时的可靠性
.
模板的可靠性
因为实例化模板
的假设,模板
可能会导致链接失败
.因此,确保从模板
继承的符号
不会自动符合
导出条件或为DllImport
模式.
相反,按二进制文件的外部
标识这些符号
时,应重新实例化
,并在设置
了适当的重复标志
后,放入目标二进制文件
中.
这要求在每个模块
的基础
上,应用外部导入路径
开关.
为了优化生成代码
,编译器可用"固定"
策略.如果在其声明的同一模块
中,非模板化符号
引用了模板
的实例化(a)
,则按"固定"
对待a
.
此固定
扩展到在模板中未封装
的变量声明
(包括全局变量
)及函数参数和返回类型
.
固定模板及其关联的符号
后,在这些符号
是二进制文件的外部
时,编译器可自行决定
,是否省略
这些符号的生成代码
.此时,编译器应遵循指定的导出
和DllImport
符号模式,以确保高效且无错误
的链接.
D接口生成器
D接口
生成器是D编译器提供的导出工具
,通过省略D文件
中的符号体
来方便创建.di
文件.
当与C解析器(ImportC)
一起使用,以生成C库
的绑定
时,此工具
特别有用.
但是,当前实现
的一个显著局限性
是在导出过程
中,它无法准确遵循符号模式
.
为此,提出
了以下修改建议
:
1,生成器,不应自动添加extern
属性到符号
中.
2,仅当按导出特定模块
设置可见性覆盖开关
时,生成器才可把导出(export)
属性添加到所有非模板化域
.
这些调整
旨在与前面概述
的变更基石
相结合.用来确保:
1,对按export
标记的代码基
,.di
生成器限制引入
其他extern
属性.
2,对静态库或目标文件
,生成的.di
文件可同-I
标准导入路径开关一起使用
.
3,或,在处理共享库依赖项
时,可把外部导入路径
开关-extI
应用至.di
文件.
导出符号模式方法
每个符号
可有三个不同模式
之一:Internal
,DllExport
和DllImport
.
1,内部模式
:这是符号的默认模式
.不管在哪个模块
中定义,同一个二进制文件
中的其他符号
都可访问内部符号
.
2,DllExport
模式:设置
后,此模式指示,不仅自己的二进制
文件,且外部二进制文件
都可访问符号
.这对编译
打算跨不同二进制文件
使用的符号
至关重要.
3,DllImport
模式:此模式告诉编译器符号
在当前二进制文件
的外部.因此,编译器生成
允许在运行时
访问此符号代码
.
确定符号适当模式
的策略
如下:
1,使用export
的正注解
:此方法表示D编译器
的默认行为,即对导出
,用export
关键字来显式
标记符号.
2,用可见性覆盖开关
负注解:默认,不会导出
未用导出(export)
注解的符号
.可用可见性覆盖开关
来反转此默认
,来强制导出
所有符号.对未显式
标记导出
,但需要导出
其符号的库
特别有用.
3,多步构建
的边角注解
:这是个特定注解
,适合需要(具体根据构建步骤
)确定符号
是Internal
还是DllImport
的方案.
在复杂或冲突的符号模式
的多步
构建过程中非常有用
.
导出注解
可按参数
取标识,来增强导出属性
.按版本
解释标识
,需要以下语法
更改:
VisibilityAttribute:
- export
Attribute:
+ export
+ export ( Identifier )
此参数
的功能根据标识
是否活动
(由version=ident
激活).活动时,除非正在编译,符号
按内部模式
.相反,当标识非活动
时,类似有外部(extern)
注解,符号按DllImport
模式.
为了标准化
标识使用,在D规范
中引入
了三个新的版本
前缀,编译器自动
提供了libc,DRuntime
和Phobos
的实例
:
前缀为Have_,InBinary_
和Compiling_
.这三个
的后缀
将是个逻辑包
.
1,Have_
前缀:指示在链接过程
中,指定逻辑包
可用作依赖项
.
2,InBinary_
前缀:表示当前正在编译
的二进制文件
中有指定逻辑包
的符号.
3,Compiling_
前缀:表示正在编译指定
逻辑包.
这些前缀
结合逻辑包后缀
,可处理按内部而不是外部
的符号的极端场景.
此外,会更新D接口
生成器,以支持插入InBinary_version
参数.需要此更新
才能通过D模块
准确表示C文件
.此处未介绍确定后缀
的具体机制,它可能依赖于C预处理器
功能.
正表示法
D
中的export
属性用于按DllExport
来注解符号.要表示
已导出符号,应直接应用此属性
至D符号
.
导出
注解不能按可见性限定器
使用.这样处理它,可能会无意
中暴露内部实现细节
,可能会在语言级别
,导致外部实体
对代码基
不安全的操作.
在(如构,类,联或模块
)封装单元
中,如果按export
标记任一成员
,则也还必须导出
所有关联生成的符号
(如TypeInfo,__initZ,opCmp
等,但不包括ModuleInfo
).
导出
关联符号,却无法导出这些生成符号
时,则可能导致链接器错误
,则如果不借助链接器脚本
,可能无法解决
这些错误.
默认,所有符号
都是隐藏
的.要按隐藏
显式标记
符号,请用core.attributes
中提供的用户定义属性(UDA)
.该方法比在要导出
的每个符号
上注解
导出更有效
.
相反,它允许你在域级
注解,并简单禁止
那些不打算导出
的符号.
否定符号
设置可见性覆盖开关
后,默认
会导出
所有符号.
要覆盖
此默认设置
,并按隐藏
显式指定符号
,应使用core.attributes
中提供的UDA
.
在适当对DRuntime
和Phobos
库,用导出全面注解
并测试前,它们依赖此符号可见性
管理方法.
内联
考虑一个二进制外模块
:
pragma(inline, true)
export extern void inlineable() {noInline;
}
@hidden void noInline();
在此例中,按隐藏
标记noInline
函数,因此无法访问内联
.如果可跨二进制边界
内联inlineable
,因为noInline
不可用,这会导致链接器错误
.
为了避免此类错误
,当这些函数
引用未导出的符号
时,必须指示
编译器不要内联二进制外模块
的函数.
用例
本节介绍场景
说明了本DIP
中提议的修改
的实际影响和应用
.
第一个情况与需要细致控制导出符号
的用户
有关.
第二个重点是,无需调整
符号的DllImport
状态,方便整合DRuntime
到二进制文件
.
正注解
此用例
概述了用导出
正注解和可选使用.di
生成器的过程.它使用窗口
文件命名约定
展示.
目录布局:
dependency/source/library.d
dependency/imports/library.di
dependency/library.dll
dependency/library.lib
dependency/library.exp
executable/source/app.d
executable/app.exe
executable/library.dll
dependency/source/library.d
的源:
module library;
export void myLibraryFunction() {import std.stdio;writeln("Hello from my libraries function!");
}
生成的dependency/source/library.di
:
module library;
export void myLibraryFunction();
executable/source/app.d
的源
:
module app;
void main() {import library;myLibraryFunction();
}
使用共享库
:
dmd of=dependency/library.dll shared Hd=dependency/imports dependency/source/library.d
cp dependency/library.dll executable/library.dll
dmd of=executable/app.exe extI=dependency/imports executable/source/app.d dependency/library.lib
使用静态库
:
dmd of=dependency/library.lib Hd=dependency/imports dependency/source/library.d
dmd of=executable/app.exe I=dependency/imports executable/source/app.d dependency/library.lib
使用共享库和静态库
的主要区别在于,编译前者
时包含-shared
,而在链接
后者时用-I
替换-extI
.
二进制中的DRuntime
此用例
侧重于整合DRuntime
到二进制文件
中的过程.不考虑Phobos
等其他库,只是个说明性示例
,因为,在静态或共享DRuntime
间选择的开关
是编译器相关
的.
对此用例,考虑把DRuntime
放入生成的二进制文件
中会怎样.不考虑其他库(如Phobos
),且选择Druntime
是静态的
还是共享的
开关是编译器相关
的,因此它只是演示了它的流程
.
目录布局:
dependency/source/dependency.d
dependency/dependency.lib
mydll/source/api.d
mydll/mydll.dll
mydll/mydll.lib
mydll/mydll.exp
dependency/source/dependency.d
的源:
module dependency;
void myLibraryFunction() {foreach(m; ModuleInfo) {//非模板化的符号都适合此例!}
}
mydll/source/api.d
的源:
module api;
export void api() {import dependency;myLibraryFunction();
}
编译命令
:
dmd of=dependency/dependency.lib lib libdruntime=static dependency/source/dependency.d
dmd of=mydll/mydll.dll shared I=dependency/source libdruntime=static mydll/source/api.d dependency/dependency.lib
特别令人感兴趣
的是-lib-druntime=static
的行为.
在典型的D编译
中,会自动添加导入路径
和静态/导入库
.使用此DIP
,要指定共享DRuntime
版本,则用-extI
替换-I
,用导入库
替换静态DRuntime
库,并按externalOnly
设置覆盖dllimport
.
该方法大大简化
了共享和静态DRuntime
之间的区别,可能允许编译器配置文件
来掩盖
这些差异.
目前,未使用export
注解DRuntime
.如果是,则不必添加-dllimport=externalOnly
,从而降低
链接器试访问
未导出符号的风险.
此DIP
旨在消除在当前环境
中使用有共享DRuntime
的共享库
时很常见的(如LNK4217
)链接器警告.
重大更改和弃用
导出
不再表示"超级公开
"的可见性
.可能会影响现有代码
中符号的可见性和可访问性
.
为了缓解
此潜在问题,可在使用导出
前的行中添加public:
来调整
代码基.此方法
向后兼容,确保它与当前和未来
编译器一起正常运行.
对那些喜欢保留传统导出
行为的人,建议这样.
此DIP
中建议的所有其他修改
基本上都是通过-extI
编译器开关选入
的.
参考
在C和C++
中,一般通过宏预处理器
交换的属性
来选择DllExport
和DllImport
.它虽然实用,但很麻烦,需要配置
每个库.
Rust
使用一个包括库名
,可通过命令行参数
调整的link
属性,以在编译过程
中切换默认符号模式
.此DIP
引入了版本命名约定InBinary_
,来指定
包是在二进制文件
的内部还是外部
.
与配音使用
的现有Have_
约定一致,并利用了D的现有机制
.
二进制外
:不在当前编译
的二进制文件
中的符号
.如果按DllExport
模式设置
,并通过DllImport
访问,则当前正在编译的二进制文件
可在运行时
访问它.