【征程 6】工具链 VP 示例为什么能运行

1.引言

在上一篇文章【征程 6】VP 简介与单算子实操 中,介绍了 VP 是什么,并以单算子 rotate 为例,介绍了 VP API 使用方法,但对于对 C++不那么熟悉的伙伴,可能会有这样的疑问:一个 main 函数就让 VP 示例跑起来了?没有什么依赖吗?CMakeLists.txt 没看到,xxx.h 头文件也没有,甚至连怎么编译的都没写,只有 main 文件中的 C++代码,还是让人有点迷迷瞪瞪。

由于本人就是属于对 C++不那么熟悉的同学,所以下面会从我的视角来介绍上一篇文章遗留的问题,如果其中有错误或表述不当的地方,欢迎评论指正。

2.代码解读

OE/samples/ucp_tutorial/目录下的结构如下:

Plain 复制代码
.
├── deps_aarch64
│   ├── appsdk
│   ├── eigen
│   ├── fmt
│   ├── gflags
│   ├── glog
│   ├── hlog
│   ├── nlohmann
│   ├── opencv
│   ├── openssl
│   ├── protobuf
│   ├── rapidjson
│   ├── ucp
│   ├── uWS
│   └── zlib
├── deps_x86
│   ├── eigen
│   ├── fmt
│   ├── gflags
│   ├── hlog
│   ├── nlohmann
│   ├── opencv
│   └── ucp
└── vp
    ├── code
    └── vp_samples

在 vp/code/07_single_rotate 目录如下:

Plain 复制代码
.
├── CMakeLists.txt
├── log_util.h
├── main.cpp
├── rotate.cpp
└── rotate.h

有个感觉即可,后面会细致的解读运行起一个 VP 示例所依赖的文件。

2.1 main.cpp

从 main.cpp 看过去,内容以及解读如下:

Plain 复制代码
#include <iostream>    // 引入标准输入输出库
#include "rotate.h"    // 引入自定义头文件 rotate.h

int32_t main(int32_t argc, char **argv) {  // 主函数,接受命令行参数
  single_rotate();   // 调用 single_rotate() 函数
  return 0;          // 返回 0,表示程序正常结束,符合 C++ 规范
}

有两处拿出来解释下:

#include "rotate.h":该文件是一个自定义头文件,用于声明函数或类,代码中调用的 single_rotate() 在 rotate.h 中被声明,并在相应的 rotate.cpp 中被实现。

int32_t main(int32_t argc, char **argv):入口函数 main(),接收两个参数:

  • argc:命令行参数个数(包括程序本身)。
  • argv:存储命令行参数的字符串数组(char*)。

2.2 rotate.h

下面看一下头文件 rotate.h,代码作用:声明 single_rotate() 函数,并防止头文件被重复包含。

Plain 复制代码
#ifndef VP_CODE_07_ROTATE_IMAGE_PROCESS_H_
#define VP_CODE_07_ROTATE_IMAGE_PROCESS_H_

#include "hobot/vp/hb_vp.h"   // 包含的头文件,不在当前目录下,为什么能包含?
#include "log_util.h"         // 包含的头文件,就在当前目录下

int32_t single_rotate();

#endif  // VP_CODE_07_ROTATE_IMAGE_PROCESS_H_

来了解一下这段代码能防止头文件被重复包含。

Plain 复制代码
#ifndef VP_CODE_07_ROTATE_IMAGE_PROCESS_H_
#define VP_CODE_07_ROTATE_IMAGE_PROCESS_H_
...
#endif  // VP_CODE_07_ROTATE_IMAGE_PROCESS_H_
  • #ifndef(如果未定义):检查宏 VP_CODE_07_ROTATE_IMAGE_PROCESS_H_ 是否已定义。
  • #define(定义宏):如果未定义,则定义该宏,并继续处理头文件的内容。
  • #endif 结束 #ifndef 逻辑块,确保头文件仅被包含一次。

这种方式称为 头文件保护(Header Guard),用于防止头文件的 重复包含,避免 编译错误。

还是有些不太理解?详细解释一下:

  1. 什么是头文件保护:在 C/C++ 语言中,头文件(。h)是用于声明变量、函数、类等的文件。为了防止头文件被 重复包含(multiple inclusion),通常使用 头文件保护(Header Guard),其基本结构是:
Plain 复制代码
#ifndef HEADER_NAME_H    // 如果 HEADER_NAME_H 未定义
#define HEADER_NAME_H    // 定义 HEADER_NAME_H

// 头文件内容
// 变量、函数、类的声明等

#endif  // 结束头文件保护
  1. 为什么要头文件保护:在大型 C++ 项目中,多个 。cpp 文件可能包含相同的头文件。例如:
  • A.h 头文件
Plain 复制代码
// A.h
#ifndef A_H
#define A_H

void foo();

#endif  // A_H
  • B.h 头文件
Plain 复制代码
// B.h
#ifndef B_H
#define B_H

#include "A.h"

#endif  // B_H
  • main.cpp 中
Plain 复制代码
// main.cpp
#include "A.h"
#include "B.h"

当 main.cpp 被编译时,它会展开 #include:在 A.h 中直接包含 void foo();,B.h 也包含 A.h,再次引入 void foo();这会导致重复声明,如果没有 头文件保护,编译器可能会报错:

Plain 复制代码
error: redefinition of 'void foo()'

当使用了上面#ifndef / #define / #endif,就可以避免这个问题,原理如下:

第一次 包含 A.h 时:

  • A_H 未定义,#ifndef A_H 成立 → 继续执行
  • 进入 #define A_H 代码块,定义 A_H
  • 头文件 A.h 正常加载

第二次 再次包含 A.h:

  • A_H 已定义,#ifndef A_H 失败 → 直接#endif,跳过整个头文件

编译器在处理头文件时会进行优化,所以头文件保护不会影响性能,为了避免 重复包含头文件 导致的编译错误,提高代码可维护性,推荐大家使用头文件保护。

最后,#ifndef 保护多个头文件需要不同的宏名,因为宏名重复,也可能导致错误,建议使用 文件名相关的宏 方便记忆排查。

2.3 log_util.h

在 rotate.h 中包含了 log_util.h,定义了一些用于日志打印的宏,具体的代码解读可见文章:【征程 6】工具链 VP 示例中日志打印解读

2.4 rotate.cpp

该文件解读可见文章:【征程 6】VP 简介与单算子实操

2.5 CMakeLists.txt

想了解 VP 示例中 CMakeLists.txt 的嵌套以及运行逻辑,可见文章:【征程 6】工具链 VP 示例中 Cmakelists 解读

2.6 build.sh

该 Bash 脚本 用于 构建 aarch64(ARM64)或 x86(PC 端)架构的项目,并支持 自动检测 gcc 版本,确保编译环境正确。

Plain 复制代码
# 默认编译 ARM64 版本、 Release 模式
arch=aarch64
build_type=release

# 显示帮助信息
function show_usage() {
cat <<EOF

Usage: bash -e $0 <options>    # 第一个参数是目标架构,后面参数可选
available options:
  -a|--arch: set arch ([aarch64|x86]), default is aarch64
  -h|--help
EOF
exit
}

# 检查gcc版本
function check_gcc() {
  export compiler=$(which gcc)
  ### get version code
  MAJOR=$(echo __GNUC__ | $compiler -E -xc - | tail -n 1)    # 获取 gcc 主版本号
  MINOR=$(echo __GNUC_MINOR__ | $compiler -E -xc - | tail -n 1)    # 获取 次版本号
  PATCHLEVEL=$(echo __GNUC_PATCHLEVEL__ | $compiler -E -xc - | tail -n 1)    # 获取 修订号
  gcc_version=${MAJOR}.${MINOR}.${PATCHLEVEL}
  # 检查 gcc 是否 >= 5.4.0
  if ((${MAJOR} < 5)) || ((${MAJOR} == 5 && ${MINOR} < 4)) || ((${MAJOR} == 5 && ${MINOR} == 4 && ${PATCHLEVEL} < 0)); then
    echo "Your gcc version is ${gcc_version}"
    echo "x86 GCC version should be >= 5.4.0, please unpack ddk/package/host/gcc-5.4.0.tar.gz to install then re-execute the install.sh."
    exit    # 版本低于 5.4.0,则终止并提示安装
  else
    echo "GCC version check success. GCC version is ${gcc_version}."
  fi
}


function build_arm() {
    # 删除旧的 build_arm 目录
    rm -rf build_arm
    rm -rf outputs
    mkdir build_arm
    cd build_arm

    # check environment for arm64
    # 检查 LINARO_GCC_ROOT 环境变量
    if [ ! $LINARO_GCC_ROOT ];then
        echo "Please set environment LINARO_GCC_ROOT correctly"
        # 若未设置,则使用默认路径
        export LINARO_GCC_ROOT=/arm-gnu-toolchain-12.2.rel1-x86_64-aarch64-none-linux-gnu
    else
        export LINARO_GCC_ROOT=${LINARO_GCC_ROOT}
    fi
    # 设置 gcc/g++ 交叉编译器
    export CC="${LINARO_GCC_ROOT}/bin/aarch64-none-linux-gnu-gcc"
    export CXX="${LINARO_GCC_ROOT}/bin/aarch64-none-linux-gnu-g++"
    # 执行 CMake 和 Make,选项 PLATFORM_AARCH64=ON
    cmake .. -Dbuild_type=${build_type} -DPLATFORM_AARCH64=ON
    make -j8
    make install
}

function build_x86() {
    rm -rf build_x86
    rm -rf outputs
    mkdir build_x86
    cd build_x86
    # 调用 check_gcc,确保 gcc 版本合格
    check_gcc
    # 不使用交叉编译器,直接使用本机 gcc/g++
    export CC=gcc
    export CXX=g++
    # cmake 选项 PLATFORM_AARCH64=OFF
    cmake .. -Dbuild_type=${build_type} -DPLATFORM_AARCH64=OFF
    make -j8
    make install
}

# 支持的架构 aarch64、x86
ARCH_OPTS=(aarch64 x86)
# getopt 命令行选项解析工具,用于处理命令行中的选项(如 -a、--arch 等),详解见下文
# -o a:h:定义短选项,a:接收一个参数,表示 --arch 选项,h:表示 --help 选项,不接参数
# -al arch:,help:定义长选项,arch:接收一个参数,--arch 后需跟一个值,help:不需要参数
# -- "$@":"$@" 是传递给脚本的所有命令行参数,-- 用于标识选项结束,防止后续的命令行参数被当作选项解析
GETOPT_ARGS=`getopt -o a:h -al arch:,help -- "$@"`
# 通过 eval 命令将 getopt 解析后的选项参数设置为当前脚本的命令行参数。确保可以使用 $1, $2 等变量访问解析后的命令行选项
eval set -- "$GETOPT_ARGS"

# 当 $1 不为空时,进入循环
# $1 是第一个命令行参数,循环会遍历所有传入的参数,直到所有参数都被处理完。
while [ -n "$1" ]
do
  case "$1" in
    -a|--arch)    # 匹配 -a 或 --arch 选项
      arch=$2    # 将第二个参数(即 --arch 后的值)赋值给变量 arch
      shift 2    # shift 命令会将位置参数左移 2 位,意味着处理过的选项被移除,接下来可以处理下一个参数
      # 检查 arch 是否是有效的选项之一。"${ARCH_OPTS[*]}" 是一个数组,包含所有有效的架构选项。
      # [[ ! "${ARCH_OPTS[*]}" =~ $arch ]]:使用正则表达式检查 $arch 是否在 ARCH_OPTS 数组中。
      # 如果无效,则打印错误信息并调用 show_usage 显示帮助
      if [[ ! "${ARCH_OPTS[*]}" =~ $arch ]] ; then
        echo "invalid arch: $arch"
        show_usage
      fi
      ;;
    # 匹配 -h 或 --help 选项,如果用户请求帮助,则调用 show_usage 函数显示帮助信息,之后使用 break 跳出循环。
    -h|--help) show_usage; break;;
    # 当遇到 -- 时,停止解析选项,后面的参数被视为位置参数
    --) break ;;
    # *用于匹配其他任何不符合上述选项的参数
    *) 
      echo $1,$2 
      show_usage; 
      break;;
  esac
done

# 根据 arch 选择 build_arm 或 build_x86
if [[ $arch == "aarch64" ]]; then
  build_arm
else
  build_x86
fi

getopt 会返回一个规范化的、已排序的选项和参数字符串,存储在 GETOPT_ARGS 变量中。例如,输入:

Plain 复制代码
./build.sh -a x86 --help

则 GETOPT_ARGS 可能会被解析成:

Plain 复制代码
--arch x86 --help

希望把 build.sh 脚本运行起来:

Plain 复制代码
开启调试模式,打印执行的每一条命令及其参数
set -x
如果任何命令执行失败(返回非零退出状态),则立即终止脚本执行
set -e
运行 build.sh,并传递参数 -a x86
bash build.sh -a x86

到这儿,项目构建编译就完成了。

3.程序执行

项目构建完成后,会在 vp/vp_samples 下准备好程序可执行的相关依赖文件

Plain 复制代码
vp_samples
.
├── data
└── script_x86
    ├── 07_single_rotate
    │   ├── rotate.jpg
    │   └── run_single_rotate.sh
    └── x86
        ├── bin
        │   └── single_rotate
        └── lib
            ├── libalog.so.1
            ├── libarm_model_gdc.so
            ├── libhb_arm_rpc.so
            ├── libhbmem.so.1
            ├── libhbucp.so
            ├── libhbvp.so
            ├── libhlog.so -> libhlog.so.1
            ├── libhlog.so.1 -> libhlog.so.1.14.3
            ├── libhlog.so.1.14.3
            ├── libhlog_wrapper.so
            ├── libopencv_world.so.3.4
            └── libperfetto_sdk.so

运行 vp_samples/script_x86/07_single_rotate/run_single_rotate.sh 脚本即可。

Plain 复制代码
# bin可执行文件路径
bin=../x86/bin/single_rotate
# 二进制文件目录 ../x86/bin/
root=../x86/bin/
# 共享库目录 ../x86/lib
lib=../x86/lib

# 指定运行时动态链接库路径,确保执行 single_rotate 时能找到 ../x86/lib 里的共享库(.so)
# ${LD_LIBRARY_PATH} 可能已经有其他路径,: 号保证新路径追加,不会覆盖原有路径
export LD_LIBRARY_PATH=${lib}:${LD_LIBRARY_PATH}
# 将 root 和 bin 目录添加到 PATH 变量,使 single_rotate 可直接运行,而无需写完整路径。
export PATH=${root}:${bin}:${PATH}

export HB_DSP_ENABLE_CONFIG_VDSP=true    # 开启 DSP 配置
# 设置 DSP 日志级别
export HB_DSP_LOG_LEVEL=3    
export HB_DSP_VDSP_LOG_LEVEL=3
export HB_UCP_ENABLE_RELAY_MODE=false    # 禁用 UCP 透传模式
export HB_DSP_CMODEL_IMAGE=${root}/image/vdsp0    # 指定 DSP 仿真镜像路径
export HB_DSP_CONNECT_RETRY_TIMES=0    # DSP 连接失败时,不进行重试

# ${XTENSA_ROOT} is the root directory where the xtensa compilation environment is installed. 
# The user needs to install the compilation environment by himself.
# For details, please see the dsp development document-Linux development environment installation chapter in the oe document
# For example, in oe document, need to set 'export XTENSA_ROOT=/opt/xtensa/XtDevTools/install/tools/RI-2021.7-linux/XtensaTools/'
export XTENSA_CORE=Vision_Q8    # 指定使用 Vision_Q8 处理器
export XTENSA_VERSION=RI-2023.11-linux    # 指定 Xtensa 版本
# 设定 Xtensa 处理器的配置文件路径
# Xtensa 开发工具的安装目录,用户需自行安装并正确设置该变量
export XTENSA_SYSTEM=${XTENSA_ROOT}/../../../builds/${XTENSA_VERSION}/${XTENSA_CORE}/config
export XTENSA_CONFIG=${XTENSA_ROOT}/../../../builds/${XTENSA_VERSION}/${XTENSA_CORE}/config

# 执行 single_rotate,并传递所有命令行参数 $*
# ${bin} 解析为 ../x86/bin/single_rotate,所以实际执行:../x86/bin/single_rotate $*
${bin} $*

至此,程序完成运行。

相关推荐
独好紫罗兰10 分钟前
洛谷题单2-P5715 【深基3.例8】三位数排序-python-流程图重构
开发语言·python·算法
序属秋秋秋39 分钟前
算法基础_基础算法【高精度 + 前缀和 + 差分 + 双指针】
c语言·c++·学习·算法
玉树临风ives40 分钟前
leetcode 2360 图中最长的环 题解
算法·leetcode·深度优先·图论
智能汽车人1 小时前
自动驾驶---学术论文的常客:nuScenes数据集的使用
人工智能·机器学习·自动驾驶
KeithTsui1 小时前
GCC RISCV 后端 -- 控制流(Control Flow)的一些理解
linux·c语言·开发语言·c++·算法
mNinGInG2 小时前
c++练习
开发语言·c++·算法
纪元A梦2 小时前
分布式锁算法——基于ZooKeeper的分布式锁全面解析
java·分布式·算法·zookeeper
Panesle2 小时前
广告推荐算法:COSMO算法与A9算法的对比
人工智能·算法·机器学习·推荐算法·广告推荐
月亮被咬碎成星星3 小时前
LeetCode[15]三数之和
数据结构·算法·leetcode
半盏茶香3 小时前
启幕数据结构算法雅航新章,穿梭C++梦幻领域的探索之旅——堆的应用之堆排、Top-K问题
java·开发语言·数据结构·c++·python·算法·链表