- 2024-11-02
-
发表了主题帖:
CMake构建实战读书笔记07-手写数字识别项目实践与分析
本篇,来学习《CMake构建实战》的实践项目——基于onnxruntime的手写数字识别库。
上篇文章,先介绍过了对onnxruntime库中文件的查找,可以回顾下上篇内容。
# 1 项目结构
项目的功能,是利用mnist.onnx手写数字识别库,来实现对图片中手写数字的识别。
- num_recohnizer.cpp、num_recohnizer.h:实现一些对mnist库的初始化与识别调用接口
- recognize.c:编写一个main函数来实现对待识别图片的参数接收、对识别接口的调用,以及识别结果的打印
- Findlibpng.cmake、Findonnxruntime.cmake:辅助CMake来查找libpng库和onnx库中的功能模块
- CMakeLists.txt:对整个项目进行编译构建的组织文件
# 2 安装zlib和linpng
此项目需要用到zlib和linpng库,因此需要先安装这两个库。
## 2.1 安装zlib
去github仓库 https://github.com/madler/zlib 下载zlib的源码,然后编译安装:
```sh
cd zlib
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --config Release
sudo cmake --install .
```
## 2.2 安装libpng
去github仓库 https://github.com/pnggroup/libpng 下载libpng的源码,然后编译安装:
```sh
cd libpng
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --config Release
sudo cmake --install .
```
## 2.3 安装onnx
上篇文章已介绍过。
# 3 升级CMake版本
之前通过Ubuntu的apt install安装的CMake的默认版本是3.16.3,按照书中步骤,编译该项目时,首先会提示版本不够,要3.20以上的。
如果直接修改项目代码中对CMake版本的要求,又会报其它错误。
为了能顺利编译项目,就选择了重新安装更高版本的CMake。
可以先确认下当前的CMake版本,然后通过apt remove指令来卸载CMake,并确认卸载完成:
然后下载更高版本的CMake源码,进行编译安装,目前安装的是3.30.5版本:
# 4 项目代码分析
在编译代码前,可以先来分析下项目代码。
## 4.1 num_recognizer
实现一些对mnist库的初始化与识别调用接口。
### 4.1.1 num_recognizer.h
头文件是声明一些对mnist库进行操作的接口。
这里只列举其中定义的接口名称
```h
//! @brief 初始化手写数字识别库
NUM_RECOGNIZER_EXPORT void num_recognizer_init();
//! @brief 创建识别器
NUM_RECOGNIZER_EXPORT void num_recognizer_create(const char *model_path,
Recognizer **out_recognizer);
//! @brief 析构识别器
NUM_RECOGNIZER_EXPORT void num_recognizer_delete(Recognizer *recognizer);
//! @brief 识别图片数据中的手写数字
NUM_RECOGNIZER_EXPORT int num_recognizer_recognize(Recognizer *recognizer,
float *input_image,
int *result);
//! @brief 识别PNG图片中的手写数字
NUM_RECOGNIZER_EXPORT int num_recognizer_recognize_png(Recognizer *recognizer,
const char *png_path,
int *result);
```
### 4.1.2 num_recognizer.cpp
cpp文件是具体实现的内容,这里定义了输入图片的宽高要求(28x28像素)。
- num_recognizer_recognize_png函数实现对png图片的读取和预处理
- num_recognizer_init和num_recognizer_create来初始化和创建一个识别器
- num_recognizer_recognize函数通过创建的识别器来进行数字识别
```c++
static const char *INPUT_NAMES[] = {"Input3"}; // 模型输入参数名
static const char *OUTPUT_NAMES[] = {"Plus214_Output_0"}; // 模型输出参数名
static constexpr int64_t INPUT_WIDTH = 28; // 模型输入图片宽度
static constexpr int64_t INPUT_HEIGHT = 28; // 模型输入图片高度
static const std::array input_shape{
1, 1, INPUT_WIDTH, INPUT_HEIGHT}; // 输入数据的形状(各维度大小)
static const std::array output_shape{
1, 10}; // 输出数据的形状(各维度大小)
static Ort::Env env{nullptr}; // onnxruntime环境
static Ort::MemoryInfo memory_info{nullptr}; // onnxruntime内存信息
//! @brief 手写数字识别类
struct Recognizer
{
Ort::Session session; //! @brief onnxruntime会话
};
//! @brief 将byte类型的颜色值转换为模型接受的二值化后的float类型数值
static float byte2float(png_byte b) { return b < 128 ? 1 : 0; }
//! @brief 获取png图片指定像素的二值化后的float类型颜色值
static float get_float_color_in_png(unsigned int x, unsigned int y,
png_uint_32 png_width,
png_uint_32 png_height, png_byte color_type,
png_bytepp png_data)
{
if (x >= png_width || x < 0)
return 0;
if (y >= png_height || y < 0)
return 0;
switch (color_type)
{
case PNG_COLOR_TYPE_RGB:
{
auto p = png_data[y] + x * 3;
return byte2float((p[0] + p[1] + p[2]) / 3);
} break;
case PNG_COLOR_TYPE_RGBA:
{
auto p = png_data[y] + x * 4;
return byte2float((p[0] + p[1] + p[2]) * p[3] / 3);
} break;
default:
return 0;
}
};
void num_recognizer_init()
{
env = Ort::Env{static_cast(nullptr)};
memory_info = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
}
void num_recognizer_create(const char *model_path, Recognizer **out_recognizer)
{
Ort::Session session{nullptr};
session = Ort::Session(env, model_path, Ort::SessionOptions(nullptr));
*out_recognizer = new Recognizer{std::move(session)};
}
void num_recognizer_delete(Recognizer *recognizer)
{
delete recognizer;
}
int num_recognizer_recognize(Recognizer *recognizer, float *input_image, int *result)
{
std::array results{};
auto input_tensor = Ort::Value::CreateTensor(
memory_info, input_image, INPUT_WIDTH * INPUT_HEIGHT,
input_shape.data(), input_shape.size());
auto output_tensor = Ort::Value::CreateTensor(
memory_info, results.data(), results.size(), output_shape.data(),
output_shape.size());
recognizer->session.Run(Ort::RunOptions{nullptr}, INPUT_NAMES,
&input_tensor, 1, OUTPUT_NAMES, &output_tensor, 1);
*result = static_cast(std::distance(
results.begin(), std::max_element(results.begin(), results.end())));
return 0;
}
int num_recognizer_recognize_png(Recognizer *recognizer, const char *png_path, int *result)
{
int ret = 0;
std::array input_image;
FILE *fp;
unsigned char header[8];
png_structp png_ptr;
png_infop info_ptr;
png_uint_32 png_width, png_height;
png_byte color_type;
png_bytep *png_data;
// 打开PNG图片文件
fp = fopen(png_path, "rb");
if (!fp) {
ret = -2;
goto exit3;
}
// 读取PNG图片文件头
fread(header, 1, 8, fp);
// 验证文件头确实是PNG格式的文件头
if (png_sig_cmp(reinterpret_cast(header), 0, 8)) {
ret = -3;
goto exit2;
}
// 创建PNG指针数据结构
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr,
nullptr);
if (!png_ptr) {
ret = -4;
goto exit2;
}
// 创建PNG信息指针数据结构
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
ret = -5;
goto exit2;
}
// 设置跳转以处理异常
if (setjmp(png_jmpbuf(png_ptr))) {
ret = -6;
goto exit2;
}
// 初始化PNG文件
png_init_io(png_ptr, fp);
png_set_sig_bytes(png_ptr, 8);
// 读取PNG信息
png_read_info(png_ptr, info_ptr);
png_width = png_get_image_width(png_ptr, info_ptr); // PNG图片宽度
png_height = png_get_image_height(png_ptr, info_ptr); // PNG图片高度
color_type = png_get_color_type(png_ptr, info_ptr); // PNG图片颜色类型
// 设置跳转以处理异常
if (setjmp(png_jmpbuf(png_ptr))) {
ret = -7;
goto exit2;
}
// 读取PNG的数据
png_data = (png_bytep *)malloc(sizeof(png_bytep) * png_height);
for (unsigned int y = 0; y < png_height; ++y) {
png_data[y] = (png_byte *)malloc(png_get_rowbytes(png_ptr, info_ptr));
}
png_read_image(png_ptr, png_data);
// 将PNG图片重新采样,缩放到模型接受的输入图片大小
for (unsigned int y = 0; y < INPUT_HEIGHT; ++y) {
for (unsigned int x = 0; x < INPUT_WIDTH; ++x) {
float res = 0;
int n = 0;
for (unsigned int png_y = y * png_height / INPUT_HEIGHT;
png_y < (y + 1) * png_height / INPUT_HEIGHT; ++png_y) {
for (unsigned int png_x = x * png_width / INPUT_WIDTH;
png_x < (x + 1) * png_width / INPUT_WIDTH; ++png_x) {
res += get_float_color_in_png(png_x, png_y, png_width,
png_height, color_type,
png_data);
++n;
}
}
input_image[y * INPUT_HEIGHT + x] = res / n;
}
}
// 识别图片数据中的手写数字
ret = num_recognizer_recognize(recognizer, input_image.data(), result);
exit1:
// 释放存放PNG图片数据的内存空间
for (unsigned int y = 0; y < png_height; ++y) {
free(png_data[y]);
}
free(png_data);
exit2:
// 关闭文件
fclose(fp);
exit3:
return ret;
}
```
## 4.2 recognize.c
编写一个main函数来实现对待识别图片的参数接收、对识别接口的调用,以及识别结果的打印。
因为是在命令中实现对手写图片的识别,该代码文件也可能称作一个命令行工具。
该代码的功能是通过命令行参数,传入要识别的图片,然后初始化并创建一个识别器,调用num_recognizer_recognize_png来进行数字识别,识别的结果在命令行中打印出来。
```c
#include
#include
int main(int argc, const char **argv) {
// 检查命令行参数个数是否为3个
if (argc != 3) {
printf("Usage: recognize mnist.onnx 3.png\n");
return -1; // 返回错误码-1
}
int ret = 0; // 返回码
int result = -1; // 识别结果
Recognizer *recognizer; // 识别器指针
num_recognizer_init(); // 初始化识别器
// 使用模型文件创建识别器,argv[1]即模型文件路径
num_recognizer_create(argv[1], &recognizer);
// 识别图片文件中的手写数字,argv[2]即图片文件路径
if (ret = num_recognizer_recognize_png(recognizer, argv[2], &result)) {
// 返回值非0,识别过程发生错误
printf("Failed to recognize\n");
goto exit_main;
}
printf("%d\n", result); // 输出识别结果
exit_main:
num_recognizer_delete(recognizer); // 析构识别器
return ret; // 返回正常退出的返回码0
}
```
## 4.3 CMakeLists.txt
最后开看下CMakeLists.txt文件,开头是指定CMake的最低版本和项目名称
然后通过set指令设置一些变量:
- 设置CMAKE_MODULE_PATH
- 设置CMAKE_CXX_STANDARD为C++11标准
- 设置onnx_version版本
- 设置onnxruntime_ROOT的目录
然后通过find_package查找要用的库
- 查找onnxruntime
- 查找libpng
接着构建num_recognizer动态库目标
- add_library创建动态库目标
- 引用GenerateExportHeader模块,调用其generate_export_header命令来为动态库生成导出头文件
- set_target_properties设置该动态库的两个属性:默认隐藏符号并仅导出显式指定的符号
最后是构建可执行文件recognize
- add_executable创建目标
- target_link_libraries指定要链接的库
```cmake
cmake_minimum_required(VERSION 3.20)
project(num_recognizer)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
set(CMAKE_CXX_STANDARD 11) # 设置C++标准为11
set(onnx_version 1.10.0) # 根据下载的版本进行设置,本例使用1.10.0版本
# 请下载onnxruntime库的压缩包,并解压至该目录中
if("$ENV{onnxruntime_ROOT}" STREQUAL "")
if(WIN32)
set(ENV{onnxruntime_ROOT} "${CMAKE_CURRENT_LIST_DIR}/onnxruntime-win-x64-${onnx_version}")
elseif(APPLE)
set(ENV{onnxruntime_ROOT} "${CMAKE_CURRENT_LIST_DIR}/onnxruntime-osx-universal2-${onnx_version}")
else()
set(ENV{onnxruntime_ROOT} "${CMAKE_CURRENT_LIST_DIR}/onnxruntime-linux-x64-${onnx_version}")
endif()
endif()
find_package(onnxruntime 1.10 REQUIRED) # 指定依赖的最小版本
find_package(libpng REQUIRED)
add_library(num_recognizer SHARED src/num_recognizer.cpp)
include(GenerateExportHeader)
generate_export_header(num_recognizer)
set_target_properties(num_recognizer PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN 1
)
target_include_directories(num_recognizer PUBLIC include ${CMAKE_BINARY_DIR})
target_link_libraries(num_recognizer PRIVATE onnxruntime::onnxruntime png_shared)
target_compile_definitions(num_recognizer PRIVATE ORT_NO_EXCEPTIONS num_recognizer_EXPORTS)
add_executable(recognize cli/recognize.c)
target_link_libraries(recognize PRIVATE num_recognizer)
```
# 5 项目运行测试
编译指令如下:
```sh
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build . --config Debug
```
编译完成之后,会生成一个recognize可执行文件,然后调用该文件即可来进行手写数字图片的数字识别:
```sh
./build/recognize ./models/mnist.onnx 2.png
```
可以看到,正确的识别到了图片中的数字2、3、4。
# 6 总结
本篇学习了《CMake构建实战》的实践项目——基于onnxruntime的手写数字识别库,首先进行编译环境的配置,包括依赖库的安装,CMake版本的升级等,然后对项目代码进行了分析,最后进行项目的编译运行测试。
- 2024-10-27
-
发表了主题帖:
CMake构建实战读书笔记06-onnx查找模块
本篇来学习CMake的查找功能,并以onnxruntime库为例,来查找需要的文件。
涉及到的查找指令有:
- find_path:查找路径
- find_library:查找库文件
- find_file:查找文件
- find_package_handle_standard_args:用于在自定义模块中判断结果变量是否都已正确赋值
# 1 onnxruntime简介与下载
## 1.1 onnxruntime简介
ONNX Runtime 是一个跨平台的推理和训练机器学习加速器。
ONNX 运行时推理可以实现更快的客户体验并降低成本,支持来自深度学习框架(如 PyTorch 和 TensorFlow/Keras)的模型,以及经典的机器学习库(如 scikit-learn、LightGBM、XGBoost 等)。ONNX 运行时与不同的硬件、驱动程序和操作系统兼容,并通过利用硬件加速器(如果适用)以及图形优化和转换来提供最佳性能。
ONNX 运行时训练可以通过为现有 PyTorch 训练脚本添加一行代码,从而加快转换器模型的多节点 NVIDIA GPU 上的模型训练时间。
## 1.2 onnxruntime下载
在GitHub中onnxruntime项目的Release页面(https://github.com/microsoft/onnxruntime/releases)下载预编译包,我的是在Ununtu中测试,因此下载Linux版本的包。
下载后解压要测试代码的目录即可。
# 2 代码分析
## 2.1 Findonnxruntime.cmake
这里将此cmake分成两部分来分析,先来看第一部分:
```cmake
find_path(onnxruntime_INCLUDE_DIR onnxruntime_c_api.h
HINTS ENV onnxruntime_ROOT
PATH_SUFFIXES include)
find_library(onnxruntime_LIBRARY
NAMES onnxruntime
HINTS ENV onnxruntime_ROOT
PATH_SUFFIXES lib)
find_file(onnxruntime_VERSION_FILE VERSION_NUMBER
HINTS ENV onnxruntime_ROOT)
if(onnxruntime_VERSION_FILE)
file(STRINGS ${onnxruntime_VERSION_FILE} onnxruntime_VERSION LIMIT_COUNT 1)
endif()
```
- find_path来查找onnxruntime库中的onnxruntime_c_api.h文件路径,并将查找结果存入onnxruntime_INCLUDE_DIR缓存变量中;环境变量onnxruntime_ROOT的值作为候选路径,include目录作为查找子目录
- find_library来查找onnxruntime库中的onnxruntime库文件,并将查找结果存入onnxruntime_LIBRARY缓存变量中;环境变量onnxruntime_ROOT的值作为候选路径,lib目录作为查找子目录
- find_file来查找onnxruntime库的版本号。版本号是在一个名为VERSION_NUMBER的文件中的,将查找的结果存入onnxruntime_VERSION_FILE缓存变量中;onnxruntime_ROOT的值作为候选路径
如果找到了版本号,再通过file指令,来读取其中的版本值,限制读取的行数为1,并将其保存在onnxruntime_VERSION缓存变量中
再来看第二部分:
```cmake
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(onnxruntime
REQUIRED_VARS onnxruntime_LIBRARY onnxruntime_INCLUDE_DIR
VERSION_VAR onnxruntime_VERSION
HANDLE_VERSION_RANGE)
if(onnxruntime_FOUND)
set(onnxruntime_INCLUDE_DIRS ${onnxruntime_INCLUDE_DIR})
set(onnxruntime_LIBRARIES ${onnxruntime_LIBRARY})
add_library(onnxruntime::onnxruntime SHARED IMPORTED)
target_include_directories(onnxruntime::onnxruntime INTERFACE ${onnxruntime_INCLUDE_DIRS})
if(WIN32)
set_target_properties(onnxruntime::onnxruntime PROPERTIES
IMPORTED_IMPLIB "${onnxruntime_LIBRARY}")
else()
set_target_properties(onnxruntime::onnxruntime PROPERTIES
IMPORTED_LOCATION "${onnxruntime_LIBRARY}")
endif()
endif()
```
FindPackageHandleStandardArgs是一个CMake预置的功能模块,通过find_package_handle_standard_args指令,来检查模块中的结果变量是否已经被正确赋值。
- 第一个参数onnxruntime是软件包名
- REQUIRED_VARS是待检查的变量,这里要检查onnxruntime_LIBRARY和onnxruntime_INCLUDE_DIR,即库文件和头文件
- VERSION_VAR是检测版本号,这里要检查onnxruntime_VERSION;若要检查版本号是否满足区间形式,加上参数HANDLE_VERSION_RANGE
经过检查之后,onnxruntime_FOUND会根据检查结果,被赋值为真或假。若为真:
- 设置onnxruntime_INCLUDE_DIRS变量为查找到的头文件目录
- 设置onnxruntime_LIBRARIES变量为查找到的库文件目录
- 调用add_library命令为项目添加动态库
- 调用target_include_directories命令为项目指定头文件目录
- 调用set_target_properties命令设置linux中动态库的导入属性为导致动态库文件自身
## 2.2 main.cpp
测试onnxruntime运行环境是否可用的代码如下:
```cpp
#include
int main() {
Ort::Env env;
Ort::Session session(env, ORT_TSTR(""), Ort::SessionOptions(nullptr));
return 0;
}
```
测试的内容仅仅是从一个非法的空路径中加载模型文件。
## 2.3 CMakeLists.txt
下面来看下用于构建项目的CMakeLists.txt:
```cmake
cmake_minimum_required(VERSION 3.20)
project(find-onnxruntime)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR};${CMAKE_MODULE_PATH}")
set(CMAKE_CXX_STANDARD 11) # 设置C++标准为11
set(onnx_version 1.10.0) # 根据下载的版本进行设置,本例使用1.10.0版本
# 请下载onnxruntime库的压缩包,并解压至该目录中
if("$ENV{onnxruntime_ROOT}" STREQUAL "")
if(WIN32)
set(ENV{onnxruntime_ROOT} "${CMAKE_CURRENT_LIST_DIR}/onnxruntime-win-x64-${onnx_version}")
elseif(APPLE)
set(ENV{onnxruntime_ROOT} "${CMAKE_CURRENT_LIST_DIR}/onnxruntime-osx-universal2-${onnx_version}")
else()
set(ENV{onnxruntime_ROOT} "${CMAKE_CURRENT_LIST_DIR}/onnxruntime-linux-x64-${onnx_version}")
endif()
endif()
find_package(onnxruntime 1.10) # 指定依赖的最小版本
add_executable(main main.cpp)
target_link_libraries(main onnxruntime::onnxruntime)
target_compile_definitions(main PRIVATE ORT_NO_EXCEPTIONS)
```
开头是指定CMake的最低版本和项目名称
然后通过set指令设置一些变量:
- 设置CMAKE_MODULE_PATH
- 设置CMAKE_CXX_STANDARD为C++11标准
- 设置onnx_version版本
- 设置onnxruntime_ROOT的目录
然后查找版本1.10的onnxruntime包
调用add_executable添加目标,调用target_link_libraries来链接库
# 3 运行结果
在项目目录中新建一个build目录,通过cmake编译
```sh
mkdir build
cd build
camke ..
camke --build .
```
运行结果如下:
# 4 总结
本篇学习了CMake的查找功能,包括查找头文件、库文件、版本号等,并以onnxruntime库为例,来测试CMake的查找功能。
- 2024-10-20
-
回复了主题帖:
CMake构建实战读书笔记05-CMake项目构建实践
Jacktang 发表于 2024-10-20 08:56
修改库的名字是必须要用set_target_properties对库文件这个目标的属性进行修改吧
是的
- 2024-10-19
-
发表了主题帖:
CMake构建实战读书笔记05-CMake项目构建实践
本篇来学习使用CMake来构建项目。
# 1 编译单个源文件
## 1.1 基础CMake
c++源文件,hello.cpp
```c++
#include
int main()
{
printf("hello\n");
return 0;
}
```
CMakeLists.txt
```cmake
project(HELLO)
add_executable(hello ./hello.cpp)
```
这个是最基础的CMakeLists
- 第一行 project:该命令用于设置工程的名称,设置工程名称并不是强制性的,但是最好加上。
- 第二行 add_executable:该命令用于生成一个可执行文件
然后编译
```sh
cmake .
make
```
实际编译运行效果如下:
cmake先生成了MakeFile文件,然后使用make指令来编译出可执行文件,然后运行hello可执行文件,打印出hello。
## 1.2 在单独的build目录进行编译
上述测试过程中,cmake编译生成的文件与代码源文件混在一起了,不太好。
可以单独新建一个build目录进行编译,后续如果要清理代码功能,直接删除build目录即可。
```sh
mkdir build
cd build
cmake ..
make
```
运行效果如下:
可以看到编译生成的文件都在build目录中了。
# 2 编译多个文件
将打印hello函数从main函数中抽离出,单独编写一个cpp文件来打印hello。
hello.h
```c++
#pragma once
void hello();
```
hello.cpp
```cpp
#include "hello.h"
#include
void hello()
{
printf("hello\n");
}
```
main.cpp
```c++
#include "hello.h"
int main()
{
hello();
return 0;
}
```
CMakeLists.txt
```cmake
project(HELLO)
set(SRC_LIST main.cpp hello.cpp)
add_executable(hello ${SRC_LIST})
```
这里用到了set指令,经cpp文件的名称赋值给SRC_LIST变量。
运行效果如下:
可以看到多个文件的情况,最终也编译出了可执行文件,并能正常运行。
# 3 生成库文件
如果想要把hello函数功能编译成库文件,CMake中可以使用add_library指令来实现
## 3.1 静态库
CMakeLists.txt
```cmake
project(HELLO)
add_library(libhello hello.cpp)
add_executable(hello main.cpp)
target_link_libraries(hello libhello)
```
默认生成的是静态库,与add_library(libhello STATIC hello.c)的效果是一样的。
再通过target_link_libraries将libhello库文件链接到目标文件。
运行效果如下:
可以看到,生成了静态库liblibhello.a和可执行文件hello
## 3.2 动态库
编译动态库也是使用add_library指令,只需要再增加SHARED参数即可实现动态库的编译。
CMakeLists.txt
```cmake
project(HELLO)
add_library(libhello SHARED hello.cpp)
add_executable(hello main.cpp)
target_link_libraries(hello libhello)
```
运行效果如下:
可以看到,生成了动态库liblibhello.so和可执行文件hello
## 3.3 修改库的名字
刚才生成的静态库liblibhello.a和动态库liblibhello.so,名字上多了一个lib,看起来比较奇怪,可以使用set_target_properties对库文件这个目标的属性进行修改,进行重命名。
CMakeLists.txt
```cmake
project(HELLO)
add_library(libhello SHARED hello.cpp)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
add_executable(hello main.cpp)
target_link_libraries(hello libhello)
```
- set_target_properties:设置目标的属性,对libhello目标的OUTPUT_NAME属性进行了设置,将其设置为hello
运行效果如下:
## 3.4 将目标文件生成到指定目录
刚才生成的库文件和可执行文件,和编译生成的其它文件,都在build目标中,混杂在一起,也不太好。
可以修改LIBRARY_OUTPUT_PATH和EXECUTABLE_OUTPUT_PATH变量的值,来执行库文件和可执行文件的位置,例如分别放到当前目录下的lib目录和bin目录。
CMakeLists.txt
```cmake
project(HELLO)
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
add_library(libhello SHARED hello.cpp)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
add_executable(hello main.cpp)
target_link_libraries(hello libhello)
```
- set_target_properties:设置目标的属性,对libhello目标的OUTPUT_NAME属性进行了设置,将其设置为hello
运行效果如下:
# 4 总结
本篇介绍了使用CMake来构建项目,包括单文件、多文件项目的构建,以及静态库、动态库的生成等。
- 2024-10-12
-
回复了主题帖:
CMake构建实战读书笔记04-CMake快速排序
秦天qintian0303 发表于 2024-10-7 11:32
CMake编写这些经典程序会比较方便吗?
可以用来学习CMake语法
- 2024-10-07
-
发表了主题帖:
CMake构建实战读书笔记04-CMake快速排序
本篇来学习CMake构建实战的第5章:CMake快速排序。
第5章只有一个使用CMake编写的快速排序的脚本,我们就来分析下这个脚本。
# 1 CMake快速排序脚本分析
## 1.1 分区算法
定义一个partition函数,参数有:
- arr:输入的数组(或称列表),作为输入参数
- pivot:主元(或称分界值),作为输入参数
- left:比主元小的数据组成的数组,作为结果输出
- right:比主元大或相等的数据组成的数组,作为结果输出
这里使用set命令中的PARENT_SCOPE来实现将内部变量传递到外层
```cmake
function(partition arr pivot left right)
foreach(x ${arr})
if(${x} LESS ${pivot})
list(APPEND _left ${x})
else()
list(APPEND _right ${x})
endif()
endforeach()
set(${left} ${_left} PARENT_SCOPE)
set(${right} ${_right} PARENT_SCOPE)
endfunction()
```
## 1.2 快速排序函数
定义一个quick_sort函数,参数有:
- input:待排序的数组(或称列表),作为输入参数
- res:排序结果数组,作为结果输出
该函数中,首先会判断待排序的数组长度是否小于等于(LESS_EQUAL)1,如果是,则不需要排序了。
然后通过list的GET方法来获取待排序数组中的第一个元素作为主元,
然后通过SUBLIST来将第一个元素去除后得到新的数组,并对该数组带哦有分区算法函数,
将得到的主元左、右的子数组,通过递归调用的方式,分别得到其排序结果,
最后将左数组的排序结果,连接主元,再连接右数组的排序结果,即可得到最终原始数组的排序结果。
```cmake
function(quick_sort input res)
list(LENGTH input input_len)
if(${input_len} LESS_EQUAL 1)
set(${res} "${input}" PARENT_SCOPE)
return()
endif()
list(GET input 0 pivot)
list(SUBLIST input 1 -1 input)
partition("${input}" ${pivot} left right)
quick_sort("${left}" left)
quick_sort("${right}" right)
list(APPEND _res ${left} ${pivot} ${right})
set(${res} "${_res}" PARENT_SCOPE)
endfunction()
```
## 1.3 客户端代码
通过for循环来接收参数的方式来获取要排序的数据,这里数字4表示要忽略前面的4个参数,后面开始是要排序的数据。
然后调用quick_sort函数进行排序,并打印排序前后的结果:
```cmake
foreach(i RANGE 4 ${CMAKE_ARGC})
list(APPEND input ${CMAKE_ARGV${i}})
endforeach()
message("排序前: ${input}")
quick_sort("${input}" res)
message("排序后: ${res}")
```
测试结果如下:
# 2 改写成C++
也可以改写成C++代码来做下对比,这里使用std::vector存储排序的数组。
## 2.1 分区算法
也是4个参数,通过for循环,来将数据根据主元(pivot)分为左右两个部分
```c++
void partition(std::vector &arr, int pivot, std::vector &left, std::vector &right)
{
left.clear();
right.clear();
for (int &it : arr)
{
(it < pivot) ? left.push_back(it) : right.push_back(it);
}
}
```
## 2.2 快速排序
可以只有一个待排序数组作为参数,并将排序结果通过返回值返回
```c++
std::vector quick_sort(std::vector &input)
{
if(input.size()
- 2024-10-01
-
发表了主题帖:
CMake构建实战读书笔记03-CMake常用命令
CMake提供了很多很多命令,这么命令可以先做个大致了解,在需要用到的时候,可以单独研究完整的用法细节。
# 1 数值操作命令math
数值操作,即进行算术运算,CMake中不能直接使用运算符运算,需要通过math这个指令来实现运算,例如:
```cmake
math(EXPR a 10*10 OUTPUT_FORMAT DECIMAL) #计算10*10
math(EXPR b "16" OUTPUT_FORMAT HEXADECIMAL) #数字16转为Hex格式
```
# 2 字符串操作命令string
list类似于C++中的字符串,下面介绍它的相关指令
## 2.1 搜索和替换
测试代码:
- 找出aba字符串中第一个a的位置
- 反向找出aba字符串中第一个a的位置
- 找出aba字符串中第一个c的位置
```cmake
string(FIND aba a res)
message("${res}")
string(FIND aba a res REVERSE)
message("${res}")
string(FIND aba c res)
message("${res}")
```
运行结果如下:
## 2.2 正则匹配和替换
CMake中也支持正则表达式。
测试代码如下:
- 定义一个正则表达式"[abc]+",表示匹配a、b、c中的任意字符,并且可匹配多次
- 依次测试各个字符串是否满足匹配规则,匹配则会输出
```cmake
set(regex "[abc]+")
string(REGEX MATCH ${regex} res aaa)
message("${res}")
string(REGEX MATCH ${regex} res aaa bbb ccc abc)
message("${res}")
string(REGEX MATCH ${regex} res aaad)
message("${res}")
```
## 2.3 取字符串长度
这个比较容易理解,就是获取字符串的长度
```cmake
string(LENGTH "abcde" res)
message("${res}")
```
## 2.4 字符串变换
CONCAT可用于将字符串连接起来
```cmake
set(res "123")
string(CONCAT res a b c)
message("${res}")
```
## 2.5 比较字符串
COMPARE可用于比较字符串,按照字典顺序比较
```cmake
string(COMPARE LESS a abc res)
message("${res}")
string(COMPARE GREATER a abc res)
message("${res}")
```
## 2.6 取哈希值
MD5可以获取字符串的哈希值
```cmake
string(MD5 res "abcde")
message("${res}")
```
## 2.7 字符串生成
ASCII可将对应的ASCII值转换为对应的字符展示
```cmake
string(ASCII 65 66 67 res)
message("${res}")
string(ASCII 228 189 160 res)
message("${res}")
```
## 2.8 字符串模板
template_define.cmake
```cmake
set(template [=[
替换变量a: ${a}
替换变量b: @b@
定义宏C
#cmakedefine C
定义0/1宏D
#cmakedefine01 C
定义值e的宏E
#cmakedefine E e
定义值为F的变量的值的宏E
#cmakedefine F @F@
]=])
```
test8_1.cmake
```cmake
include(template_define.cmake)
set(a "a的值")
set(b "b的值")
set(C "C的值")
set(D "D的值")
set(E "E的值")
set(F "F的值")
string(CONFIGURE ${template} res)
message("${res}")
```
运行结果如下
# 3 列表操作命令list
list类似于C++中的列表,下面介绍它的相关指令
## 3.1 列表
列表可以通过set命令来创建,通过分号来分隔元素
```cmake
set(x "a;b;c")
foreach(i 0 1 2 -1 -2 -3)
list(GET x ${i} res)
message("x[${i}] = ${res}")
endforeach()
```
结果如下:
## 3.2 访问列表元素
通过GET可以获取列表中指定索引的元素,例如,访问第0个,以及访问第0个和第2个
```cmake
set(x "a;b;c")
list(GET x 0 res)
message("${res}")
list(GET x 0 2 res)
message("${res}")
```
## 3.3 获取列表长度
获取长度,也比较好理解
```cmake
set(x a;b;c)
list(LENGTH x res)
message("${res}")
```
## 3.4 列表元素增删
通过INSERT,可以在指定索引位置之后插入元素到列表中,可一次插入多个元素
```cmake
set(x "a;d;e")
list(INSERT x 1 b c)
message("${x}")
```
## 3.5 列表变换
通过JOIN可以实现列表元素的连接,该命令于string中的连接功能类似
```cmake
set(x "a;b;c")
list(JOIN x "-" res)
message("${res}")
string(JOIN "-" res ${x})
message("${res}")
```
## 3.6 列表重排
通过SORT可实现列表中元素的排序
```cmake
set(x "a;b;c;D;e")
list(SORT x)
message("${x}")
```
## 3.7 列表元素变换
通过TRANSFORM,可以实现在元素之前或之后添加自定义的字符串,例如,给每个元素加上括号的代码如下
```cmake
set(x "x a b c d e")
list(TRANSFORM x PREPEND "(")
list(TRANSFORM x AEPEND ")")
message("${x}")
```
# 4 文件操作命令file
文件操作命令的内容有很多,这里仅介绍下文件的读取和遍历
## 4.1 读取文件
读取文件中的内容,可以指定读取的长度
```cmake
file(READ example.txt res)
message("${res}")
file(READ example.txt res LIMIT 6)
message("${res}")
```
## 4.2 遍历路径
可以遍历指定目录下的文件
```cmake
file(GLOB res
RELATIVE "${CMAKE_CURRNT_LIST_DIR}"
*)
message("${res}")
```
# 5 路径操作命令cmake_path
追加路径
```cmake
cmake_path(APPEND res a b c)
message("${res}")
cmake_path(APPEND_STRING res a b c)
message("${res}")
cmake_path(APPEND res d e)
message("${res}")
```
cmake_path指令需要CMake3.20以上的版本,否则会提示不支持cmake_path指令
# 6 路径操作命令get_filename_component
get_filename_component命令也用于路径相关的操作,但其绝大多数功能已被cmake_path命令取代
```cmake
function(f mode)
get_filename_component(res "a/b.c.txt" ${mode})
message("${res}")
endfunction()
f(DIRECTORY)
f(NAME)
f(EXT)
f(NAME_WE)
f(LIST_EXT)
f(NAME_WLE)
```
# 7 配置模板文件configure_file
test471.cmake
```cmake
set(a "a的值")
set(b "b的值")
set(C "C的值")
set(D "D的值")
set(E "E的值")
set(F "F的值")
configure_file(template.h.in res1.h)
configure_file(template.h.in res2.h @ONLY)
configure_file(template.h.in res3.h COPYONLY)
```
template.h.in
```cmake
// 替换变量a:${a}
// 替换变量b:@b@
// 定义宏C
#cmakedefine C
// 定义0/1宏D
#cmakedefine01 D
// 定义值为e的宏E
#cmakedefine E e
// 定义值为F变量的值的宏F
#cmakedefine F @F @
```
# 8 日志输出命令message
类似与C/C++中的printf,用来输出信息。
```cmake
message("hello")
message(WARNING "一般警告")
message(AUTHOR_WARNING "开发警告")
message(SEND_ERROR "一般错误")
message(FATAL_ERROE "致命错误")
message("这条消息不会被输出")
```
除了基础的打印功能,还可以输出不同级别的警告和错误
FATAL_ERROE这种错误输出后,程序应该会退出的,但实际最后一句的输出也打印了出来,没看出哪里问题。
# 9 执行程序execute_process
创建一个脚本,execute_process使其输出两次变量text的值,并且第2次要延迟1s再输出
delay_message.cmake
```cmake
message("${text}")
execute_process(COMMAND ${CMAKE_COMMAND} -E sleep 1)
message("${text}")
```
创建另一个脚本,通过execute_process可以并行执行上述的脚本
test491.cmake
```cmake
execute_process(
COMMAND ${CMAKE_COMMAND} -Dtext=1 -P delay_message.cmake
COMMAND ${CMAKE_COMMAND} -Dtext=2 -P delay_message.cmake
COMMAND ${CMAKE_COMMAND} -Dtext=3 -P delay_message.cmake
)
```
# 10 引用CMake程序include
类似与C/C++中的#inclue,用来引入其它文件中的内容。
test_a.cmake
```cmake
message("模块被执行")
set(a "变量a")
```
test410_1.cmake,主要功能为:
- 先引入test_a.cmake,这种带扩展名的,可以是绝对路径,也可以是相对路径
- 然后引入test_a,这种不带扩展名的,会被当做CMake模块,只能是相对路径(但并非当前目录,而是CMAKE_MODULE_PATH变量中的目录),若模块不存在,则会报错,但加上OPTIONAL参数后,则会忽略错误
- 设置CMAKE_MODULE_PATH变量,再次执行include
```cmake
include(test_a.cmake)
message("a: ${a}")
include(test_a OPTIONAL RESULT_VARIABLE out)
message("include(test_a): ${out}")
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
include(test_a RESULT_VARIABLE out)
message("${out}")
```
## 11 总结
本篇介绍了CMake构建实战中的CMake常用命令的相关内容,由于CMake的命令非常多,全部学会也很耗时间,因此可以先对CMake的命令有个大致了解,后续用到哪个再单独查阅对应命令的详细介绍。
- 2024-09-15
-
发表了主题帖:
CMake构建实战读书笔记02-简单构建与CMake基础语法
本篇来学习《CMake构建实践 项目开发卷》的第1~3章中的一些知识点。
# 1 简单构建
## 1.1 构建静态库
按照书中内容,编写测试代码:
a.c
```c
#include
void a()
{
printf("a\n");
}
```
b.c
```c
#include
void b()
{
printf("b\n");
}
```
libab.h
```c
void a();
void b();
```
main.c
```c
#include "libab.h"
#include
int main()
{
a();
b();
return 0;
}
```
然后编写Makefile来进行编译
```makefile
main: main.o libab.a
gcc main.o -o main -L. -lab #链接静态库并生成主程序
main.o: main.c
gcc -c main.c -o main.o #c文件生成目标文件
libab.a: a.o b.o
ar rcs libab.a a.o b.o #归档,将a.o和b.o打包为静态库
a.o: a.c
gcc -c a.c -o a.o #c文件生成目标文件
b.o: b.c
gcc -c b.c -o b.o #c文件生成目标文件
clean:
rm *.o *.a main || true
```
编译运行的结果如下:
## 1.2 构建动态库
动态库的测试代码可以直接使用刚才静态库的测试代码
只需要修改Makefile
```makefile
main: main.o libab.so
gcc main.o -o main -L. '-Wl,-R$$ORIGIN' -lab #链接静态库并生成主程序
main.o: main.c
gcc -c main.c -o main.o #c文件生成目标文件
libab.so: a.o b.o
gcc -shared a.o b.o -o libab.so #生成动态库
a.o: a.c
gcc -fPIC -c a.c -o a.o #c文件生成目标文件
b.o: b.c
gcc -fPIC -c b.c -o b.o #c文件生成目标文件
clean:
rm *.o *.so main || true
```
编译运行的结果如下:
# 2 安装CMake
本篇的测试环境的Ubuntu20.04,默认是没有CMake的
可以使用如下指令来安装CMake
```sh
sudo apt-get install cmake
```
安装完成之后,可以写一个cmake文件进行简单测试:
# 3 CMake基础语法
## 3.1 CMake程序种类
CMake程序根据文件名,分为两类:
- 名为CMakeLists.txt的文件:用于组织构建项目源程序的目录结构
- 扩展名为.cmake的程序:又可分为脚本程序和模块程序两种
## 3.2 CMake注释
CMake注释可分为单行注释嗯和多行注释两种
```cmake
# 单行注释
#[[ 多行注释 可以换行]]
```
## 3.3 命令调用
CMake的命令调用类似于C语言中的函数调用。
先书写命令名称,后面括号里的命令参数
```cmake
message(a b c) # 输出 “abc”
```
## 3.4 命令参数
### 3.4.1 引号参数
用引号包裹在内的参数
```cmake
message("hello
cmake!")
```
### 3.4.2 非引号参数
未被引号包裹的参数,这种参数不能包含任何空白字符,也不能包含圆括号,#符号,双引号或反斜杠
```cmake
message("x;y;z") # 引号参数
message(x y z) # 多个非引号参数
message(x;y;z) # 非引号参数
```
### 3.4.3 括号参数
```cmake
message([===[
abc
def
]===])
```
## 3.5 变量
### 3.5.1 普通变量
通过set方式进行变量的定义。
示例代码:
```cmake
function(f)
set(a "我是修改后的a")
set(b "我是b")
set(c "我是c" PARENT_SCOPE)
endfunction()
set (a "我是a")
f()
message("a: ${a}")
message("b: ${b}")
message("c: ${c}")
```
运行结果
分析运行结果:
- 第一行是输出变量a的值,即全局中set的a的值
- 第二行是输出变量b的值,b在全局中没有定义,输出为空
- 第三行是输出变量c的值,c虽然在全局中没有定义,但在函数中,c通过PARENT_SCOPE将其定义到父级的作用域中
### 3.5.2 缓存变量
缓存变量比普通变量多了CACHE和FORCE参数,缓存变量具有全局的作用域,因此不需要PARENT_SCOPE参数。
示例代码:
```cmake
cmake_minimum_required(VERSION 3.16)
project(MatchOrder)
set(a 缓存变量 CACHE STRING "")
set(a 普通变量)
message("\${a}: ${a}")
message("\$CACHE{a}: $CACHE{a}")
```
运行结果:
### 3.5.3 环境变量
环境变量具有全局的作用域,不支持使用参数列表来定义值。
main.cmake
```makefile
message("main \$ENV{PATH}: $ENV{PATH}")
set(ENV{PATH} "path")
message("main \$ENV{PATH}: $ENV{PATH}")
execute_process(
COMMAND ${CMAKE_COMMAND} -P setenv.cmake
OUTPUT_VARIABLE out
)
message("${out}")
message("main \$ENV{PATH}: $ENV{PATH}")
```
这里将PATH环境变量设置为“path”,查看PATH的值,然后再调用setenv.cmake程序后,查看PATH的值
setenv.cmake的内容如下,就是清空了PATH的值
```makefile
message("before setenv \$ENV{PATH}: $ENV{PATH}")
set(ENV{PATH}) #清空
message("after setenv \$ENV{PATH}: $ENV{PATH}")
```
运行结果如下:
分析运行结果:
- PATH默认我Linux系统的环境变量
- 在main.cmake中修改之后,变为了path
- 在setenv.cmake子进程中的PATH,使用的是父进程的PATH,因此也是path
- 在setenv.cmake中将PATH清空
- 再回到主进程查看PATH,仍为主进程修改的“path”
这是因为,CMake的set命令仅对当前CMake进程有些,因此setenv.cmake中将PATH清空,不会影响setenv.cmake中PATH的值。
## 3.6 列表
列表,即用分号隔开的字符串,
## 3.7 控制结构
类似于C语言中的if、while、for语句
### 3.7.1 if条件分支
if条件分支的语法
```cmake
if()
...
elseif()
...
else()
...
endif()
```
### 3.7.2 while判断分支
while判断分支的语法
```cmake
while()
...
...break()
...
...continue()
...
endwhile()
```
### 3.7.3 foreach遍历循环
foreach遍历循环的语法
```cmake
foreach( )
...
endforeach()
```
例子
```cmake
foreach(x A;B;C D E F)
message("x: ${x}")
endforeach()
message("---")
set(list X;Y;Z)
foreach(x ${list})
message("x: ${x}")
endforeach()
```
运行结果如下:
## 3.8 条件语法
条件语法,即判断某个条件为真或假
### 3.8.1 常量、变量和字符串条件
常量条件,CMake中支持的真和假的常量定义如下
| 常量类型 | 常量值 | 条件结果 |
| -------- | ------------------------------------------------ | -------- |
| 真值常量 | 1、ON、YES、TRUE、Y,或非零数值 | 真 |
| 假值常量 | 0、OFF、NO、FALSE、Y、IGNORE、空字符串、NOTFOUND | 假 |
### 3.8.2 逻辑运算
三种逻辑运算:
- 与:AND
- 或:OR
- 非:NOT
### 3.8.3 单条件参数
根据单个参数进行判断的条件,例如
```cmake
set(a 1)
if(DEFINED a)
meaasge("DEFINED a为真")
endif()
```
这里if中的a是一个参数
### 3.8.4 双条件参数
通过两个参数的取值来判断的条件,例如
```cmake
set(a 1)
if(2 GREATER a)
meaasge("2 GREATER a为真")
endif()
```
这里if中的2和a是两个参数
### 3.8.5 括号和优先级
CMake中条件语法求值的优先级从高到低:
- 当前最内层括号中的条件
- 单参数条件
- 双参数条件
- 逻辑运算条件NOT
- 逻辑运算条件AND
- 逻辑运算条件OR
## 3.9 命令定义
可以自定义一些宏,以及函数
### 3.9.1 宏定义
宏定义的语法,可以传参数
```cmake
macro( [...])
...
endmacro()
```
### 3.9.2 函数定义
函数定义的语法,可以传参数
```cmake
function( [...])
...
endfunction()
```
### 3.9.3 参数的访问
- ${ARGC}:表示参数的个数
- ${ARGV}:表示完整的实参列表
- ${ARGN}:表示无对应形式参数的实际参数列表
- ${ARGV0}:表示第1个参数,依此类推
实例代码:
```cmake
macro(my_macro p)
message("ARGC: ${ARGC}")
message("ARGV: ${ARGV}")
message("ARGN: ${ARGN}")
message("ARGV0: ${ARGV0}, ARGV1: ${ARGV1}")
endmacro()
function(my_func p)
message("ARGC: ${ARGC}")
message("ARGV: ${ARGV}")
message("ARGN: ${ARGN}")
message("ARGV0: ${ARGV0}, ARGV1: ${ARGV1}")
endfunction()
my_macro(x y z)
my_func(x y z)
```
运行结果如下:
宏与函数的运行结果类似,这里开分析宏的运行结果:
- ARGC表示接收到的参数个,有3个参数
- ARGV表示完整的实参,为x;y;z
- ARGN表示无对应形式参数的实参,这里的宏在定义的时候,仅定义了一个参数p,实际传入的是x;y;z,因此x对应p,无对应的实参为y;z
- ARGV0和ARG1为传入的前两个参数
# 4 总结
本篇介绍了前3章的内容,先是使用gcc与makefile学习简单构建,并在Linux中安装CMake工具,然后开始学CMake的一些基础语法,并进行实际测试。
- 2024-09-08
-
发表了主题帖:
CMake构建实战读书笔记01-全书内容概览
收到《CMake构建实践 项目开发卷》了,感谢EE。
1 纸质书实物
看下书的正面
反面
随便翻开一页
2 全书内容概览
2.1 章节介绍
全书分为11章
2.2 第1~3章
第1~3章属于基础,第1章还未涉及到CMake
2.2 第4~6章
第4~6章也属于基础,第4章是常用命令,第5章是一个简单的实践,第6章是CMake构建初探
2.2 第7~9章
第7~9章应该属于CMake的一些进阶知识
2.2 第10~11章
第10章是CMake的策略与兼容介绍,第11章是一个综合性的CMake实践
- 2024-08-31
-
回复了主题帖:
读书入围名单: 《CMake构建实战:项目开发卷》
个人信息无误,确认可以完成阅读分享计划
- 2024-05-18
-
发表了主题帖:
【2023 DigiKey大赛参与奖】开箱帖 舵机云台+RGB8x8+RP2040板子
参与奖换了3样: 舵机云台x2,RGB8x8,RP2040板子
这个是舵机云台,散件,需要自己组装,有说明书
这个是RGB8x8点阵,需要搭配树莓派使用
这个是RP2040主控的一个板子
舵机云台组装好之后的样子
点阵屏和树莓派组装到一起的样子
以后有时间再玩