TEN-framework / ten_framework

TEN, the Next-Gen AI-Agent Framework, the world's first truly real-time multimodal AI agent framework.
https://doc.theten.ai/
Other
245 stars 19 forks source link

RFC: rust development when some dependency is compiled with asan enabled #191

Closed halajohn closed 3 hours ago

halajohn commented 3 hours ago
  1. 背景

    ten_framework 中会存在 C 和 Rust 依赖的问题, 依赖关系如下:

    • ten_rust (Rust project) 会依赖 ten_utils (C project) 的静态库.
    • ten_runtime (C project) 会依赖 ten_rust (Rust project) 输出的 静态库.

    ten_utils 和 ten_runtime 是通过 TGN 工具链编译的, 通过 --enable_sanitizer 参数控制是否开启 asan. 同时, 对 libasan-rt 的依赖是 clang 或者 gcc 提供的.

    而 ten_rust 和 ten_manager 作为 Rust 工程, 触发编译的场景有 3 个:

    • 通过 TGN 中的 tamplate, 包装了 cargo build 命令, 由 TGN 触发编译. 这种情况下, 会统一根据 TGN 中的 --enable_sanitizer 参数来判断是否开启 RUST 的 asan. 如果开启, 在 cargo build 的环境变量中添加需要的 RUSTFLAGS.

      能够正常编译 + 运行

    • 在 vscode 开发, 通过 vscode 中的 Run 按钮触发编译; 或者在命令行执行 cargo 相关的命令, 如 cargo build, cargo test, cargo check 等.

      没办法正常编译, 无法链接 asan 相关的 symbol.

    • 在 vscode 中, 通过 launch.json 开启 debug 任务, 可以设置 env 参数.

      能够正常编译 + 运行

    在 Rust 中开启 ASAN 的方式有如下 3 种:

    • 通过环境变量, RUSTFLAGS=-Zsanitizer=address.

      需要注意的是, 该环境变量是作用于 cargo 的, 不是 rustc. cargo 会根据该环境变量, 设置 rustc 执行的 flag (-Z sanitizer=address). 也就是说, 在 build.rs 中通过如 env::set_var 的方式设置环境变量并不会生效.

    • 通过在 .cargo/config.toml 中设置 rustflags. 如:

      [target.x86_64-unknown-linux-gnu]
      rustflags = ["-Z", "sanitizer=address"]

      同样, 也不能在 build.rs 中, 通过如 `println!("cargo::rustc-flags=-Zsanitizer=address") 的方式设置. 在 build.rs 中, 只允许设置 -L, -l 等, 不允许 -Z 参数.

    • build.rs 中通过设置 linker, 如 println!("cargo::rustc-link-lib=asan"). 但这种方式, 会有如下问题:

      • rustc-link-lib 是作用于 rustc 的 ld 的, 意味着如果不指定 rustc-link-search 的话, 找到的是系统下的 asan, 而不是 rustc toolchain 下的 asan (一般在 ~/.rustup/toolchains/<target_cpu>/lib/rustlib/<target_cpu>/lib/librustc-nightly_rt.asan.a). 相当于是依赖外部的 libasan-rt, 对于 rust 来说, 需要搭配 rustflags = ["-Z", "external-clangrt"] 使用.
      • 同时, 对于 crate-type 是 bin 的 crate 来说, 就是动态依赖 asan, 需要在执行时, 通过 LD_PRELOAD 优先加载 asan.
      • 另外, 这种方式是指定 libasan-rt 的源, 而不是在决定是否 开启 asan.

    同时, 在 build.rs 中, 通过如下方式设置均不生效:

    • env::set_var("RUSTFLAGS", "-Zsanitizer=address")
    • println!("cargo:rustc-env=RUSTFLAGS=-Zsanitizer=address")
    • println!("cargo:rustc-flags=-Zsanitizer=address")

    build.rs 的作用是 cargo 会将 build.rs 中的输出(即 println! ) 保存到 target/deps/<crate>/build/output 文件中, 作为 rustc 编译器的参数设置. 所以, 在 build.rs 中设置环境变量, 并不会作用到 rustc 的执行链中.

    cargo 中查找 RUSTFLAGS 的顺序(参考 cargo 源码):

    • 先找 CARGO_ENCODED_RUSTFLAGSRUSTFLAGS 系统环境变量.
    • 如果环境变量不存在, 则从 .cargo/config.toml 中查找. 查找的优先级顺序为:
      • target.\*.rustflags
      • target.cfg(..).rustflags
      • host.\*.rustflags, 但依赖开启 -Zhost-config.
      • build.rustflags

在开启 asan 时, 执行 cargo 相关的命令, 必须加上 --target 命令, 或者执行命令时, 带上 -Zhost-config 参数.

  1. 预期达到的目标 在使用 TGN 编译 ten_utils, 并且开启 ASAN 时, 依赖 ten_utils 的 ten_rust 和 ten_manager 可以同时满足以下开发场景:

    • 通过 vscode 中的 Run/Debug Test 按钮, 可以正常运行和调试用例.
    • 在命令行, 通过 cargo build 或 cargo build --tests, 可以正常编译.
    • 在命令行, 通过 cargo test, 可以执行测试用例.
    • 在命令行, 通过 tgn build ... 可以正常执行编译.
  2. 实现方式

    如上所述, 在 vscode 中开发时, 影响因素有两个:

    • 需要通知 cargo 开启 asan
    • 需要不指定 --target 参数, 或者说, 指定默认的 target 参数.

    基于此, 最佳的实现方式是, 在使用 TGN 编译 ten_utils 时, 如果开启了 ASAN, 在 ten_framework 目录下生成 .cargo/config.toml, 内容如下:

     [target.x86_64-unknown-linux-gnu]
     rustflags = ["-Z", "sanitizer=address"]
    
     [build]
     target = "x86_64-unknown-linux-gnu"

    具体 target 的值, 根据 os 和 cpu 判断.

    在下次编译, 删除该配置.

    cargo 中 config.toml 的作用范围如下:

    假如在 /projects/foo/bar/baz 目录下执行编译, config.toml 的查找路径为:

    • /projects/foo/bar/baz/.cargo/config.toml
    • /projects/foo/bar/.cargo/config.toml
    • /projects/foo/.cargo/config.toml
    • /projects/.cargo/config.toml
    • /.cargo/config.toml
    • $CARGO_HOME/config.toml, 默认是: Windows: %USERPROFILE%\.cargo\config.toml Unix: $HOME/.cargo/config.toml

      所以, 在 ten_framework/.cargo/config.toml 中设置, 可以满足上述所有场景, 同时对开发者无感知.

halajohn commented 3 hours ago

如果 ten_framework/.cargo/config.toml 存在, 内容如下:

[target.x86_64-unknown-linux-gnu]
rustflags = ["-Z", "sanitizer=address"]

[build]
target = "x86_64-unknown-linux-gnu"

同时, ten_rust/.cargo/config.toml 存在, 内容如下:

[target.x86_64-unknown-linux-gnu]
rustflags = ["-l", "ten_utils_static"]

测试结果显示, 在编译 ten_rust 时, rustflags 是合并了, 而不是 cargo 在找到当前 crate 下的 config.toml 中包含 rustflags 就不往上找了.


关于 config.toml 继承的原则: 如果同一个 key 在多个层级的 config.toml 中都有定义, 则会被 merge. 规则是:

参考: https://github.com/rust-lang/cargo/blob/master/src/doc/src/reference/config.md#hierarchical-structure

halajohn commented 3 hours ago

The difference between GCC and Clang's ASan libraries has already been handled in TGN. When needed, the appropriate ASan shared library will be copied to the tests/standalone/ directory, and during testing, the presence of libasan.so will be automatically detected, triggering the use of LD_PRELOAD. This mechanism can and should be leveraged to avoid duplicating the same handling logic for the ASan shared library outside of the logic already managed by TGN.

./out/linux/x64/tests/standalone$ tree . -L 1
.
├── go_standalone_test
├── libasan.so    <== here
├── ten_manager/
├── ten_runtime_smoke_test
└── ten_rust/
halajohn commented 3 hours ago

关于 asan runtime 的动态链接或静态链接 (补充)

总结来说 (非 rust 场景):

参考: https://github.com/google/sanitizers/issues/1086.

对于 rust, 情况如下.

非 executable 情况

假设 crate type 的输出包括 staticlib, rlib, cdylib, 如下:

[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[lib]
name = "hello"
crate-type = ["staticlib", "rlib", "cdylib"]
test = true

executable 的情况

默认情况下, rust executable 编译时, clang 和 gcc 默认都是 静态链接 asan runtime.

如果要开启 dynamic link asan runtime, 与上述配置方式相同. 如 clang:

[target.x86_64-unknown-linux-gnu]
rustflags = [
    "-C",
    "linker=clang",
    "-C",
    "link-arg=-fuse-ld=lld",
    "-Z",
    "external-clangrt",
    "-Z",
    "sanitizer=address",
    "-C",
    "link-args=-fsanitize=address -shared-libsan",
]

[build]
target = "x86_64-unknown-linux-gnu"

这种情况下, 可以配合 LD_PRELOAD 或者 设置 rpath 的方式在运行时查找 asan runtime.

dynamic link 和 static link 混用的场景

如果 ten_manager 采用 dynamic link 的方式, ten_rust 采用 static link 的方式; ten_manager 以 rlib 的方式依赖 ten_rust, 测试可行.

rpath

rustc 支持 -C rpath 来设置 lib 或 executable 的 rpath, 但值只能是 on, 并不是设置路径. 如果设置为 on, 默认指向的是 toolchain 下的 lib, 如: Library runpath: [$ORIGIN/../../../../../../../../../../.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib].

TODO

halajohn commented 3 hours ago

windows 下使用 msvc, 开启 asan

参考: https://devblogs.microsoft.com/cppblog/msvc-address-sanitizer-one-dll-for-all-runtime-configurations/

TODO: 待验证.

halajohn commented 3 hours ago

应该不需要在 .cargo/config.toml 里面显示的指定 build 字段.

[build]
target = "x86_64-unknown-linux-gnu"

最终需要先有一个总结, 在 ten framework 内如何设置, 使得可以达成最初想要的目标.

halajohn commented 3 hours ago

实现方案

目标

实现

config.toml 配置示例

以执行 tgn gen linux x64 debug 为例.

增加 GN 控制参数: enable_cargo_config_generated

因为 cargo 支持在 rust crate 源码目录之外触发编译, 通过 --manifest-path 参数指定编译源码路径即可. 如, 可以在 ten_framework 同级目录下执行如下命令来编译 ten_rust:

$ cargo build --tests --manifest-path=ten_framework/core/src/ten_rust/Cargo.toml

.cargo/config.toml 的作用机制是基于 cargo 命令的执行目录的. 也就是说, 这种场景下在 ten_framework 目录下创建上述 .cargo/config.toml 并不会生效.

所以可以通过 enable_cargo_config_generated 参数来决定是生成 .cargo/config.toml 文件, 还是直接增加 cargo 执行参数或者环境变量.

在 CI 构建时, 会 false. 这样也不会在源码目录下生成临时文件, 不用考虑执行结束后删除的问题.

halajohn commented 3 hours ago

在 TGN 中增加 template, 根据构建参数中的 os, cpu, type 来决定是否开启 rust asan.

这个应该要能直接使用 tgn 的 enable_sanitizer 即可, 不需要另外有新的逻辑判断. 也就是说, enable_sanitizer = true, 则开启 rust sanitizer, 反之则关闭.

同时, 如果开启 asan, 在 ten_framework 目录下生成 .cargo/config.toml 文件.

这个 .cargo/config.toml file 内, 一定需要 [build] 字段吗? 没有一定需要的话, 先拿掉这个字段.

[target.x86_64-unknown-linux-gnu] rustflags = [ "-C", "linker=clang", "-C", "link-arg=-fuse-ld=lld", "-Z", "external-clangrt", "-Z", "sanitizer=address", "-C", "link-args=-fsanitize=address -static-libsan", ]

以 clang 的 case 来说, clang 默认 的行为是 static asan, 这样有需要下 -static-libsan 的参数嘛? 如果不需要的话, 加个 comment 说明下即可.

[target.x86_64-unknown-linux-gnu] rustflags = [ "-C", "linker=gcc", "-Z", "external-clangrt", "-Z", "sanitizer=address", "-l", "asan", "-C", "link-arg=-Wl,-rpath=$ORIGIN:/usr/lib/gcc/x86_64-linux-gnu/13", ]

以 gcc 的 case 来说, tgn 已经处理了 asan.so 的 location 问题, tgn 会把它复制到 tests/standalone/ 下, 使用那个路径即可, 不然这边还会有一个散落在外的逻辑要判断 gcc 版本所搭配的 asan 路径, 所有这些 asan 版本跟路径, 都统一在 tgn 复制 asan.so 那边即可.

halajohn commented 3 hours ago

在 TGN 中增加 template, 根据构建参数中的 os, cpu, type 来决定是否开启 rust asan.

这个应该要能直接使用 tgn 的 enable_sanitizer 即可, 不需要另外有新的逻辑判断. 也就是说, enable_sanitizer = true, 则开启 rust sanitizer, 反之则关闭.

同时, 如果开启 asan, 在 ten_framework 目录下生成 .cargo/config.toml 文件.

这个 .cargo/config.toml file 内, 一定需要 [build] 字段吗? 没有一定需要的话, 先拿掉这个字段.

[target.x86_64-unknown-linux-gnu] rustflags = [ "-C", "linker=clang", "-C", "link-arg=-fuse-ld=lld", "-Z", "external-clangrt", "-Z", "sanitizer=address", "-C", "link-args=-fsanitize=address -static-libsan", ]

以 clang 的 case 来说, clang 默认 的行为是 static asan, 这样有需要下 -static-libsan 的参数嘛? 如果不需要的话, 加个 comment 说明下即可.

[target.x86_64-unknown-linux-gnu] rustflags = [ "-C", "linker=gcc", "-Z", "external-clangrt", "-Z", "sanitizer=address", "-l", "asan", "-C", "link-arg=-Wl,-rpath=$ORIGIN:/usr/lib/gcc/x86_64-linux-gnu/13", ]

以 gcc 的 case 来说, tgn 已经处理了 asan.so 的 location 问题, tgn 会把它复制到 tests/standalone/ 下, 使用那个路径即可, 不然这边还会有一个散落在外的逻辑要判断 gcc 版本所搭配的 asan 路径, 所有这些 asan 版本跟路径, 都统一在 tgn 复制 asan.so 那边即可.

  1. build 一定需要, 在执行 cargo build 或者 vscode 中, 并无法指定 target. 而 rustc 要求开启 asan 时, 必须指定 target, 否则会有编译的错误.
  2. clang 下, 如果是编译 executable, 只需要指明 -C link-arg=-fsanitizer=address, 相当于 ldflags 中增加该参数.
  3. 需要考虑直接执行 cargo test 或者 cargo build 的情况, 不通过 tgn 触发.