AI赋能下的开源库移植实践:从“两周预期”到“两天落地”的思考——基于CEL-CPP移植经验的复盘分享

背景

在最新的边缘计算story中,需要引入 表达式处理库。 经过内部同事评估,最终选择了Google的CEL-CPP开源项目。

这种在项目中引入开源库的工作,以前做一线程序员时也经常做,本来觉得没什么特别的。但是由于最近几年一直在做管理工作,没有接触具体的编码任务,因此对AI的认识也一直停留在提效的口号中,没有深刻的感受。但是在本次移植过程,AI工具对工作的提效,让我真实感受到了冲击。

按照我以往的经验,开源库的移植一般都是以下步骤:

  1. 下载源码,现在宿主机完成编译,输出目标库。这个过程需要解决各种依赖问题,编译错误。最常见的就是外部依赖模块的下载。

  2. 设置环境变量,进行交叉编译。在步骤一的前提下,设置交叉编译的环境变量,再进行编译。这个过程主要就是解决编译问题。

若没有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 知道每个依赖模块的版本信息,依赖关系和源码下载地址从而自动解析和管理外部依赖。

解决方式:

  1. 在允许的网络环境中。下载 bcr 仓, 上传到本地项目。

  2. 在 .bazelrc 末尾,添加 common --registry=file://%workspace%/bcr 。 指定从本地离线加载brc 仓库,不联网下载。

问题三:三方依赖库下载

解决方式:

  1. 在允许的网络环境,下载提示的三方库,上传到本地项目 distdir 目录下

  2. 在 .bazelrc 末尾,添加 common --distdir=distdir 。指定从本地离线加载三方库,不联网下载。

  3. 一直重复,根据错误提示下载所有依赖三方开源库。最终约需要下载如下三方依赖库。

最终编译生成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索取"脚手架",自己承担堆砌的责任。这才是未来技术人员的核心竞争力。