lvgl cplusplus移植踩坑记录

仅仅个人学习记录,非专业人士,并未系统学习CMake,以下均为从实践中总结,错误之处,万望指出。另,3日连续调试,疲惫不堪,AI润色,万望谅解。

来自C++的下马威

请检查这段代码,你发现什么问题了吗?

情况是这样的:当我编译完这个用C++移植的LVGL工程后,出现了一个奇怪的现象。在Debug模式下程序运行正常,但在Release模式下,程序总是会在LVGL初始化前卡住。更诡异的是,这个问题还具有一定的随机性,有时会卡住,有时又不会。

这个奇怪的bug困扰了我好几天。今天终于发现,问题出在我定义的一个全局时钟变量上。这个变量在main函数之前就被初始化了。由于LVGL的定时器必须在LVGL初始化完成后才能正常工作,在初始化前操作这个定时器就会导致未定义行为,从而引发这个bug。

Cmake使用心得

项目架构

管理大型项目时,手动输入编译指令显然不现实,因此我选择使用CMake来优化整个编译流程。首先,合理的项目结构至关重要:

对于工具

  1. 项目根目录下创建src文件夹用于存放项目源代码
  2. src目录下按功能模块划分,每个模块单独存放源代码
  3. 如项目引用第三方库,建议在src下创建third_party文件夹存放第三方代码

在项目根目录的CMakeLists.txt文件中,可以通过add_subdirectory指令将第三方库一并编译,避免手动处理各种依赖关系。

项目根目录还应当包含include文件夹:

  1. 在include下创建与项目同名的子文件夹
  2. 在该子文件夹中存放模块头文件(如module.hpp)
  3. 配置CMake的include_directories路径时,采用这种结构可以防止不同库的头文件引用冲突

此外,建议在项目根目录下创建test文件夹:

  1. 为每个模块编写对应的单元测试
  2. 可以使用Google Test框架(gtest/gmock)进行测试开发

这种结构化的项目布局能有效提升代码的可维护性和编译效率。

对于一个工具库,可以是这样的::

├─example
│  ├─algorithm
│  │  ├─a_start_find
│  │  ├─kmp_string_find
│  │  └─simulated_annealing
│  ├─chaotic_swing
│  ├─mesh_pbd
│  └─three_body
├─include
│  └─lv_toolkit
├─src
│  ├─core
│  ├─thirdparty
│  │  ├─box2d
│  │  ├─googletest
│  │  ├─lvgl
└─test
    ├─math_test
    └─wgpu_test

        CMakeList.txt(for testing)

CMakeList.txt(main toolkit)
 

对于main,可以是这样的

项目架构说明如下。首先介绍项目引用的库文件结构:

主文件目录下包含一个Src文件夹用于存放源码。Src目录内包含:

  1. lib文件夹:
    • 存放工具库LV toolkit
    • eez studio自动生成的文件(位于UI子文件夹内)
    • main文件
    • CMakeLists文件
  2. ui文件夹:
    • 存放配置好UI界面的MVC架构相关文
  3. event directory
  • 用于初始化UI系统后设置控件的信号和回调函数

项目根目录除了lib文件夹外,还包含log、document等其他辅助目录,这些可根据实际需求进行添加。

注:考虑到文件组织规范,目前没有将所有main文件放在根目录下,而是选择将其保留在src文件夹内。

eg:


build

log

res

src

        lib

        event

        ui

        CMakeLists.txt

Cmake脚本

1.最小可运行

那么说完了这两种架构的思路,现在我们开始详细讲解CMake的编译流程。首先,我们需要用project()命令来声明当前项目的名称和使用的编程语言。例如:

project(MyProject LANGUAGES C CXX)

这个命令不仅指定了项目名称为"MyProject",还明确了该项目会使用C和C++两种编程语言。项目名称在后续的target_link_libraries等命令中会被引用。

接下来,我们可以选择性地声明一些编译兼容性设置。对于C++项目来说,最常见的设置是:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

这些设置确保了我们的项目需要使用C++17标准,并且如果编译器不支持该标准,CMake会报错而不是降级处理。此外,我们还可以设置编译器警告级别等选项:

if(MSVC)
    add_compile_options(/W4 /WX)
else()
    add_compile_options(-Wall -Wextra -Werror)
endif()

对于一个最小可运行的CMake项目示例,我们首先需要收集整个项目的源文件。源文件的收集方式有多种:

  1. 手动列出所有源文件:
set(SRCS 
    src/main.cpp
    src/util.cpp
    src/parser.cpp
)

  1. 使用file(GLOB...)自动收集:
file(GLOB SRCS "src/*.cpp")

需要注意的是,File指令它会覆盖这个变量里面的东西,如果说你使用多条file指令,里面所有的源文件并不会累积,而是会被覆盖,请在这个file后面把多个这个路径写到一块,或者说使用list指令追加啊

这个错误困扰了我一天,最后使用message调试才发现

收集完源文件后,我们可以使用add_executable()命令来创建可执行文件:

add_executable(myapp ${SRCS})

这个命令会生成一个名为"myapp"的可执行文件。在Linux系统下,这个文件默认会生成在build目录中;在Windows系统下,则会生成带有.exe后缀的可执行文件。

如果我们需要生成静态库而不是可执行文件,可以使用add_library()命令并指定STATIC参数:

add_library(mylib STATIC ${SRCS})

静态库在项目中有多种重要用途,比如:

  • 代码复用:将常用功能封装成库供多个项目使用
  • 加快编译速度:只需编译一次,后续链接即可
  • 保护源代码:可以分发库文件而不暴露源代码

在后续的讨论中,我们会详细展开静态库的使用场景、优缺点,以及如何将其集成到更大的项目结构中。特别是会讲解如何通过target_link_libraries()命令将静态库链接到可执行文件中,以及如何处理静态库与动态库的选择问题。

#CMake file for different scenarios: Native build on Linux or Windows (automatic), and cross-build from Unix to Windows (by defining build-host as Unix and build-target as Windows)

cmake_minimum_required( VERSION 3.10 )

project( phybox C CXX)

# 显式指定 C 和 C++ 编译器
set(CMAKE_C_COMPILER "D:/mingw64/bin/gcc.exe")
set(CMAKE_CXX_COMPILER "D:/mingw64/bin/g++.exe")

# 可选:禁用编译器 ABI 检测(进一步减少检测步骤)
set(CMAKE_C_COMPILER_WORKS TRUE)
set(CMAKE_CXX_COMPILER_WORKS TRUE)

file( GLOB_RECURSE PHYBOX_SRCS 
    ${PROJECT_SOURCE_DIR}/event/*.c
    ${PROJECT_SOURCE_DIR}/event/*.cpp
    
    ${PROJECT_SOURCE_DIR}/ui/**/*.c
    ${PROJECT_SOURCE_DIR}/ui/**/*.cpp

    
    ${PROJECT_SOURCE_DIR}/ui/*.c
    ${PROJECT_SOURCE_DIR}/ui/*.cpp
)
add_executable( ${PROJECT_NAME} main.cpp ${PHYBOX_SRCS})
target_link_libraries(phybox toolkit )

main项目配置

刚才我们讨论的是构建一个最小可执行项目的基本配置,然而在实际开发环境中,我们还需要处理更复杂的构建需求。首先,我们需要解决头文件包含路径的问题,否则在代码中使用#include指令时可能会找不到对应的头文件。为此,我们需要通过include_directories()命令来添加头文件的搜索路径。

具体来说,我们需要执行以下步骤:

  1. 使用include_directories()命令添加主项目的头文件目录
  2. 由于我们的lib文件夹下还包含多个子库,我们需要使用foreach循环遍历这些子库目录
  3. 将每个子库的头文件目录都添加到include路径中

例如:

include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB LIB_DIRS "${PROJECT_SOURCE_DIR}/lib/*")
foreach(LIB_DIR ${LIB_DIRS})
    include_directories(${LIB_DIR}/include)
endforeach()

此外,我们还需要特别注意库的链接问题。在我们的项目中,toolkit库为了便于调试被编译为静态库。因此,在构建主可执行文件时,需要正确链接这个静态库。这里需要特别注意CMake命令的执行顺序:

  1. 必须先使用add_executable()定义可执行文件目标
  2. 然后才能使用target_link_libraries()进行链接

错误的顺序示例(会导致编译失败):

target_link_libraries(my_app PRIVATE toolkit)  # 错误:此时my_app尚未定义
add_executable(my_app main.cpp)

正确的顺序应该如下:

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE toolkit)

这是因为CMake需要先知道你要构建的是可执行文件还是库文件,才能正确处理后续的链接指令。这种顺序要求是CMake工作流程中的重要特性。

以上讨论主要针对主项目的构建配置,在实际项目中,随着项目规模的扩大,构建配置可能会变得更加复杂。例如:

  • 需要处理多个子项目的构建
  • 需要管理不同构建类型(Debug/Release)的配置
  • 需要处理跨平台的构建差异
  • 需要集成第三方库的查找和链接

这些复杂情况都需要我们在CMake配置文件中进行更细致的处理,这也是为什么大型项目的CMakeLists.txt文件往往比较长的原因。

lib当中的toolkit库配置

在管理工具项目时,除了要处理好自身代码外,还需要妥善管理第三方库的依赖关系。这是一个相对复杂的问题,需要系统性地解决。以下是具体的处理步骤和注意事项:

  1. 源码收集与静态库声明
  • 使用file()命令收集子项目源码
  • 通过add_library()命令声明静态库
  • 将收集的源码全部纳入静态库编译范围
  1. 处理第三方依赖
  • 以LVGL工具包为例,它包含多个第三方依赖
  • 这些依赖关系可能相当复杂(如lvgl_conf.h中引入SDL依赖)
  • 需要确保LVGL能正确包含SDL的头文件
  1. 包含目录设置
  • 使用target_include_directories()命令
  • 参数说明:
    • 目标名称:当前构建目标
    • 作用域:
      • PUBLIC:用于共享依赖
      • PRIVATE:仅内部使用
      • INTERFACE:仅给依赖者使用
  • 路径可以重复添加,系统会自动累加处理
  1. 文件收集注意事项!!!
  • 必须一次性收集所有文件,不能分多次添加
  • 两种实现方式:
    • 将所有文件路径写在单个file()命令后
    • 使用list(APPEND)进行追加
  1. 依赖关系处理
  • 处理好子项依赖后
  • 通过include_directories()暴露全局头文件
  • 确保更大的构建目标能够访问
  1. 链接设置!!!
  • 使用target_link_libraries()确保正确链接
  • 处理静态库与第三方库的链接关系
  • 保证所有引用都能正确解析
  1. 可选模块编译
  • 通过option()命令选择编译模块
  • 典型可选内容:
    • 测试模块(test)
    • 示例程序(examples)
  • 在这些模块的目录中单独编写CMakeLists.txt

这种分层管理方式具有以下优势:

  • 代码简洁高效
  • 各层级职责明确
  • 依赖关系清晰
  • 有效减少潜在bug
  • 便于维护和扩展

实际应用示例:

# 收集源码
file(GLOB_RECURSE SOURCES "src/*.c" "src/*.h")

# 声明静态库
add_library(my_tool STATIC ${SOURCES})

# 处理第三方依赖
target_include_directories(my_tool
    PUBLIC 
    ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/lvgl
    ${SDL2_INCLUDE_DIRS}
)

# 链接第三方库
target_link_libraries(my_tool
    PRIVATE
    lvgl
    SDL2
)

# 可选模块
option(BUILD_TESTS "Build tests" OFF)
if(BUILD_TESTS)
    add_subdirectory(tests)
endif()

调试心得

在实际使用CMake进行项目编译时,经常会遇到各种编译错误和配置问题。下面详细介绍几种常见的错误类型及其解决方案:

1. 引用无法找到的错误(cannot find reference)

这是非常常见的编译错误类型,通常表现为:

error: cannot find reference to 'xxx'

解决方案流程

  1. 首先检查CMakeLists.txt文件中的target_link_libraries是否正确设置了依赖关系
  2. 使用message()命令打印调试信息,查看源文件路径是否正确:
    message(STATUS "Source directory: ${CMAKE_CURRENT_SOURCE_DIR}")
    message(STATUS "Looking for file in: ${PROJECT_SOURCE_DIR}/src")
    

    advanced:


    message("*********************************************************************")
    foreach(src_file ${PHYBOX_SRCS})
        get_filename_component(src_bs ${src_file} NAME)
        message(${src_bs})
        if (${src_bs} STREQUAL "load.cpp")
            message("``````````````````````````````````````````got it")
        endif()
    endforeach()
     

  3. 确认被引用的文件是否确实存在于打印的路径中
  4. 如果引用的是第三方库,检查是否正确设置了find_package()find_library()

2. 头文件包含错误(include错误)

这类错误通常表现为:

fatal error: xxx.h: No such file or directory

针对不同情况的解决方案

对于子项目间的依赖:

当项目结构复杂,有多个子项目相互依赖时,推荐使用target_include_directories

这边的跟那个用法差不多,只不过需要注意的是需要把这个放在add executable或者说add library命令之后

然后第一个参数是填写当前目标,第二个参数填写public private这些访问修饰符,然后第三个就跟正常的include director i是一致的

对于简单的项目依赖:

如果只是外部项目依赖toolkit中的头文件,可以使用更简单的include_directories

include_directories(
    ${PROJECT_SOURCE_DIR}/toolkit/include
    ${PROJECT_SOURCE_DIR}/thirdparty/include
)

第三方库依赖的特殊情况:

当项目依赖多个第三方库且它们之间存在依赖关系时:

  1. 确保每个库都正确使用find_package()
  2. 使用target_link_libraries指定依赖关系链
  3. 考虑使用CMake的INTERFACE库来处理复杂的依赖关系

示例

# 声明一个接口库来处理复杂依赖
add_library(complex_dependencies INTERFACE)
target_link_libraries(complex_dependencies
    INTERFACE
    dependency1
    dependency2
)

# 主项目链接这个接口库
target_link_libraries(main_project
    complex_dependencies
)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值