Windows+MinGW使用CMake生成.dll动态链接库

序言

在完成接口开发后,为了给其他开发人员调用,通常需要将这部分代码打包生成动态链接库文件。

动态链接库很好地保证了代码的封装性和独立性,作为接口,它可以很好地独立于主程序,便于更新。调用接口的主程序编译时也会绕过已生成的动态链接库,仅作连接,在运行时使用它,减少了前端人员编译的时间消耗。

同时由于动态链接库很难被反编译,只有头文件是可见的,这也同时保证了代码的安全性。

在Windows系统上,以.dll为后缀的是动态链接库,以.a结尾的是静态链接库。

静态连接库就是把(lib)文件中用到的函数代码直接链接进目标程序,程序运行的时候不再需要其它的库文件;动态链接就是把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从DLL中寻找相应函数代码,因此需要相应DLL文件的支持。

*Linux系统上的动态链接库后缀为.so,静态链接库后缀还是.a。本篇不做过多介绍。

环境概览

  • 操作系统:Windows 10
  • 编译器:MinGW-w64

目录结构

│  CMakeLists.txt
│
├─build
│
├─examples
│      test_api.cpp
│          
├─include
│  └─detectionapi
│          DetectionApi.h
│          
├─lib
│      libopencv_calib3d450.dll
│      libopencv_core450.dll
│      
└─src
       DetectionApi.cpp

编写CMakeLists.txt

首先,为保证目录结构的干净,先将待编译的文件整理为上述目录结构。
上述目录中,include存放头文件(.h, .hpp),src存放源文件(.c, .cpp),examples存放测试代码,lib存放各种支持程序运行的链接库。CMakeLists.txt放在根目录下,将编译生成的内容全部放在build目录下。

然后开始编辑CMakeLists.txt文件,一个样例如下所示。

# 最低CMake版本要求
cmake_minimum_required(VERSION 3.9.0)

# 项目名称,编译好dll或exe的名称
project(detection)

# 设置C/C++标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 头文件路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/detectionapi)
include_directories(../opencv/build/include)
include_directories(../opencv/build/include/opencv2)

# 枚举头文件
file(GLOB_RECURSE INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/include/detectionapi/*.h)

# 指定引用的外部库的搜索路径
LINK_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/lib)

# 枚举源文件
file(GLOB SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

# 链接
set(LINK_LIBS
    libopencv_calib3d450.dll
    libopencv_core450.dll
)

# 生成dll库
ADD_LIBRARY(detectionapi SHARED ${INCLUDES} ${SOURCES})
TARGET_LINK_LIBRARIES(detectionapi ${LINK_LIBS})
INSTALL(TARGETS detectionapi DESTINATION  ${CMAKE_CURRENT_SOURCE_DIR}/lib)

# 设置目标属性
SET_TARGET_PROPERTIES(detectionapi PROPERTIES LINKER_LANGUAGE C
        ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib
        LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib
        RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib
        OUTPUT_NAME "DetectionApi"
        PREFIX "")

# 生成可执行文件,仅用作测试
add_executable(detection examples/test_api.cpp ${INCLUDES})
TARGET_LINK_LIBRARIES(detection ${LINK_LIBS})
TARGET_LINK_LIBRARIES(detection ${PROJECT_BINARY_DIR}/lib/libDetectionApi.dll.a)

如上述样例所示,一个标准的CMake文件包含了以下几大部分:

  • 基础配置:完成一些CMake所需的基础配置,包括了版本要求,项目名称,C++标准等。
  • 输入配置:所有需要的头文件,源文件,外部链接库。
  • 输出配置:生成内容,生成属性等。包括了输出路径,输出类别(可执行程序,dll库等)。

上述示例,基础配置和输入配置是比较显而易见的,这里着重说一下输出配置。

ADD_LIBRARY(detectionapi SHARED ${INCLUDES} ${SOURCES})
声明这是一个lib库,第一个参数是链接库属性,第二个是类型(SHARED表示动态链接库),第三个参数是输入参数中的头文件和源文件。

TARGET_LINK_LIBRARIES(detectionapi ${LINK_LIBS})
这个是链接的第三方库,如自行编译出的opencv的各种dll等。

INSTALL(TARGETS detectionapi DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/lib)
dll的实际安装指令,按照上述写法将被安装在/lib目录下。

SET_TARGET_PROPERTIES(detectionapi PROPERTIES LINKER_LANGUAGE C
        ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib
        LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib
        RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib
        OUTPUT_NAME "DetectionApi"
        PREFIX "")

设置dll属性的实际内容。

  • PROPERTIES LINKER_LANGUAGE C:声明为C链接库,提高DLL库的兼容性。
  • OUTPUT_NAME:DLL库输出名称。
  • PREFIX:DLL库前缀,若不设置,可能会加默认前缀。

add_executable(detection examples/test_api.cpp ${INCLUDES})
生成可执行程序,这里是利用examples/test_api.cpp来测试封装的dll是否生效。程序主要就是引用了DetectionApi.h,调用给出的函数,观察是否有打印信息。可执行程序仅用作测试dll,没有实际意义,CMake文件可以不包含这段代码。

TARGET_LINK_LIBRARIES(detection ${PROJECT_BINARY_DIR}/lib/libDetectionApi.dll.a)
为了测试dll,就不包含cpp,而是使用静态链接库替代cpp进行编译(原来exe的生成方式是.h + .cpp,现在是.h + .dll.a)。这样,可执行程序在运行时就会去同级目录下找你的dll文件了。而dll.a在此之后也就没有用了。

需要注意的是,本文操作全过程不需要修改原始代码,CMake过程是独立于代码之外的。

CMake语法内容非常庞大,在实际使用中,要多查阅CMake官方文档。
CMake Documentation

添加版本信息

为了便于测试和发布,最好能在编译时为我们的dll库添加上版本信息。

我们需要首先在CMakeLists.txt的同级目录下建立version.rc.in文件,里面存放Cmake时用到的一些变量。
这里给出了一个模板,具体根据需求修改。

// version.rc.in
#define VER_FILEVERSION             @MY_PRODUCT_NUMBER@,@MY_PRODUCT_VERSION@,@MY_BUILD_NUMBER@,0
#define VER_FILEVERSION_STR         "@MY_PRODUCT_NUMBER@.@MY_PRODUCT_VERSION@.@MY_BUILD_NUMBER@.0\0"

#define VER_PRODUCTVERSION          @MY_PRODUCT_NUMBER@,@MY_PRODUCT_VERSION@,@MY_BUILD_NUMBER@,0
#define VER_PRODUCTVERSION_STR      "@MY_PRODUCT_NUMBER@.@MY_PRODUCT_VERSION@.@MY_BUILD_NUMBER@\0"
//

1 VERSIONINFO
FILEVERSION VER_FILEVERSION
PRODUCTVERSION VER_PRODUCTVERSION
BEGIN
  BLOCK "StringFileInfo"
  BEGIN
    BLOCK "040904E4"
    BEGIN
      VALUE "FileVersion", VER_FILEVERSION_STR
      VALUE "ProductVersion", VER_PRODUCTVERSION_STR
    END
  END
  /* For some reason the ProductVersion would not appear unless I add the following section: VarFileInfo */
  BLOCK "VarFileInfo"
  BEGIN
    VALUE "Translation", 0x409, 1252
  END
END

然后补充CmakeLists.txt文件里的内容。

# 设置版本信息
set(MY_PRODUCT_NUMBER 1)
set(MY_PRODUCT_VERSION 0)

string(TIMESTAMP COMPILE_TIME %m%d%H%M)
set(build_time ${COMPILE_TIME})
set(MY_BUILD_NUMBER ${build_time})

...

ADD_LIBRARY(detectionapi SHARED ${INCLUDES} ${SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/version.rc)

为了图方便,这里在版本号的第三位,使用编译时当前的时间信息自动生成。

string(TIMESTAMP COMPILE_TIME %m%d%H%M)
set(build_time ${COMPILE_TIME})
set(MY_BUILD_NUMBER ${build_time})

编译

可以在build/目录下通过cmake ..命令,或者通过CMake-GUI编译到build/目录下。
然后在build/下通过make命令执行生成的Makefile脚本,生成可执行程序和.dll文件。
最后通过make install在指定位置/lib生成.dll文件。

完成后新的目录结构为:

│  CMakeLists.txt
│
├─build
│  ├─bin
│  │       detection.exe
│  ├─CMakeFiles
│  ├─lib
│  │       detectionapi.dll
│  │       libdetectionapi.dll.a
│  └─Makefile
│
├─examples
│      test_api.cpp
│          
├─include
│  └─detectionapi
│          DetectionApi.h
│          
├─lib
│      detectionapi.dll
│      libdetectionapi.dll.a
│      libopencv_calib3d450.dll
│      libopencv_core450.dll
│      
└─src
       DetectionApi.cpp

测试

生成的可执行程序位于build/bin下,将所有需要的第三方dll库放在同级目录下,并将DetectionApi.dll也放于此。双击.exe,成功运行。
完成dll封装,给其他开发人员调用时,就只需要提供.h和.dll了,非常的方便。
如果生成了版本信息的话,可以查看一下dll库的属性——详细信息。如果生成正确的话,文件版本和产品版本都可以看到了。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐