DDZZ669

  • 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主控的一个板子 舵机云台组装好之后的样子 点阵屏和树莓派组装到一起的样子 以后有时间再玩    

统计信息

已有663人来访过

  • 芯积分:1922
  • 好友:2
  • 主题:137
  • 回复:154

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言