Lazarus/FPC Libraries/zh CN
│
English (en) │
español (es) │
français (fr) │
日本語 (ja) │
русский (ru) │
中文(中国大陆) (zh_CN) │
本文介绍了如何用 Lazarus/FPC 创建库并在项目和包中调用。
相关主题
- 创建 C 语言动态链接库的绑定 - 如何将 C 语言头文件(.h)转换为 pascal 单元。
概述
静态链接
默认情况下,FPC 会编译并链接为静态可执行文件,也即让链接器将项目的所有 .o 文件和软件包都合并为一个较大的可执行文件。
- 优点:
- 无外部依赖项
- 缺点:
- 同一台机器上的不同程序无法共享代码
- 无法加载/卸载插件
动态库
静态链接的替代方案是使用动态库,意在程序间共享代码。
- 优点:
- 节省内存
- 减少常用库的启动时间
- 允许采用那个插件的工作方式
- 缺点:
- 不常用库的加载时间较慢
- 程序结构和内部机制更复杂(主要是编译器面临的问题)
- 初始化过程不同(见下文)
- 代码共享需通过版本控制系统实现仅将兼容代码作融合
操作系统
操作系统 | 动态库扩展名 | 静态库扩展名 | 前缀 |
---|---|---|---|
FreeBSD | .so | .a | lib |
macOS | .dylib | .a | lib |
Linux | .so | .a | lib |
Windows | .dll | .lib | n/a |
Haiku | .so | .a | lib |
上表列出了操作系统解析和创建库名的规则。
FreeBSD
有待加入。
macOS
在 macOS 系统中创建库时,名称总会带有 lib 前缀。如果创建了名为 test 的动态库,那么生成的文件会是
libtest.dylib。在导入共享库中的函数时,无需给出库前缀或库文件扩展名。
2008年8月FPC 2.2.2 之前的版本中,32 位的 .dylib 文件导出的符号前需要加下划线。现在已不需要了。
动态库的标准存放路径是 ~/lib、/usr/local/lib 和 /usr/lib。从 macOS 10.15(Catalina)开始,/usr/lib 位于系统只读卷中,因此第三方库无法使用。虽然 .dylib 文件也可置于非标准路径下,但此路径必须加入以下环境变量之一:
- LD_LIBRARY_PATH
- DYLD_LIBRARY_PATH
- DYLD_FALLBACK_LIBRARY_PATH
私有共享库应包含于应用程序的应用程序打包文件中。对于没有稳定应用程序二进制接口(ABI)的库(如 OpenSSL)而言,这点尤为重要。这样库就能与应用程序的版本锁定,不会被操作系统中可能存在的其他版本(即便是更新的版本)所替代。
更多有关 macOS 的信息,请参考文后 参见部分。
Linux
动态库的文件名总是采用“lib”+包名+“.so”+版本号的格式。例如:libz.so.1
和 libz.so.1.2.2
。
Linux 按照以下顺序搜索库:
- 首先是环境变量
LD_LIBRARY_PATH
定义的路径 - 然后是 /lib
- 然后 /usr/lib
- 最后是
/etc/ld.so.conf
将库文件拷贝到 lib 目录后,可用 ldconfig
命令解决缓存刷新问题。
要在 Linux 下与其他库(非 FPC 编写)共享内存(GetMem
/FreeMem
, strings),应引用 cmem 单元。且必须是项目主文件(通常是 .lpr)uses 部分的 第一个单元,确保其初始化部分能在其他单元分配内存之前完成调用。
Windows
Windows 按以下顺序搜索库:
- 首先是当前目录
- 然后是系统目录
- 最后是环境变量 PATH(系统和用户)
待加入:不知 Vista 后续版本是否有变化?
如果 FPC 生成 DLL 和非 FPC 应用一起使用,异常处理过程可能会存在问题。解决方案在 初始化 一节有介绍。
ppumove、.ppu、.ppl
FPC 通常会为每个单元创建一个 .ppu 文件和一个 .o 文件。.ppu 文件包含 .pas/.pp 文件的全部重要信息(类型、用到的 .o 文件),而.o 文件则包含汇编代码和供当前系统解释的修饰符。
FPC 自带的 ppumove 工具可将一个或多个 .ppu 文件、.o 文件转换为动态库。通过调用链接程序,ppumove 会将所有 .o 文件整合到一个 .so 文件(在 Windows 中是 .dll 文件)中,并移除 .ppu 文件中的 .o 文件名引用。新 .ppu 文件通常称为 .ppl 文件。
假设软件包的输出目录(.ppu 文件的存放位置)如下:
ppumove -o packagename -e ppl *.ppu
以上命令会把所有 .ppu 文件转换为 .ppl 文件,并创建一个 libpackagename.so(在 Windows 中是 packagename.dll)。请注意,Linux 下总会加上“lib”前缀。
于是此库已可被其他编程语言(如 C 语言)或带 external 修饰符的 FPC 程序调用了。但 Initialization/Finalization 部分只能是自动调用,包括堆管理器的初始化/终止。这意味着字符串或 GetMem 都无法使用。当然,FPC 程序员都是宠儿,可以拥有更多功能。
外部库 - 静态加载动态库
利用关键字 external,动态库可用代码进行静态加载,以下示例展示了 gtk 函数的载入过程:
const
{$ifdef win32}
gtklib = 'libgtk-win32-2.0-0.dll';
{$else}
{$ifdef darwin}
gtklib = 'gtk-x11-2.0';
{$linklib gtk-x11-2.0}
{$else}
gtklib = 'libgtk-x11-2.0.so';
{$endif}
{$endif}
procedure gtk_widget_set_events(widget:PGtkWidget; events:gint); cdecl; external gtklib;
Loadlibrary - 动态库的动态加载
只要通过 dynlibs 单元中的 Loadlibrary 函数,就能实现动态库的加载。
主要的问题是如何获取文件名,这与版本和操作系统都有关系。自 2.2.2 版本起,dynlibs 单元中声明了一个常量“sharedsuffix”来简化这一过程。sharedsuffix 映射为对应的扩展名(dll/so/dylib)。
以下是 dynlibs 中的相关函数声明:
Function SafeLoadLibrary(Name : AnsiString) : TLibHandle;
Function LoadLibrary(Name : AnsiString) : TLibHandle;
Function GetProcedureAddress(Lib : TlibHandle; ProcName : AnsiString) : Pointer;
Function UnloadLibrary(Lib : TLibHandle) : Boolean;
SafeLoadLibrary 的功能与 LoadLibrary 一样,只是禁用了异常触发,有些库在加载时要求关闭异常。
这些函数调用的代码示例如下:
uses dynlibs;
var
MyLibC: TLibHandle;
MyProc: TMyProc;
begin
MyLibC := LoadLibrary('libc.' + SharedSuffix);
if MyLibC = 0 then Exit;
MyProc := TMyProc(GetProcedureAddress(MyLibC, 'getpt');
if MyProc = nil then Exit;
end;
Pseoudocode for using a function, taking parameters:
uses ...dynlibs...
procedure UseDLL;
type
TMyFunc=function (aInt:Integer; aStr: string):String; StdCall;
var
MyLibC: TLibHandle= dynlibs.NilHandle;
MyFunc: TMyFunc;
FuncResult: string;
begin
MyLibC := LoadLibrary('libc.' + SharedSuffix);
if MyLibC = dynlibs.NilHandle then Exit; //DLL was not loaded successfully
MyFunc:= TMyFunc(GetProcedureAddress(MyLibC, 'MyFunc');
FuncResult:= MyFunc (5,'Test'); //Executes the function
if MyLibC <> DynLibs.NilHandle then if FreeLibrary(MyLibC) then MyLibC:= DynLibs.NilHandle; //Unload the lib, if already loaded
end;
Linux 下的共享库加载过程简述
上图整体展示了如何在 /etc/ld.so.conf 中配置共享库(至少由 soname 和 real-name 引用),利用“ldconfig”工具更新库缓存,通知 Linux 系统库加载器“ldd”(以及加载共享库的其他方法,如通过 LD_LIBRARY_PATH 之类的环境变量、通过代码、通过二进制 ELF 文件等)。为了获取必要的信息,通常至少还得检查 soname 和 real-name 之间是否存在软符号链接。
在安装开发包(在 Debian 系统中是 *-dev*.deb 文件)时,总会发布一个指向开发包所依赖 soname(如 abc.so.{9})的软符号链接(如 abc.so)。其实发布包是最适合将旧版本指向新安装的 real-name 库的地方,但有时会漏做了。确实,共享库应该保证至少在同一 soname 内优先继承现有 API和功能(向下兼容)。
库的创建
参见 Free Pascal 文档:http://www.freepascal.org/docs-html/prog/progse55.html
Initialization
每个单元文件均可包含一个 Initialization 节,其顺序取决于单元的 uses 子句(即所引用的其他单元)。
RTL 本身的初始化(位于 system.pp 中)在进入自编单元 Initialization 之前就已完成,普通程序和共享库的 RTL 初始化过程略有差异。最显著的区别在于异常处理过程的初始化方式。
异常处理
Windows
已有 报告称,Windows 系统未提供捕获硬件异常的处理程序,如 access violation 或浮点错误;不过其他 Raise 触发的 Pascal 原生异常均能正常工作。
如果确实需要捕获这些硬件异常,则必须在 Windows 中安装自己的处理程序(Windows XP 以上版本可用 AddVectoredExceptionHandler() 和 RemoveVectoredExceptionHandler() 方便地实现),以上 bug 报告中给出了一些代码示例。注意:FPC 2.7.1/主分支中已开始着手利用32位 SEH 解决此问题。
UNIX 和类 UNIX 系统
UNIX 和类 UNIX 系统(如 BSD、Darwin、Linux 等)未提供捕获共享库中的硬件异常(信号)的处理程序,如 access violation。若要在库代码中捕获这些异常(信号),则需要在库的 Initialization 阶段显式调用 HookSignal(RTL_SIGDEFAULT)
,并在 Finalization 阶段调用 UnhookSignal(RTL_SIGDEFAULT)
。
请记住,自编共享库中启用的信号处理程序,将会替代当前进程中由宿主应用或其他共享库安装的处理程序。这可能会导致意外后果。已知 Linux 中的 Java 用到了 SIGSEGV 信号实现其内部功能,如果第三方 JNI 库覆盖了 SIGSEGV 信号处理程序,Java 就会崩溃。
FPU 取消屏蔽
为了与 Delphi 保持兼容,i386 架构的 Free Pascal 会 取消屏蔽浮点运算单元(FPU)中的所有 FPU 异常(但不会为 DLL 安装处理程序,见上文)。取消屏蔽的执行先于自编 DLL 的 Initialization 部分。如果 DLL 要与其他编译器(除 FPC 和 Delphi/C++ Builder C++ Builder 需验证 之外的几乎所有编译器)编译的宿主应用协同工作,这就会严重干扰宿主应用的异常处理过程。
调用 LoadLibrary() 后,宿主应用就不能再捕获浮点异常了。浮点运算单元(FPU)将突然获得抛出硬件异常的能力(在调用 LoadLibrary() 的线程中),而宿主应用很可能并不期望如此,也没有具备正确处理的能力。解决方法是在 Initialization 部分放入以下语句:
Set8087CW(Get8087CW or $3f);
让加载 DLL 的线程恢复对 FPU 异常的屏蔽。这只是恢复了宿主应用的一贯状态,不会产生什么意外的副作用。DLL 中本就无法捕获这些异常(至少在 Windows 中如此,参见上文)。如果确实要在为非 FPC 应用编写的 DLL 中捕获除零之类的错误,必须:
- 已安装异常处理程序,如上所述
- 在 try 之前暂时取消 FPU 屏蔽
- 在返回宿主应用之前恢复屏蔽
Finalization 部分
每个单元文件均可包含一个 Initialization 节,其顺序与 Initialization 部分相反。
Initialization 部分执行完毕后,将会调用之前赋给 Dll_Process_Detach_Hook 的过程。
此时的代码要非常小心:
- 其他单元都已释放了。
- 部分 RTL 可能再也无法正常工作了。
- 在内存堆上创建/销毁对象,甚至使用 Pascal string,无疑都是自找麻烦。
如果一定要停止线程和释放对象,必须是在库卸载之前完成,此时已经太迟了。
版本号
随着时间的推移,库往往会不断发展和变化。添加新功能没有问题,但删除公共方法或更改参数,就会让库失去兼容性。这就意味着,要么用一个保持兼容的库替换已安装库(.so、.dll、.dylib),要么就得在系统中添加新库。因此每个库都带有版本号。
有待加入:解释如何在 Windows 和 类 unix 系统中设置/使用版本号信息。
若要加载动态库(用 dynlibs 单元中的 dlopen),必须知道正确的文件名。在 Linux 下,意味着必须知道版本号。
有待加入:如何在 IDE 中创建版本号。
参见
- macOS 编程技巧 - 库
- macOS 动态库
- macOS 静态库
- FPC 共享库
- FPC 软件包
- 翻译/i18n/本地化