版本:1.0
作者:明
日期:2010年元月
综述
易语言静态编译技术手册,主要介绍易语言静态编译方案,以及支持库改造方法。
易语言5.0“基于第三方链接器的”静态编译方案的核心是:把易语言编译器生成的中间数据,编译成COFF格式的obj文件,然后把它交给第三方链接器,与各支持库的静态库(*.lib文件)一起链接生成EXE/DLL。
为了配合静态编译,易语言编译器、核心支持库、集成开发环境(IDE)均已做出重大更新,绝大多数官方支持库已完成自身改造。
第三方支持库需要作者按照本文介绍的方法完成支持库改造,以便支持静态编译。未经静态编译改造的原有支持库,仍可在新版易语言中使用,只是不能支持静态编译。
支持库静态编译改造
目的:使该支持库可以支持静态编译
途径:1、在原支持库(.fne)基础上额外输出部分信息;2、提供支持库的静态库,并按要求导出特定的函数符号
为了支持静态编译,对支持库框架做了扩展性修改,这种修改不会对现有的支持库造成任何负面影响。即:如果不需要支持静态编译,现有的支持库不需要做任何修改;如果已经做了修改,也不影响原有功能和使用方式。最大程度的保持支持库的向前向后兼容性。
今后支持库要分为两个版本:一个动态库版(即现有的fne/fnr等),一个静态库版。
动态库版支持库,除了提供向后兼容的运行时支持外,还要在静态编译连接期间提供数据支持。
静态库版支持库,用于静态编译连接,编译连接期间需要动态库版支持库的支持,但编译连接后生成的EXE/DLL不依赖任何非系统库。
动态库版和静态库版,基本上还是使用同一套源代码,只是在必要的地方用C/C++预定义宏(__E_STATIC_LIB)区分。
以下说明中将明确区分静态库和动态库。
为支持静态编译,易语言支持库开发包接口文件 lib2.h 中增加了以下通知项(系统发送给支持库的通知, PFN_NOTIFY_LIB):
#define NL_GET_CMD_FUNC_NAMES            14
// 返回所有命令实现函数的的函数名称数组(char*[]), 支持静态编译的动态库必须处理
#define NL_GET_NOTIFY_LIB_FUNC_NAME      15
// 返回处理系统通知的函数名称(PFN_NOTIFY_LIB函数名称), 支持静态编译的动态库必须处理
#define NL_GET_DEPENDENT_LIBS            16
// 返回静态库所依赖的其它静态库文件名列表(格式为\0分隔的文本,结尾两个\0), 支持静态编译的动态库必须处理
// kernel32.lib user32.lib gdi32.lib 等常用的系统库不需要放在此列表中
// 返回NULL或NR_ERR表示不指定依赖文件
为了支持静态编译,支持库必须做出的改动,详述如下。示例请参考易语言安装目录 sdk
\cpp\samples 中的HtmlView支持库源代码。
一、部分函数需要修改函数名称和符号导出方式
所有命令和方法的实现函数(PFN_EXECUTE_CMD)、处理系统通知的函数(PFN_NOTIFY_LIB),均需要修改函数名称,添加“库名称前缀”,并修改为以C符号形式导出(在C++中使用 extern "C");
所有数据类型的接口获取函数(PFN_GET_INTERFACE),需要统一命名为 <;库名称前缀>_GetInterface_<;数据类型英文名称>,并修改为以C符号形式导出(在C++中使用 extern "C");非窗口组件数据类型不需要接口获取函数。
这些改动,对静态库来说是必须的,对动态库来说是可有可无的。考虑到两者使用同一套源代码,可统一修改,以减少分开维护的成本。
上面提到的“库名称前缀”,是指其所在支持库(动态库)的文件名的全小写形式,不包含路径和文件名后缀。以核心库 krnln.fne 为例,其库名称前缀就是 krnln 。注意,库名称前缀一定是全部小写字母、数字和下划线的组合,且不能以数字开头,如果库名称确实是以数字开头的,请在库名称前缀前加下划线。对于文件名中有汉字的支持库,其库名称前缀应是,下划线加上库文件名的UTF-8编码的十六进制文本全小写形式(如支持库“汉字.fne”的库名称前缀为 _e6b189e5ad97)。但目前静态编译版本暂不支持有汉字的支持库文件名,和以数字开头的支持库文件名。
修改示例:
假设之前的核心支持库中有一个命令的实现函数是这么定义的:void fnMessageBox (PMDATA_INF pRetData, INT nArgCount, PMDATA_INF pArgInf)
修改之后应该是:extern "C" void krnln_fnMessageBox (PMDATA_INF pRetData, INT nArgCount, PMDATA_INF pArgInf)
假设之前的核心支持库中有一个数据类型的接口获取函数是这么定义的:PFN_INTERFACE WINAPI Button_GetInterface (INT nInterfaceNO)
修改之后应该是:extern "C" PFN_INTERFACE WINAPI krnln_GetInterface_Button (INT nInterfaceNO)
为减少修改工作量,可定义类似如下的C/C++宏(注意把XXX和xxx替换为自已的库名称前缀):
#ifdef __cplusplus
#define EXTERN_C extern "C"
#else
#define EXTERN_C
#endif
#define DEFINE_XXX_EXECUTE_CMD(fnName) \
EXTERN_C void xxx##_##fnName (PMDATA_INF pRetData, INT nArgCount, PMDATA_INF pArgInf)
然后命令或方法实现函数的定义就可以修改为:
DEFINE_XXX_EXECUTE_CMD (fnMessageBox)
{
//...
}
在VC6中,还可以通过正则表达式替换进一步简化操作:搜索正则表达式“void\:b+\(\:i\)\:b*(PMDATA_INF\:b+pRetData\:b*\,\:b*INT\:b+nArgCount\:b*\,\:b*PMDATA_INF\:b+pArgInf\:b*)”,
将其替换为“DEFINE_XXX_EXECUTE_CMD (\1)”(注意其中XXX部分需自行修改)。
二、在处理系统通知的函数中返回特定的信息
在动态库中,需增加对以下三类通知
的处理,为静态库提供链接时支持:
当通知参数为 NL_GET_CMD_FUNC_NAMES 时,应返回所有命令和方法实现函数的函数名称数组,此数组必须与命令和方法定义数组一一对应,数组各成员均为对应函数的函数名称文本指针(char*);
当通知参数为 NL_GET_NOTIFY_LIB_FUNC_NAME 时,应返回“处理系统通知的函数”(即自身函数)的函数名称(char*);
当通知参数为 NL_GET_DEPENDENT_LIBS 时,应返回“依赖的第三方静态库文件列表”,格式为\0分隔的文本,结尾两个\0。
我们可以借助于类似如下的C/C++宏 XXX_CMD 自动搜集函数名称(此处及后面出现的 XXX 和 xxx,应替换为相应的“库名称前缀”):
#if defined(__E_STATIC_LIB) || defined(__COMPILE_FNR)
#define XXX_CMD(name)  xxx_##name  // 对于不同的库, 请做相应的名称修改
#define XXX_NULL_CMD(name)  NULL
#else if defined(__cplusplus)
static CFreqMem _g_CmdNames;
static PFN_EXECUTE_CMD _RecordCmdName (PFN_EXECUTE_CMD func, const TCHAR* szFuncName)
{
_g_CmdNames.AddDWord ((DWORD)szFuncName);
return func;
}
static const TCHAR** _GetCmdNames ()
{
return (const TCHAR**)_g_CmdNames.GetPtr ();
}
#define XXX_CMD(name)  _RecordCmdName (xxx_##name, "xxx_" #name)  // 对于不同的库, 请做相应的名称修改
#define XXX_NULL_CMD(name)  _RecordCmdName(NULL, NULL)
#endif
CFreqMem类定义于mem.cpp/h中。本文后面有一个迷你版的CFreqMem类,可在应急时使用。
然后修改实现函数表:
#ifndef __E_STATIC_LIB
PFN_EXECUTE_CMD s_RunFunc[] =
{
XXX_CMD (fnFuntion1),
XXX_CMD (fnFuntion2),
};
#endif
以上这步修改工作,在VC6中可以使用正则表达式替换。操作方法:选中PFN_EXECUTE_CMD数组中{}以内的文本,Ctrl+H,搜索正则表达式“\(\:i\)”,替换为“XXX_CMD (\1)”。
最后在收到 NL_GET_CMD_FUNC_NAMES 通知时返回 _GetCmdNames() 即可:
EXTERN_C INT WINAPI xxx_ProcessNotifyLib (INT nMsg, DWORD dwParam1, DWORD dwParam2)
{
#ifndef __E_STATIC_LIB
if(nMsg == NL_GET_CMD_FUNC_NAMES)
return (INT) _GetCmdNames();
else if(nMsg == NL_GET_NOTIFY_LIB_FUNC_NAME)
return (INT) "xxx_ProcessNotifyLib";
else if(nMsg == NL_GET_DEPENDENT_LIBS)
return (INT) NULL;
#endif
return ProcessNotifyLib(nMsg, dwParam1, dwParam2);
}
静态库中不需要处理这类通知,但需保证动态库中返回的函数名称在静态库中是真实存在的。其实只要满足了前面对函数名称和导入形式的要求,这个条件必然是成立的。
易语言静态编译链接期间,将向动态库发送以上通知,获取相应的函数名称等信息,然后再去静态库中查并链接指定名称的函数。动态库在静态编译时充当的角是为静态库提供辅助信息。(注:如果深入到编译链接过程,其实真
正用到的不是函数名称,而是符号名称(symbol's name)。易语言编译时将根据函数名称和调用约定确定其符号名称。)
三、从静态库中去除库定义相关的所有信息
与库定义有关的所有信息,包括命令和方法及其参数的定义信息、命令和方法的实现函数数组(m_pCmdsFunc)、数据类型及其属性事件方法的定义信息、GetNewInf()函数的定义等等,都不应该包含在静态库中。
从静态库中去除这些信息并不是必须的,但如果不去除,往往会造成链接时符号冲突,或导致链接生成的文件过大等一系列问题。
当然,以上这些信息都必须在动态库中得到完整保留。一般通过宏 __E_STATIC_LIB 进行代码屏蔽,如:
#ifndef __E_STATIC_LIB
LIB_INFO* WINAPI GetNewInf() { return &s_libinfo; }
#endif
四、其它
在静态库中,去除全局变量 theApp 的定义,代码中用到它之处,请替换为 AfxGetApp()。如果单纯替换为 AfxGetApp() 不能解决,需自行设法处理。这是必须的。
在静态库中,CWinApp继承类中的初始化和清理代码,可在一个static的全局类变量的构造和析构函数中调用;动态库中在GetNewInf()中的初始化代码,也可用类似方法解决。
在静态库中,应尽量减少导出符号对外部的影响,尽量定义为static符号,或添加自定义前缀。这有助于避免链接时的符号重复定义之类的链接错误。
在静态库中因使用 fnshare.cpp, untshare.cpp 等支持库开发包中的源文件而导出的一些函数,会导致链接时符号冲突,发布前请用 resym 程序对静态库文件进行处理。
编译静态库时的链接选项:静态链接MFC(或不用MFC),静态链接多线程C/C++库。VC 6.0 中设置MFC库链接选项的操作方法:菜单 Project -> Settings,General子夹中第一项即是;VC 6.0 中设置C/C++运行库的操作方法:菜单 Project -> Settings,进 C/C++ 子夹,Category选"Code Generation",Use run-time library选"Multithreaded"或"Debug Multithreaded"。建议先设置MFC库,再设置C/C++库,因为修改前者可能会自动调整后者。
符号重命名程序()
由于C/C++编译链接系统(通用编译链接系统)存在某种缺陷,导致许多本应属于“库内私有”的符号必须从静态库(.lib)中公开导出(比如跨源代码文件调用函数的情况),进而又导致多个静态库同时参与链接时产生符号冲突的可能性增大。
而且,由于易语言支持库都使用了开发包中的某些源代码文件(如fnshare.cpp, untshare.cpp),也不排除其它共用源代码的情况,如不特别处理,必然会在静态链接时产生大量“符号重复定义”之类的错误,导致链接失败。
手工修改所有支持库源代码,把所有函数和全局变量都重命名,例如统一添加某个名称前
缀,理论上也算是一个解决方案,但工作量太大了,准确性和全面性都不好保证。易语言公司一开始也走过这个弯路,效果上显然并不理想。
我们最终选择开发一个自动化的符号重命名程序,,用它对所有静态库(.lib)中的符号进行批量重命名。此程序位于易语言安装目录下的 sdk\tools 子目录中。它是一个控制台程序,请通过命令行启动(Windows XP 开始菜单 -> 执行 -> 输入 后回车)。
下面重点介绍此 resym 程序的功能和用法。
功能:它接收并处理一个LIB或OBJ文件,把其中的某些符号重命名(统一添加名称前缀),最终生成一个新的LIB或OBJ文件。
工作模式:resym 程序有两种工作模式,第一种是把“用户指定的某些符号”重命名(称为“!all模式”),第二种是把“除去用户指定的某些符号以外的其它所有符号”重命名(称为“all模式”)。默认工作在“!all模式”;当指定all参数时工作在“all模式”。处理易语言支持库的静态库时,一般使用其“all模式”。
最常用示例:
resym all infile="c\full\path\to\xxx.lib"
它等效于:resym all infile="c\full\path\to\xxx.lib" outfile="resymed.lib" prefix=xxx。大多数情况下,这种用法处理易语言支持库的静态就足够了。默认生成的LIB文件为resymed.lib,通过 outfile 参数可以指定输出路径和文件名。
如果您要明确阻止某些符号被重命名,请使用 symfile 参数,指定一个“列出所有欲阻止其重命名的符号名称的”文本文件(文件格式见下文):resym all infile="c\full\path\to\xxx.lib"
参数说明:
参数名称 含义 解释
易语言收费版和免费版的区别all 指定工作模式 如果不使用此参数,或指定参数值为no,表示采用“!all模式”,即,把“用户指定的某些符号”重命名;
如果使用了此参数或指定参数值为yes,表示采用“all模式”,即,把“除去用户指定的某些符号以外的其它所有符号”重命名。
使用all模式与否,将决定sym,symregex,symfile三个参数的含义(是要求还是阻止该符号被重命名),详见下文
如果工作在“all模式”下,将自动排除对以下符号的重命名:符号名称以prefix开头;符号名称以全小写形式的prefix, _prefix, __prefix开头
示例:
resym all ...
resym all=yes ...
resym all=no ...
resym ...
infile 指定输入文件 必须是LIB或OBJ文件,内部必须是COFF格式
outfile 指定输出文件 程序将把处理后新生成的文件输出到这里。可以等同于infile,表示覆盖原有文件。
默认值为 resymed.lib 或 resymed.obj
prefix 指定符号名称前缀 符号重命名的规则是,在原有符号名称前面添加此前缀
默认值为infile文件的文件名(不含后缀)
如果prefix以"_static"结尾(忽

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。