背景
在最新的边缘计算story中,需要引入 表达式处理库。 经过内部同事评估,最终选择了Google的CEL-CPP开源项目。
这种在项目中引入开源库的工作,以前做一线程序员时也经常做,本来觉得没什么特别的。但是由于最近几年一直在做管理工作,没有接触具体的编码任务,因此对AI的认识也一直停留在提效的口号中,没有深刻的感受。但是在本次移植过程,AI工具对工作的提效,让我真实感受到了冲击。
按照我以往的经验,开源库的移植一般都是以下步骤:
-
下载源码,现在宿主机完成编译,输出目标库。这个过程需要解决各种依赖问题,编译错误。最常见的就是外部依赖模块的下载。
-
设置环境变量,进行交叉编译。在步骤一的前提下,设置交叉编译的环境变量,再进行编译。这个过程主要就是解决编译问题。
若没有AI的协助,按照我的经验,本次CEL-CPP库移植,**至少需要2周的时间。但实际上只用了两天。**第一天完成X86 环境编译,第二天完成了arm64 环境编译,并且全程没有压力。
本文,将我移植过程中遇到的问题一一处理进行分享,希望能够帮助到遇到同样问题的同学。
核心心得:人机协作的新范式
在本次移植过程中,我总结出一套高效的工作流,这也是本次能极速完成任务的关键:
具备自己的思路 + 遇到问题直接咨询AI + 获取模板与内容 + 深度理解与适配
-
具备思路: 明确知道目标是什么(例如:我要把静态库转动态库,我要配置Bazel交叉编译),而不是让AI盲目尝试。
-
索取模板: 遇到不懂的Bazel规则或复杂的配置(如
cc_toolchain),直接要求AI给出代码模板。-
例如:"我需要用Bazel封装静态库为动态库,请给我一个BUILD文件模板。"
-
例如:"请提供一个基于Bazel的aarch64交叉编译工具链配置模板。"
-
-
理解与内化 : AI给出的往往是通用逻辑,必须结合自己的项目环境(如Yocto SDK路径、具体的依赖关系)进行修改。不能只做"复制粘贴工程师",要理解每一行配置的作用,才能在报错时迅速定位问题。
技术实践复盘
x86 环境编译与动态库封装
问题一:如何编译输出动态库
因为CEL-CPP 默认输出静态库,并且静态库对三方库的依赖层次很多。我尝试在CEL-CPP项目编译完成之后,手动通过命令,将所有相关静态库链接成动态库,但是一直没有成功。主要原因是:CEL-CPP 依赖的三方模块很多,但是编译完成后,却找不到对应的静态库。这就导致链接生成的动态库时,会提示缺少相关符号定义。
于是,我就采用二次封装,新增 customized_lib 模块。通过依赖 Bazel 的依赖机制,将 CEL 核心能力打包成一个动态库链接,而非手动打包静态库。新增内容如下:
新增 customized_lib 目录,在里面创建 BUILD 和 main.cc 文件。 内容如下:
# /path/to/cel-cpp/customized_lib/BUILD
cc_binary(
name = "libmy_cel_app.so",
srcs = ["main.cc"],
linkstatic = 1,
linkshared = 1,
deps = [
"//parser:parser",
"//eval/public:builtin_func_registrar", # 🌟 核心:强行把基础算子的机器码实体打包进静态库
"//eval/public:activation",
"//eval/public:cel_expr_builder_factory",
"//eval/public:cel_expression",
"//eval/public:cel_options",
"//eval/public:cel_value",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/base",
],
)
filegroup(
name = "extract_fat_static_lib",
srcs = [":libmy_cel_app.so"],
output_group = "static_library",
)
// local_test/main.cc
#include <iostream>
#include <string>
#include "parser/parser.h"
#include "eval/public/cel_value.h"
// 暴露给外部 CMake 项目调用的封装函数 , 实际上上层应用无需调用该接口
bool EvaluateCelLocally(const std::string& expression_str) {
auto test_val = google::api::expr::runtime::CelValue::CreateInt64(2026);
auto parse_status = google::api::expr::parser::Parse(expression_str);
return parse_status.ok();
}
// 引入全家桶静态库所支持的底层核心头文件
#include "parser/parser.h"
#include "eval/public/cel_value.h"
#include "eval/public/activation.h"
#include "eval/public/cel_options.h"
#include "eval/public/cel_expr_builder_factory.h"
#include "eval/public/cel_expression.h"
#include "google/protobuf/arena.h"
#include "eval/public/builtin_func_registrar.h"
// 业务变量上下文(支持传入数字或字符串等规则因子)
struct InternalRuleContext {
std::map<std::string, int64_t> int_vars;
std::map<std::string, std::string> string_vars;
};
bool InternalEvaluateRule(const std::string& expression, const InternalRuleContext& context) {
// 1. 解析阶段 (Parsing AST)
auto parse_status = google::api::expr::parser::Parse(expression);
if (!parse_status.ok()) {
return false; // 表达式语法错误
}
auto parsed_expr = std::move(parse_status).value();
// 2. 运行时引擎构建 (Engine Preparing)
google::api::expr::runtime::InterpreterOptions options;
auto builder = google::api::expr::runtime::CreateCelExpressionBuilder(options);
// 🌟 核心修复:通过 .GetRegistry() 获取函数注册表指针,消除类型转换报错
auto reg_status = google::api::expr::runtime::RegisterBuiltinFunctions(
builder->GetRegistry(), // 👈 关键改动:传入注册表指针,而不是 builder 自身
options
);
if (!reg_status.ok()) {
std::cerr << "[-] 无法注册 CEL 内置标准算子: " << reg_status.message() << std::endl;
return false;
}
// 2. 运行时引擎构建 (Engine Preparing)
// google::api::expr::runtime::InterpreterOptions options;
// auto builder = google::api::expr::runtime::CreateCelExpressionBuilder(options);
auto cel_expr_status = builder->CreateExpression(&parsed_expr.expr(), &parsed_expr.source_info());
if (!cel_expr_status.ok()) {
return false; // 编译执行流失败
}
auto cel_expression = std::move(cel_expr_status).value();
// 3. 变量激活与数据上下文绑定 (Activation Mapping)
google::api::expr::runtime::Activation activation;
// 注入整型变量
for (const auto& [key, val] : context.int_vars) {
activation.InsertValue(key, google::api::expr::runtime::CelValue::CreateInt64(val));
}
// 注入字符串变量
for (const auto& [key, val] : context.string_vars) {
activation.InsertValue(key, google::api::expr::runtime::CelValue::CreateString(&val));
}
// 4. 执行现代安全求值 (Safe Evaluation)
google::protobuf::Arena arena;
// 适配 2026 现代规范,不传或显式传入原生底层分配转换
//auto memory_manager = google::api::expr::runtime::NewThreadSafeCelEvaluationMemoryManager(&arena);
//auto eval_status = cel_expression->Evaluate(activation, memory_manager.get());
auto eval_status = cel_expression->Evaluate(activation, &arena);
if (!eval_status.ok()) {
return false; // 计算过程中发生类型不匹配或除以 0 等异常
}
// 5. 提取并规整最终结果
google::api::expr::runtime::CelValue result = eval_status.value();
if (result.IsBool()) {
return result.IsBool();
}
return false;
}
核心思想: 通过 main.cc 中引用了cel-cpp核心接口(应用层需要依赖cel-cpp库的接口),bazel 在生成动态库时,会将所有依赖的静态库进行打包。
编译指令:build //customized_lib:libmy_cel_app.so -c opt --cxxopt="-std=c++17" --linkopt="-static-libgcc" --linkopt="-static-libstdc++"
问题二:下载中央模块仓库 bcr。
bcr 让Bazel 知道每个依赖模块的版本信息,依赖关系和源码下载地址从而自动解析和管理外部依赖。

解决方式:
-
在允许的网络环境中。下载 bcr 仓, 上传到本地项目。
-
在 .bazelrc 末尾,添加 common --registry=file://%workspace%/bcr 。 指定从本地离线加载brc 仓库,不联网下载。
问题三:三方依赖库下载

解决方式:
-
在允许的网络环境,下载提示的三方库,上传到本地项目 distdir 目录下
-
在 .bazelrc 末尾,添加 common --distdir=distdir 。指定从本地离线加载三方库,不联网下载。
-
一直重复,根据错误提示下载所有依赖三方开源库。最终约需要下载如下三方依赖库。

最终编译生成libmy_cel_app.so 动态库:

arm64 交叉编译攻坚
交叉编译的核心是要告诉bazel 工具的交叉编译链的路径。因此需要进行以下操作。
一、 修改 .bazelrc 文件
# 指定本地bcr 路径,避免重新下载
common --registry=file://%workspace%/bcr
# 指定依赖模块源码路径,避免重新下载
common --distdir=distdir
# 定义cross 配置块,通过 --config=cross启用,指导Bazel如何选择和使用工具链
## 编译目标代码时,不要用系统默认的编译器,而是用自定义的aarch64_suite
build:cross --crosstool_top=//toolchain:aarch64_suite
## 指定目标 CPU 架构为 aarch64
build:cross --cpu=aarch64
build:cross --compiler=gcc
## 宿主机平台工具链配置。因为在编译过程中,Bazel会生成一些工具,在宿主机编译,运行。因此这些工具的编译就不能使用交叉工具链。
### 使用Bazel 自带的默认工具链
build:cross --host_crosstool_top=@bazel_tools//tools/cpp:toolchain
### 指定宿主机 CPU 为 k8(即 x86_64)
build:cross --host_cpu=k8
## 关闭 Bazel 新的 C++ 工具链自动解析机制,才能让 --crosstool_top 生效
build:cross --incompatible_enable_cc_toolchain_resolution=false
## 强制所有 C/C++ 编译生成位置无关代码(PIC),否则 arrch64 架构,在将静态库链接成动态库时,会报错。
build:cross --force_pic
# 透传 SDK 路径环境变量
build:cross --repo_env=OECORE_NATIVE_SYSROOT
build:cross --repo_env=SDKTARGETSYSROOT
build:cross --action_env=OECORE_NATIVE_SYSROOT
build:cross --action_env=SDKTARGETSYSROOT
build:cross --host_action_env=OECORE_NATIVE_SYSROOT
build:cross --host_action_env=SDKTARGETSYSROOT
# 新增:清空 Yocto 脚本注入的编译器环境变量,防止干扰宿主机工具链配置
build:cross --repo_env=CC=
build:cross --repo_env=CXX=
build:cross --repo_env=CPP=
build:cross --repo_env=LD=
二、定义toolchain:aarch64_suite ,让bazel 知道如何进行交叉编译。以及从环境变量中读取相关配置
新增三个文件,toolchain/BUILD、toolchain/cc_aarch64_toolchain_config.bzl、toolchain/sdk_repo.bzl。修改MODULE.bazel
// cel-cpp/toolchain/BUILD
load(":cc_aarch64_toolchain_config.bzl", "cc_aarch64_toolchain_config")
# 1. 创建一个空的文件组,用于满足 cc_toolchain 的必要属性
filegroup(name = "empty_files", srcs = [])
# 2. 生成工具链配置
cc_aarch64_toolchain_config(name = "aarch64_toolchain_config")
# 3. 定义具体的 cc_toolchain
cc_toolchain(
name = "aarch64_cc_toolchain",
toolchain_identifier = "aarch64-poky-linux-toolchain",
toolchain_config = ":aarch64_toolchain_config",
all_files = ":empty_files",
compiler_files = ":empty_files",
dwp_files = ":empty_files",
linker_files = ":empty_files",
objcopy_files = ":empty_files",
strip_files = ":empty_files",
supports_param_files = 1,
)
# 4. 将 toolchain 注册到 suite 中,键是 "aarch64|gcc" 和 "aarch64"。 与.bazelrc 里设置的 --cpu 和 --compiler 对应
cc_toolchain_suite(
name = "aarch64_suite",
toolchains = {
"aarch64|gcc": ":aarch64_cc_toolchain",
"aarch64": ":aarch64_cc_toolchain",
},
)
# toolchain/cc_aarch64_toolchain_config.bzl
load("@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl",
"feature", "flag_group", "flag_set", "tool_path")
load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
# 关键:load 仓库规则生成的路径常量
load("@sdk_info//:sdk_paths.bzl",
"TARGET_SYSROOT", "BIN_PATH", "GCC_INCLUDE", "GCC_INCLUDE_FIXED")
def _impl(ctx):
tool_paths = [
tool_path(name = "gcc", path = BIN_PATH + "/aarch64-poky-linux-gcc"),
tool_path(name = "g++", path = BIN_PATH + "/aarch64-poky-linux-g++"),
tool_path(name = "cpp", path = BIN_PATH + "/aarch64-poky-linux-cpp"),
tool_path(name = "ld", path = BIN_PATH + "/aarch64-poky-linux-ld"),
tool_path(name = "ar", path = BIN_PATH + "/aarch64-poky-linux-ar"),
tool_path(name = "strip", path = BIN_PATH + "/aarch64-poky-linux-strip"),
tool_path(name = "nm", path = BIN_PATH + "/aarch64-poky-linux-nm"),
tool_path(name = "objdump", path = BIN_PATH + "/aarch64-poky-linux-objdump"),
]
sysroot_feature = feature(
name = "sysroot",
enabled = True,
flag_sets = [
flag_set(
actions = [
ACTION_NAMES.c_compile,
ACTION_NAMES.cpp_compile,
ACTION_NAMES.cpp_link_executable,
ACTION_NAMES.cpp_link_dynamic_library,
ACTION_NAMES.cpp_link_nodeps_dynamic_library,
],
flag_groups = [
flag_group(flags = ["--sysroot=" + TARGET_SYSROOT]),
],
),
],
)
return cc_common.create_cc_toolchain_config_info(
ctx = ctx,
toolchain_identifier = "aarch64-poky-linux-toolchain",
host_system_name = "x86_64-poky-linux",
target_system_name = "aarch64-poky-linux",
target_cpu = "aarch64",
target_libc = "glibc",
compiler = "gcc",
abi_version = "gcc",
abi_libc_version = "glibc",
tool_paths = tool_paths,
builtin_sysroot = TARGET_SYSROOT,
cxx_builtin_include_directories = [
GCC_INCLUDE,
GCC_INCLUDE_FIXED,
TARGET_SYSROOT + "/usr/include",
TARGET_SYSROOT + "/usr/include/c++/v1",
],
features = [sysroot_feature],
)
cc_aarch64_toolchain_config = rule(
implementation = _impl,
attrs = {},
provides = [CcToolchainConfigInfo],
)
# toolchain/sdk_repo.bzl
def _sdk_repo_impl(ctx):
native_sysroot = ctx.os.environ.get("OECORE_NATIVE_SYSROOT")
target_sysroot = ctx.os.environ.get("SDKTARGETSYSROOT")
if not native_sysroot or not target_sysroot:
fail("请先 source Yocto SDK 的 environment-setup 脚本," +
"确保 OECORE_NATIVE_SYSROOT / SDKTARGETSYSROOT 已导出")
sdk_path = native_sysroot.rsplit("/sysroots", 1)[0]
bin_path = native_sysroot + "/usr/bin/aarch64-poky-linux"
gcc_lib_parent = native_sysroot + "/usr/lib/aarch64-poky-linux/gcc/aarch64-poky-linux"
parent_p = ctx.path(gcc_lib_parent)
if not parent_p.exists:
fail("找不到 GCC 库目录: " + gcc_lib_parent)
version = None
for entry in parent_p.readdir():
b = entry.basename
if entry.is_dir and b[0:1].isdigit():
version = b
break
if not version:
fail("在 " + gcc_lib_parent + " 下未找到 GCC 版本目录")
gcc_lib_dir = gcc_lib_parent + "/" + version
content = """\
# 由 sdk_repo repository_rule 自动生成,请勿手动编辑
SDK_PATH = %r
TARGET_SYSROOT = %r
NATIVE_SYSROOT = %r
BIN_PATH = %r
GCC_LIB_DIR = %r
GCC_INCLUDE = %r
GCC_INCLUDE_FIXED = %r
""" % (
sdk_path,
target_sysroot,
native_sysroot,
bin_path,
gcc_lib_dir,
gcc_lib_dir + "/include",
gcc_lib_dir + "/include-fixed",
)
ctx.file("sdk_paths.bzl", content = content)
ctx.file("BUILD", "exports_files(['sdk_paths.bzl'])")
sdk_repo = repository_rule(
implementation = _sdk_repo_impl,
attrs = {},
)
# ---- 模块扩展:bzlmod 下通过它触发 sdk_repo ----
def _sdk_extension_impl(module_ctx):
sdk_repo(name = "sdk_info")
sdk_extension = module_extension(
implementation = _sdk_extension_impl,
)
在MOUDLE.bazel 文件末尾,新增如下内容:
# 引入刚刚定义的模块扩展
sdk_ext = use_extension("//toolchain:sdk_repo.bzl", "sdk_extension")
# 将扩展生成的仓库 (@sdk_info) 暴露给当前项目使用
use_repo(sdk_ext, "sdk_info")
编译命令:
source setenv
../bazel build //customized_lib:libmy_cel_app.so - -config=cross -c opt --cxxopt="-std=c++17" --linkopt="-static-libgcc" --linkopt="-static-libstdc++"
最终编译生成交叉编译版libmy_cel_app.so 动态库:

总结
本次CEL-CPP的移植成功,让我深刻体验到了"程序员思路+AI 模板库"的巨大威力。 AI 帮我跨越了bazel 复杂语法的门槛(按照以往经验,从了解bazel 语法,到编写相关模块,至少需要一周多的时间),让我能专注于解决具体的依赖和逻辑问题。
最大的感悟:AI是一个强大的搜索工具,程序员不再需要埋头苦干。要学会将任务分解,向AI索取"脚手架",自己承担堆砌的责任。这才是未来技术人员的核心竞争力。

