动态库编程

Posted by CoderLeonidas on April 6, 2021

动态库编程

简介

应用程序很少作为一个单一代码模块来实现,因为操作系统实现了应用程序在库中需要的大部分功能。为了开发应用程序,程序员将他们的自定义代码链接到这些库中,以获得基本的功能,比如编写标准输出或使用显卡绘制复杂_的能力。然而,链接到库会创建很大的可执行文件并浪费内存。减少应用文件大小和内存占用的一种方法是减少应用启动时加载的代码量。

要在运行时加载动态库,应用程序应该使用一组高效且可移植的函数(dynamic loader compatibility functions),称为动态加载器兼容函数。使用这些函数可以确保以最有效的方式加载动态库,并方便将应用程序从一个平台移植到另一个平台。

本文档适用于动态库的开发人员以及在应用程序中使用动态库的开发人员。熟悉Mac OS、UNIX、Solaris和Linux操作系统。你也应该是一个有经验的C、c++或Objective-C程序员。

动态库概览

决定应用性能的两个重要因素是它们的启动时间和内存占用。减少应用程序的可执行文件的大小,并在它启动时最小化它的内存使用,能使应用程序启动时更快,使用更少的内存。使用动态库而不是静态库可以减少应用程序的可执行文件大小。它们还允许应用程序只有在需要的时候才延迟加载具有特殊功能的库,而不是在启动时。这个特性进一步减少了启动时间和有效的内存使用。

本章节介绍了动态库,并展示了如何使用动态库而不是静态库来减少使用它们的应用程序的文件大小和初始内存占用。本章节还概述了应用程序在运行时使用动态库时使用的动态加载器兼容函数。

什么是动态库

应用程序的大部分功能是在可执行代码库中实现的。当应用程序使用静态链接器与库链接时,应用程序使用的代码将被复制到生成的可执行文件中。静态链接器将已编译的源代码(称为目标代码object code)和库代码library code收集到一个可执行文件中,该文件在运行时全部加载到内存中。成为应用程序可执行文件一部分的那种库称为静态库。静态库是目标文件的集合或归档。

注意:静态库也称为静态归档库和静态链接共享库。

当一个应用程序被启动时,包含了它所链接的静态库代码的应用程序代码会被加载到应用程序的地址空间中。将许多静态库链接到应用程序中会生成大型应用程序可执行文件。图1显示了使用静态库中实现的功能的应用程序的内存使用情况。具有大型可执行文件的应用程序启动时间慢,内存占用大。此外,当一个静态库被更新时,它的客户端应用程序不会从对它的改进中受益。要访问改进后的功能,应用开发者必须将应用的目标文件与新版本的库链接起来。而这些应用程序的用户必须用最新版本替换他们的应用程序副本。因此,使用静态库提供的最新功能来更新应用程序需要开发人员和终端用户进行破坏性的工作。

一个更好的方法是当应用程序确实需要时,才加载代码到它的地址空间时,无论是在启动时或运行时。提供这种灵活性的库类型称为动态库。动态库不是静态地链接到客户端应用程序;它们不会成为可执行文件的一部分。相反,动态库可以在应用程序启动或运行时加载(并链接)到应用程序中。

注意:动态库也称为动态共享库、共享对象或动态链接库。

使用动态库,程序可以从自动使用的库的改进中受益,因为它们到库的链接是动态的,而不是静态的。也就是说,客户端应用程序的功能可以得到改进和扩展,而不需要应用程序开发人员重新编译应用程序。为OS X编写的应用程序可以从这个特性中受益,因为OS X中的所有系统库都是动态库。这就是使用Carbon 或 Cocoa 技术的应用程序从OS X的改进中受益的原因。

如何使用动态库

当一个应用程序启动时,OS X内核将应用程序的代码和数据加载到一个新进程的地址空间中。内核还将动态加载程序(/ usr / lib / dyld)加载到进程中,并将控制权传递给该进程。然后动态加载器加载应用程序的依赖库。这些都是应用程序链接到的动态库。静态链接器在链接应用程序时记录每个依赖库的文件名。此文件名称为动态库的安装名称(install name)。动态加载器使用应用程序依赖库的安装名(install name)在文件系统中定位它们。如果动态加载器在启动时没有找到应用程序的所有依赖库,或者如果任何库与应用程序不兼容,启动过程将中止。动态库开发人员可以在使用gcc -install name选项编译库时为库设置不同的安装名称。有关依赖库兼容性的更多信息,请参见《Managing Client Compatibility With Dependent Libraries》。有关详细信息,请参阅gcc手册页。

动态加载器只解析应用程序在启动过程中实际使用的未定义的外部符号。其他的符号在应用程序使用之前都没有解决。当应用程序启动时,动态加载器的详细过程可以参考 Executing Mach-O Files

除了在启动时自动加载动态库之外,动态加载器还会在运行时根据应用程序的请求加载动态库。也就是说,如果应用在启动时不需要加载动态库,开发人员可以选择不将应用的对象文件与动态库链接,而是只在需要的部分加载动态库。以这种方式使用动态库可以加快启动过程。在运行时加载的动态库称为动态加载库。为了在运行时加载库,应用程序可以使用与其所运行系统的动态加载器交互的函数。

注意:客户端和动态库的目标架构必须相同。否则,动态加载器不会加载库。

不同的平台以不同的方式实现它们的动态加载器。它们还可能具有定制的动态代码加载接口,这使得代码难以跨平台移植。例如,为了方便将应用程序从UNIX移植到Linux, Jorge Acereda和Peter O’gorman开发了动态加载器兼容(DLC)函数。它们为开发者提供了一种标准的、可移植的方式来在他们的应用程序中使用动态库。

DLC函数在/usr/include/dlfcn.h中声明。 其中有五个:

  • dlopen(3) OS X Developer Tools Manual Page:
  • 打开一个动态库。应用程序在使用任何库导出的符号之前调用这个函数。如果当前进程还没有打开动态库,那么这个库就会被加载到进程的地址空间中。这个函数返回一个句柄,这个句柄在dlsym和dlclose调用中用来引用打开的库。这个句柄称为动态库句柄。这个函数维护一个引用计数,它指示当前进程使用dlopen打开特定动态库的次数。

  • dlsym(3) OS X Developer Tools Manual Page:
  • 返回动态加载库导出的符号的地址。应用程序在通过调用dlopen获得库句柄后调用这个函数。dlsym函数以dlopen返回的句柄或指定符号搜索范围和符号名称的常量作为参数。

  • dladdr(3) OS X Developer Tools Manual Page:
  • 返回所提供地址的信息。如果这个地址对应于应用程序地址空间中动态加载的库,这个函数将返回关于这个地址的信息。该信息以Dl info结构返回,该结构封装了动态库的路径名、库的基址以及与提供的地址最近的符号的地址和值。如果在提供的地址中没有找到动态库,那么该函数将不返回任何信息。

*** dlclose(3) OS X Developer Tools Manual Page: ** - 关闭动态加载的库。这个函数接受dlopen返回的句柄作为参数。当该句柄的引用计数达到0时,库将从当前进程的地址空间中卸载。

  • dlerror(3) OS X Developer Tools Manual Page:
  • 返回一个字符串,该字符串描述最后一次调用dlopen、dlsym或dlclose时遇到的错误条件。

有关DLC函数的更多信息,请参见《OS X ABI Dynamic Loader Reference.》。

动态库设计指南

动态库除了对常用功能进行分组外,还有助于减少应用程序的启动时间。然而,如果设计不当,动态库可能会降低其客户端的性能。(A dynamic library client is an app or a library that either is linked with the library or loads the library at runtime. 。本文档还使用image来指代动态库客户端。)因此,在创建动态库之前,必须定义它的用途和预期用途。为库的功能设计一个小而有效的接口对促进其他库或应用程序采用它有很长的路要走。

本章节讨论了动态库开发人员在设计和实现动态库时面临的主要问题。本文的重点是向您展示如何以一种方式设计库,使其能够通过版本来促进改进,并使库用户能够轻松地正确地与库交互。

设计一个最佳的库

动态库包含可以被用户计算机中的多个应用程序共享的代码。因此,它们应该包含一些应用程序可以使用的代码。它们不应该包含特定于一个应用程序的代码。这些是最优动态库的特性:

  • 目标明确:
  • 动态库应该专注于少数的、高度相关的目标。高度专注的库比多功能库更容易实现和使用。

  • 易于使用:
  • 库的接口,即库的客户端用来与之交互的符号,应该少且容易理解。一个简单的接口可以让库的用户更快地理解它的功能,而不是理解一个大型的接口。

  • 易于维护:
  • 库的开发人员必须能够对库进行更改,以提高其性能并添加特性。库的私有接口和公共接口的明确分离,使库开发人员可以自由地对库的内部工作进行深入的改变,而对客户端的影响最小。如果设计得当,使用库的早期版本创建的客户端可以不加修改地使用库的最新版本,并从其提供的改进中获益。

管理客户端与依赖库的兼容性

动态库的客户端可以以两种方式使用该库:

  • 作为依赖库
  • 作为运行时加载的库

从客户端的角度来看,依赖库是客户端所链接的动态库。依赖库被加载到客户端正在加载的同一进程中,作为其加载进程的一部分。例如,当一个应用程序启动时,它的依赖库在主函数执行之前,作为启动过程的一部分加载。当动态库加载到正在运行的进程中时,在将控制传递给打开该库的例程(routine)之前,将其相关库加载到该进程中。

定义客户端兼容性

当现有客户所依赖的库被修改时,对它的更改可能会影响客户使用新版本库的能力。客户端可以使用与其链接的依赖库的早期或晚期版本的程度称为客户端兼容性( client compatibility)。

有些变化很小;其中包括添加客户端未知的符号。其他变化也很大:这些改变包括删除一个符号,改变符号的大小或可见性,以及改变函数的语义。一个库向客户端公开的所有符号组成了库的ABI (app binary interface)。库的API只包含库为其客户端提供的函数。对于使用较早版本的库开发的客户端来说,库的ABI保持一致的程度决定了库的稳定性。确保库的ABI保持稳定,可以确保客户端可以使用更新版本的库。也就是说,依赖于定期更新的库的应用程序的用户可以在更新库(想想OS X软件更新机制)时看到他们的应用程序的性能改善,而不需要获得应用程序的新版本。

图1说明了Draw动态库及其一个客户端的生命周期。

这个列表描述了库和客户端的版本:

  • Draw 1.0是这个库的初始版本。它输出了两个函数,draw_linedraw_square
  • Client 1.0链接了Draw 1.0。因此,它可以使用库导出的两个符号。
  • Draw 1.1有更快的draw_linedraw_square版本,但是它们的语义没有改变,保持了客户端兼容性。这是一个兼容的小版本,因为客户端1.0可以使用Draw 1.1。
  • Draw 1.2介绍了draw_polygon的功能。新版本的API是以前版本API的超集。1.2版本的Draw 1.1 API子集没有改变。因此,Client 1.0可以使用Draw 1.2。然而,Client 1.0不知道draw_polygon的存在,因此,它没有使用它。这是一个小的版本,因为API Client 1.0知道的在Draw 1.2中没有改变。但这也是一个不兼容的版本,因为API发生了变化。与该库的此版本(1.2)链接的Client不能使用较早的版本(如1.0)。
  • Client 1.1链接了Draw 1.2,并使用draw_polygon。Client 1.1不能使用该库的早期版本,因为它使用了draw_polygon函数,这些版本没有导出该函数。但是,如果库的开发人员将weak_import属性添加到符号定义中,Client 1.1将能够使用该库的早期版本,方法是在使用它之前确保draw_polygon存在于其名称空间中。如果符号没有定义,客户端可以使用其他方法来执行所需的任务,或者不执行该任务。有关详细信息,请参阅《Symbol Exporting Strategies》。
  • Draw没有导出draw_square。这是一个重要的版本,因为在库的前一个版本中导出的符号在这个版本中不导出。与该库的此版本链接的客户端不能使用较早的版本。

客户端应该能够使用他们所链接的库的所有小版本,而不需要重新链接。一般来说,要使用库的主要版本版本,客户端必须链接到新版本。客户端也可能需要更改以利用新的符号,以适应其对已修改的符号的使用,或者不使用新版本未导出的符号。

注意:库的头文件应该只包含库客户端实际应该使用的符号。如果客户端使用的符号不是您指定的符号,它们将限制其产品与您的库的新版本或早期版本的兼容性。

指定版本信息

动态库的文件名通常包含带有lib前缀和.dylib扩展名。例如,一个名为Dynamo的库的文件名为libDynamo.dylib。但是,如果一个库在发布后可能会经历一个或多个版本,那么它的文件名必须包含该版本的主版本号。链接了文件名包含了版本的主版本号的客户端,永远不会使用一个新的该库的主版本,因为主版本是在不同的文件名下发布的。这种版本控制模型防止客户端使用API与客户端已知API不兼容的库版本。

当你发布一个动态库以获得未来的修订版时,你必须在它的文件名中披露库的主版本号。例如,在定义客户端兼容性时引入的Draw库第一个版本的文件名可以是libDraw.A.dylib。字母A表示首次发布的主版本号。主版本可以使用任何命名法。例如,绘制库也可以命名为libDraw.1.dyliblibDraw.I.dylib。重要的是,库的后续主要修订的文件名具有不同的(最好是增量的)主要版本号。继续Draw库的例子,对库的主要修订可以命名为libDraw.B.dylib libDraw.2.dylib或libDraw.II.dylib。库的小修订版以与前一个大修订版相同的文件名发布。

除了主版本号之外,库还有次要版本号。次要版本号是使用这种格式的增量号: X[.Y[.Z]],其中X为0 ~ 65535之间的数字,Y和Z为0 ~ 255之间的数字。例如,Draw库第一次发布的次要版本号可以是1.0。要设置动态库的次要版本号,使用clang -current version 选项。

兼容版本号与次要版本号相似;它是通过编译器-compatibility_version命令行选项设置的。一个库发行版的兼容性版本号指定了与该发行版相链接的客户端可以使用的最早的小版本。比如,定义客户端兼容性的例子表明,Client 1.1不能使用1.2之前的Draw库版本,因为它们不导出draw_polygon函数。要查看库的当前版本和兼容性版本,使用otool -L <library>命令。

在加载动态库之前,动态加载器会将用户文件系统中的.dylib文件的当前版本与客户端在开发人员文件系统中链接的.dylib文件的兼容版本进行比较。如果当前版本比兼容版本早(少),则不加载依赖库。因此,启动进程(针对客户端应用程序)或加载进程(针对客户端库)将被中止。

注意:动态加载器只对依赖库执行版本兼容性测试。使用dlopen在运行时打开的动态库不会通过这个测试。

指定库的接口

在实现动态库之前要定义的最重要的方面是它与客户端的接口。公共接口影响客户端对库的使用、库的开发和维护以及使用库的应用程序的性能等几个方面:

  • 易用:带有一些容易理解的公共符号的库要比导出它定义的所有符号的库容易得多。
  • 易维护: 拥有一小组公共符号和一组足够的私有符号的库维护起来要容易得多,因为需要测试的客户端入口点很少。此外,开发人员可以更改私有符号,以在较新的版本中改进库,而不会影响与较早版本链接的客户端的功能。
  • 性能:设计动态库以使其导出最小数量的符号,可优化动态加载器将库加载到进程中所花费的时间。库中导出的符号越少,动态加载器加载它的速度就越快。

下面几节将展示如何确定导出哪些库符号、如何命名它们以及如何导出它们。

决定输出什么符号

减少库导出的符号集使库易于使用和维护。通过简化符号集,库的用户只会看到与他们相关的符号。使用很少的公共符号,您可以自由地对内部接口进行实质性的更改,例如添加或删除不影响库客户端的内部符号。

不应该导出全局变量。对库的全局变量提供不受控制的访问,会使库容易因客户端给这些变量赋值不当而引起问题。在库的一个版本到另一个版本之间更改全局变量,而不做出与未链接它们的客户端不兼容的新版本,也是很困难的。动态库的主要特性之一是,如果正确实现,客户端可以使用它们的新版本,而无需重新链接。如果客户端需要访问存储在全局变量中的值,那么库应该导出访问器函数,而不是全局变量本身。遵循这一准则,库开发人员可以在库的不同版本之间更改全局变量的定义,而不会引入不兼容的修订版本。

如果您的库需要由它导出的函数实现的功能,您应该考虑实现函数的内部版本,向它们添加包装函数,并导出包装器。例如,您的库可能有一个必须验证其参数的函数,但您确定在调用该函数时库总是提供有效值。函数的内部版本可以通过删除验证代码来进行优化,使内部使用更有效。然后可以将验证代码放在包装器函数中,为客户端维护验证过程。此外,您可以进一步更改函数的内部实现,以包含更多的参数,同时保持外部版本相同。

让包装函数调用内部版本会降低应用程序的性能,特别是当该函数被客户端反复调用时。然而,灵活的维护和客户端稳定的接口的优点远远超过了这种微不足道的性能影响。

命名导出的符号

动态加载器不会检测它所加载的动态库导出的符号之间的命名冲突。当一个客户端包含对一个符号的引用,该符号被两个或多个依赖库导出时,动态加载器将该引用绑定到客户端依赖库列表中导出该符号的第一个依赖库。依赖库列表是客户端依赖库的列表,顺序是在客户端与它们链接时指定的顺序。同样,当dlsym函数被调用时,动态加载器返回它在指定范围(全局、本地或next)中找到的第一个符号的地址,该符号具有匹配的名称。有关符号搜索范围的详细信息,请参见《Using Symbols》

为了确保库的客户端总是能够访问库导出的符号,这些符号在进程的命名空间中必须具有唯一的名称。一种方法是应用程序使用两级名称空间。另一种方法是为每个导出的符号添加前缀。这是大多数OS X框架使用的约定,比如Carbon和Cocoa。有关两级命名空间的更多信息,see Executing Mach-O Files in Mach-O Programming Topics.

符号导出策略

在确定了希望向库用户公开的符号之后,必须设计出导出这些符号或不导出其余符号的策略。这个过程也被称为设置符号的可见性,也就是说,它们是否可以被客户端访问。客户端可以访问公共或导出的符号;客户端无法访问私有、隐藏或未导出的符号。在OS X中,有几种方法可以指定库符号的可见性:

  • static存储类:这是表示不想导出符号的最简单方法。
  • 导出的符号列表或未导出的符号列表: 该列表是一个文件,包含要导出的符号名称或要保持私有的符号列表。符号名称必须包含下划线 (_) 前缀。在生成动态库文件时,只能使用一种类型的列表。
  • visibility属性:将此属性放置在实现文件中的符号定义中,以单独设置符号的可见性。它可以让你更细致地控制哪些符号是公共的,哪些是私有的。
  • 编译器的-fvisibility命令行选项:该选项指定编译时实现文件中未指定可见性的符号的可见性。该选项结合可见性属性,是识别公共符号最安全、最方便的方式。
  • weak_import属性:把这个属性放在头文件中的符号声明中,告诉编译器生成一个对该符号的弱引用。这种特性被称为弱链接;带有弱导入属性的符号称为弱链接符号。使用弱链接,当在启动时或加载时找到的依赖库版本没有导出客户端引用的弱链接符号时,客户端不会启动失败。在库的客户端源文件使用的头文件中放置weak_import属性是很重要的,这样客户端开发人员就知道他们在使用该符号之前必须确保该符号的存在。否则,客户端在尝试使用该符号时将崩溃或功能不正确。有关弱链接符号的详细信息,请参阅《Using Weakly Linked Symbols 》。有关符号定义的更多信息,see Executing Mach-O Files in Mach-O Programming Topics
  • 编译器 -weak_library命令行选项:这个选项告诉编译器将所有库导出的符号视为弱链接符号。

清单1 一个简单的动态库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
* File: Person.h */
char* name(void);
void set_name(char* name);
 
/* File: Person.c */
#include "Person.h"
#include <string.h>
char _person_name[30] = {'\0'};
char* name(void) {
    return _person_name;
}
 
void _set_name(char* name) {
   strcpy(_person_name, name);
}
 
void set_name(char* name) {
    if (name == NULL) {
        _set_name("");
    }
    else {
        _set_name(name);
    }
}

库开发人员的目的是为客户端提供使用set_name函数设置_person_name值的能力,并让客户端使用name函数获取变量的值。但是,库导出的不仅仅是nameset name函数,如nm命令行工具的输出所示:

1
2
3
4
5
6
7
8
% clang -dynamiclib Person.c -o libPerson.dylib
% nm -gm libPerson.dylib
                 (undefined) external ___strcpy_chk (from libSystem)
0000000000001020 (__DATA,__common) external __person_name     // Inadvertently exported
0000000000000e80 (__TEXT,__text) external __set_name          // Inadvertently exported
0000000000000e70 (__TEXT,__text) external _name
0000000000000ec0 (__TEXT,__text) external _set_name
                 (undefined) external dyld_stub_binder (from libSystem)

注意,_person_name全局变量和_set_name函数与name和set_name函数一起导出。有许多选项可以从库导出的符号中删除_person_name和_set_name。本节将探讨一些。

第一个选项是将静态存储类添加到在Person .c中的_person_name和 _set_name的定义中,如清单2所示。

清单2 Person模块用静态存储类隐藏了一个符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* File: Person.c */
#include "Person.h"
#include <string.h>
 
static char _person_name[30] = {'\0'};        // Added 'static' storage class
char* name(void) {
    return _person_name;
}
 
static void _set_name(char* name) {           // Added 'static' storage class
   strcpy(_person_name, name);
}
 
void set_name(char* name) {
    if (name == NULL) {
        _set_name("");
    }
    else {
        _set_name(name);
    }
}

现在,nm输出,像这样:

1
2
3
4
                 (undefined) external ___strcpy_chk (from libSystem)
0000000000000e80 (__TEXT,__text) external _name
0000000000000e90 (__TEXT,__text) external _set_name
                 (undefined) external dyld_stub_binder (from libSystem)

这意味着库只导出name和set name。实际上,标准库还导出了一些未定义的符号,包括strcpy。它们是对库从其相关库中获得的符号的引用。

注意:您应该始终使用静态存储类来存储您希望为特定文件保留私有的符号。这是一种非常有效的自动防故障措施,防止无意中暴露本应对客户端隐藏的符号。

这种方法的问题是,它对库中的其他模块隐藏了内部_set_name函数。如果库的开发人员相信任何对_set_name的内部调用不需要验证,但想要验证所有的客户端调用,则该符号必须对库中的其他模块可见,但对库的客户端不可见。因此,静态存储类不适合对客户端隐藏符号,而是向所有库模块公开它们。

仅公开用于客户端使用的符号的第二种选择是拥有一个导出符号文件,该文件列出了要导出的符号;所有其他的符号都被隐藏起来了。清单3显示了export_list文件。

清单3文件列出了要导出的符号的名称

1
2
3
# File: export_list
_name
_set_name

要编译这个库,可以使用clang -exported_symbols_list选项来指定包含要导出的符号名称的文件,如下所示

1
clang -dynamiclib Person.c -exported_symbols_list export_list -o libPerson.dylib

仅公开name和set_name的第三个也是最方便的选项是,在编译库的源文件时,将实现中的visibility属性设置为“default”,并将-fvisibility 编译器命令行选项设置为hidden。清单4显示了为要导出的符号设置visibility属性后的Person.c文件的样子。

清单4使用visibility属性导出符号的Person模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* File: Person.c */
#include "Person.h"
#include <string.h>
 
// Symbolic name for visibility("default") attribute.
#define EXPORT __attribute__((visibility("default")))
 
char _person_name[30] = {'\0'};
 
EXPORT                        // Symbol to export
char* name(void) {
    return _person_name;
}
 
void _set_name(char* name) {
   strcpy(_person_name, name);
}
 
EXPORT                        // Symbol to export
void set_name(char* name) {
    if (name == NULL) {
        _set_name("");
    }
    else {
        _set_name(name);
    }
}

然后使用以下命令编译库:

1
 % clang -dynamiclib Person.c -fvisibility=hidden -o libPerson.dylib

fvisibility=hidden命令行选项告诉编译器将任何没有可见性属性的符号的可见性设置为hidden,从而对库的客户端隐藏它们。

遵循这些符号导出指导原则可以确保库只导出您想让客户端可用的符号,从而简化了客户端对库的使用,并便利了开发人员对库的维护。

查找外部资源

当您需要定位库或程序在运行时需要的资源(如框架、_等)时,您可以使用下列方法之一:

  • Executable-relative location: 要指定相对于主可执行文件位置的文件路径,而不是相对于引用库的文件路径,请将@executable_path宏放在路径的开头。例如,在包含私有框架(又包含共享库)的应用程序包中,任何一个库都可以通过指定路径@executable_path/../Resources/MyImage.tiff在包内找到名为MyImage.tiff的应用程序资源。因为@executable路径解析为app bundle中的MacOS目录中的二进制文件,所以资源文件路径必须将Resources目录指定为MacOS父目录(Contents目录)的子目录。有关目录绑定包的详细讨论,请参见《Bundle Programming Guide》。
  • Library-relative location: 要指定相对于库本身位置的文件路径,请将@loader_path宏放在路径名的开头。库相对位置允许您在目录层次结构中定位库资源,而不管主可执行文件位于何处。

库依赖

在开发动态库时,可以通过链接源代码来指定其依赖库。当库的客户端试图加载库时,与库相关的库必须存在于文件系统中,才能成功加载库。(请参阅运行路径依赖库,了解如何在可重定位目录中安装依赖库。)根据客户端加载库的方式,部分或全部库对其相关库导出的符号的引用将被解析。你应该考虑使用dlsym(3) OS X开发人员工具手册页面函数来获得需要的符号的地址,而不是总是需要在加载时解析的引用。

库的依赖性越强,加载库所需的时间就越长。因此,您应该只将库与加载时所需的动态库链接。编译库之后,可以使用otool -L命令在shell编辑器中查看它的相关库。

您的库很少使用的任何动态库,或者只有在执行特定任务时才需要其功能的动态库,都应该作为运行时加载库使用;也就是说,使用dlopen(3) OS X Developer Tools Manual页面功能打开。例如,当一个模块在你的库需要执行一项任务,需要使用非依赖的库, 模块应该使用dlopen加载库,使用这个库来执行其任务,并在完成后使用dlclose (3) OS X开发工具手册页面关闭库。

您还应该将依赖库中对符号的外部引用数量保持在最低限度。这种做法进一步优化了库的加载时间。

你必须向你的库用户披露你的库所使用的所有库,以及它们是否是从属的库。当动态库的用户链接他们的image时,静态链接器必须能够找到所有与库相关的库,无论是通过链接行还是符号链接。另外,因为即使在运行时打开的一些或所有库在加载时不存在,动态库也能成功加载,所以库的用户必须知道库在运行时打开了哪些动态库,以及在哪些情况下打开了哪些动态库。库的用户可以在调查库的意外行为时使用这些信息。

模块初始化器和终结器

加载动态库时,它们可能需要在执行任何其他操作之前准备资源或执行特殊的初始化。相反,当库被卸载时,它们可能需要执行一些结束过程。这些任务由初始化器函数和终结器函数(也称为构造函数和析构函数)执行。

注意:应用程序也可以定义和使用初始化器和终结器。然而,本节主要关注它们在动态库中的使用。

初始化器可以安全地使用依赖库中的符号,因为动态加载器在调用image的静态初始化器之前会执行image依赖库的静态初始化器。

通过向函数定义中添加构造函数属性,可以指明函数是初始化器。析构函数属性标识终结器函数。不能导出初始化器和终结器。动态库的初始化式按编译器遇到它们的顺序执行。另一方面,它的终结器按照编译器遇到的相反顺序执行。

例如,清单5显示了一组初始化器和终结器,它们在名为Inifi的动态库中的两个文件File1.c和File2.c中相同地定义。

清单5 Inifi初始化器和终结器定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Files: File1.c, File2.c */
#include <stdio.h>
__attribute__((constructor))
static void initializer1() {
    printf("[%s] [%s]\n", __FILE__, __FUNCTION__);
}
 
__attribute__((constructor))
static void initializer2() {
    printf("[%s] [%s]\n", __FILE__, __FUNCTION__);
}
 
__attribute__((constructor))
static void initializer3() {
    printf("[%s] [%s]\n", __FILE__, __FUNCTION__);
}
 
__attribute__((destructor))
static void finalizer1() {
    printf("[%s] [%s]\n", __FILE__, __FUNCTION__);
}
 
__attribute__((destructor))
static void finalizer2() {
    printf("[%s] [%s]\n", __FILE__, __FUNCTION__);
}
 
__attribute__((destructor))
static void finalizer3() {
    printf("[%s] [%s]\n", __FILE__, __FUNCTION__);
}

继续这个例子,Inifi动态库是Trial程序的唯一依赖库,它由Trial.c文件生成,如清单6所示。

清单6 Trial.c文件

1
2
3
4
5
6
/* Trial.c */
#include <stdio.h>
int main(int argc, char** argv) {
    printf("[%s] [%s] Finished loading. Now quitting.\n", __FILE__, __FUNCTION__);
    return 0;
}

清单7显示了Trial 程序生成的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
% clang -dynamiclib File1.c File2.c -fvisibility=hidden -o libInifi.dylib
% clang Trial.c libInifi.dylib -o trial
% ./trial
[File1.c] [initializer1]
[File1.c] [initializer2]
[File1.c] [initializer3]
[File2.c] [initializer1]
[File2.c] [initializer2]
[File2.c] [initializer3]
[Trial.c] [main] Finished loading. Now quitting.
[File2.c] [finalizer3]
[File2.c] [finalizer2]
[File2.c] [finalizer1]
[File1.c] [finalizer3]
[File1.c] [finalizer2]
[File1.c] [finalizer1]

尽管在一个_中可以有任意多个静态初始化器和终结器,但应该根据需要将初始化和终结器代码合并到每个模块一个初始化器和一个终结器中。您还可以选择为每个库设置一个初始化器和一个终结器。

在OS X v10.4及更高版本中,静态初始化器可以访问给定给当前程序的实参。通过定义初始化器的形参,就像定义程序的主函数的形参一样,可以得到给定参数的数量、参数本身以及进程的环境变量。此外,为了防止初始化器或终结器被调用两次,应该在函数内部对初始化和终结代码进行条件化。清单8显示了一个静态初始化器的定义,该初始化器可以访问程序的参数并对其初始化代码进行条件化。

清单8静态初始化器的定义

1
2
3
4
5
6
7
8
__attribute__((constructor))
static void initializer(int argc, char** argv, char** envp) {
    static initialized = 0;
    if (!initialized) {
        // Initialization code.
        initialized = 1;
    }
}

注意:一些操作系统支持初始化器和终结器init和fini的命名约定。这个约定在OS X中不受支持。

C++依赖库

使用c++来实现一个动态库会带来一些挑战,主要是导出符号名以及创建和销毁对象。下面几节详细介绍了如何从基于c++的动态库导出符号,以及如何为客户端提供创建和销毁类实例的函数。

导出c++符号

定义c++类接口

创建和销毁c++对象

基于objective-c的库

在设计或更新基于Objective-C的动态库时,有几个问题需要考虑

  • 发布一个Objective-C类或类别的公共接口与在C中导出符号的方式不同。在Objective-C中,每个类的每个方法在运行时都是可用的。
    • 客户端可以内省类以找出可用的方法。然而,为了使客户端开发人员不会收到关于缺少方法实现的一连串警告,库开发人员应该将类和类别的接口作为协议发布给客户端开发人员。
  • 基于Objective-C的库比基于c的库可以访问更多的初始化工具
  • Objective-C有一个类别名工具,它允许库开发人员在修订中重命名类,但允许客户端与该修订链接,以继续使用早期修订中使用的名称。

下面几节将详细探讨这些领域。

定义类和接口类

因为客户端开发人员通常不能访问动态库中定义的Objective-C类和类别的实现,所以库开发人员必须在头文件中作为协议发布类和类别的公共接口。客户端开发人员使用这些头文件来编译他们的产品,并且能够通过向变量定义中添加必要的协议名来正确地实例化类。清单12显示了基于Objective-C的库中的Person类的头文件和实现文件。清单13显示了同一个库中Titling类别的头文件和实现文件,它向Person类添加了-setTitle方法。

清单12 Person类的头文件和实现文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* File: Person.h */
#import <Foundation/Foundation.h>
 
@protocol Person
- (void)setName:(NSString*)name;
- (NSString*)name;
@end
 
@interface Person : NSObject <Person> {
    @private
    NSString* _person_name;
}
@end
 
/* File: Person.m */
#import <Foundation/Foundation.h>
#import "Person.h"
 
@implementation Person
- (id)init {
    if (self = [super init]) {
        _person_name = @"";
    }
    return self;
}
 
- (void)setName:(NSString*)name {
    _person_name = name;
}
 
- (NSString*)name {
    return _person_name;
}
@end

清单13 Person类的标题类别的头文件和实现文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* File: Titling.h */
#import <Foundation/Foundation.h>
#import "Person.h"
 
@protocol Titling
- (void)setTitle:(NSString*)title;
@end
 
@interface Person (Titling) <Titling>
@end
 
/* File: Titling.m */
#import <Foundation/Foundation.h>
#import "Titling.h"
 
@implementation Person (Titling)
- (void)setTitle:(NSString*)title {
    [self setName:[[title stringByAppendingString:@" "]
        stringByAppendingString:[self name]]];
}
@end

清单14显示了客户端如何使用这个库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* File: Client.m */
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <dlfcn.h>
#import "Person.h"
#import "Titling.h"
 
int main() {
    @autoreleasepool {
        // Open the library.
        void* lib_handle = dlopen("./libPerson.dylib", RTLD_LOCAL);
        if (!lib_handle) {
            NSLog(@"[%s] main: Unable to open library: %s\n",
            __FILE__, dlerror());
            exit(EXIT_FAILURE);
        }
 
        // Get the Person class (required with runtime-loaded libraries).
        Class Person_class = objc_getClass("Person");
        if (!Person_class) {
            NSLog(@"[%s] main: Unable to get Person class", __FILE__);
            exit(EXIT_FAILURE);
        }
 
        // Create an instance of Person.
        NSLog(@"[%s] main: Instantiating Person_class", __FILE__);
        NSObject<Person,Titling>* person = [Person_class new];
 
        // Use person.
        [person setName:@"Perrine LeVan"];
        [person setTitle:@"Ms."];
        NSLog(@"[%s] main: [person name] = %@", __FILE__, [person name]);
 
        // Close the library.
        if (dlclose(lib_handle) != 0) {
            NSLog(@"[%s] Unable to close library: %s\n",
                __FILE__, dlerror());
            exit(EXIT_FAILURE);
        }
 
    }
    return(EXIT_SUCCESS);
}

下面的命令编译库和客户端程序:

1
2
3
clang -framework Foundation -dynamiclib Person.m Titling.m -o libPerson.dylib
clang -framework Foundation Client.m -o client

初始化objective-c类

基于Objective-C的动态库为模块、类和类别提供了一些初始化工具。下面的列表按照执行的顺序描述了这些设施。

  • +load方法:
  • 初始化类或类别所需的资源。Objective-C运行时向库实现的每个类发送load消息;然后,它向库实现的每个类别发送load消息。尚未确定发送load消息的兄弟类的顺序。实现+load方法来初始化类或类别所需的资源。注意,没有相应的”unload”方法。

  • 模块初始化器:
  • 初始化模块。动态加载器调用每个库模块中的所有初始化器函数(用constructor属性定义)。有关模块初始化器的更多信息,请参阅模块初始化器和结束器。

  • +initialize方法:
  • 在创建任何实例之前初始化类实例所需的资源。Objective-C运行时在创建一个类的实例之前将初始化消息发送给一个类。请注意,在卸载库或进程终止时,没有向类发送相应的finalize消息。

为类创建别名

当您在动态库的修订中重命名类时,您可以通过在库的头文件中为新名称添加别名来减少客户端开发人员的采用负担。这种做法允许客户端开发人员发布能够快速利用新版本库的客户端。客户端开发人员可以稍后在空闲时更新对该类的引用。

设计指南清单

此列表提供了改进动态库特定方面的指导方针的摘要

  • 易用
  • 减少库导出的符号数量。
  • 为公共接口提供唯一的名称。

  • 易维护
  • 导出变量的访问函数。不要导出变量。
  • 将公共接口作为内部私有接口的包装器来实现。

  • 性能
  • 尽量减少对相关库中符号的引用数量。使用dlsym(RTLD GLOBAL, )来获取依赖库导出的符号的地址。
  • 最小化依赖库的数量。如果绝对必要,可以考虑使用dlopen加载库。记住在完成时用dlclose关闭库。
  • 将公共接口作为内部私有接口的包装器实现。

  • 兼容性
  • 将符号导出为弱链接符号。
  • 在文件名中将库的主要版本号编码。

动态库使用指南

动态加载器兼容函数提供了一种在运行时加载代码的可移植和有效的方法。但是,不正确地使用这些功能会降低应用程序的性能。本文将展示如何在应用程序中正确加载和使用动态库。

动态库有助于将应用程序的功能分配到不同的模块中,这些模块可以在需要时加载。动态库可以在应用启动或运行时加载。在启动时加载的库称为依赖库(dependent libraries)。在运行时加载的库称为动态加载库(dynamically loaded libraries.)。你可以指定你的应用程序依赖哪些动态库,并且链接他们。然而,使用动态加载库比使用依赖库更有效。也就是说,你应该在要使用库导出的符号时打开库,完成后关闭库。在某些情况下,当系统确定动态加载的库没有被使用时,就会卸载它们。 本文使用image这个词来指代应用程序文件或动态库。应用程序二进制文件包含应用程序代码和应用程序使用的静态库中的代码。应用程序在启动时或运行时加载的动态库是独立的image。

打开动态库

当image被打开时,动态加载器加载一个image的依赖库;也就是说,当一个应用程序被加载或者一个动态库被打开时。动态加载器将引用延迟绑定到依赖库导出的符号。惰性绑定意味着只有当image实际使用符号时才绑定符号引用。作为一种调试措施,您可以指定在动态加载器打开库时绑定对库导出符号的所有引用。在生成动态库时,可以使用编译器的-bind_at_load命令行选项。

要使用动态库,而不是你的image的依赖库,请使用dlopen(3) OS X开发人员工具手册页面函数。这个函数告诉动态加载器将特定的动态库加载到当前进程的地址空间中。这个函数还允许您指定动态加载器何时将库引用绑定到其依赖库中相应的导出符号,以及是否将库导出的符号放置在当前进程的全局作用域中还是局部作用域中。这个函数返回一个称为库句柄的句柄。这个句柄表示在调用dlsym(使用导出的符号)和dlclose(关闭库)时动态加载的库。库句柄为dlsym提供了一个有限的域,可以在其中搜索符号(详细信息请参阅使用符号)。客户端在使用动态加载的库完成时必须调用dlclose(例如,打开库的模块完成了它的任务)。

动态库本身可能有依赖库。使用otool -L 命令查找动态库所依赖的库。在使用该库之前,必须确保计算机中存在它的所有相关库。否则,在启动时或使用dlopen打开库时,动态加载器不会加载你的应用程序或库。

进程可以多次打开同一个动态库而不关闭它。dlopen函数返回它在第一次调用中返回的库句柄,但是它还增加与该句柄相关联的引用计数。调用dlclose递减库句柄的引用计数。因此,您必须在每次对dlopen的调用和一次对dlclose的调用之间进行平衡。当库句柄的引用计数达到0时,动态加载器可以从应用程序的地址空间中删除库。

库搜索的过程

dlopen(3) OS X开发人员工具手册的第一个参数是要打开的动态库的名称。这可以是文件名,也可以是部分或完全限定的路径名​​。 例如,libCelsus.dylib,lib/libCelsus.dylib或/usr/local/libCelsus.dylib。

动态加载器在一组环境变量和进程当前工作目录指定的目录中搜索库。定义这些变量时,必须包含一个以冒号分隔的路径名列表(绝对的或相对的),动态加载器在其中搜索库。表1列出了变量。

表1定义动态加载器搜索路径的环境变量

Environment variable Default value
LD_LIBRARY_PATH No default value
DYLD_LIBRARY_PATH No default value
DYLD_FALLBACK_LIBRARY_PATH $HOME/lib; /usr/local/lib; /usr/lib

当库名是一个文件名时(也就是说,当它不包含目录名时),动态加载器在几个位置搜索库,直到找到它,顺序如下

  • $LD_LIBRARY_PATH
  • $DYLD_LIBRARY_PATH
  • 进程的工作目录
  • $DYLD_FALLBACK_LIBRARY_PATH

当库名包含至少一个目录名时,即当名称是路径名(相对的或完全限定的)时,动态加载器将按照以下顺序搜索库:

  • $DYLD_LIBRARY_PATH 使用文件名
  • 给定路径名
  • $DYLD_FALLBACK_LIBRARY_PATH 给定文件名

例如,假设您按照下表所示设置了前面介绍的环境变量。

Environment variable Default value
LD_LIBRARY_PATH ./lib
DYLD_LIBRARY_PATH /usr/local/dylibs
DYLD_FALLBACK_LIBRARY_PATH /usr/local/lib

假设你的应用程序以文件名libCelsus调用dlopen。动态加载器将尝试使用以下路径名依次打开库:

Pathname Description
./lib/libCelsus.dylib LD_LIBRARY_PATH 环境变量
/usr/local/dylibs/libCelsus.dylib DYLD_LIBRARY_PATH 环境变量
libCelsus.dylib 当前工作目录
/usr/local/lib/libCelsus.dylib DYLD_FALLBACK_LIBRARY_PATH 环境变量

指定导出符号的范围和绑定行为

dlopen(3) OS X开发人员工具手册页面函数的第二个参数指定了两个属性:库导出的符号在当前进程中的作用域,以及何时绑定应用程序引用这些符号。

符号作用域直接影响应用程序的性能。因此,为应用程序在运行时打开的库设置适当的范围是很重要的。

动态加载的库导出的符号可以在当前进程的两个级别作用域中:全局本地。作用域之间的主要区别是全局作用域中的符号对进程中的所有image都可用,包括其他动态加载的库。局部范围内的符号只能由打开该库的image使用。有关更多信息,请参见使用符号。

当动态加载器搜索符号时,它会对搜索范围内的每个符号执行字符串比较。减少动态加载器查找所需符号的符号数量可以提高应用程序的性能。将所有动态加载的库打开到局部作用域中而不是全局作用域中,可以最大化符号搜索性能。

用于指定符号作用域的参数还用于指定动态加载的库中未定义的外部符号何时被解析(或与它们在库自身相关库中的定义绑定)。动态加载库中未定义的外部符号可以立即解析,也可以延迟解析。

清单1使用立即绑定在调用dlopen期间解析的绑定

1
2
3
4
5
6
7
8
9
10
11
dyld: lazy bind: client:0x107575050 = libdyld.dylib:_dlopen, *0x107575050 = 0x7FFF88740922
dyld: bind: libPerson.dylib:0x1075A9000 = libdyld.dylib:dyld_stub_binder, *0x1075A9000 = 0x7FFF887406A0
dyld: bind: libPerson.dylib:0x1075A9220 = libobjc.A.dylib:__objc_empty_cache, *0x1075A9220 = 0x7FFF7890EC10
dyld: bind: libPerson.dylib:0x1075A9248 = libobjc.A.dylib:__objc_empty_cache, *0x1075A9248 = 0x7FFF7890EC10
dyld: bind: libPerson.dylib:0x1075A9228 = libobjc.A.dylib:__objc_empty_vtable, *0x1075A9228 = 0x7FFF7890CF60
dyld: bind: libPerson.dylib:0x1075A9250 = libobjc.A.dylib:__objc_empty_vtable, *0x1075A9250 = 0x7FFF7890CF60
dyld: bind: libPerson.dylib:0x1075A9218 = CoreFoundation:_OBJC_CLASS_$_NSObject, *0x1075A9218 = 0x7FFF77C40BA8
dyld: bind: libPerson.dylib:0x1075A9238 = CoreFoundation:_OBJC_METACLASS_$_NSObject, *0x1075A9238 = 0x7FFF77C40B80
dyld: bind: libPerson.dylib:0x1075A9240 = CoreFoundation:_OBJC_METACLASS_$_NSObject, *0x1075A9240 = 0x7FFF77C40B80
dyld: bind: libPerson.dylib:0x1075A9260 = CoreFoundation:___CFConstantStringClassReference, *0x1075A9260 = 0x7FFF77C72760
dyld: bind: libPerson.dylib:0x1075A9280 = CoreFoundation:___CFConstantStringClassReference, *0x1075A9280 = 0x7FFF77C72760

第一个日志消息表明客户端应用程序的dlopen未定义符号被绑定。其余的消息是动态加载器在将控制权返回给调用例程之前作为加载过程的一部分在动态库上执行的绑定。当使用延迟绑定时,动态加载器只解析客户端对dlopen函数的引用,从而更快地将控制权返回给调用例程。有关动态加载器日志记录的更多信息,请参见记录动态加载器事件。

一旦用dlopen打开了一个库,为它定义的作用域就不能通过后续调用dlopen来加载相同的库而改变了。例如,如果进程打开了一个尚未加载到局部作用域的库,然后在全局作用域中打开了相同的库,则打开的库将保持其本地状态。也就是说,库导出的符号在后一个调用中不会在全局作用域中可用。即使库在同一进程中重新打开之前已经关闭,也是如此。

重要提示:所有运行时加载的动态库都应该在本地范围内打开。遵循此规则可以使在运行时尽可能快地查找符号。

立即绑定会减慢动态库的加载速度,特别是当这些库包含许多未定义的外部符号时。然而,立即绑定可以在动态库的开发和测试过程中提供帮助,因为当动态加载器不能解析动态加载库中所有未定义的外部符号时,应用程序将以一个错误终止。然而,在部署应用程序时,你应该使用惰性加载,因为未定义的外部符号只在必要时绑定。以这种方式加载动态库可以让你的应用感觉对用户的响应更灵敏。

依赖库中的外部未定义符号在第一次使用时被绑定,除非客户端image的编译行包含了-bind_at_load 选项。详细信息请参见ld手册页。

使用符号

在使用dlopen(3) OS X开发人员工具手册页面打开动态库后,image在使用之前使用dlsym(3) OS X开发人员工具手册页面函数来获取所需符号的地址。第一个参数指定了动态加载器在哪些库中查找该符号。第二个参数指定符号的名称。例如

1
symbol_pointer = dlsym(library_handle, "my_symbol")

这个调用告诉动态加载器在由library_handle变量表示的动态加载库导出的符号中搜索名为my_symbol的符号。

动态加载器可以搜索符号的三个作用域:特定的动态库、当前image的依赖库和进程的全局作用域:

  • 本地范围: 要搜索使用dlopen加载的特定动态库导出的符号,需要向dlsym提供该库的句柄。这是最有效的使用模型。
  • 次要范围: 只有当模块插入了依赖库导出的符号时,此搜索范围才有用。在这种情况下,在函数的自定义定义中,通过使用RTLD_NEXT特殊句柄(而不是特定库的句柄)调用dlsym,可以获得所插入的函数的地址。这样的调用将返回函数的地址,如果您没有用自己的实现掩盖该实现,则该函数将被执行。因此,只搜索当前image的相关库;不搜索任何其他库,包括image调用dlsym所打开的库。同样,在扁平的命名空间中,当应用程序被链接时,搜索从当前库之后列出的第一个依赖库开始。
  • 全局范围: 要搜索全局范围,可以使用RTLD_DEFAULT特殊句柄调用dlsym。动态加载器在依赖库(在启动时加载)和动态加载库(在运行时使用RTLD_GLOBAL加载)中搜索给dlsym的符号名的第一个匹配项。

注意:动态共享库之间的名称冲突不会在编译时、链接时或运行时被发现。dlsym函数使用字符串匹配来查找符号。如果两个库对一个函数使用相同的名称,动态加载器将返回第一个与dlsym给定的符号名称匹配的库。

为了说明本节中介绍的概念,请使用图1中描述的应用程序。 它显示该应用程序有两个依赖库,libArt.dylib 和libBus.dylib。 libBus.dylib库本身有两个依赖库libBus1.dylib libBus2.dylib。 libBus1.dylib库有一个依赖的库libBus1a.dylib。 此外,还有四个应用程序不依赖的动态库:libCar.dylib libCar1.dylib libDot.dylib, libDot1.dylib。 libCar1.dylib库是libCar的一个依赖库。libDot1.dylib是libDot.dylib的一个依赖库。 所有的库,除了libArt,都导出依赖项函数。 每个库都有一个…name函数的唯一实现。

图1应用程序依赖库层次结构

应用程序image直接可以访问libArt.dylib 和 libBus.dylib中导出的符号。如清单2所示。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
extern char* A_name();          // libArt.dylib
extern char* dependencies();    // libBus.dylib
 
int main(void) {
    printf("[%s] libArt.A_name() = %s\n", __FILE__, A_name());
    printf("[%s] libBus.dependencies() = %s\n", __FILE__, dependencies());
}

然而,应用程序image不能直接访问 libBus1.dylib, libBus1a.dylib, and libBus2.dylib 所导出的符号。因为这些库不是依赖于应用image的库。 要访问这些符号,应用程序image必须使用dlopen打开相应的库,如清单3所示。

清单3应用程序image使用了一个在运行时加载的动态库导出的符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <dlfcn.h>
 
int main(void) {
    void* Bus1a_handle = dlopen("libBus1a.dylib", RTLD_LOCAL);
    if (Bus1a_handle) {
        char* (*b1a_name)() = dlsym(Bus1a_handle, "B1a_name");
        if (b1a_name) {
            printf("[%s] libBus1a.B1a_name() = %s\n",
                __FILE__, b1a_name());
        }
    }
    else {
        printf("[%s] Unable to open libBus1a.dylib: %s\n",
            __FILE__, dlerror());
    }
    dlclose(Bus1a_handle);
}

到目前为止,您已经了解了如何通过引用导入的符号或通过使用相应库的句柄或RTLD_DEFAULT特殊句柄调用dlsym获取所需符号的地址来访问符号。 如前所述,插入符号提供了更改依赖库导出的符号定义的功能。

要访问插入符号的原始定义,可以使用RTLD_NEXT特殊句柄调用dlsym。清单4显示了Bus库中dependencies函数的实现(Bus1和Bus1a中的实现是相同的)。Bus中的函数返回库的名称(包含在k_lib_name变量中),并连接分隔符字符串和下一个dependencies定义返回的文本,下一个dependencies定义在Bus1库中找到。Bus1中的定义将其名称与分隔符字符串和Bus1a中的定义返回的文本连接起来。如果没有客户端image定义它们自己的版本,那么Bus1a中的定义是最后一个可以找到的定义。因此,当Bus1a调用dlsym(RTLD_NEXT, “dependencies”)时,找不到dependencies的其他定义。这就是dependencies函数的介入层次结构的结束。

清单4使用插入符号的库image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <string.h>
static char* k_lib_name = "libBus";
char* dependencies(void) {
    char _dependencies[50] = "";
    strcpy(_dependencies, k_lib_name);
    char* (*next_dependencies)() =
        dlsym(RTLD_NEXT, "dependencies");// look for next definition
    if (next_dependencies) {
        strncat(_dependencies, ", ",
            sizeof(_dependencies) - strlen(_dependencies) - 1);
        strncat(_dependencies, next_dependencies(),
            sizeof(_dependencies) - strlen(_dependencies) - 1);
    }
    return strdup(_dependencies);
}

当image调用Bus库中的dependencies函数时,它获得Bus依赖的所有库的名称,如清单5所示。

清单5应用程序image调用一个插入函数

1
2
3
4
5
6
7
#include <stdio.h>
extern char* dependencies();    // libBus.dylib
 
int main(void) {
    printf("[%s] libBus.dependencies() = %s\n",
        __FILE__, dependencies());
}

使用弱链接符号

为了促进与早期或后期版本的兼容性,动态库可以将其部分或全部公共符号导出为弱链接符号。弱链接符号(weakly linked symbol)是指当客户端与库链接时,编译器为其生成弱引用的符号。弱链接符号可能在库头文件的声明中有weak_import属性,或者库的开发人员可能会记录哪些库的公共符号是弱链接的。第三种识别弱链接符号的方法是执行命令:

1
nm -m <client_file> | grep weak

此命令列出从依赖库导入的弱链接符号。

弱链接符号可以由依赖库定义,也可以不定义。也就是说,尽管该符号是在头文件中声明的,但相应的动态库文件可能不包含该符号的实现。清单6显示了如何在动态库的头文件中声明弱链接符号。使用此头文件作为其对应依赖库接口的客户端保证定义了name和set_name。当库没有实现弱链接符号时,动态加载器将任何对该符号的客户端引用设置为0。

清单6带有弱链接符号声明的头文件

1
2
3
4
5
6
/* File: Person.h */
#define WEAK_IMPORT __attribute__((weak_import))
char* name(void);
void set_name(char* name);
WEAK_IMPORT
void clear_name(void);

库开发人员使用弱链接符号来最大化客户端与依赖库的早期或新版本的兼容性。例如,在库的特定修订中实现的符号可能在以后的修订中不可用。但是与第一个修订版相关联的客户也可以使用第二个修订版。然而,客户端开发人员在执行该符号之前必须确保该符号存在于正在运行的进程中。这种机制还用于为插件提供标准接口,插件可能实现也可能不实现整个接口。

清单7显示了确保在使用特定函数之前定义该函数的代码。当没有找到该函数时,客户端使用不同的函数来完成所需的任务。在这种情况下,回退函数不是弱链接符号,所以不需要测试。其他情况可能不提供替代接口。在这种情况下,客户端可能无法执行所需的任务。

清单7使用弱链接符号

1
2
3
4
5
6
7
/ Clear the 'name' property.
if (clear_name) {
    clear_name();
}
else {
    set_name(" ");
}

使用C++类

使用Objective-C类

要使用在动态库中实现的Objective-C类或类别,客户端应该有一个到类或类别的接口。通过了解类的正确接口,客户端可以创建具有适当类型的类的实例。否则,编译器会对缺少声明的方法产生警告。

Objective-C类和类别的接口以协议的形式发布在库的头文件中。实例化在依赖库中实现的类与对局部定义的类进行同样的操作没有什么不同。 然而,当你使用dlopen(3) OS X开发人员工具手册在运行时加载一个动态库时,你必须通过调用 objc_getClass函数来获得相应的类。例如,清单12包含Person类的接口和该类的标题类别,这是由Person动态库实现的。

清单12 Person类及其Titling类别的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* File: Person.h */
#import <Foundation/Foundation.h>
 
@protocol Person
- (void)setName:(NSString*)name;
- (NSString*)name;
@end
 
@interface Person : NSObject <Person> {
    @private
    NSString* _person_name;
}
@end
 
/* File: Titling.h */
#import <Foundation/Foundation.h>
#import "Person.h"
 
@protocol Titling
- (void)setTitle:(NSString*)title;
@end
 
@interface Person (Titling) <Titling>
@end

使用这些接口编译并链接到Person库的客户端可以创建以非常直接的方式实现接口的对象,如清单13所示。

清单13使用Person库作为依赖库的客户端示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* File: Client.m */
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Titling.h"
 
int main() {
    @autoreleasepool {
        // Create an instance of Person.
        Person<Titling>* person = [[Person alloc] init];
 
        // Use person.
        [person setName:@"Perrine LeVan"];
        [person setTitle:@"Ms."];
        NSLog(@"[%s] main: [person name] = %@", __FILE__, [person name]);
    }
    return(EXIT_SUCCESS);
}

然而,当Person库是一个运行时加载的库时,客户端必须在加载库后使用objc_getClass从Objective-C运行时中获取Person类的引用。然后,它可以使用该引用来实例化Person对象。然而,保存实例的变量必须通过实现Person和Titling协议的NSObject类型来避免编译器的警告。完成后,客户端关闭库,如使用弱链接符号所示。

清单14使用Person库作为运行时加载库的客户端示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* File: Client.m */
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <dlfcn.h>
#import "Person.h"
#import "Titling.h"
 
int main() {
    @autoreleasepool {
        // Open the library.
        void* lib_handle = dlopen("./libPerson.dylib", RTLD_LOCAL);
        if (!lib_handle) {
            NSLog(@"[%s] main: Unable to open library: %s\n",
            __FILE__, dlerror());
            exit(EXIT_FAILURE);
        }
 
        // Get the Person class (required with runtime-loaded libraries).
        Class Person_class = objc_getClass("Person");
        if (!Person_class) {
            NSLog(@"[%s] main: Unable to get Person class", __FILE__);
            exit(EXIT_FAILURE);
        }
 
        // Create an instance of Person.
        NSLog(@"[%s] main: Instantiating Person_class", __FILE__);
        NSObject<Person,Titling>* person = [[Person_class alloc] init];
 
        // Use person.
        [person setName:@"Perrine LeVan"];
        [person setTitle:@"Ms."];
        NSLog(@"[%s] main: [person name] = %@", __FILE__, [person name]);
 
        // Close the library.
        if (dlclose(lib_handle) != 0) {
            NSLog(@"[%s] Unable to close library: %s\n",
                __FILE__, dlerror());
            exit(EXIT_FAILURE);
        }
    }
    return(EXIT_SUCCESS);
}

获取关于特定地址上的符号的信息

动态加载器兼容性(DLC)函数之一,daddr (3) OS X开发人员工具手册页,提供image和最近的符号对应的地址的信息。您可以使用此函数来获取有关导出特定符号的库的信息。

dladdr提供的信息通过类型为Dl_info的输出参数返回。这些是结构字段的名称及其描述:

  • dli_fname: image的路径名
  • dli_fbase: 在进程中image的基础地址
  • dli_sname: 符号的名称,其地址等于或低于提供给dladdr的地址
  • dli_saddr: dli_sname指示的符号地址

清单15显示了image如何获取有关符号的信息

清单15获取关于符号的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <dlfcn.h>
 
extern char* dependencies();
 
int main(void) {
    // Get information on dependencies().
    Dl_info info;
    if (dladdr(dependencies, &info)) {
        printf("[%s] Info on dependencies():\n", __FILE__);
        printf("[%s]    Pathname: %s\n",         __FILE__, info.dli_fname);
        printf("[%s]    Base address: %p\n",     __FILE__, info.dli_fbase);
        printf("[%s]    Nearest symbol: %s\n",   __FILE__, info.dli_sname);
        printf("[%s]    Symbol address: %p\n",   __FILE__, info.dli_saddr);
    }
    else {
        printf("[%s] Unable to find image containing the address %x\n",
    __FILE__, &dependencies);
    }
}

创建动态库

当您创建或更新动态库时,您应该仔细考虑开发人员如何在其产品中使用它。同样重要的是,要给这些开发人员灵活性,允许他们的产品使用库的早期或更高版本,而不必更新他们的产品。

本文演示如何编写一个动态库,以便希望在自己的开发中利用它的开发人员易于使用。本文还描述了如何更新现有库并管理它们的版本信息,以最大化客户端兼容性。

创建库

在创建动态库时,您应该执行这些任务:

  • 定义库的用途:该信息提供了定义库公共接口所需的重点。
  • 定义库的接口(头文件):库的客户端通过这个接口访问其功能。
  • 实现库(实现文件):在这里定义库客户端使用的公共函数。您可能还需要定义实现接口所需但客户端不需要的私有变量和函数。
  • 设置库的版本信息:库的版本信息分为major、minor和compatibility三个部分。在创建.dylib文件时指定库版本信息的所有部分。有关详细信息,请参见管理客户端与依赖库的兼容性。
  • 测试库:至少,您应该为库公开的每个公共函数定义一个测试,以确保它们在给定特定测试输入或执行特定操作集之后执行正确的操作。

以下各节提供了开发简单的动态库(称为“Ratings”)的过程示例。 以下各节中提到的文件包含在此文档的伴随文件包的Ratings/1.0中。

定义库的目的

Ratings库的目的是为其客户端提供一个评级分析器。评分是由星号(*)组成的字符串,表示对特定项目的满意程度。例如,在苹果iTunes应用程序中,你可以用五星评价(*****)来指定你是否非常喜欢某首歌,或者用一星评价(*)来指定你是否完全不喜欢这首歌。

库的初始版本为客户提供了一种添加评级、获取已添加评级的计数、获取平均评级和清除评级集的方法。

定义库的接口

在实现库之前,必须定义库的客户端用于与库交互的接口。您应该仔细指定每个函数的语义。清晰的定义对您有好处,因为它明确了每个公共函数的目的。它也有利于使用您的库的开发人员,因为它准确地告诉他们在代码中调用每个函数时应该期待什么。

这是一个带有每个函数语义的接口函数列表的示例:

  • void addRating(char *rating): 向库保存的集合添加一个评级值。rating参数是一个字符串;它不能为空。字符串中的每个字符代表一个评分点。例如,一个空字符串(““)等同于评级为0,”*“表示1,”**“表示2,以此类推。字符串中实际使用的字符并不重要。因此,“”代表1,“3456”代表4。每次调用这个函数都会增加ratings函数返回的值。
  • int ratings(void): 返回集合中评分值的数目。此功能无副作用。
  • char *meanRatings(void): 以字符串的形式返回评级集中的平均评级,每个评级点使用一个星号。此功能无副作用
  • clearRatings(void): 清除评级集。在调用这个函数之后(没有后续调用addRating), ratings函数返回0。

清单1显示了评级库客户端用于访问其接口的头文件。

清单1评级1.0的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* File: Ratings.h
 * Interface to libRatings.A.dylib 1.0.
 *************************************/
 
/* Adds 'rating' to the set.
 *      rating: Each character adds 1 to the numeric rating
 *              Example: "" = 0, "*" = 1, "**" = 2, "wer " = 4.
 */
void addRating(char *rating);
 
/* Returns the number of ratings in the set.
 */
int ratings(void);
 
/* Returns the mean rating of the set.
 */
char *meanRating(void);
 
/* Clears the set.
 */
void clearRatings(void);

实现库

定义库接口时声明的接口在Ratings.c中实现,如清单2所示。

清单2 Ratings 1.0的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/* File: Ratings.c
 * Compile with -fvisibility=hidden.                        // 1
 **********************************/
 
#include "Ratings.h"
#include <stdio.h>
#include <string.h>
 
#define EXPORT __attribute__((visibility("default")))
#define MAX_NUMBERS 99
 
static int _number_list[MAX_NUMBERS];
static int _numbers = 0;
 
// Initializer.
__attribute__((constructor))
static void initializer(void) {                             // 2
    printf("[%s] initializer()\n", __FILE__);
}
 
// Finalizer.
__attribute__((destructor))
static void finalizer(void) {                               // 3
    printf("[%s] finalizer()\n", __FILE__);
}
 
// Used by meanRating, middleRating, frequentRating.
static char *_char_rating(int rating) {
    char result[10] = "";
    int int_rating = rating;
    for (int i = 0; i < int_rating; i++) {
        strncat(result, "*", sizeof(result) - strlen(result) - 1);
    }
    return strdup(result);
}
 
// Used by addRating.
void _add(int number) {                                     // 4
    if (_numbers < MAX_NUMBERS) {
        _number_list[_numbers++] = number;
    }
}
 
// Used by meanRating.
int _mean(void) {
    int result = 0;
    if (_numbers) {
        int sum = 0;
        int i;
        for (i = 0; i < _numbers; i++) {
            sum += _number_list[i];
        }
        result = sum / _numbers;
    }
    return result;
}
 
EXPORT
void addRating(char *rating) {                            // 5
    if (rating != NULL) {
        int numeric_rating = 0;
        int pos = 0;
        while (*rating++ != '\0' && pos++ < 5) {
            numeric_rating++;
        }
        _add(numeric_rating);
    }
}
 
EXPORT
char *meanRating(void) {
    return _char_rating(_mean());
}
 
EXPORT
int ratings(void) {
    return _numbers;
}
 
EXPORT
void clearRatings(void) {
    _numbers = 0;
}

下面的列表描述了带标记的行。

  • 这个注释只是提醒库的开发人员使用编译器-fvisibility=hidden选项来编译这个文件,这样只有带有visibility("default")属性的符号才会被导出。
  • 定义这个初始化器只是为了显示动态加载器在客户端执行时调用库初始化器
  • 定义此终结器只是为了显示动态加载器在客户端执行时调用库终结器。
  • _add函数是内部函数的一个例子。客户端不需要知道它,因此,它不被导出。另外,因为内部调用是可信的,所以不执行验证。但是,并不要求内部使用的函数缺乏验证。
  • addRating函数是导出函数的一个例子。为了确保只添加正确的评级,要验证函数的输入参数。

设置库的版本信息

当您将库的源文件编译为.dylib文件时,您可以设置版本信息,该信息指定客户端是否可以使用它们所链接的版本之前的库版本或之后的库版本。当客户端加载到进程中时,动态加载器会在库搜索路径中查找.dylib文件,如果找到了,就会将.dylib文件的版本信息与客户端image中记录的版本信息进行比较。如果客户端与.dylib文件不兼容,动态加载器不会将客户端加载到进程中。实际上,客户端加载进程被中止,因为动态加载器无法找到兼容的依赖库

清单3显示了用于生成Ratings库1.0版本的命令。

清单3生成Ratings动态库的1.0版本

1
2
[Ratings/1.0]% make dylib
clang -dynamiclib -std=gnu99 Ratings.c -current_version 1.0 -compatibility_version 1.0 -fvisibility=hidden -o libRatings.A.dylib

此列表指明指定主版本、次要版本和兼容性版本的位置:

  • 主版本号在库的文件名中指定为A(-o libRatings.A.dylib)。
  • 次要版本号在-current_version 1.0中指定。
  • 兼容性版本号在-compatibility_version 1.0中指定。

注意:您可以使用libtool从一组目标文件创建一个动态库。

测试库

在发布动态库之前,应该测试它的公共接口,以确保它按照接口文档中指定的方式执行(请参阅定义库接口)。为客户提供最大的灵活性,你应该确保你的库可以用作依赖库(客户端链接了它, 并且库加载客户端加载时)或r运行时加载库(客户端不链接,使用dlopen (3) OS X开发工具手动页面加载它)。

清单4显示了一个使用Rating库作为依赖库的测试客户端示例。

清单4测试Rating 1.0作为一个依赖库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* Dependent.c
 * Tests libRatings.A.dylib 1.0 as a dependent library.
 *****************************************************/
 
#include <stdio.h>
#include <string.h>
#include "Ratings.h"
 
#define PASSFAIL "Passed":"Failed"
#define UNTST "Untested"
 
int main(int argc, char **argv) {
    printf("[start_test]\n");
 
    // Setup.
    addRating(NULL);
    addRating("");
    addRating("*");
    addRating("**");
    addRating("***");
    addRating("*****");
    addRating("*****");
 
    // ratings.
    printf("[%s] ratings(): %s\n",
        __FILE__, (ratings() == 6? PASSFAIL));
 
    // meanRating.
    printf("[%s] meanRating(): %s\n",
        __FILE__, (strcmp(meanRating(), "**") == 0)? PASSFAIL);
 
    // clearRatings.
    clearRatings();
    printf("[%s] clearRatings(): %s\n",
        __FILE__, (ratings() == 0? PASSFAIL));
 
    printf("[end_test]\n");
    return 0;
}

以下命令生成依赖的客户端程序。注意,libRatings.A.dylib包含在编译行中。

1
clang Dependent.c libRatings.A.dylib -o Dependent

Dependent程序产生输出,显示调用每个库导出函数是否产生预期的结果。此外,库中定义的初始化器和终结器函数产生输出行,指示它们在与程序的正常进程相关的情况下何时被调用。该输出如清单5所示。

清单5作为依赖库的Rating1.0的测试结果

1
2
3
4
5
6
7
8
> ./Dependent
[Ratings.c] initializer()
[start_test]
[Dependent.c] ratings(): Passed
[Dependent.c] meanRating(): Passed
[Dependent.c] clearRatings(): Passed
[end_test]
[Ratings.c] finalizer()

注意,initializer在main函数之前被调用。另一方面,终结器函数在退出main函数后被调用。

将Rating1.0库测试为运行时加载的库需要另一个测试程序,即Runtime。清单6显示了它的源文件。

清单6测试Rating1.0 1.0作为运行时加载的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/* Runtime.c
 * Tests libRatings.A.dylib 1.0 as a runtime-loaded library.
 ***********************************************************/
 
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>
#include "Ratings.h"
 
#define PASSFAIL "Passed":"Failed"
#define UNTST "Untested"
 
int main(int argc, char **argv) {
    printf("[start_test]\n");
 
    // Open the library.
    char *lib_name = "./libRatings.A.dylib";
    void *lib_handle = dlopen(lib_name, RTLD_NOW);
    if (lib_handle) {
        printf("[%s] dlopen(\"%s\", RTLD_NOW): Successful\n", __FILE__, lib_name);
    }
    else {
        printf("[%s] Unable to open library: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
 
    // Get the symbol addresses.
    void (*addRating)(char*) = dlsym(lib_handle, "addRating");
    if (addRating) {
        printf("[%s] dlsym(lib_handle, \"addRating\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    char *(*meanRating)(void) = dlsym(lib_handle, "meanRating");
    if (meanRating) {
        printf("[%s] dlsym(lib_handle, \"meanRating\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    void (*clearRatings)(void) = dlsym(lib_handle, "clearRatings");
    if (clearRatings) {
        printf("[%s] dlsym(lib_handle, \"clearRatings\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    int (*ratings)(void) = dlsym(lib_handle, "ratings");
    if (ratings) {
        printf("[%s] dlsym(lib_handle, \"ratings\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
 
    // Setup.
    addRating(NULL);
    addRating("");
    addRating("*");
    addRating("**");
    addRating("***");
    addRating("*****");
    addRating("*****");
 
    // ratings.
    printf("[%s] ratings(): %s\n", __FILE__, (ratings() == 6? PASSFAIL));
 
    // meanRating.
    printf("[%s] meanRating(): %s\n", __FILE__, (strcmp(meanRating(), "**") == 0)? PASSFAIL);
 
    // clearRatings.
    clearRatings();
    printf("[%s] clearRatings(): %s\n", __FILE__, (ratings() == 0? PASSFAIL));
 
    // Close the library.
    if (dlclose(lib_handle) == 0) {
        printf("[%s] dlclose(lib_handle): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to open close: %s\n",
            __FILE__, dlerror());
    }
 
    printf("[end_test]\n");
    return 0;
}

Runtime程序与依赖程序非常相似。但运行时必须通过dlopen(3) OS X Developer Tools Manual页面加载llibRuntime.A.dylib。之后,在使用它之前,它必须使用dlsym(3) OS X开发人员工具手册页面获得库导出的每个函数的地址。

以下命令生成运行时客户端程序。

1
2
[Ratings/1.0]% make runtime
clang Runtime.c -o Runtime

清单7显示了Runtime生成的输出。 清单7作为运行时加载库的Rating1.0的测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> ./Runtime
[start_test]
[Ratings.c] initializer()
[Runtime.c] dlopen("./libRatings.A.dylib", RTLD_NOW): Successful
[Runtime.c] dlsym(lib_handle, "addRating"): Successful
[Runtime.c] dlsym(lib_handle, "meanRating"): Successful
[Runtime.c] dlsym(lib_handle, "clearRatings"): Successful
[Runtime.c] dlsym(lib_handle, "ratings"): Successful
[Runtime.c] ratings(): Passed
[Runtime.c] meanRating(): Passed
[Runtime.c] clearRatings(): Passed
[Runtime.c] dlclose(lib_handle): Successful
[end_test]
[Ratings.c] finalizer()

注意,在对dlopen的调用返回之前,评级库的初始化器函数在main函数执行中被调用,这与清单5中的依赖程序中的执行点不同。然而,终结器函数是在main退出后调用的,在Dependent的执行中也是在同一点调用的。在编写动态库初始化器和终结器时,您应该考虑这一点。

更新库

对先前发布的动态库进行修订是一项微妙的任务。如果您希望现有的客户端能够使用这个库(也就是说,在不重新编译的情况下加载.dylib文件的新版本),您必须确保那些客户端知道的API是不变的,包括它的语义含义。

当您可以保证库的新版本的API与与早期版本链接的客户端所知道的API兼容时,您可以将新版本视为一个小修订。当您想要将动态库的一个小修订发布给库客户端的用户(例如,使用您的库的程序的用户)时,惟一需要更改的版本信息项是库的当前版本。主版本(文件名)和兼容版本的库必须保持相同。当最终用户在其计算机上用新版本替换库的早期版本时,库的客户端使用新版本时没有任何问题。

####

从客户端image的角度来看,与其使用的动态库的兼容性有两个方面:与库版本的兼容性晚于客户端所知道的版本,即前向兼容性;与库版本的兼容性,早于客户端熟悉的版本,即向后兼容性。通过确保库的API在各个版本之间保持兼容,可以保持向前兼容性。通过将新函数导出为弱引用,可以促进向后兼容性。反过来,库的客户端必须确保弱导入函数在使用它们之前确实存在

在创建库中引入的Ratings库有第二个版本1.1。清单8显示了Ratings库的更新头。

清单8与Ratings 1.1的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* File: Ratings.h
 * Interface to libRatings.A.dylib 1.1.
 *************************************/
 
#define WEAK_IMPORT __attribute__((weak_import))
 
/* Adds 'rating' to the set.
 *      rating: Each character adds 1 to the numeric rating
 *      Example: "" = 0, "*" = 1, "**" = 2, "wer " = 4.
 */
void addRating(char* rating);
 
/* Returns the number of ratings in the set.
 */
int ratings(void);
 
/* Returns the mean rating of the set.
 */
char* meanRating(void);
 
/* Returns the medianRating of the set.
 */
WEAK_IMPORT
char *medianRating(void);                       // 1
 
/* Returns the most frequent rating of the set.
 */
WEAK_IMPORT
char *frequentRating(void);                     // 2
 
/* Clears the set.
 */
void clearRatings(void);

标记行声明了新函数。请注意,这两个声明都包含了weak_import属性,通知客户端开发人员函数是弱链接的。因此,客户端在调用函数之前必须确保函数存在。

清单9显示了库的新实现文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/* File: Ratings.c
 * Compile with -fvisibility=hidden.
 **********************************/
 
#include "Ratings.h"
#include <Averages.h>
#include <stdio.h>
#include <string.h>
#include <float.h>
 
#define EXPORT __attribute__((visibility("default")))
#define MAX_NUMBERS 99
//#define MAX_NUMERIC_RATING 10               // published in Ratings.h
 
static char *_char_rating(float rating) {
    char result[10] = "";
    int int_rating = (int)(rating + 0.5);
    for (int i = 0; i < int_rating; i++) {
        strncat(result, "*", sizeof(result) - strlen(result) - 1);
    }
    return strdup(result);
}
 
EXPORT
void addRating(char *rating) {
    if (rating != NULL) {
        int numeric_rating = 0;
        int pos = 0;
        while (*rating++ != '\0' && pos++ < 5) {
            numeric_rating++;
        }
        add((float)numeric_rating);     // libAverages.A:add()
    }
}
 
EXPORT
char *meanRating(void) {
    return _char_rating(mean());        // libAverages.A:mean()
}
 
EXPORT
char *medianRating(void) {
    return _char_rating(median());      // libAverages.A:median()
}
 
EXPORT
char *frequentRating(void) {
    int lib_mode = mode();                // libAverages.A:mode()
    return _char_rating(lib_mode);
}
 
EXPORT
int ratings(void) {
    return count();                     // libAverages.A:count()
}
 
EXPORT
void clearRatings(void) {
    clear();                            // libAverages.A:clear()
}
 
/* Ratings.c revision history
 * 1. First version.
 * 2. Added medianRating, frequentRating.
 *    Removed initializer, finalizer.
 */
 

清单10显示了测试Ratings 1.1库的运行时程序的更新源代码。

清单10测试Ratings 1.1作为运行时加载的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/* Runtime.c
 * Tests libRatings.A.dylib 1.1 as a runtime-loaded library.
 **********************************************************/
 
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>
#include "Ratings.h"
 
#define PASSFAIL "Passed":"Failed"
#define UNTST "Untested"
 
int main(int argc, char** argv) {
    printf("[start_test]\n");
 
    // Open the library.
    char *lib_name = "./libRatings.A.dylib";
    void *lib_handle = dlopen(lib_name, RTLD_NOW);
    if (lib_handle) {
        printf("[%s] dlopen(\"%s\", RTLD_NOW): Successful\n", __FILE__, lib_name);
    }
    else {
        printf("[%s] Unable to open library: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
 
    // Get the function addresses.
    void (*addRating)(char*) = dlsym(lib_handle, "addRating");
    if (addRating) {
        printf("[%s] dlsym(lib_handle, \"addRating\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    char* (*meanRating)(void) = dlsym(lib_handle, "meanRating");
    if (meanRating) {
        printf("[%s] dlsym(lib_handle, \"meanRating\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    void (*clearRatings)(void) = dlsym(lib_handle, "clearRatings");
    if (clearRatings) {
        printf("[%s] dlsym(lib_handle, \"clearRatings\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    int (*ratings)(void) = dlsym(lib_handle, "ratings");
    if (ratings) {
        printf("[%s] dlsym(lib_handle, \"ratings\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    char *(*medianRating)(void) = dlsym(lib_handle, "medianRating");        // weak import
    char *(*frequentRating)(void) = dlsym(lib_handle, "frequentRating");    // weak import
 
    // Setup.
    addRating(NULL);
    addRating("");
    addRating("*");
    addRating("**");
    addRating("***");
    addRating("*****");
    addRating("*****");
 
    // ratings.
    printf("[%s] ratings(): %s\n", __FILE__, (ratings() == 6? PASSFAIL));
 
    // meanRating.
    printf("[%s] meanRating(): %s\n", __FILE__, (strcmp(meanRating(), "**") == 0)? PASSFAIL);
 
    // medianRating.
    if (medianRating) {
        printf("[%s] medianRating(): %s\n", __FILE__, (strcmp(medianRating(), "**") == 0? PASSFAIL));
    }
    else {
        printf("[%s] medianRating(): %s\n", __FILE__, UNTST);
    }
 
    // frequentRating.
 
    if (frequentRating) {
        char* test_rating = "*****";
        int test_rating_size = sizeof(test_rating);
        printf("[%s] frequentRating(): %s\n", __FILE__, strncmp(test_rating, frequentRating(), test_rating_size) == 0? PASSFAIL);
    }
    else {
        printf("[%s] mostFrequentRating(): %s\n", __FILE__, UNTST);
    }
 
    // clearRatings.
    clearRatings();
    printf("[%s] clearRatings(): %s\n", __FILE__, (ratings() == 0? PASSFAIL));
 
    // Close the library.
    if (dlclose(lib_handle) == 0) {
        printf("[%s] dlclose(lib_handle): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to open close: %s\n",
            __FILE__, dlerror());
    }
 
    printf("[end_test]\n");
    return 0;
}

更新库的版本信息

清单11显示了用于生成Rating库1.1版本的命令。 清单11生成Ratings动态库的1.1版本

1
2
[Ratings/1.1]% make dylib
clang -dynamiclib -std=gnu99 Ratings.c -I<user_home>/include <user_home>/lib/libAverages.dylib -current_version 1.1 -compatibility_version 1.0 -fvisibility=hidden -o libRatings.A.dylib

注意,这次库的当前版本被设置为1.1,但是它的兼容性版本仍然是1.0。当一个客户端被链接到这个库的1.1版本时,兼容性版本将被编码到客户端image中。因此,动态加载器将加载客户端image,无论它找到的是libRatings.A.dylib的1.1还是1.0版本。也就是说,客户端向后兼容该库的1.0版。 有关为什么将/usr/local/lib/libAverages.dylib添加到编译行的详细信息,请参见使用依赖库。

注意:弱引用是OS X v10.2及更高版本的一个特性。因此,当使用弱引用时,您的库只能被运行在OS X v10.2或更高版本的程序使用。

测试库的新版本

与创建库的初始版本类似,更新库需要彻底的测试。您至少应该为添加到测试程序中的每个函数添加测试。如果使用弱引用,测试程序应该在调用新函数之前确保它们存在。

本文档随附文件包中的Ratings /1.1目录包含Ratings 1.1库的源文件。 清单12显示了Dependent程序的更新版本。 标记行显示了如何在使用功能之前检查其功能。

清单12测试Rating1.1作为依赖库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/* Dependent.c
 * Tests libRatings.A.dylib 1.1 as a dependent library.
 *****************************************************/
 
#include <stdio.h>
#include <string.h>
#include "Ratings.h"
 
#define PASSFAIL "Passed":"Failed"
#define UNTST "Untested"
 
int main(int argc, char **argv) {
    printf("[start_test]\n");
 
    // Setup.
    addRating(NULL);
    addRating("");
    addRating("*");
    addRating("**");
    addRating("***");
    addRating("*****");
    addRating("*****");
 
    // ratings.
    printf("[%s] ratings(): %s\n",
        __FILE__, (ratings() == 6? PASSFAIL));
 
    // meanRating.
    printf("[%s] meanRating(): %s\n",
        __FILE__, (strcmp(meanRating(), "**") == 0)? PASSFAIL);
 
    // medianRating.
    if (medianRating) {                         // 1
        printf("[%s] medianRating(): %s\n",
            __FILE__, (strcmp(medianRating(), "**") == 0? PASSFAIL));
    }
    else {
        printf("[%s] medianRating(): %s\n", __FILE__, UNTST);
    }
 
    // frequentRating.
    if (frequentRating) {                         // 2
 
        char *test_rating = "*****";
        int test_rating_size = sizeof(test_rating);
        printf("[%s] frequentRating(): %s\n",
            __FILE__, strncmp(test_rating, frequentRating(),
            test_rating_size) == 0? PASSFAIL);
    }
    else {
        printf("[%s] mostFrequentRating(): %s\n",
            __FILE__, UNTST);
    }
 
    // clearRatings.
    clearRatings();
    printf("[%s] clearRatings(): %s\n",
        __FILE__, (ratings() == 0? PASSFAIL));
 
    printf("[end_test]\n");
    return 0;
}

Rating1.1取决于Averages 1.1。 因此,要构建库和测试程序,必须在~/lib.中安装libAverages.A.dylib。 为此,请在打开此文档的伴随文件包后运行以下命令:

1
[Avarages/1.1]% make install

这些是从Ratings / 1.1目录编译库和测试程序所需的命令:

1
2
3
4
[Ratings/1.1]% make
clang -dynamiclib -std=gnu99 Ratings.c -I<user_home>/include <user_home>/lib/libAverages.dylib -current_version 1.1 -compatibility_version 1.0 -fvisibility=hidden -o libRatings.A.dylib
clang Dependent.c libRatings.A.dylib <user_home>/lib/libAverages.dylib -o Dependent
clang Runtime.c -o Runtime

该程序产生的输出如清单13所示。

清单13作为依赖库使用的Ratings 1.1的测试结果

1
2
3
4
5
6
7
8
> ./Dependent
[start_test]
[Dependent.c] ratings(): Passed
[Dependent.c] meanRating(): Passed
[Dependent.c] medianRating(): Passed
[Dependent.c] frequentRating(): Passed
[Dependent.c] clearRatings(): Passed
[end_test]

如果使用dlopen加载库的程序无法通过dlsym获取弱导出函数的地址,那么它们不应该失败。因此,将库测试为运行时加载库的程序应该允许不存在弱导入函数。

清单14显示了运行时程序的更新版本。注意,它使用dlsym来尝试获取新函数的地址,但如果它们不可用,则不会带着错误退出。但是,就在程序使用新函数之前,它会确定它们是否实际存在。如果它们不存在,它就不会执行测试。

清单14测试Ratings 1.1作为运行时加载的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/* Runtime.c
 * Tests libRatings.A.dylib 1.1 as a runtime-loaded library.
 **********************************************************/
 
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>
#include "Ratings.h"
 
#define PASSFAIL "Passed":"Failed"
#define UNTST "Untested"
 
int main(int argc, char **argv) {
    printf("[start_test]\n");
 
    // Open the library.
    char* lib_name = "./libRatings.A.dylib";
    void* lib_handle = dlopen(lib_name, RTLD_NOW);
    if (lib_handle) {
        printf("[%s] dlopen(\"%s\", RTLD_NOW): Successful\n", __FILE__, lib_name);
    }
    else {
        printf("[%s] Unable to open library: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
 
    // Get the function addresses.
    void (*addRating)(char*) = dlsym(lib_handle, "addRating");
    if (addRating) {
        printf("[%s] dlsym(lib_handle, \"addRating\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    char* (*meanRating)(void) = dlsym(lib_handle, "meanRating");
    if (meanRating) {
        printf("[%s] dlsym(lib_handle, \"meanRating\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    void (*clearRatings)(void) = dlsym(lib_handle, "clearRatings");
    if (clearRatings) {
        printf("[%s] dlsym(lib_handle, \"clearRatings\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    int (*ratings)(void) = dlsym(lib_handle, "ratings");
    if (ratings) {
        printf("[%s] dlsym(lib_handle, \"ratings\"): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to get symbol: %s\n",
            __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    char* (*medianRating)(void) = dlsym(lib_handle, "medianRating");        // weak import
    char* (*frequentRating)(void) = dlsym(lib_handle, "frequentRating");    // weak import
 
    // Setup.
    addRating(NULL);
    addRating("");
    addRating("*");
    addRating("**");
    addRating("***");
    addRating("*****");
    addRating("*****");
 
    // ratings.
    printf("[%s] ratings(): %s\n", __FILE__, (ratings() == 6? PASSFAIL));
 
    // meanRating.
    printf("[%s] meanRating(): %s\n", __FILE__, (strcmp(meanRating(), "**") == 0)? PASSFAIL);
 
    // medianRating.
    if (medianRating) {
        printf("[%s] medianRating(): %s\n", __FILE__, (strcmp(medianRating(), "**") == 0? PASSFAIL));
    }
    else {
        printf("[%s] medianRating(): %s\n", __FILE__, UNTST);
    }
 
    // frequentRating.
    if (frequentRating) {
        char* mfr = "*****";
        printf("[%s] frequentRating(): %s\n", __FILE__, strncmp(mfr, frequentRating(), sizeof(mfr)) == 0? PASSFAIL);
    }
    else {
        printf("[%s] mostFrequentRating(): %s\n", __FILE__, UNTST);
    }
 
    // clearRatings.
    clearRatings();
    printf("[%s] clearRatings(): %s\n", __FILE__, (ratings() == 0? PASSFAIL));
 
    // Close the library.
    if (dlclose(lib_handle) == 0) {
        printf("[%s] dlclose(lib_handle): Successful\n", __FILE__);
    }
    else {
        printf("[%s] Unable to open close: %s\n",
            __FILE__, dlerror());
    }
 
    printf("[end_test]\n");
    return 0;
}

清单15显示了运行时生成的输出。

清单15用于作为运行时加载库的Ratings 1.1的测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Ratings/1.1]% ./Runtime
[start_test]
[Runtime.c] dlopen("./libRatings.A.dylib", RTLD_NOW): Successful
[Runtime.c] dlsym(lib_handle, "addRating"): Successful
[Runtime.c] dlsym(lib_handle, "meanRating"): Successful
[Runtime.c] dlsym(lib_handle, "clearRatings"): Successful
[Runtime.c] dlsym(lib_handle, "ratings"): Successful
[Runtime.c] ratings(): Passed
[Runtime.c] meanRating(): Passed
[Runtime.c] medianRating(): Passed
[Runtime.c] frequentRating(): Passed
[Runtime.c] clearRatings(): Passed
[Runtime.c] dlclose(lib_handle): Successful
[end_test]

为确保与libRatings.A.dylib的1.0版链接的客户端可以使用该库的1.1版,可以将Ratings/1.1/libRatings.A.dylib 复制到Ratings/1.0。当您运行第一个Dependent程序时,它的输出与使用该库1.0版本时产生的输出相同。第一个Dependent程序对Ratings库1.1版中引入的函数一无所知;因此,它不会调用它们。

然而,一个更有趣的测试是,确保当Rating库的副本被1.0版本替换时,链接到1.1版本的客户端能够运行。要测试这一点,请在Terminal中执行以下命令:

1
2
3
4
5
6
7
[           ]% cd <companion_dir>/Ratings/1.1
[Ratings/1.1]% make
[Ratings/1.1]% cd ../1.0
[Ratings/1.0]% make
[Ratings/1.0]% cp libRatings.A.dylib ../1.1
[Ratings/1.0]% cd ../1.1

清单16显示了Dependent的第二个版本(链接到Ratings 1.1)加载Ratings 1.0而不是Ratings 1.1时产生的输出。突出显示的行显示了客户端程序在运行时没有找到特定函数的地方,因为该函数在其使用的评Rating库版本中不存在。

清单17使用评级1.0时,评级1.1的客户端使用评级1.0作为运行时加载库的评级1.0的测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Ratings/1.1]% ./Runtime
[start_test]
[Ratings.c] initializer()
[Runtime.c] dlopen("./libRatings.A.dylib", RTLD_NOW): Successful
[Runtime.c] dlsym(lib_handle, "addRating"): Successful
[Runtime.c] dlsym(lib_handle, "meanRating"): Successful
[Runtime.c] dlsym(lib_handle, "clearRatings"): Successful
[Runtime.c] dlsym(lib_handle, "ratings"): Successful
[Runtime.c] ratings(): Passed
[Runtime.c] meanRating(): Passed
[Runtime.c] medianRating(): Untested
[Runtime.c] mostFrequentRating(): Untested
[Runtime.c] clearRatings(): Passed
[Runtime.c] dlclose(lib_handle): Successful
[end_test]
[Ratings.c] finalizer()

使用动态库

当您需要在您的产品中使用动态库时,您必须在计算机中安装该库。你可以使用动态库作为依赖库(通过在你的产品链接中指定它们)或运行时加载库(通过使用dlopen(3) OS X开发人员工具手册页加载它们)。

本文描述了安装和使用动态库的过程。它基于Ratings动态库和StarMeals, StarMeals2和Grades程序,这些程序都包含在本文档的companion-file包中。本文还展示了如何将动态库作为依赖库或运行时加载库使用。最后,本文演示了如何插入动态库导出的函数。

安装依赖库

在将动态库用作依赖库之前,必须在计算机上安装该库及其头文件。 头文件的标准位置是:

  • ~/include
  • /usr/local/include
  • /usr/include

您还可以将.dylib文件放置在文件系统中的非标准位置,但必须将该位置添加到这些环境变量之一:

  • LD_LIBRARY_PATH
  • DYLD_LIBRARY_PATH
  • DYLD_FALLBACK_LIBRARY_PATH

有关如何向这些环境变量添加路径的详细信息,请参见打开动态库。若要了解如何在可重定位目录中安装依赖库,请参见运行路径依赖库。

如果不希望更改环境变量,并且希望将动态库放置在非标准位置,则必须指定链接image时将库放置在文件系统中的位置。有关详细信息,请参见http://gcc.gnu.org/onlinedocs/gcc/Darwin-Options.html#Darwin-Options中有关编译器-dylib_file选项的说明。

例如,在OS X中,应用程序的可执行代码可以与包含为特定应用程序创建的库的framework打包在一起。这些框架被称为私有嵌入框架(private embedded frameworks)。使用私有嵌入框架以及框架本身的应用程序必须专门构建。 See “Creating a Framework” in Framework Programming Guide and “Loading Code at Runtime” in Mach-O Programming Topics for details.

使用依赖库需要在您的计算机上安装average 1.1和Ratings 1.1动态库。要安装这些库:

  • 打开这个文档的companion-file包
  • 在终端,执行这些命令:

    1
    2
    
      [Averages/1.1]% make install
      [Ratings/1.1]% make install
    

    注意:要卸载库,请执行以下命令:

    1
    2
    
      [Averages/1.1]% make uninstall
      [Ratings/1.1]% make uninstall
    

使用依赖库

通过将image与动态库链接,将它们作为依赖库使用,有几个好处,包括生成更小的可执行文件,以及在代码中使用库导出符号之前不必获取它们的地址。但是,在使用弱导入符号之前,您仍然必须确保它存在。

要使用动态库作为依赖库,您所需要做的就是在源代码中include库的头文件,并将库与您的程序或库链接起来。库头文件描述了你可以使用的符号。您不应该使用任何其他符号来访问库的功能。否则,您可能会得到意想不到的结果,或者当用户更新其计算机中的依赖库时,您的image可能会停止工作。

清单1显示了一个使用Ratings 1.1的小程序的源代码,该程序是在创建动态库时开发的。

清单1使用Ratings 1.1作为依赖库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/* File: StarMeals.c
 * Uses functions in libRatings.A.dylib.
 **************************************/
 
#include <stdio.h>
#include <string.h>
#include <Ratings.h>
 
#define MAX_NAMES 100
#define MAX_NAME_LENGTH 30
#define MAX_RATING_LENGTH 5
 
static char* name_list[MAX_NAMES];
static char* rating_list[MAX_NAMES];
static int names = 0;
 
void addNameAndRating(char* name, char* rating) {
    name_list[names] = strdup(name);
    rating_list[names] = (strlen(rating) > MAX_RATING_LENGTH)? "*****" : strdup(rating);
    names++;
}
 
void test_data(void) {
    addNameAndRating("Spinach", "*");
    addNameAndRating("Cake", "****");
    addNameAndRating("Steak", "****");
    addNameAndRating("Caviar", "*");
    addNameAndRating("Broccoli", "****");
    addNameAndRating("Gagh", "*****");
    addNameAndRating("Chicken", "*****");
}
 
int main(int argc, char* argv[]) {
    int test_mode = 0;
    if (argc == 2) {
        if (strcmp(argv[1], "test") == 0) {
            test_mode = 1;
            printf("[start_test]\n");
            test_data();
        }
    }
    else {
        printf("Enter meal names and ratings in the form <name> <rating>.\n");
        printf("No spaces are allowed in the name and the rating.\n");
        printf("The rating can be * through *****.\n");
        printf("To finish, enter \"end\" for a meal name.\n");
        while (names < MAX_NAMES) {
            char name[MAX_NAME_LENGTH];
            char rating[MAX_RATING_LENGTH + 1];
            printf("\nName and rating: ");
            scanf("%s", &name);
            if (strcmp(name, "end") == 0) {
                break;
            }
            scanf("%s", rating);
            addNameAndRating(name, rating);
        }
        printf("\n");
    }
 
    if (names) {
        // Print data entered and call libRatings.addRating().
        printf("This is the data you entered:\n");
        for (int i = 0; i < names; i++) {
            printf("%s (%s)\n", name_list[i], rating_list[i]);
            addRating(rating_list[i]);
        }
 
        // Print statistical information.
        printf("\nThe mean rating is %s\n", meanRating()); // 1
        if (medianRating) {                                // 2
            printf("The median rating is %s\n", medianRating());
        }
        if (frequentRating) {                              // 3
            printf("The most frequent rating is %s\n", frequentRating());
        }
 
        //printf("\n");
    }
 
    if (test_mode) {
        printf("[end_test]\n");
    }
    return 0;
}

这个列表描述了突出显示的行:

  • 第1行 :meanRating函数保证存在于libRating.A.dylib的所有版本中。因此,不需要存在性测试。
  • 第2行和第3行: 在Rating1.1中可以使用medianRating和frequentRating函数,但在Rating1.1.0中不能使用。因为StarMeals要向后兼容Ratings 1.0,所以在使用它们之前必须检查这些函数是否存在。否则,StarMeals可能会崩溃。

要编译StarMeals.c文件,请使用清单2中所示的命令。

清单2编译和链接StarMeals

1
2
[StarMeals]% make
clang -std=gnu99 StarMeals.c <user_home>/lib/libRatings.dylib <user_home>/lib/libAverages.dylib -o StarMeals

请注意,链接行提供了StarMeals库直接依赖(libRatings.dylib)的确切位置。路径名/lib/libRatings.dylib实际上是指向/lib/libRatings.A.dylib的符号链接。 链接时,静态链接器解析链接,并将库的实际文件名存储在其生成的image中。使用这种方法,动态链接器在查找与image相关的库时总是使用库的完整名称。

清单3显示了StarMeals在测试模式下运行时产生的输出

清单3 StarMeals程序的测试输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> ./StarMeals test
start_test
This is the data you entered:
Spinach (*)
Cake (****)
Steak (****)
Caviar (*)
Broccoli (****)
Gagh (*****)
Chicken (*****)
 
The mean rating is ***
The medianRating is ****
The most frequent rating is ****
[end_test]

使用运行时加载库

使用动态库作为运行时加载库的image比使用相同库作为依赖库的image更小,加载速度更快。静态链接器不会将关于运行时加载库的信息添加到image中。并且动态加载器在加载image时不需要加载库的相关库。然而,这种灵活性是有代价的。在image使用非依赖库的动态库之前,它必须通过dlopen(3) OS X开发人员工具手册页面加载该库,并通过dlsym(3) OS X开发人员工具手册页面获得所需的每个符号的地址。image也必须调用dlclose(3) OS X开发人员工具手册页,当它完成使用库

StarMeals2程序提供与StarMeals相同的功能。但是StarMeals2使用了Ratings 1.1动态库作为运行时加载的库。清单4显示了程序的源代码。

清单4使用Ratings 1.1作为运行时加载的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/* File: StarMeals2.c
 * Uses functions in libRatings.A.dylib.
 **************************************/
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <Ratings.h>
 
#define MAX_NAMES 100
#define MAX_NAME_LENGTH 30
#define MAX_RATING_LENGTH 5
 
static char *name_list[MAX_NAMES];
static char *rating_list[MAX_NAMES];
static int names = 0;
 
void addNameAndRating(char *name, char *rating) {
    name_list[names] = strdup(name);
    rating_list[names] =
        (strlen(rating) > MAX_RATING_LENGTH)?
        "*****" : strdup(rating);
    names++;
}
 
void test_data(void) {
    addNameAndRating("Spinach", "*");
    addNameAndRating("Cake", "****");
    addNameAndRating("Steak", "****");
    addNameAndRating("Caviar", "*");
    addNameAndRating("Broccoli", "****");
    addNameAndRating("Gagh", "*****");
    addNameAndRating("Chicken", "*****");
}
 
int main(int argc, char *argv[]) {
    int test_mode = 0;
    if (argc == 2) {
        if (strcmp(argv[1], "test") == 0) {
            test_mode = 1;
            printf("[start_test]\n");
            test_data();
        }
    }
    else {
        printf("Enter restaurant names and ratings in the form <name> <rating>.\n");
        printf("No spaces are allowed in the name and the rating.\n");
        printf("The rating can be * through *****.\n");
        printf("To finish, enter \"end\" for a restaurant name.\n");
        while (names < MAX_NAMES) {
            char name[MAX_NAME_LENGTH];
            char rating[MAX_RATING_LENGTH + 1];
            printf("\nName and rating: ");
            scanf("%s", &name);
            if (strcmp(name, "end") == 0) {
                break;
            }
            scanf("%s", rating);
            addNameAndRating(name, rating);
        }
        printf("\n");
    }
 
    if (names) {
        // Open Ratings library.
        void* lib_handle = dlopen("libRatings.A.dylib", RTLD_LOCAL|RTLD_LAZY);
        if (!lib_handle) {
            printf("[%s] Unable to load library: %s\n", __FILE__, dlerror());
            exit(EXIT_FAILURE);
        }
 
        // Print data entered and call libRatings.A:addRating().
        void (*addRating)(char*) = dlsym(lib_handle, "addRating");
        if (!addRating) {       // addRating is guaranteed to exist in libRatings.A.dylib
            printf("[%s] Unable to get symbol: %s\n", __FILE__, dlerror());
            exit(EXIT_FAILURE);
        }
        printf("This is the data you entered:\n");
        for (int i = 0; i < names; i++) {
            printf("%s (%s)\n", name_list[i], rating_list[i]);
            addRating(rating_list[i]);
        }
 
        // Print statistical information.
        char *(*meanRating)(void) = dlsym(lib_handle, "meanRating");
        if (!meanRating) {      // meanRating is guaranteed to exist in libRatings.A.dylib
            printf("[%s] Unable to get symbol: %s\n", __FILE__, dlerror());
            exit(EXIT_FAILURE);
        }
        printf("\nThe mean rating is %s\n", meanRating());
 
        char *(*medianRating)(void) = dlsym(lib_handle, "medianRating");
        if (medianRating) {     // Backwards compatibility with Ratings 1.0
            printf("The median rating is %s\n", medianRating());
        }
        char *(*frequentRating)(void) = dlsym(lib_handle, "frequentRating");
        if (frequentRating) {   // Backwards compatibility with Ratings 1.0
            printf("The most frequent rating is %s\n", frequentRating());
        }
 
        // Close Ratings library
        if (dlclose(lib_handle) != 0) {
            printf("[%s] Problem closing library: %s", __FILE__, dlerror());
        }
    }
 
    if (test_mode) {
        printf("[end_test]\n");
    }
    return 0;
}

清单5显示了如何编译StarMeals2程序。

清单5编译和链接StarMeals2

1
2
[StarMeals2]% make
clang -std=gnu99 StarMeals2.c -I<user_home>/include -o StarMeals2

静态链接器不会抱怨libRatings.A.dylib中有未解析的外部引用,因为它没有包含在链接行中。动态链接器在StarMeals2使用dlopen(3) OS X开发人员工具手册页面加载libRatings.A.dylib时解析这些引用。

在依赖库中插入函数

有时,您需要在调用函数收集统计数据或修改其输入或输出之前或之后执行操作。例如,您可能想知道程序调用某个特定函数的次数,以确定是否应该优化某个算法。但是,您可能并不总是能够访问函数的源代码来进行修改。插入是一种机制,通过这种机制,您可以定义自己的函数版本,该函数定义在映像相关库中。在你的版本中,你可以或者不可以调用原始函数。

注意:在OS X中,您只能插入依赖库。 无法插入运行时加载的库中的符号。

要从自定义调用插入函数,可以使用 dlsym(RTLD_NEXT, “")调用来获取真实函数的地址。例如,清单6显示了如何编写动态库中定义的函数的自定义版本。

清单6插入一个函数

1
2
3
4
5
6
char *name(void) {
    static int name_calls = 0;
    printf("[STATS] name() has been called %i times\n", name_calls);
    char *(*next_name)(void) = dlsym(RTLD_NEXT, "name");
    return next_name();
}

您可以使用interposition使现有的动态库适应您的特定需要,而无需更改其API。例如,本文档的companion包包括两个动态库Ratings和RatingsAsGrades的实现。Ratings库实现了一个基于星级的评级系统(它可以用来统计餐厅和酒店的评级;例如,**)。RatingsAsGrades库实现了一个基于字母的评分系统,可用于统计学生成绩;例如A和c,它没有编写新的算法来管理字母等级,而是利用了Ratings库的功能。清单7显示了RatingsAsGrades库的接口和实现。

清单7 RatingsAsGrades插入Ratings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/* File: RatingsInterposed.h
 * Interface to the RatingsAsGrages dynamic library.
 **************************************************/
 
/* Adds 'grade' to the set.
 *      grade: Non-NULL string. Contains zero or
 *             one of these characters:
 *             A, B, C, D, F.
 *             Examples: "", "A", "B".
 */
void addRating(char* grade);
 
/* Returns the median grade.
 */
char *medianRating(void);
 
/* Returns the most frequent grade.
 */
char *frequentRating(void);
 
/* File: RatingsAsGrades.c
 * Compile with -fvisibility=hidden.
 ***********************************/
 
#include "RatingsAsGrades.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
 
#define EXPORT __attribute__((visibility("default")))
#define MAX_STAR_RATING_LEN 6
 
static char *_ratingAsGrade(char *rating) {
    int rating_length = strlen(rating);
    if (rating_length > MAX_STAR_RATING_LEN - 1) {
        rating_length = MAX_STAR_RATING_LEN - 1;
    }
    char grade;
    switch (rating_length) {
        case 5:
            grade = 'A';
            break;
        case 4:
            grade = 'B';
            break;
        case 3:
            grade = 'C';
            break;
        case 2:
            grade = 'D';
            break;
        case 1:
            grade = 'F';
            break;
        default:
            grade = '\0';
    }
    char char_grade[2] = { grade, '\0' };
    return strdup(char_grade);
}
 
// Interpose libRatings.B.dylib:addRating.
EXPORT
void addRating(char* grade) {
    char rating[MAX_STAR_RATING_LEN] = { '\0' };
    switch (*grade) {
        case 'A':
            strcat(rating, "*");
        case 'B':
            strcat(rating, "*");
        case 'C':
            strcat(rating, "*");
        case 'D':
            strcat(rating, "*");
        case 'F':
            strcat(rating, "*");
        default:
            ;
    }
    void (*next_addRating)(char *) =
dlsym(RTLD_NEXT, "addRating");
    if (next_addRating) {
        next_addRating(rating);
    }
    else {
        printf("[%s] Fatal problem: %s", __FILE__, dlerror());
    }
}
 
// Interpose libRatings.B.dylib:medianRating.
EXPORT
char *medianRating(void) {
    char medianGrade[2] = { '\0' };
    char *(*next_medianRating)(void) =
dlsym(RTLD_NEXT, "medianRating");
    if (next_medianRating) {
        strcpy(medianGrade,
_ratingAsGrade(next_medianRating()));
    }
    else {
        printf("[%s] Fatal problem: %s", __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    return strdup(medianGrade);
}
 
// Interpose libRatings.B.dylib:frequentRating.
EXPORT
char *frequentRating(void) {
    char frequentGrade[2] = { '\0' };
    char *(*next_frequentRating)(void) =
        dlsym(RTLD_NEXT, "frequentRating");
    if (next_frequentRating) {
        strcpy(frequentGrade,
            _ratingAsGrade(next_frequentRating()));
    }
    else {
        printf("[%s] Fatal problem: %s", __FILE__, dlerror());
        exit(EXIT_FAILURE);
    }
    return strdup(frequentGrade);
 
}

请注意添addRating、medianRating和frequentRating如何修改它们所隐藏的定义的输入和输出。

companion-files包包括了Rating程序的源代码。这个程序使用RatingsAsGrades库来统计学生的成绩。

按照这些说明来构建和运行Rating程序:

  • 打开这个文档的companion-files包
  • 在终端中执行以下命令

    1
    2
    
      [Ratings/1.1]% make install
      [Grades]% make
    

    清单8显示了在测试模式下运行的Grades程序的输出。

清单8 Grades程序的测试输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Grades]% ./Grades test
[start_test]
This is the data you entered:
Eloise  (F)
Karla   (B)
Iva     (B)
Hilaire (F)
Jewel   (B)
Simone  (A)
Yvette  (A)
Renee   (A)
Mimi    (A)
 
The median grade is B
The most frequent grade is A
[end_test]

run-path 依赖的库

应用程序用户通常需要在文件系统中组织应用程序,以提高使用效率。这个功能很容易为单个二进制文件提供,因为它的相关库的位置很容易确定:它们可能位于文件系统中的一个标准位置,或者相对于二进制文件本身的一个位置。但是,当处理一组共享依赖库的应用程序时(例如,在应用程序套件中),为用户提供重定位套件目录的能力就更加困难:要么套件的依存库必须位于套件目录之外,要么; 套件的每个可执行文件都必须链接,要考虑其在套件中的位置。在OS X v10.5及以后版本中,链接器和动态加载器提供了一种简单的方法,允许一个应用程序套件目录中的多个可执行文件共享依赖的库,同时为套件用户提供了重新定位套件目录的选项。使用运行路径依赖库(un-path dependent libraries ),您可以创建包含可执行文件和依赖库的目录结构,用户可以重新定位这些文件而不会破坏它们。

运行路径依赖库是在创建库时不知道其完整安装名称的依赖库(请参阅如何使用动态库)。相反,库指定动态加载器在加载依赖于库的可执行文件时必须解析库的安装名称。

要使用与运行路径相关的库,可执行文件提供了运行路径搜索路径列表,动态加载器在加载时遍历该列表以查找库。

本文描述如何创建依赖于运行路径的库,以及如何在可执行文件中使用它们。

创建依赖于运行路径的库

若要创建依赖于运行路径的库,请指定运行路径相对路径名作为库的安装名称。运行路径相对路径名使用@rpath宏来指定相对于要在运行时确定的目录的路径。运行路径的相对路径名使用以下格式

1
@rpath/<path_to_dynamic_library>

这些是运行路径相对路径名的例子:

  • @rpath/libMyLib.dylib
  • @rpath/MyFramework.framework/Versions/A/MyFramework

运行路径安装名称( run-path install name )是使用运行路径相对路径名的安装名称。在使用gcc -install name选项创建依赖库时,您可以指定运行路径安装名称。有关更多信息,请参阅gcc手册页。当动态加载器(dyld)加载可执行文件时,它在运行路径搜索路径中按照链接时指定的顺序查找与运行路径相关的库。

使用赖于运行路径的库

要在可执行文件上使用依赖于运行路径的库(使用运行路径安装名称的库),您可以使用ld -rpath选项指定一个或多个运行路径搜索路径(每个-rpath子句指定一个运行路径位置)。

这是一个运行路径搜索路径列表的示例:

1
2
3
@loader_path/../Library/Frameworks
@loader_path/../Library/OpenSource
/usr/lib

注意:通过在-rpath子句中指定绝对路径名而不是运行路径相对路径名,并确保库位于指定的位置,与运行路径相关的库也可以作为常规依赖库使用。

记录动态加载程序事件

在开发和使用动态库时,您可能想知道某些事件何时发生。例如,你想知道动态加载器何时绑定一个特定的未定义的外部符号,或者应用程序启动需要多长时间。

本文确定了可以设置的环境变量,以及它们激活的动态加载器日志记录的类型。

表1列出了由动态加载器启动日志记录的环境变量。 表1影响动态加载器日志记录的环境变量

Environment variable Description
DYLD_PRINT_LIBRARIES 加载映像时记录日志。
DYLD_PRINT_LIBRARIES_POST_LAUNCH 通过dlopen调用加载映像时的日志。包括一个动态库依赖库。
DYLD_PRINT_APIS 记录导致动态加载程序返回符号地址的调用。
DYLD_PRINT_STATISTICS 记录应用程序启动过程的统计信息,例如应用程序完成启动时加载了多少图像。
DYLD_PRINT_INITIALIZERS 记录动态加载器调用初始化器和终结器函数时的日志。
DYLD_PRINT_SEGMENTS 当动态加载器将动态库的一个段映射到当前进程的地址空间时记录日志。
DYLD_PRINT_BINDINGS 当动态加载器将未定义的外部符号与其定义绑定时,记录它的日志。

参考文章

Dynamic Library Programming Topics

Framework Programming Guide

Code Loading Programming Topics

Mach-O Programming Topics