【征程 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} $*

至此,程序完成运行。

相关推荐
HERR_QQ11 分钟前
强化学习的数学原理 学习笔记
人工智能·笔记·学习·自动驾驶
kebidaixu25 分钟前
两轮BMS 防打火策略详解
算法
wabs6661 小时前
关于动态规划【力扣1035.不相交的线和53.最大子数组和的思考】
算法·leetcode·动态规划
退休倒计时1 小时前
【每日一题】LeetCode 199. 二叉树的右视图 TypeScript
算法·leetcode·typescript
可编程芯片开发1 小时前
通过MATLAB实现PID控制器,积分分离控制器以及滑模控制器
算法
kebidaixu2 小时前
两轮BMS 短路保护策略详解
算法
zwenqiyu2 小时前
非线性字符串数据结构串讲
数据结构·c++·学习·算法
z小猫不吃鱼2 小时前
03 Optimal Brain Surgeon 详解:Hessian 剪枝为什么有效?
算法·机器学习·剪枝
硕风和炜2 小时前
【LeetCode: 1301. 最大得分的路径数目 + DP】
java·算法·leetcode·动态规划·dp·记忆化搜索
用户99045017780092 小时前
做了一个AI诊断,参考倪海厦中医理论,科学养生
算法