深入理解 Rustcore::ffi::c_longlong:FFI 互操作中的 64 位 C 类型桥梁
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
c_longlong是 Rust 标准库core::ffi模块中用于表示 C 语言signed long long类型的平台相关类型别名,是 Rust 与 C 代码互操作(FFI)时处理 64 位有符号整数的规范工具。本文基于 Rust 官方仓库中该类型的定义文档与源码实现,讲清它的语义承诺、目标平台上的实际映射、在 FFI 声明与可变参数调用中的用法,以及它与c_long等相邻类型的差异,帮助你在编写extern "C"接口时选对类型、避免跨平台位宽陷阱。
c_longlong 的语义承诺:几乎总是 i64
c_longlong的官方定义文档 c_longlong.md 给出的核心表述是:
Equivalent to C's
signed long long(long long) type.This type will almost always be
i64, but may differ on some systems. The C standard technically only requires that this type be a signed integer that is at least 64 bits and at least the size of along, although in practice, no system would have along longthat is not ani64, as most systems do not have a standardisedi128type.
这里包含三层关键信息,值得逐条理解:
- 等价对象:
c_longlong等价于 C 的long long(有符号形式),而不是 Rust 的某个固定位宽类型。它的存在意义是让 Rust 的 FFI 签名与 C 头文件中的long long参数精确对齐。 - 理论下限:C 标准只要求
long long是「至少 64 位、且至少与long一样大」的有符号整数。也就是说,标准上并不排除某个假想平台上long long为 128 位(即对应 Rust 的i128)的可能。 - 工程现实:由于目前没有主流 C 工具链将 128 位整数标准化为
long long的实现,实践中几乎所有平台的long long都是 64 位。文档中「almost always」的措辞正是为了在标准语义与工程实践之间保持诚实。
源码中的定义机制:type_alias 宏与文档内嵌
从源码结构看,c_longlong并不是一个独立的 Rust 类型,而是一个条件编译驱动的类型别名。其定义位于 primitives.rs:
type_alias! { "c_longlong.md", c_longlong = i64; } type_alias! { "c_ulonglong.md", c_ulonglong = u64; }type_alias!宏在同文件的 第 6~16 行 定义,其展开逻辑是:
macro_rules! type_alias { { $Docfile:tt, $Alias:ident = $Real:ty; $( $Cfg:tt )* } => { #[doc = include_str!($Docfile)] $( $Cfg )* #[stable(feature = "core_ffi_c", since = "1.64.0")] pub type $Alias = $Real; } }从源码结构看,这解释了三个工程细节:
- 文档即代码的一部分:
c_longlong.md通过include_str!内嵌为类型别名上的#[doc]属性,这正是你在 rustdoc 中看到的说明文字。仓库把每个 C 类型的文档拆成独立.md文件(如 c_long.md),便于维护时精确控制每段 API 文档。 - 稳定性版本:宏统一为这些别名打上
#[stable(feature = "core_ffi_c", since = "1.64.0")],因此c_longlong自 Rust 1.64.0 起可在稳定版代码中使用。core::ffi模块本身的c_void更早,自 1.30.0 稳定(见 mod.rs)。 - 与目标相关类型的区分:注意
c_char、c_int、c_long等别名指向c_xxx_definition私有模块(内部用cfg_select!按目标架构展开),而c_longlong直接写死为i64,没有任何条件分支——这与文档「几乎总是 i64」的承诺在代码层面是一致的:维护者选择为它保留理论上的差异空间(文档措辞),但当前所有目标统一映射到i64。
类型最终通过 mod.rs 的pub use重导出到core::ffi:
mod primitives; #[stable(feature = "core_ffi_c", since = "1.64.0")] pub use self::primitives::{ c_char, c_double, c_float, c_int, c_long, c_longlong, c_schar, c_short, c_uchar, c_uint, c_ulong, c_ulonglong, c_ushort, };std侧同样将其透传,见 std 的 ffi 模块,因此std::ffi::c_longlong与core::ffi::c_longlong指向同一别名。
对照整个 C 类型家族:谁随目标变化,谁恒定
理解c_longlong的最好方式是把它放进core::ffi的完整类型表中。结合 primitives.rs 中各别名右侧的实际类型,可以得到如下映射(依据当前仓库源码):
| C 类型 | Rust 别名 | 当前映射 | 是否随目标变化 |
|---|---|---|---|
long long | c_longlong | i64 | 否 |
unsigned long long | c_ulonglong | u64 | 否 |
long | c_long | i32或i64 | 是 |
int/unsigned int | c_int/c_uint | i32/u32(avr、msp430 上为i16/u16) | 是 |
char | c_char | i8或u8 | 是 |
double | c_double | f64(avr 上为f32) | 是 |
其中与c_longlong对比最常被踩坑的是c_long。其定义在 primitives.rs 的 c_long_definition 模块:
crate::cfg_select! { any( all(target_pointer_width = "64", not(windows)), // wasm32 Linux ABI uses 64-bit long all(target_arch = "wasm32", target_os = "linux") ) => { pub(super) type c_long = i64; pub(super) type c_ulong = u64; } _ => { // The minimal size of `long` in the C standard is 32 bits pub(super) type c_long = i32; pub(super) type c_ulong = u32; } }这意味着:在 64 位 Linux/macOS 上c_long是i64,而在 Windows(以及 32 位目标)上是i32。这正是官方文档 c_long.md 强调的差异点。实践结论很直接:
- 当 C 接口签名中写的是
long long(例如strtoll、time_t在多数平台、64 位文件偏移off_t常见展开)时,用c_longlong,跨平台位宽恒为 64; - 当 C 签名写的是
long时,用c_long,并接受它在不同平台上的位宽差异; - 如果 C 头文件里显式写
int64_t,那才是真正的「固定 64 位」承诺——此时用c_longlong通常安全,但严格起见应确认该平台的int64_t基础类型。
另外,c_size_t、c_ptrdiff_t、c_ssize_t这三个别名仍标记为 unstable(featurec_size_t,跟踪 issue 88345,见 primitives.rs 第 156~175 行),当前均等价于usize/isize;而c_longlong是稳定 API,不需要任何 feature gate。
实战用法一:extern "C" 函数声明
c_longlong的典型出场位置是extern "C"块。假设某 C 库提供如下头文件:
// c 库头文件 int64_t compute_hash(const char *s, long long len);对应 Rust 侧声明(len参数按 C 签名用c_longlong对齐;若库实际写long long,这是最贴切的映射):
use core::ffi::{c_char, c_int, c_longlong}; // 或等价地:use std::ffi::{c_char, c_int, c_longlong}; unsafe extern "C" { // 注意:返回类型若头文件是 int64_t,同样可用 c_longlong 对齐; // 若头文件写 long,则应使用 c_long 而不是 c_longlong。 fn compute_hash(s: *const c_char, len: c_longlong) -> c_longlong; }关键要点:
- 签名逐字对齐:FFI 的类型错配(如把
long写成c_longlong、在 Windows 上把c_long当作i64)是典型的未定义行为来源。上表中的「是否随目标变化」列就是排查依据。 - 别名而非裸整数:虽然当前目标上
c_longlong展开就是i64,仍建议使用别名——它表达的是「这里对应 C 的long long」这一语义,且在理论上某目标出现差异时,别名比裸i64更稳健(这正是c_long已实际发生的差异)。 unsafe边界:调用方需保证参数指针有效、值域与 C 侧一致;对*const c_char等参数,可读到的内容需符合 C 侧预期(如 NUL 结尾字符串语义由 C 约定)。
实战用法二:读取 C 可变参数(VaList)
c_longlong还有一个较少被注意但很实用的场景:Rust 1.99.0 起稳定了core::ffi的 C 可变参数支持(featurec_variadic)。在 va_list.rs 中:
VaArgSafe文档明确列出c_int、c_long与 [c_longlong](以及无符号对应物、c_double、裸指针)是保证可读取的变参类型,而i32/usize等原生类型不应被直接依赖(因为它们在部分平台上可能不适用),见 VaArgSafe 文档;unsafe impl VaArgSafe for i64与u64(即c_longlong/c_ulonglong的底层类型)在所有目标上都成立,见 第 347~359 行;- 库内还有一段
const _编译期断言va_arg_safe_check::<crate::ffi::c_longlong>()(第 424~443 行),确保别名与其底层类型的VaArgSafe实现保持一致。
因此,桥接 C 的printf风格可变参数函数时,应使用c_longlong而非i64来读取 64 位整数实参——这与「C 变参中比c_int小的整数会被整型提升」的安全契约相一致,该契约同样写在VaArgSafe的文档注释中。
与 C 标准的对照:为什么「至少 64 位」仍恒为 i64
回到定义文档的第一性原理。C 标准对long long的约束是int≤long≤long long且位宽至少 64 位。从源码结构看,仓库对这条约束的处理方式是「文档声明理论空间 + 代码固定映射」:c_longlong的别名定义不进入cfg_select!分支(对比c_long_definition的双分支实现),即当前 Rust 支持的所有目标上它都是i64。文档措辞保留「may differ on some systems」,是为未来可能出现非标准 64 位long long的目标(例如假想的 128 位long long)留出语义余地;在VaArgSafe的注释中也能看到仓库对「平台无__int128类型时不提供 128 位变参支持」这类平台差异的一致处理思路(见 第 361~414 行),可作为理解这一类型族整体设计哲学的旁证。
小结
c_longlong等价于 C 的signed long long,当前在所有 Rust 目标上稳定映射为i64(定义,自 1.64.0 稳定)。- 它是
core::ffi类型族中位宽「恒定」的代表;与它的c_long(32/64 位随平台)、c_char(有符号性随平台)不同,跨平台 FFI 中表达 64 位有符号量应优先选择它。 - 适用场景包括
extern "C"声明中对应 C 头文件的long long参数,以及 C 可变参数函数中通过VaList安全读取 64 位整数实参。 - 选型口诀:C 签名写
long long用c_longlong;写long用c_long;写固定位宽整数(int64_t)时确认平台基础类型后再定。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考