likes
comments
collection
share

写 Rust 后,总是听说 ABI、C ABI 和 ABI 稳定,到底是个啥?

作者站长头像
站长
· 阅读数 29

ABI 是什么?

ABI 是 Application Binary Interface 的缩写,翻译为应用二进制接口。它是一个二进制程序的公开接口,决定了其他程序如何调用它。

维基百科对此有更简明的解释:

在计算机软件中,应用二进制接口(ABI)是两个二进制程序之间的接口;通常,这些模块中的一个是库或操作系统功能,另一个是用户正在运行的程序。

ABI 描述了两个主要方面:

  1. 数据在内存中的布局方式
  2. 函数的调用方式

例如,在 Windows x64 C ABI 约定中,前4个参数通过寄存器传递给函数。这些参数使用的具体寄存器取决于参数的位置和类型。而对于超过4个的参数,这些参数按照从右到左的顺序被推送到堆栈上。

ABI 稳定是什么?

ABI 稳定性意味着编译器在不同版本之间始终生成相同的 ABI。这确保了二进制文件的兼容性,使在一个版本中编译的库和程序可以在另一版本中正常工作。

然而,Rust 编译器目前并不承诺 ABI 稳定。原因可能是:

  1. 语言的快速发展:Rust 仍在快速发展,不断引入新的特性和优化策略。这些改进可能导致 ABI 的变化。如果在语言早期阶段稳定 ABI,未来的优化和扩展将受到限制,无法进行与 ABI 不兼容的改进。
  2. 性能和内存布局优化:为了实现最佳性能和内存安全,编译器需要灵活地对内存布局和调用约定进行优化。不同版本的编译器可能采用不同的优化策略,从而影响 ABI。

尽管如此,相比于 Rust 语言的核心库和语言特性,ABI 的变化相对缓慢。我试图寻找一个 Rust ABI 破坏性更新的具体例子,但尚未找到。如果你有相关的例子,请在文章下方评论告诉我。

ABI 稳定的好处是什么?

ABI 稳定能够为 Rust 带来了许多好处:

  1. 动态链接支持:稳定的 ABI 允许 Rust 二进制程序之间实现动态链接,这使得 Rust 程序可以支持动态加载插件,这在 C/C++ 中是常见的功能。
  2. 缩短编译时间:动态链接将缩短编译时间,因为多个项目可以链接到同一个动态库,而不需要分别编译所有代码。
  3. 减少磁盘空间使用:多个项目可以共享同一个动态库,从而减少磁盘空间的使用。

C ABI 是什么?

我们经常听到 C ABI,它被大多数静态编译语言(包括 Rust)所支持,使得不同语言编写的二进制程序可以进行动态链接。

C ABI 被认为是稳定的,但实际上,C 语言本身并没有定义 ABI。事实是,C 语言尽量避免定义 ABI。

ABI 是将编程语言的执行模型映射到特定机器、操作系统和编译器组合的一套约定。在语言规范中定义 ABI 是没有意义的,因为这样可能会阻碍某些特定架构上的 C 语言实现。

然而,不同的操作系统供应商为其平台定义了特定的 C ABI 规范。C 语言历史悠久,并且广泛应用于各种平台和架构上,绝大多数编译器都遵循这些平台的规范来生成二进制代码。

不同的操作系统和架构有不同的 ABI 规范文档。以下是一些常见的 C ABI 规范文档:

  1. System V ABI(适用于 x86 和 x86-64 架构)
  2. Microsoft x64 Calling Conventions
  3. ARM Procedure Call Standard (AAPCS)

尝试:C 调用由 Rust 编写的 C ABI 函数

我们使用 Rust 编写一个遵循 C ABI 的函数,并在 C 语言中调用它。

在 C 项目中使用 Rust 代码主要涉及两个部分:

  1. 创建一个对 C 友好的 API。
  2. 将你的 Rust 动态链接库嵌入到外部构建系统中。

创建项目

创建一个新的 cargo 项目。

通过配置来告诉 cargo 输出一个动态链接库。

[lib]
name = "my_lib"
crate-type = ["cdylib"]      # 创建动态库

编写一个 C ABI 函数

#[no_mangle]
pub extern "C" fn say() {
    println!("hello");
}
  • #[no_mangle] 该注解告诉 Rust 编译器不要对函数名进行改编(mangle)。Rust 编译器通常会对符号名称进行改编,以避免命名冲突。但对于需要从其他语言中调用的函数,这样可以确保函数名称在编译后保持不变。

  • extern "C" 个关键字声明函数遵循 C 编程语言的调用约定(calling convention),使得函数可以从 C 语言中调用。

从 C 调用 Rust 函数

那么,问题的一半已经解决了。现在该如何使用它呢?

cargo 将根据你的平台和设置在 target/release 目录中生成一个名为 libmy_lib.dylib(在 Windows 上为 my_lib.dll,在 Linux 上为 libmy_lib.so)的动态库。这个库可以被构建系统链接。

创建一个新的 C 文件 main.c,内容如下:

#include <stdio.h>

// 声明 Rust 函数
void say();

int main() {
    say();
    return 0;
}

编译并链接 C 程序

使用 GCC 编译并链接 C 程序:

gcc -o main main.c -L ./target/release -l my_lib

然后运行生成的可执行文件,输出应为:

hello

C ABI 的限制

C ABI 是相对简单的,这就导致它无法表示 Rust 的许多复杂类型。当你编写以下函数时:

#[no_mangle]
pub extern "C" fn say(name: String) {
    println!("hello {}", name)
}

Rust 编译器会明确提示你:

`extern` fn uses type `String`, which is not FFI-safe

这是因为 String 类型不是 FFI(Foreign Function Interface)安全的。这意味着它不能直接用于与其他编程语言(如 C)进行接口,因为它依赖于 Rust 的内存管理和所有权模型,而这些特性在 C 语言中是没有的。

你只能通过以下方式传递字符串:

use std::ffi::{c_char, CStr};

#[no_mangle]
pub extern "C" fn say(s: *const c_char) {
    // 确保 C 指针不为空
    if s.is_null() {
        println!("Received null pointer");
        return;
    }

    // 将 C 字符串转换为 Rust 的 &str
    let c_str = unsafe { CStr::from_ptr(s) };
    let name = c_str.to_str().expect("Invalid UTF-8");
    println!("hello {}", name)
}

这种方法通过使用 *const c_char 指针来接收来自 C 的字符串,并利用 Rust 的 CStr 类型将其转换为 Rust 的字符串切片 &str。这不仅保证了与 C 语言接口的兼容性,同时还能安全地处理字符串转换和内存管理问题。

参考

转载自:https://juejin.cn/post/7388686202599505932
评论
请登录