使用 Clion 阅读 Envoy 源码

背景

要想深入学习 istio,还得学习下数据面的实现,istio 的数据面使用了 envoy,在 istio group 下有个叫 proxy 的仓库,包含了一些 istio 用到的一些 envoy 扩展,编译时将 envoy 代码作为库来引用,最终使用 bazel 编译出 istio 版本的 Envoy。

代码量非常庞大,如果没有智能的代码跳转、查找引用与实现,读起来简直低效的要命。如何高效的阅读呢?关键在于 IDE/编辑器 的代码索引能力要好,需要能够准确跳转和查询,vscode 用的同学比较多,但它的 c/c++ 插件不够智能,很多情况无法跳转,而且效率极低;它还有个 clangd 的插件,基于 LSP,但不够成熟。这方面做的最好的目前还是来自 JetBrains CLion,不过它需要依赖 CMakeLists.txt 文件来解析项目结构,由于 c/c++ 没有统一的结构标准,不同项目结构千差万别,不太好自动生成 CMakeLists.txt,需要我们先理解项目结构,然后编写 CMakeLists.txt 来让 CLion 进行解析。

虽然社区有人针对 bazel 构建的项目写了一个通用脚本 bazel-cmakelists ,但很久没维护,测试了用它来生成最新 envoy 的 CMakeLists.txt ,由于代码量庞大,最终会 OOM 而失败。

所以我们需要另寻更好的方法,不太了解这方面的同学弄起来会比较麻烦,本人也折腾了好一段时间才搞定,本文记录下方法和心得,以供大家参考。

克隆代码

首先克隆 istio-proxy 的代码:

git clone https://github.com/istio/proxy.git istio-proxy

最好切到某个稳定的 release 分支上:

cd istio-proxy
git checkout -b release-1.9 origin/release-1.9

项目分析

istio-proxy 代码库中主要只包含了在 istio 里用到的一些 envoy 扩展,代码量不大,源码主要分布在 src 与 extensions 目录,但编译需要很久,因为它实际编译的是 envoy,只是利用 bazel 将自身代码作为扩展编译进 envoy (得益于 envoy 的扩展机制),从这个 bazel 的 BUILD 文件 就能看得出来:

envoy_cc_binary(
    name = "envoy",
    repository = "@envoy",
    visibility = ["//visibility:public"],
    deps = [
        "//extensions/access_log_policy:access_log_policy_lib",
        "//extensions/attributegen:attributegen_plugin",
        "//extensions/metadata_exchange:metadata_exchange_lib",
        "//extensions/stackdriver:stackdriver_plugin",
        "//extensions/stats:stats_plugin",
        "//src/envoy/extensions/wasm:wasm_lib",
        "//src/envoy/http/alpn:config_lib",
        "//src/envoy/http/authn:filter_lib",
        "//src/envoy/tcp/forward_downstream_sni:config_lib",
        "//src/envoy/tcp/metadata_exchange:config_lib",
        "//src/envoy/tcp/sni_verifier:config_lib",
        "//src/envoy/tcp/tcp_cluster_rewrite:config_lib",
        "@envoy//source/exe:envoy_main_entry_lib",
    ],
)

其中 @envoy 表示引用 envoy 代码库,main 函数也位于 envoy 代码库中。那么 envoy 代码库从哪儿来的呢?bazel 在构建时会自动下载指定的依赖,envoy 的代码来源在 WORKSPACE 中有指定:

http_archive(
    name = "envoy",
    sha256 = ENVOY_SHA256,
    strip_prefix = ENVOY_REPO + "-" + ENVOY_SHA,
    url = "https://github.com/" + ENVOY_ORG + "/" + ENVOY_REPO + "/archive/" + ENVOY_SHA + ".tar.gz",
)

bazel 会自动下载指定版本的源码包来编译。

如果获取依赖源文件?

由于 istio-proxy 依赖了大量的第三方源文件,我们要阅读代码需要将这些源文件都下下来,只要将它编译一次,所有依赖源文件以及 generated 的代码都可以自动给你备好,所以我们需要对它进行一次编译。

由于编译 envoy 有复杂的工具链依赖,官方推荐使用容器进行编译,在执行 make 前加个 BUILD_WITH_CONTAINER=1 即可指定使用容器编译,免去复杂的环境依赖。但 bazel 编译会将依赖和 generated 的源文件都软链到临时目录,如果用容器编译,就会丢失这部分代码,而我们阅读 istio-proxy 代码时最关键的就是这部分代码了,所以不能用容器编译。

安装 bazelisk

不用容器编译就需要本机环境基本满足工具链要求,首先是需要安装 bazel,由于 bazel 版本很多,不同 istio-proxy(envoy) 版本依赖的 bazel 版本也不一样,我们可以直接安装 bazelisk ,一个用于 bazel 多版本管理的工具,它可以自动识别项目中 .bazelversion 文件,选取指定版本的 bazel 来进行构建(可以自动下载对应版本的 bazel 二进制)。

如果是 macOS 用户,安装很简单:

brew install bazelisk

如果之前已安装过 bazel,可以使用 brew link --overwrite bazelisk 强制覆盖。

其它平台的可以在 release 页面下载最新的二进制,重命名为 bazel 然后放到 PATH 下。

其它依赖

如果是 macOS 用户,确保务必安装好 xcode,方便跳转系统库函数,安装命令:

xcode-select --install

另外主要还有 python3 (macOS 自带),其它依赖通常都系统自带,可以先不用管,等如果编译报错了再看。

更多依赖可参考 官方文档

编译

在 istio-proxy 代码根目录执行以下命令进行编译:

make build_envoy

环境没问题的话会经过漫长的构建和编译,通常可能几十分钟,取决于电脑配置。

编译完后会发现 bazel 为我们生成了一些目录软链:

bazel 输出目录结构可参考官方文档 Output Directory Layout

我们主要关注以下两个目录:

  • bazel-istio-proxy: 包含构建 istio-proxy 用到的源文件(包含依赖)。
  • bazel-bin: 包含一些 generated 代码。

生成源码文件列表

在 istio-proxy 根目录创建脚本文件 generate-srcs.sh:

#!/bin/bash

set -ex

bazel_dir="bazel-${PWD##*/}"

find -L -E $bazel_dir/external src extensions -regex '.*\.(cc|c|cpp)' > sourcefiles.txt

执行此脚本可以生成 istio-proxy 及其依赖的源文件列表 (sourcefiles.txt),用于在 CMakeLists.txt 中引用。

注: $bazel_dir/external 下包含内容较多,全部索引的话 CLion 可能会比较卡,很多代码基本也都不会看,可以适当缩小范围,按需来配置,比如先只添加 $bazel_dir/external/envoy,后续有需要再添加其它目录,然后 Reload Cmake Project 重新索引。

生成 CMakeLists.txt

然后就可以在 istio-proxy 项目根目录创建下 CMakeLists.txt:

cmake_minimum_required(VERSION 3.15)
STRING( REGEX REPLACE ".*/(.*)" "\\1" CURRENT_FOLDER ${CMAKE_CURRENT_SOURCE_DIR} )
project(istio-proxy)

macro(print_all_variables)
    message(STATUS "print_all_variables------------------------------------------{")
    get_cmake_property(_variableNames VARIABLES)
    foreach (_variableName ${_variableNames})
        message(STATUS "${_variableName}=${${_variableName}}")
    endforeach()
    message(STATUS "print_all_variables------------------------------------------}")
endmacro()

set(CMAKE_CXX_STANDARD 17)
add_definitions(-DNULL_PLUGIN) # enable wasm nullvm navigation

file(STRINGS sourcefiles.txt all_SRCS)

message(STATUS "CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_HOME_DIRECTORY=${CMAKE_HOME_DIRECTORY}")

add_executable(istio-proxy ${all_SRCS})

set(istio_include_dirs
        "./"
        "./src"
        "./extensions"

        "./bazel-${CURRENT_FOLDER}/external/envoy"
        "./bazel-${CURRENT_FOLDER}/external/envoy/source"
        "./bazel-${CURRENT_FOLDER}/external/envoy/include"
        "./bazel-${CURRENT_FOLDER}/external/envoy/api/wasm/cpp"
        "./bazel-${CURRENT_FOLDER}/external/boringssl/src/include/"
        "./bazel-${CURRENT_FOLDER}/external/com_github_gabime_spdlog/include"
        "./bazel-${CURRENT_FOLDER}/external/com_github_c_ares_c_ares"
        "./bazel-${CURRENT_FOLDER}/external/com_google_absl"
        "./bazel-${CURRENT_FOLDER}/external/com_google_cel_cpp"
        "./bazel-${CURRENT_FOLDER}/external/com_google_protobuf/src"
        "./bazel-${CURRENT_FOLDER}/external/com_github_fmtlib_fmt/include"
        "./bazel-${CURRENT_FOLDER}/external/com_github_eile_tclap/include"
        "./bazel-${CURRENT_FOLDER}/external/com_github_grpc_grpc/include"
        "./bazel-${CURRENT_FOLDER}/external/com_envoyproxy_protoc_gen_validate/"
        "./bazel-${CURRENT_FOLDER}/external/com_github_tencent_rapidjson/include/"
        "./bazel-${CURRENT_FOLDER}/external/com_github_datadog_dd_opentracing_cpp/include/"
        "./bazel-${CURRENT_FOLDER}/external/com_github_libevent_libevent/include"
        "./bazel-${CURRENT_FOLDER}/external/com_github_mirror_tclap/include"
        "./bazel-${CURRENT_FOLDER}/external/com_github_grpc_grpc"
        "./bazel-${CURRENT_FOLDER}/external/com_github_circonus_labs_libcircllhist/src/"
        "./bazel-${CURRENT_FOLDER}/external/com_github_nodejs_http_parser"
        "./bazel-${CURRENT_FOLDER}/external/com_github_nghttp2_nghttp2/lib/includes/"
        "./bazel-${CURRENT_FOLDER}/external/com_github_cyan4973_xxhash/"
        "./bazel-${CURRENT_FOLDER}/external/com_github_google_flatbuffers/include/"
        "./bazel-${CURRENT_FOLDER}/external/com_github_fmtlib_fmt/test"
        
        "./bazel-bin"
        "./bazel-bin/external/envoy_api"
        "./bazel-bin/external/mixerapi_git"
        "./bazel-bin/external/com_envoyproxy_protoc_gen_validate"
        "./bazel-bin/external/com_google_googleapis"
        "./bazel-bin/external/com_github_cncf_udpa"
)

target_include_directories(istio-proxy PUBLIC ${istio_include_dirs})

解释一下:

  • add_executable 将需要索引的源文件列表 (sourcefiles.txt) 加进索引。
  • target_include_directories 将用到的一些纯头文件目录加进索引 (不包含实现代码,主要是一些接口),这里也是可以按需进行增删。

使用 CLion 阅读

不要直接打开 istio-proxy 目录,而是 Open 时选中 CMakeLists.txt,然后 Open as Project:

弹出 Load Project 时不要勾选 Clean project,不然退出 CLion 时会执行 make clean,导致把 bazel 生成的源文件都给删除掉,就没法跳转了:

然后就会开始索引,完成后就可以愉快的看代码了,先从 main 看起吧(bazel-istio-proxy/external/envoy/source/exe/main.cc):

查找引用:

跳转到实现:

小结

本文介绍了如何使用 CLion 来阅读 istio-proxy (envoy) 的代码,包含源码结构分析、环境搭建,以及生成 CLion 所需要的 CMakeLists.txt 文件的方法,最后也展示了效果,希望对你有所帮助。

上一页