Featured image of post memo: CMake实践

memo: CMake实践

Table of contents

(Feature figure from g++ → make → cmake - 二圈妹的文章 - 知乎)

《CMake实践》

三、cmake工程

  1. 建立工程目录 t1

    1
    2
    3
    
    mkdir t1
    cd t1
    touch main.c CMakeLists.txt
    
  2. 编辑源文件 main.c

    1
    2
    3
    4
    5
    6
    
    # include <stdio.h>
    int main()
    {
        printf("Hello World from t1 Main!\n");
        return 0;
    }
    
  3. 编辑CMakeLists.txt (原书最后一行有误)

    1
    2
    3
    4
    5
    
    PROJECT(HELLO)			#定义工程名称
    SET(SRC_LIST main.c)	#定义变量SRC_LIST, 值为main.c源文件
    MESSAGE(STATUS "This is BINARY dir" ${HELLO_BINARY_DIR})	#向终端输出用户定义的信息
    MESSAGE(STATUS "This is SOURCE dir" ${HELLO_SOURCE_DIR})
    ADD_EXECUTABLE(hello ${SRC_LIST})	#编译源文件生成可执行文件hello
    
    1. PROJECT(projectname [CXX] [C] [JAVA])

      定义工程名称projectname,可指定工程支持的语言(支持的语言列表可缺省),默认支持所有语言。这条projecct指令隐式地定义了2个cmake变量

      1
      2
      
      <projectname>_BINARY_DIR	#此例为:HELLO_BINARY_DIR 二进制文件目录
      <projectname>_SOURCE_DIR	#源文件目录
      

      因为采用的是内部编译,2个变量目前指的都是工程所在路径 /backup/cmake/t1

      同时 cmake 系统也帮我们预定义了2变量:

      1
      2
      
      PROJECT_BINARY_DIR
      PROJECT_SOURCE_DIR
      
      • 他们的值分别跟 HELLO_BINARY_DIRHELLO_SOURCE_DIR 一致。
      • 为了统一起见,建议以后直接使用 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR,即使修改了工程名称,也不会影响这两个变量。
      • 如果使用了**<projectname>_SOURCE_DIR**,修改工程名称后,需要同时修改这些变量。
    2. SET (VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])

      set 指令可以用来显式的定义变量。

      1
      2
      
      SET(SRC_LIST main.c)
      SET(SRC_LIST main.c;t1.c;t2.c)	#源文件列表也可以是多个文件
      
    3. MESSAG ([SEND_ERROR | STATUS | FATAL_ERROR] “message to display”…)

      向终端输出用户定义的信息,包含了三种类型:

      1. SEND_ERROR 产生错误,生成过程被跳过
      2. STATUS 输出前缀为 - 的信息
      3. FATAL_ERROR 立即终止所有 cmake 过程
    4. ADD_EXECUTABLE(hello ${SRC_LIST})

      定义了这个工程会生成一个文件名为 hello 的可执行文件,相关的源文件是 SRC_LIST中定义的源文件列表。用 ${ } 来引用变量

  4. 开始构建

    在工程目录下:

    1
    
    cmake .	#构建当前目录,生成了Makefile
    

    根据Makefile编译源代码,连接,生成目标文件、可执行文件(hello)

    1
    2
    3
    
    make
    #或者
    make VERBOSE=1	#可以看到make构建的详细过程,以便排查错误
    
  5. 运行可执行文件(hello)

    1
    
    ./hello
    

cmake基本语法

  1. 变量使用 ${ } 方式取值,但是在IF控制语句中是直接使用变量名

  2. 指令(参数1 参数2 …)

    参数用括号括住,参数之间用空格分号隔开:

    1
    2
    
    ADD_EXECUTABLE(hello main.c func.c)
    ADD_EXECUTABLE(hello main.c;func.c)
    
  3. 指令是大小写无关的,参数和变量是大小写敏感的。(推荐全部大写指令)

  4. 工程名HELLO 和 可执行文件名hello 是没有任何关系的

  5. 如果文件名中有空格,使用双引号括住:

    1
    
    SET(SRC_LIST "func.c")
    
  6. 清理工程:

    清除上次的make命令所产生的目标(object)文件(后缀为“.o”的文件)及可执行文件

    1
    
    make clean
    

    make disclean 对cmake无效,所以需要用外部构建(out-of-source),来使工程目录整洁

  7. 安装

    将编译成功的可执行文件安装到系统目录中,一般为/usr/local/bin 目录中

    1
    
    make install
    
  8. 生成发行版软件包

    将可执行文件及相关文件打包成一个tar.gz压缩的文件用来作为发布软件的软件包。

    1
    
    make dist
    

    它会在当前目录下生成一个名字类似“PACKAGE-VERSION.tar.gz”的文件。PACKAGE和VERSION,是我们在configure.in中定义的AM_INIT_AUTOMAKE(PACKAGE, VERSION)。

  9. 检查发行软件包

    生成发布软件包并对其进行测试检查,以确定发布包的正确性。

    1
    
    make distcheck
    

    这个操作将自动把压缩包文件解开,然后执行configure命令,并且执行make,来确认编译不出现错误,最后提示你软件包已经准备好,可以发布了。

外部构建:

编译会生成一些无法自动删除的中间文件,所以在工程目录下建立build目录,用于存放中间文件,然后 cmake .. 对上层目录编译,在build目录中生成了make需要的Makefile和其他的中间文件。运行make编译,就会在build目录下获得目标文件 hello.o

  • PROJECT_SOURCE_DIR 仍指代工程目录,即 /backup/cmake/t1
  • PROJECT_BINARY_DIR 则指代编译目录,即 /backup/cmake/t1/build

四、规范的工程

规范要求

  1. 为工程添加一个子目录 src, 用来放置工程源代码
  2. 添加一个子目录 doc,用来放置工程的文档 hello.txt;
  3. 在工程目录添加文本文件 COPYRIGHT,README
  4. 在工程目录添加一个 runhello.sh 脚本,用来调用可执行文件 hello
  5. 将构建后的目标(object)文件放入构建目录的 bin 子目录
  6. 最终安装这些文件:将可执行文件 hello 与 runhello.sh 安装至 /usr/bin, 将doc 目录的内容以及 COPYRIGHT/README 安装到 /usr/share/doc/cmake/t2,

编译

  1. 准备工作

    在 /backup/cmake 目录下建立 /t2 目录,将 /t1 工程的 main.c 和 CMakeLists.txt 拷贝到 /t2 目录下。

  2. 构建工程

    1. 添加子目录 src:

      1
      2
      
      mkdir src
      mv main.c src	#源代码放入 src 目录
      
    2. 进入 /t2/src,编写 CMakeLists.txt (需要为任何子目录建立一个 CMakeLists.txt)

      1
      
      ADD_EXECUTABLE(hello main.c)	#把mainc.c源码编译成一个名为hello的可执行文件
      
    3. 修改 /t2 工程目录下的CMakeLists.txt ,指定源代码文件夹和编译输出(包括中间结果:每个子文件夹下都有CMakeLists.txt,都会产生编译结果)文件夹

      1
      2
      
      PROJECT(HELLO)
      ADD_SUBDIRECTORY(src bin)	#指定源码目录/t2/src,指定make编译结果放入/build/bin, 
      
      • ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FORM_ALL])

        此指令用于向当前工程添加存放源文件的子目录(source_dir),并可以指定编译输出存放的位置(binary_dir)。[EXCLUDE_FROM_ALL] 参数的含义是将这个目录从编译过程中排除,比如,工程的example,可能就需要工程编译完成后,再进入example 目录单独进行构建。

        如果不指定 bin 目录,编译结果(包括中间结果)都将存放在 build/src 目录下,指定 bin 目录后,相当于在编译时将 /build/src 重命名为 /bin。

      • 换个地方保存可执行文件和库文件

        不论是 SUBDIRS 还是 ADD_SUBDIRECTORY 指令(不论是否指定编译输出目录),我们都可以通过 SET 指令重新定义 EXECUTABLE_OUTPUT_PATHLIBRARY_OUTPUT_PATH 变量,来指定最终的目标二进制的位置(即最终生成的可执行文件 hello 或者最终的共享库,不包含编译生成的中间文件)

        1
        2
        
        SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)	#即为/build/bin
        SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)	#即为/build/lib
        

        ADD_EXECUTABLEADD_LIBRARY后面,写这条指令。

    4. 建立build,进入build目录,进行外部编译

      1
      2
      3
      
      cd build
      cmake ..
      make
      

      构建完成后,你会发现生成的可执行文件 hello 位于 build/bin 目录中。

INSTALL指令

有两种安装方法,一种是从代码编译后直接 make install 安装,一种是打包时的指定目录安装。可以通过:

1
make install	#将 hello 直接安装到 /usr/bin 目录

或者:

1
make install DESTDIR=/tmp/test	#安装在/tmp/test/usr/bin 目录,打包时这个方式经常被使用。

稍微复杂一点的是还需要定义 PREFIX,一般autotools工程,会运行这样的指令:

./configure -prefix=/usr 或者 **./configure --prefix=/usr/local**来指定 PREFIX

对于cmake来说,使用:

INSTALL 指令:

用于定义安装规则,安装的内容可以包括二进制、动态库、静态库以及文件、目录、脚本等:

1
2
3
4
5
6
INSTALL(TARGETS <target>... [...])			#安装二进制
INSTALL({FILES | PROGRAMS} <file>... [...])
INSTALL(DIRECTORY <dir>... [...])
INSTALL(SCRIPT <file> [...])
INSTALL(CODE <code> [...])
INSTALL(EXPORT <export-name> [...])

有时候,也会用到一个非常有用的变量**CMAKE_INSTALL_PREFIX**,用于指定cmake install时的相对地址前缀。用法如:

1
cmake -DCMAKE_INSTALL_PREFIX=/usr ..

CMAKE_INSTALL_PREFIX变量类似与configure 脚本的 -prefix。

INSTALL 指令的各种安装类型:

1. 目标文件的安装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
INSTALL(TARGETS targets ...		#各种目标文件
	    [EXPORT <export-name>]
        [[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
          PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE] #目标文件类型
         [DESTINATION <dir>]					#指定各文件的安装目录<dir>
         [PERMISSIONS permissions...]			#文件的权限
         [CONFIGURATIONS [Debug|Release|...]]	 #指定安装规则适用的构建配置列表(DEBUG或RELEASE等)
         [COMPONENT <component>]
         [NAMELINK_COMPONENT <component>]
         [OPTIONAL] 							#如果要安装的文件不存在,则指定不是错误。
         [EXCLUDE_FROM_ALL]			#指定该文件从完整安装中排除,仅作为特定于组件的安装的一部分进行安装;
         [NAMELINK_ONLY|NAMELINK_SKIP]
        ] [...]
        [INCLUDES DESTINATION [<dir> ...]]
        )

参数中的TARGET可以是很多种目标文件,最常见的是通过ADD_EXECUTABLE或者ADD_LIBRARY定义的目标文件,即可执行二进制、动态库、静态库:以下是默认的安装路径

目标文件 内容 安装目录变量 默认安装文件夹
ARCHIVE 静态库 ${CMAKE_INSTALL_LIBDIR} lib
LIBRARY 动态库 ${CMAKE_INSTALL_LIBDIR} lib
RUNTIME 可执行二进制文件 ${CMAKE_INSTALL_BINDIR} bin
PUBLIC_HEADER 与库关联的PUBLIC头文件 ${CMAKE_INSTALL_INCLUDEDIR} include
PRIVATE_HEADER 与库关联的PRIVATE头文件 ${CMAKE_INSTALL_INCLUDEDIR} include

为了符合一般的默认安装路径,如果设置了DESTINATION参数,推荐配置在安装目录变量下的文件夹。

例如:

1
2
3
4
5
INSTALL(TARGETS myrun mylib mystaticlib
       RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
       LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
       ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

上面的例子会将:可执行二进制myrun安装到${CMAKE_INSTALL_BINDIR}目录,动态库libmylib.so安装到${CMAKE_INSTALL_LIBDIR}目录,静态库libmystaticlib.a安装到${CMAKE_INSTALL_LIBDIR}目录。

INSTALL命令的其他一些参数的含义:

  • DESTINATION:指定磁盘上要安装文件的目录;
  • PERMISSIONS:指定安装文件的权限。有效权限是OWNER_READ,OWNER_WRITE,OWNER_EXECUTE,GROUP_READ,GROUP_WRITE,GROUP_EXECUTE,WORLD_READ,WORLD_WRITE,WORLD_EXECUTE,SETUID和SETGID;(11种权限)
  • CONFIGURATIONS:指定安装规则适用的构建配置列表(DEBUG或RELEASE等);
  • EXCLUDE_FROM_ALL:指定该文件从完整安装中排除,仅作为特定于组件的安装的一部分进行安装;
  • OPTIONAL:如果要安装的文件不存在,则指定不是错误。

注意一下CONFIGURATIONS参数,此选项指定的值仅适用于此选项之后列出的选项:例如,要为调试发布配置设置单独的安装路径,请执行以下操作:

1
2
3
4
5
6
INSTALL(TARGETS target
        CONFIGURATIONS Debug
        RUNTIME DESTINATION Debug/bin)
INSTALL(TARGETS target
        CONFIGURATIONS Release
        RUNTIME DESTINATION Release/bin)

也就是说,DEBUG和RELEASE版本的DESTINATION安装路径不同,那么DESTINATION必须在CONFIGUATIONS后面。

2. 普通文件的安装

1
2
3
4
5
6
INSTALL(<FILES|PROGRAMS> files...
        TYPE <type> | DESTINATION <dir>
        [PERMISSIONS permissions...]
        [CONFIGURATIONS [Debug|Release|...]]
        [COMPONENT <component>]
        [RENAME <name>] [OPTIONAL] [EXCLUDE_FROM_ALL])

FILES|PROGRAMS若为相对路径给出的文件名,将相对于当前源目录进行解释。其中,FILES为普通的文本文件,PROGRAMS指的是非目标文件的可执行程序(如脚本文件)

如果未提供PERMISSIONS参数,默认情况下,普通的文本文件将具有OWNER_WRITE,OWNER_READ,GROUP_READ和WORLD_READ权限,即644权限;而非目标文件的可执行程序将具有OWNER_EXECUTE, GROUP_EXECUTE,和WORLD_EXECUTE,即755权限

其中,不同的TYPEcmake也提供了默认的安装路径,如下表:

TYPE类型 安装目录变量 默认安装文件夹
BIN ${CMAKE_INSTALL_BINDIR} bin
SBIN ${CMAKE_INSTALL_SBINDIR} sbin
LIB ${CMAKE_INSTALL_LIBDIR} lib
INCLUDE ${CMAKE_INSTALL_INCLUDEDIR} include
SYSCONF ${CMAKE_INSTALL_SYSCONFDIR} etc
SHAREDSTATE ${CMAKE_INSTALL_SHARESTATEDIR} com
LOCALSTATE ${CMAKE_INSTALL_LOCALSTATEDIR} var
RUNSTATE ${CMAKE_INSTALL_RUNSTATEDIR} /run
DATA ${CMAKE_INSTALL_DATADIR}
INFO ${CMAKE_INSTALL_INFODIR} /info
LOCALE ${CMAKE_INSTALL_LOCALEDIR} /locale
MAN ${CMAKE_INSTALL_MANDIR} /man
DOC ${CMAKE_INSTALL_DOCDIR} /doc

请注意,某些类型的内置默认值使用DATAROOT目录作为前缀,以CMAKE_INSTALL_DATAROOTDIR变量值为内容。

该命令的其他一些参数的含义:

  • DESTINATION:指定磁盘上要安装文件的目录;
  • PERMISSIONS:指定安装文件的权限。有效权限是OWNER_READ,OWNER_WRITE,OWNER_EXECUTE,GROUP_READ,GROUP_WRITE,GROUP_EXECUTE,WORLD_READ,WORLD_WRITE,WORLD_EXECUTE,SETUID和SETGID
  • CONFIGURATIONS:指定安装规则适用的构建配置列表(DEBUG或RELEASE等);
  • EXCLUDE_FROM_ALL:指定该文件从完整安装中排除,仅作为特定于组件的安装的一部分进行安装;
  • OPTIONAL:如果要安装的文件不存在,则指定不是错误;
  • RENAME:指定已安装文件的名称,该名称可能与原始文件不同。仅当命令安装了单个文件时,才允许重命名。

3. 目录的安装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
INSTALL(DIRECTORY dirs...
        TYPE <type> | DESTINATION <dir>
        [FILE_PERMISSIONS permissions...]
        [DIRECTORY_PERMISSIONS permissions...]
        [USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER]
        [CONFIGURATIONS [Debug|Release|...]]
        [COMPONENT <component>] [EXCLUDE_FROM_ALL]
        [FILES_MATCHING]
        [[PATTERN <pattern> | REGEX <regex>]
         [EXCLUDE] [PERMISSIONS permissions...]] [...])

该命令将一个或多个目录的内容安装到指定的目的地,目录结构被逐个复制到目标位置。每个目录名称的最后一个组成部分都附加到目标目录中,但是可以使用后跟斜杠来避免这种情况,因为它将最后一个组成部分留空。这是什么意思呢?

比如,DIRECTORY后面如果是abc意味着abc这个目录会安装在目标路径下,abc/意味着abc这个目录的内容会被安装在目标路径下,而abc目录本身却不会被安装。即,如果目录名不以"/“结尾,那么这个目录将被安装为目标路径下的abc,如果目录名以/结尾,代表将这个目录中的内容安装到目标路径,但不包括这个目录本身

FILE_PERMISSIONSDIRECTORY_PERMISSIONS选项指定对目标中文件目录的权限。如果指定了USE_SOURCE_PERMISSIONS而未指定FILE_PERMISSIONS,则将从源目录结构中复制文件权限。如果未指定权限,则将为文件提供在命令的FILES形式中指定的默认权限(644权限),而目录将被赋予在命令的PROGRAMS形式中指定的默认权限(755权限)。

可以使用PATTERNREGEX选项以精细的粒度控制目录的安装,可以指定一个通配模式或正则表达式以匹配输入目录中遇到的目录或文件PATTERN仅匹配完整的文件名,而REGEX将匹配文件名的任何部分,但它可以使用/和$模拟PATTERN行为

某些跟随PATTERN或REGEX表达式后的参数,仅应用于满足表达式的文件或目录。如:EXCLUDE选项将跳过匹配的文件或目录。PERMISSIONS选项将覆盖匹配文件或目录的权限设置。

例如:

1
2
3
4
5
6
INSTALL(DIRECTORY icons scripts/ 
	    DESTINATION share/myproj
        PATTERN "CVS" EXCLUDE
        PATTERN "scripts/*"
        PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ
                    GROUP_EXECUTE GROUP_READ)

这条命令的执行结果是:将icons目录安装到share/myproj,将scripts/中的内容安装到share/myproj,两个目录均不包含目录名为CVS的子目录,对于scripts/*的文件指定权限为OWNER_EXECUTE,OWNER_WRITE,OWNER_READ,GROUP_EXECUTE,GROUP_READ。

4. 安装时脚本的运行

有时候需要在install的过程中打印一些语句,或者执行一些cmake指令:

1
2
INSTALL([[SCRIPT <file>] [CODE <code>]]
        [COMPONENT <component>] [EXCLUDE_FROM_ALL] [...])

SCRIPT参数将在安装过程中调用给定的CMake脚本文件(即.cmake脚本文件),如果脚本文件名是相对路径,则将相对于当前源目录进行解释。CODE参数将在安装过程中调用给定的CMake代码。将代码指定为双引号字符串内的单个参数

例如:

1
INSTALL(CODE "MESSAGE(\"Sample install message.\")")

这条命令将会在install的过程中执行cmake代码,打印语句。

安装

就是把文件复制到到指定目录下

  1. 添加 doc 目录及文件

    1
    2
    3
    
    cd /backup/cmake/t2
    mkdir doc	#存储工程文档
    touch doc/hello.txt
    
  2. **添加脚本:**在工程目录添加 runhello.sh,内容为:

    1
    2
    
    cd /home/jack/backup/cmake/t2/build/bin
    ./hello		#调用可执行文件
    
  3. **添加文件:**工程目录中的 COPYRIGHT 和 README

    1
    
    touch COPYRIGHT README
    
  4. 修改 CMakeLists.txt ,使之可以支持各种文件的安装

    1. 安装文档,修改工程目录下的 CMakeLists.txt

      1
      2
      3
      4
      5
      
      # 安装 COPYRIGHT/README 到 /<prefix>/share/doc/cmake/t2
      INSTALL(FILES COPYRIGHT README DESTINATION share/doc/cmake/t2)
      
      # 安装 runhello.sh 到 /<prefix>/bin
      INSTALL(PROGRAMS runhello.sh DESTINATION bin)
      
    2. 安装 doc 中的 hello.txt , 两种方式:

    3. 通过 doc 目录建立 CMakeLists.txt 并将 doc 目录通过 ADD_SUBDIRECTORY 加入工程来完成。

    4. 直接在工程目录通过 INSTALL(DIRECTORY 来完成):

      因为 hello.txt 要安装到 /<prefix>/share/doc/cmake/t2,所以我们不能直接安装整个 doc 目录,这里采用的方式是安装 doc 目录中的内容,也就是使用 " doc/ “。 在工程目录下的CMakeLists.txt 中添加:

      1
      
         INSTALL(DIRECTORY doc/ DESTINATION share/doc/cmake/t2)
      
  5. 编译并安装

    进入build 目录进行外部编译,注意使用 CMAKE_INSTALL_PREFIX 参数,这里将它安装到了 /tmp/t2 目录:

    1
    2
    3
    
    cmake -DCMAKE_INSTALL_PREFIX=/tmp/t2/usr ..
    make
    make install
    

    cd 进入 /tmp/t2 目录看以下安装结果:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    ./usr
    ./usr/share
    ./usr/share/doc
    ./usr/share/doc/cmake
    ./usr/share/doc/cmake/t2
    ./usr/share/doc/cmake/t2/hello.txt
    ./usr/share/doc/cmake/t2/README
    ./usr/share/doc/cmake/t2/COPYRIGHT
    ./usr/bin
    ./usr/bin/hello
    ./usr/bin/runhello.sh
    

    如果要直接安装到系统,可以使用如下指令:

    1
    
    cmake -DCMAKE_INSTALL_PREFIX=/usr ..
    

    如果没有额外定义,CMAKE_INSTALL_PREFIX默认定义/usr/local

五、静态库与动态库构建

动态库、静态库与可执行文件的区别:

动态链接库(Dynamic Link Library,缩写为DLL)是在程序运行时动态调用的,可以被其它应用程序共享的程序模块,其中封装了一些可以被共享的例程和资源。动态链接库文件的扩展名一般是dll,也有可能是drv、sys和fon,它和可执行文件(exe)非常类似,区别在于DLL中虽然包含了可执行代码却不能单独执行,而应由Windows应用程序直接或间接调用

Lib称为静态链接库(static link library),是在编译的链接期间使用的,他里面其实就是源文件的函数实现。Lib只是Dll的附带品,是DLL导出的函数列表文件而已。

Dll其实和Exe是几乎完全一样的,唯一的不一样就是Exe的入口函数式WinMain函数(console程序是main函数),而Dll是DllMain函数,其他完全是一样的。所以有人也戏称Dll是不能自己运行的Exe。

  • 静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分(拷贝函数)
  • 动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在操作系统的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,操作系统才转去执行DLL中相应的函数代码。

可执行文件和动态库之间的区别:可执行文件中有main函数,动态库中没有main函数,可执行文件可以被程序执行,动态库需要依赖程序调用者。

本节任务:

  1. 建立一个静态库动态库提供 HelloFunc 函数供其他程序编程使用,HelloFunc 向终端输出 Hello World 字符串。
  2. 安装头文件与共享库。

1. 准备工作

在 /back/cmake 目录下建立 t3 目录,用于存放本节工程

1
2
3
4
5
6
cd /backup/cmake/t3
touch CMakeLists.txt	#建立工程文件
mkdir lib				#建立库文件夹
cd lib
touch hello.c hello.h	#在lib目录下建立两个源文件
touch CMakeLists.txt

2. 建立共享库

编辑工程目录下的 CMakeLists.txt 文件内容为:

1
2
PROJECT(HELLOLIB)		#定义工程名
ADD_SUBDIRECTORY(lib)	#指定 库 的文件夹

编辑 lib文件夹下的 hello.h 内容如下:( #if、#ifdef、#ifndef 区别 )

1
2
3
4
5
#ifndef HELLO_H		//如果当前的宏未被定义,HELLO_H是宏名
#define HELLO_H		//定义宏
#include <stdio.h>	//引入头文件
void HelloFunc();	//声明函数
#endif

编辑 lib文件夹下的 hello.c 内容为:

1
2
3
4
5
#include "hello.h"	//""括住表示: 预处理程序先到当前目录下寻找文件,再到预定义的缺省路径(通常由INCLUDE环境变量指定)下寻找文件
void HelloFunc()
{
    printf("Hello world\n");
}

编辑 /lib/CMakeLists.txt :

1
2
SET(LIBHELLO_SRC hello.c)	#定义变量
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})	#将指定的源文件编译成库

采用外部编译,在工程目录下建立一个 build 目录,

1
2
3
4
mkdir build
cd build
cmake ..
make

这时,在 /build/lib 目录下得到一个 libhello.so,这就是我们期望的共享库

如果要指定 libhello.so 生成的位置,可以通过在主工程文件 CMakeLists.txt 中修改 ADD_SUBDIRECTORY(lib) 指令来指定一个编译输出位置或者在 lib/CMakeLists.txt 中添加:

1
SET(LIBRARY_OUTPUT_PATH <路径>)	#指定新的位置

ADD_LIBRARY

主要作用就是将指定的源文件生成链接文件,然后添加到工程中去。

1
2
3
4
ADD_LIBRARY(libname 				#生成的库文件的名字
			[SHARED|STATIC|MODULE]	#库文件类型
			[EXCLUDE_FROM_ALL]		#指定这个库不会被默认构建,除非有其他的组件依赖或者手动构建
			source1 source2 ... sourceN)	#各个源文件
  • SHARED 库:会被动态链接(动态链接库),在运行时会被加载。
  • STATIC 库:是目标文件的归档文件,在链接其它目标的时候使用。
  • MODULE 库:是一种不会被链接到其它目标中的插件,但是可能会在运行时使用dlopen-系列的函数。

3. 添加静态库

同样使用上面的指令,我们在支持动态库的基础上再为工程添加一个静态库,按照一般的习 惯,静态库名字跟动态库名字应该是一致的,只不过后缀是.a 罢了。

添加静态库的指令:

1
ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})

然后再在 build 目录进行外部编译,我们会发现,静态库根本没有被构建,仍然只生成了 一个动态库。因为 hello 作为一个 target 是不能重名的,所以,静态库构建指令无效。

如果我们把上面的 hello 修改为 hello_static:

1
ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})

就可以构建一个 libhello_static.a 的静态库了。

但是这种结果显示不是我们想要的,我们需要的是名字相同的静态库和动态库,因为 target 名 称是唯一的,所以,我们肯定不能通过 ADD_LIBRARY 指令来实现了。这时候我们需要用到 另外一个指令:

1
2
3
4
SET_TARGET_PROPERTITES(target1 target2 ...
					   PROPERTIES prop1 value1
					   			  prop2 value2
					   			  ...)

这条指令可以用来设置输出的名称,对于动态库,还可以用来指定动态库版本和 API 版本。

在本例中,我们需要作的是向 lib/CMakeLists.txt 中添加一条:

1
SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")

这样,我们就可以同时得到 libhello.solibhello.a 两个库了。

SET_TARGET_PROPERTIES对应的指令是:

1
GET_TARGET_PROPERTY(VAR target property)	#得到属性值

具体用法如下:我们向 lib/CMakeLists.txt 中添加:

1
2
GET_TARGET_PROPERTY(OUTPUT_VALUE hello_static OUTPUT_NAME)
MESSAGE(STATUS "This is the hello_static OUTPUT_NAME:"${OUTPUT_VALUE})

如果没有这个属性定义,则返回 NOTFOUND。

4. 动态库版本号

按照规则,动态库是应该包含一个版本号的,我们可以看一下系统的动态库,一般情况是:

1
2
3
libhello.so.1.2
libhello.so ->libhello.so.1
libhello.so.1->libhello.so.1.2

为了实现动态库版本号,我们仍然需要使用 SET_TARGET_PROPERTIES 指令。 具体使用方法如下:

1
SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)
  • VERSION :指代动态库版本,
  • SOVERSION:指代API 版本

将上述指令加入 lib/CMakeLists.txt 中,重新构建看看结果。 在 build/lib 目录会生成: libhello.so.1.2 libhello.so.1->libhello.so.1.2 libhello.so ->libhello.so.1

5. 安装共享库和头文件

以上面的例子,我们需要将 libhello.a, libhello.so.x 以及 hello.h 安装到系统目录,才能真正让其他人开发使用,在本例中我们将 hello 的共享库安装到 <prefix>/lib 目录,将 hello.h 安装到**<prefix>/include/hello** 目录。

利用上一节了解到的 INSTALL 指令,我们向 lib/CMakeLists.txt 中添加如下指令:

1
2
3
4
5
INSTALL(TARGETS hello hello_static 
		LIBRARY DESTINATION lib 
		ARCHIVE DESTINATION lib)	#注意,静态库要使用 ARCHIVE 关键字
INSTALL(FILES hello.h 
		DESTINATION include/hello)

终端输入:

1
2
3
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
make
make install	#Permission denied

我们就可以将头文件和共享库安装到系统目录/usr/lib 和/usr/include/hello 中了。

完整代码:

lib/CMakeLists.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
SET(LIBHELLO_SRC hello.c)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})	#将源码编译成动态库
ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})	#编译成静态库
SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello") #重命名
GET_TARGET_PROPERTY(OUTPUT_VALUE hello_static OUTPUT_NAME) #读取名字输出显示
MESSAGE(STATUS "This is the hello_static OUTPUT_NAME:"${OUTPUT_VALUE})
SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1) #版本号
INSTALL(TARGETS hello hello_static	#安装动态库和静态库
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib)	#注意,静态库要使用 ARCHIVE 关键字
INSTALL(FILES hello.h				#安装头文件
        DESTINATION include/hello)

六、如何使用外部共享库和头文件

上一节我们已经完成了 libhello 动态库的构建以及安装,本节我们的任务很简单: 编写一个程序使用我们上一节构建的共享库。

1. 准备工作

建立工程文件夹:在/backup/cmake 目录建立 t4 目录,本节所有资源将存储在 t4 目录。

1
2
3
4
5
cd t4
touch CMakeLists.txt
mkdir src
cd src
touch main.c CMakeList.txt

2. 编写源文件

建立 src 目录,编写源文件 main.c :

1
2
3
4
5
#include <hello.h>
int main()
{
    HelloFunc();
}

编辑工程目录下的 CMakeLists.txt:

1
2
PROJECT(NEWHELLO)
ADD_SUBDIRECTORY(src)

编辑 /src/CMakeLists.txt:

1
ADD_EXECUTABLE(main main.c)

如果直接编译会出错:/backup/cmake/t4/src/main.c:1:19: error: hello.h: 没有那个文件或目录

3. 引入头文件搜索路径

hello.h 位于 /usr/include/hello 目录中,并没有位于系统标准的头文件路径,为了让我们的工程能够找到 hello.h 头文件,我们需要引入一个新的指令:INCLUDE_DIRECTORIES,其完整的语法是:

1
2
3
INCLUDE_DIRECTORIES([AFTER|BEFORE]
					[SYSTEM]
					dir1 dir2 ...)

这条指令可以用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来,默认的行为是追加到当前的头文件搜索路径的后面,你可以通过两种方式来进行控制搜索路径添加的方式:

  1. CMAKE_INCLUDE_DIRECTORIES_BEFORE,通过 SET 这个 cmake 变量为 on,可以 将添加的头文件搜索路径放在已有路径的前面
  2. 通过 AFTER 或者 BEFORE 参数,也可以控制是追加还是置前。

现在我们在 src/CMakeLists.txt 中添加一个头文件搜索路径,方式很简单,加入:

1
INCLUDE_DIRECTORIES(/usr/include/hello)

进入 build 目录,重新进行构建,这是找不到 hello.h 的错误已经消失,但是出现了一个新的错误: main.c:(.text+0x12): undefined reference to HelloFunc’`

这是因为我们并没有 link共享库 libhello 上。

4. 为 target 链接共享库

我们现在需要完成的任务是将目标文件(target)链接到 libhello,这里我们需要引入两个新的指令**LINK_DIRECTORIES** 和 TARGET_LINK_LIBRARIES

1
LINK_DIRECTORIES(directory1 directory2 ...)

这个指令非常简单,添加非标准的共享库搜索路径,比如,在工程内部同时存在共享库可执行二进制,在编译时就需要指定一下这些共享库的路径。这个例子中我们没有用到这个指令。

1
2
3
TARGET_LINK_LIBRARIES(target library1
					  <debug | optimized> library2
					  ...)

这个指令可以用来为 target 添加需要链接的共享库,本例中是一个可执行文件,但是同样可以用于为自己编写的共享库添加共享库链接

为了解决我们前面遇到的 HelloFunc 未定义错误,我们需要作的是向 src/CMakeLists.txt 中添加如下指令:

1
2
3
TARGET_LINK_LIBRARIES(main hello)	#hello 是共享库 libhello.so
# 也可以写成
TARGET_LINK_LIBRARIES(main libhello.so)

进入 build 目录重新进行构建,就得到了一个链接到 libhello 的可执行程序 main,位于 build/src 目录,运行 main 的结果是输出:Hello world

让我们来检查一下 main 的链接情况:

1
ldd src/main	#可以用來尋找此執行檔鏈接了哪一些函式庫
1
2
3
4
linux-gate.so.1 =>  (0xb7ee7000)
libhello.so.1 => /usr/lib/libhello.so.1 (0xb7ece000)
libc.so.6 => /lib/libc.so.6 (0xb7d77000)
/lib/ld-linux.so.2 (0xb7ee8000)

可以清楚的看到 main 确实链接了共享库 libhello,而且链接的是动态库 libhello.so.1

那如何链接到静态库呢? 方法很简单: 将 TARGET_LINK_LIBRRARIES 指令修改为:

1
TARGET_LINK_LIBRARIES(main libhello.a)

重新构建后再来看一下 main 的链接情况:

1
ldd src/main
1
2
3
linux-gate.so.1 => (0xb7fa8000)
libc.so.6 => /lib/libc.so.6 (0xb7e3a000)
/lib/ld-linux.so.2 (0xb7fa9000)

说明,main 确实链接到了静态库 libhello.a

特殊的环境变量:

CMAKE_INCLUDE_PATHCMAKE_LIBRARY_PATH 务必注意,这两个是环境变量而不是 cmake 变量。 使用方法是要在 bash 中用 export 或者在 csh 中使用 set 命令设置或者 CMAKE_INCLUDE_PATH=/home/include cmake ..等方式。

这两个变量主要是用来解决以前 autotools 工程中 --extra-include-dir 等参数的支持的。也就是,如果头文件没有存放在常规路径 (/usr/include, /usr/local/include 等),则可以通过这些变量就行弥补

我们以本例中的 hello.h 为例,它存放在**/usr/include/hello** 目录,所以直接查找肯定是找不到的。前面我们直接使用了**绝对路径****INCLUDE_DIRECTORIES(/usr/include/hello)**告诉工程这个头文件目录。

为了将程序更智能一点,我们可以使用 **CMAKE_INCLUDE_PATH**来进行,使用 bash 的方法如下:

1
export CMAKE_INCLUDE_PATH=/usr/include/hello

然后在头文件中将 **INCLUDE_DIRECTORIES(/usr/include/hello)**替换为:

1
2
3
4
FIND_PATH(myHeader hello.h)
IF(myHeader)
INCLUDE_DIRECTORIES(${myHeader})
ENDIF(myHeader)

上述的一些指令我们在后面会介绍。这里简单说明一下,FIND_PATH 用来在指定路径中搜索文件名,比如: FIND_PATH(myHeader NAMES hello.h PATHS /usr/include /usr/include/hello)

这里我们没有指定路径,但是,cmake 仍然可以帮我们找到 hello.h 存放的路径,就是因为我们设置了环境变量 CMAKE_INCLUDE_PATH

如果你不使用 FIND_PATH,**CMAKE_INCLUDE_PATH**变量的设置是没有作用的,你不能指望它会直接为编译器命令添加参数 -I<CMAKE_INCLUDE_PATH>

以此为例,CMAKE_LIBRARY_PATH 可以用在 FIND_LIBRARY 中。

同样,因为这些变量直接为 FIND_指令所使用,所以所有使用 FIND_指令的 cmake 模块都会受益。

Built with Hugo
Theme Stack designed by Jimmy