在Android Studio 2.2开始,正式支持cmake编译,在与android studio结合之前,cmake就已经作为一个广泛使用的构建系统,应用在许多项目中。通过cmake与ndk,我们可以将c/c++源码编译成动/静态库、可执行程序等,非常的方便。
认识CMake
在使用cmake之前,我们需要先了解一下cmake,最直接的了解方式是官网。当然还有tutorial最好需要看一下,这样你就能大概理解cmake的一些用法。如果你还不了解c/c++的编译过程,请自行百度学习,不在本文叙述范围内。
CMake的基本操作
在看过官网的资料和tutorial之后,我们需要动手实操一下,如果使用cmake进行编译,这样我们才能更好的掌握cmake的一些用法。对于我们后续cmake与ndk的结合有莫大的帮助。我们这里通过clion作为ide进行demo的学习。
创建可执行程序
# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)
# 工程名字 工程语言
project(cmakedemo C)
# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)
# 将main.c文件加入到可执行程序cmakedemo中
add_executable(cmakedemo main.c)
add_executable
第一个参数是生成可执行程序名称,第二个参数是源码文件,如果有多个源码文件,依次加入,用空格隔开。最后会生成一个cmakedemo
可执行程序。当然,我们日常使用过程中,更多的是将源码编译成动/静态库。
目录结构如下:
多源文件编译
# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)
# 工程名字 工程语言
project(cmakedemo C)
# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)
# 方法一:将多个文件加入到可执行程序中编译
# 将main.c MathFunctions.c文件加入到可执行程序cmakedemo中
#add_executable(cmakedemo2 main.c MathFunctions.c)
# 方法二:采用目录形式
# 将当前目录下的文件,都保存在DIR_SRCS变量中
aux_source_directory(. DIR_SRCS)
# 将变量代表的文件路径加入到可执行程序中编译
add_executable(cmakedemo2 ${DIR_SRCS})
在CMakeLists.txt
文件中,我们有两种方式将多个源码文件加入到编译,方法一将MathFunction.c
文件放在add_executable
最后,并用空格隔开。方法二中,我们用了一个aux_source_directory
,第一个参数表示搜寻的目录,第二个参数DIR_SRCS
表示将目录下的文件,表示成变量,并在下面应用。最后在add_executable
中加入给变量${DIR_SRCS}
。
目录结构如下:
多级目录
我们源码的目录结构,不会前两个例子中,都在同一级目录下,经常我们的源码有多级目录。这时候我们应该怎么编译呢?多级目录我们也有两种编译方式。
# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)
# 工程名字 工程语言
project(cmakedemo C)
# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)
## 方法一:
## 将当前目录下的文件,都保存在DIR_SRCS变量中
#aux_source_directory(. DIR_SRCS)
## 将math目录下的文件,都保存在DIR_MATH_SRCS变量中
#aux_source_directory(./math DIR_MATH_SRCS)
## 将变量代表的文件路径加入到可执行程序中编译
#add_executable(cmakedemo3 ${DIR_SRCS} ${DIR_MATH_SRCS})
# 方法二:
# 将当前目录下的文件,都保存在DIR_SRCS变量中
aux_source_directory(. DIR_SRCS)
# 将math目录加入编译
add_subdirectory(math)
# 将变量代表的文件路径加入到可执行程序中编译
add_executable(cmakedemo3 ${DIR_SRCS})
# 添加链接库
target_link_libraries(cmakedemo3 MathFunctions)
## 方法三:
## 将当前目录下的源码,都保存在DIR_SRCS变量中
#aux_source_directory(. DIR_SRCS)
## 将math目录下的文件,都保存在DIR_MATH_SRCS变量中
#aux_source_directory(./math DIR_MATH_SRCS)
## 将DIR_MATH_SRCS保存的文件,都编译进入静态库libMathFunctions.a
#add_library(MathFunctions ${DIR_MATH_SRCS})
## 将变量代表的文件路径加入到可执行程序中编译
#add_executable(cmakedemo3 ${DIR_SRCS})
## 添加链接库
#target_link_libraries(cmakedemo3 MathFunctions)
方法一将来自目录中的源文件,保存成DIR_MATH_SRCS
变量,然后在add_executable中应用即可,这样就把多级目录下的源码都加入到构建中。方法二使用add_subdirectory
将math
子目录加入编译,这时候math
中的CMakeLists.txt
文件和源码也将作为一个编译子目录进行处理。target_link_libraries
指定cmakedemo3
可执行程序将链接MathFunctions
库,MathFunctions
库将在math子目录中生成。
在math
子目录CMakeLists.txt
如下:
aux_source_directory(. DIR_MATH_SRCS)
add_library(MathFunctions ${DIR_MATH_SRCS})
add_library
表示将默认生成libMathFunctions.a
静态库。
目录结构如下:
方法三是将子目录中的源码,都编译成libMathFuntions.a
,最后同样的将静态库链接到目标可执行程序中,与方法二的区别是在通过一个CMakeLists.txt文件,就可以将静态库的编译包含在内,无需像方法二一样在./math
目录下写一份CMakeLists.txt文件用于专门编译静态库。方法二和方法三各自有各自的好处,方法二更适合单模块编译,可以将某个目录下的源文件作为一个模块来编译,适合庞大的目录结构与模块层级编译。
自定义编译选项
# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)
# 工程名字 工程语言
project(cmakedemo C)
# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)
# 加入一个配置头文件,用于处理 CMake 对源码的设置
configure_file(
"${PROJECT_SOURCE_DIR}/config.h.in"
"${PROJECT_BINARY_DIR}/config.h"
)
# 设置USE_LOCALMATH打开
option(USE_LOCALMATH "TRUE USE LOCAL MATH LIBRARY" OFF)
if (USE_LOCALMATH)
include_directories("${PROJECT_SOURCE_DIR}/math")
add_subdirectory(math)
endif (USE_LOCALMATH)
aux_source_directory(. DIR_SRCS)
# 将变量代表的文件路径加入到可执行程序中编译
add_executable(cmakedemo4 ${DIR_SRCS})
# 添加链接库
if (USE_LOCALMATH)
target_link_libraries(cmakedemo4 MathFunctions)
endif (USE_LOCALMATH)
在这里我们加入了一个config.h.in
文件,这个文件,主要用来预定义宏,通过config.h.in
,在编译之后可以生成config.h
文件。config.h.in
文件内容如下:
#cmakedefine USE_LOCALMATH
这里我们还使用到了option
,主要是为了在进行编译时,在CMakeLists.txt同级目录下,通过ccmake .
,来进行USE_LOCALMATH变量的选择,是否打开
我们看到,最后有一个
USE_LOCALMATH
变量,可以用过enter键来选择ON或者OFF,如果是ON,那么在生成的config.h中,预定义宏被打开,如下:
#define USE_LOCALMATH
在main.c
中,我们就能够使用该宏定义了
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "config.h"
#ifdef USE_LOCALMATH
#include "math/MathFunctions.h"
#endif
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("Usage: %s base exponent \n", argv[0]);
return 1;
}
double base = atof(argv[1]);
int exponent = atoi(argv[2]);
#ifdef USE_LOCALMATH
printf("Now we use our own Math library. \n");
double result = power(base, exponent);
#else
printf("Now we use the standard library. \n");
double result = pow(base, exponent);
#endif
printf("%g ^ %d is %g\n", base, exponent, result);
return 0;
}
目录结构如下:
环境检查
我们有时候需要在编译过程中,检查系统的环境,是否支持某些函数,这个例子中,我们检查是否编译环境自带pow函数,如果自带pow函数,则使用pow函数,如果没有则使用自定义的power函数。
# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)
# 工程名字 工程语言
project(cmakedemo C)
# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)
# 检查系统是否支持 pow 函数
include(${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
# 如果pow函数存在,则定义HAVE_POW宏,这个宏可以在下面的if条件中使用,也可以在config.h.in中预定义cmakedefine HAVE_POW
check_function_exists(pow HAVE_POW)
# 加入一个配置头文件,用于处理 CMake 对源码的设置
configure_file(
"${PROJECT_SOURCE_DIR}/config.h.in"
"${PROJECT_BINARY_DIR}/config.h"
)
# 如果宏未定义,则引入自定义的power函数
if (!HAVE_POW)
include_directories("${PROJECT_SOURCE_DIR}/math")
add_subdirectory(math)
endif (!HAVE_POW)
aux_source_directory(. DIR_SRCS)
# 将变量代表的文件路径加入到可执行程序中编译
add_executable(cmakedemo6 ${DIR_SRCS})
# 添加链接库
if (!HAVE_POW)
target_link_libraries(cmakedemo4 MathFunctions)
endif (!HAVE_POW)
首先在顶层 CMakeLists.txt
文件中添加 CheckFunctionExists.cmake
宏,并调用 check_function_exists
命令测试链接器是否能够在链接阶段找到 pow 函数。如果找到pow函数,则定义HAVE_POW
宏,当然,在config.h.in
中需要预定义HAVE_POW
宏,如下:
#cmakedefine USE_LOCALMATH
#cmakedefine HAVE_POW
#cmakedefine HAVE_LOCALPOWER
随后在生成的config.h
中,就会定义上该宏,如下:
/* #undef USE_LOCALMATH */
#define HAVE_POW
/* #undef HAVE_LOCALPOWER */
这时候,就可以在源码中使用宏了。
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "config.h"
#ifdef USE_LOCALMATH
#include "math/MathFunctions.h"
#endif
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("Usage: %s base exponent \n", argv[0]);
return 1;
}
double base = atof(argv[1]);
int exponent = atoi(argv[2]);
#ifndef HAVE_POW
printf("Now we use our own Math library. \n");
double result = power(base, exponent);
#else
printf("Now we use the standard library. \n");
double result = pow(base, exponent);
#endif
printf("%g ^ %d is %g\n", base, exponent, result);
#ifdef HAVE_LOCALPOWER
printf("HAVE_LOCALPOWER . \n");
#elif defined(HAVE_POW)
printf("HAVE_POW . \n");
#endif
return 0;
}
这里有个注意点:check_function_exists
需要在configure_file定义config.h.in
之前调用,否则对于config.h.in
中预定义的宏无效。同样的,在CMakeLists.txt
中也能够使用HAVE_POW
宏来判断是否引入math
目录,是否将math
作为子目录加入编译,最后是否链接MathFunctions
静态库。
添加版本号
在应用程序中,维护库或者可执行程序的版本号是一个好的习惯,配合changelog,能够很直观的看到库的更新迭代过程。在cmake中我们怎样添加版本号管理呢?
# 要求最低的cmake版本
cmake_minimum_required(VERSION 3.14)
# 工程名字 工程语言
project(cmakedemo C)
# 设置cmake c 的标准c99
set(CMAKE_C_STANDARD 99)
set(VERSION_MAJOR 1)
set(VERSION_MINOR 0)
在config.h.in
文件中,添加预定义
#define VERSION_MAJOR @VERSION_MAJOR@
#define VERSION_MINOR @VERSION_MINOR@
这样,在生成的config.h
文件中就有VERSION_MAJOR
与VERSION_MINOR
的定义,在代码中使用如下:
printf("major version %d , minor version %d \n", VERSION_MAJOR, VERSION_MINOR);
编译动静态库
上面我们都是生成可执行程序,如果我们想要生成动态库或者静态库应该怎么做呢?
动态库
aux_source_directory(. DIR_MATH_SRCS)
add_library(MathFunctions SHARED ${DIR_MATH_SRCS})
生成动态库如下:
静态库
aux_source_directory(. DIR_MATH_SRCS)
add_library(MathFunctions STATIC ${DIR_MATH_SRCS})
生成静态库如下:
主要区别是在
add_library
时指定STATIC/SHARED
参数即可。
基本操作总结
通过以上基本操作,我们了解了如何生成可执行程序,生成动/静态库,如何添加版本号、如何进行环境检查、如何预定义宏、如何对多级目录进行编译。对于cmake,我们已经有了一个大概的了解,后续继续讲一下在android中如何与cmake配合使用,来完成我们的目标。
CMake与Android
在android平台中,系统已经为我们内置了很多的原生api供我们链接调用,不同的系统api,android为我们提供了不同的库,具体可以参考Android NDK 原生 API。这些预构建的库,已经存在在android平台上了,我们无需将他们打包到apk中,因为NDK库已经是cmake搜索路径的一部分,所以找到提供库的名字,链接到所需库即可。那我们要怎么做才能使用这些库呢?
find_library用法
添加find_library()
命令到你的cmake构建脚本用于定位ndk库路径,并且将路径存储变量中。你可以在脚本的其他地方使用这个变量,下面例子是查找android平台的log库,将路径存储在log-lib
变量中。
find_library( # Defines the name of the path variable that stores the
# location of the NDK library.
log-lib
# Specifies the name of the NDK library that
# CMake needs to locate.
log )
接下来我们需要将ndk库,链接到我们的目标程序或者目标库中:
# Links your native library against one or more other native libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the log library to the target library.
${log-lib} )
这里target_link_libraries
含义是将${log-lib} 路径的ndk库,链接到native-lib.so中
添加预构建的动态库
添加一个预先构建的库,类似于为CMake指定另一个本地构建库。然而因为库已经预构建,你需要使用IMPORTED
告诉cmake,你需要引入库到你的构建工程中。
add_library( imported-lib
SHARED
IMPORTED )
这里只是指定了引入一个动态库,并且动态库名称存储在本地变量imported-lib
中。接着需要设置该动态库imported-lib
的属性,首先指定具体库的位置
set_target_properties( # Specifies the target library.
imported-lib
# Specifies the parameter you want to define.
PROPERTIES IMPORTED_LOCATION
# Provides the path to the library you want to import.
imported-lib/src/${ANDROID_ABI}/libimported-lib.so )
这里set_target_properties()
定义了一个imported-lib
库的属性IMPORTED_LOCATION,指定了该库的在本地操作系统中的位置,这样,结合上面add_library
我们就完整的在cmake中引入了一个动态库,并且存储在imported-lib本地变量中,待后续使用。
当然,我们引入了动态库还不够,编译时,经常还需要用到动态库的头文件,那么头文件该怎么引入呢?
include_directories(imported-lib/include/)
include_directories
中是头文件在操作系统中的相对路径或者绝对路径,相对路径是对于当前CMakeLists.txt的位置而定。
添加预构建的静态库
添加预构建的静态库,与动态库类似,只是在add_library
和set_target_properties
中有所不同
add_library(imported-static-lib STATIC IMPORTED)
set_target_properties(imported-static-lib PROPERTIES IMPORTED_LOCATION imported-lib/src/${ANDROID_ABI}/libimported-lib.a)
主要区别是静态库在add_library
中是STATIC,而动态库是SHARED,静态库会编译进目标动态库中,而动态库,最后编译完apk后,通过APK Analyzer查看,在apk的lib/${ANDROID_ABI}/目录下,有你所链接的动态库。
编译过程构建静态库
在编译过程中,可能会存在整个c工程会很庞大,例如笔者目前工作中的一个工程源码就很庞大,有多个不同的模块,组件,多级目录。那这种情况下我们可以将某些组件,先编译成静态库,然后将静态库参与最终目标动态库的编译。参考CMake基本操作->多级目录章节,有三种方法可以参考。
多工程编译
多工程编译类似于CMake基本操作->多级目录章节中的方法二,这里就不重新讲。参考示例如下:
# Sets lib_src_DIR to the path of the target CMake project.
set( lib_src_DIR ../gmath )
# Sets lib_build_DIR to the path of the desired output directory.
set( lib_build_DIR ../gmath/outputs )
file(MAKE_DIRECTORY ${lib_build_DIR})
# Adds the CMakeLists.txt file located in the specified directory
# as a build dependency.
add_subdirectory( # Specifies the directory of the CMakeLists.txt file.
${lib_src_DIR}
# Specifies the directory for the build outputs.
${lib_build_DIR} )
# Adds the output of the additional CMake build as a prebuilt static
# library and names it lib_gmath.
add_library( lib_gmath STATIC IMPORTED )
set_target_properties( lib_gmath PROPERTIES IMPORTED_LOCATION
${lib_build_DIR}/${ANDROID_ABI}/lib_gmath.a )
include_directories( ${lib_src_DIR}/include )
# Links the top-level CMake build output against lib_gmath.
target_link_libraries( native-lib ... lib_gmath )
CMake与Android结合总结
本章主要讲解了cmake与android和结合,如何在android中使用cmake,cmake如何使用android平台自带的系统库,构建动/静态库的过程,以及多工程编译,这里已经基本满足我们日常NDK开发过程中遇到的大部分情况。
CMake与Gradle
未完待续......