Lazarus/FPC Libraries/zh CN

From Free Pascal wiki
Jump to navigationJump to search

English (en) español (es) français (fr) 日本語 (ja) русский (ru) 中文(中国大陆) (zh_CN)

本文介绍了如何用 Lazarus/FPC 创建库并在项目和包中调用。

相关主题

概述

静态链接

默认情况下,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.1libz.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;
Light bulb  Note: 在1.9.4以下的版本中,动态加载库用 dl 单元实现。自1.9.4版本起,dynlibs 提供了一种可移植的替代方案。请注意,几乎所有无法用 dynlibs 替代的 dl 单元用法,通常在 Unix 系统间已不具备可移植性。仅凭这一点,就建议尽可能采用 dynlibs 单元。

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 中创建版本号。

参见