返回

Rust 与 C/C++ 混合编程:如何控制 LTO 导出函数?

Linux

在 Rust 与 C/C++ 代码进行 LTO 时如何控制导出函数的可见性

在追求极致性能的道路上,混合语言编程往往是不可避免的选择。将 Rust 和 C/C++ 代码链接在一起,取长补短,可以实现 1+1>2 的效果。而为了将性能压榨到极致,链接时优化 (LTO) 就成了必不可少的利器。LTO 可以跨越语言的边界,将函数内联到调用方,最大程度地减少函数调用开销。

然而,在享受 LTO 带来的性能红利的同时,我们也需要面对一个棘手的问题:如何控制导出函数的可见性。

试想一下,我们有一个 Rust 库,其中包含许多函数,但只想将其中一部分暴露给 C/C++ 代码使用。如果将所有函数都导出为公共符号,不仅会增加库的体积,还会影响 LTO 的效果。因为链接器无法对公共符号进行内联优化,毕竟它不知道其他库是否会依赖这些符号。

那么,有没有一种方法,既可以将 Rust 函数暴露给 C/C++ 代码使用,又能利用 LTO 进行内联优化呢?答案是肯定的,我们可以借助链接器版本脚本和 #[link_section] 属性来实现这一目标。

链接器版本脚本:掌控符号可见性的利器

链接器版本脚本,如同它的名字一样,是一个用于控制链接过程的脚本文件。通过它,我们可以精细地控制最终生成库中的符号可见性。

想象一下,链接器版本脚本就像是一个过滤器,它可以将符号分为“全局可见”和“局部可见”两类。只有被标记为“全局可见”的符号才会被导出到最终的库文件中,而“局部可见”的符号则会被隐藏起来,仅供库内部使用。

{
    global:
        rust_function_to_export;
    local:
        *;
};

以上就是一个简单的版本脚本示例。它将 rust_function_to_export 函数标记为“全局可见”,而其他所有符号,包括那些使用 #[no_mangle]#[export_name] 标记的 Rust 函数,都会被视为“局部可见”。

#[link_section] 属性:将函数归类到特定段

光有版本脚本还不够,我们还需要使用 #[link_section] 属性将 Rust 函数放入特定的段中。

段,是 ELF 文件中的一个基本概念,它就像是一个个容器,用于存放不同类型的代码或数据。链接器在链接时,会根据段名将不同目标文件中的代码或数据合并在一起。

#[link_section = "my_rust_functions"]
#[no_mangle]
pub fn rust_function_to_export() {
    // ...
}

#[link_section = "my_rust_functions"]
#[no_mangle]
fn another_rust_function() {
    // ...
}

以上代码中,rust_function_to_exportanother_rust_function 都被放入了名为 .my_rust_functions 的段中。

构建脚本:将版本脚本与代码联系起来

最后,我们需要在构建脚本中将版本脚本链接到最终的共享库。

rustc --crate-type=cdylib -C link-arg=-Wl,--version-script=version.lds ...

这行命令告诉链接器,在链接时使用 version.lds 文件作为版本脚本。

通过以上三个步骤,我们就可以精细地控制 Rust 导出函数的可见性,将需要暴露给 C/C++ 代码的函数导出为公共符号,同时将其他函数隐藏起来,以便 LTO 进行内联优化。

常见问题解答

  1. 为什么不能直接使用 #[no_mangle] 将所有函数都导出?

    如果将所有函数都导出为公共符号,会导致库文件体积增大,同时也会影响 LTO 的效果。因为链接器无法对公共符号进行内联优化,因为它不知道其他库是否会依赖这些符号。

  2. 链接器版本脚本应该放在哪里?

    链接器版本脚本一般放在项目的根目录下,与 Cargo.toml 文件在同一级目录。

  3. 除了控制函数可见性,链接器版本脚本还能做什么?

    链接器版本脚本的功能非常强大,它还可以用于控制符号版本、定义弱符号、排除特定符号等。

  4. 除了 #[link_section],还有其他方法可以将函数放入特定段吗?

    可以使用 #[cfg_attr(target_os = "...", link_section = "...")] 来根据目标操作系统选择不同的段。

  5. 如何验证 LTO 是否生效?

    可以使用 objdump -d 命令查看目标文件的汇编代码,如果看到函数被内联了,就说明 LTO 生效了。

希望这篇文章能够帮助你更好地理解如何在 Rust 与 C/C++ 代码进行 LTO 时控制导出函数的可见性。如果你有任何问题或建议,欢迎在评论区留言。