接上一篇bindgen基础使用的内容,今天咱们聊聊C里那些“非主流但超实用”的结构——联合(union)、位域(bitfield)和柔性数组(flexible array)。这些结构在C里用来抠内存、省空间,但是Rust里没有直接对应的语法,bindgen作为C/Rust的“翻译官”,处理这些结构有自己的一套逻辑。
一、先搞定C联合(union):一块内存,多种身份
首先回忆下C里的union:和struct长得差不多,但所有字段共享同一块内存,整个union的大小等于最大字段的大小。比如我们定义一个能存两种结构体的union,先写个C头文件(union_demo.h):
#include <stdint.h>
// 定义两个基础结构体
typedef struct {
int32_t a;
int32_t b;
} alpha_t;
typedef struct {
uint32_t c;
uint16_t d;
uint16_t e;
uint8_t f;
} beta_t;
// 联合:共享内存,要么存alpha_t,要么存beta_t
typedef union {
alpha_t alfa;
beta_t bravo;
} greek_t;
接下来用bindgen生成Rust绑定,终端执行:
bindgen union_demo.h --output bindings.rs
打开生成的bindings.rs,你会发现bindgen会根据你的Rust版本生成两种类型:
- Rust 1.19+:生成Rust内置的
union类型(推荐,更符合Rust习惯) - 低版本Rust:生成
BindgenUnion自定义类型
1.1 Rust 1.19+ 使用内置union
写一个完整的Rust示例(main.rs):
// 引入生成的绑定代码
mod bindings;
fn main() {
// 方式1:零初始化union
let _u1 = bindings::greek_t::default();
// 方式2:指定具体变体初始化
let u2 = bindings::greek_t {
alfa: bindings::alpha_t {
a: 1, b: -1 },
};
// 关键:访问union字段必须在unsafe块里
// 因为Rust无法保证你访问的字段是当前有效的那个
unsafe {
println!("u2.alfa: a={}, b={}", u2.alfa.a, u2.alfa.b);
// 共享内存特性:访问bravo会读到alfa二进制值的转换结果
println!("u2.bravo: c={}, d={}, e={}, f={}",
u2.bravo.c, u2.bravo.d, u2.bravo.e, u2.bravo.f);
}
}
直接运行即可:
rustc main.rs && ./main
1.2 低版本Rust 使用BindgenUnion
如果你的Rust版本低于1.19,bindgen会生成BindgenUnion,访问方式略有不同(不能直接初始化,需用辅助方法):
mod bindings;
fn main() {
let mut u3 = bindings::greek_t::default();
unsafe {
// 用as_mut()获取可变引用,赋值
*u3.alfa.as_mut() = bindings::alpha_t {
a: 2, b: -2 };
// 用as_ref()读取值
println!("u3.alfa: a={}, b={}", u3.alfa.as_ref().a, u3.alfa.as_ref().b);
}
}
小提示:如果想指定生成某个版本的Rust绑定,可加--rust-target参数,比如指定nightly:
bindgen union_demo.h --rust-target nightly --output bindings.rs
二、位域(bitfield):抠字节的极致操作
C里的位域是把多个字段挤在一个整数的不同位里,比如用1个位存布尔值、2个位存小整数,主打一个省内存。先写C头文件(bitfield_demo.h):
#include <stdio.h>
#include <stdint.h>
// 位域结构体:a占1位,b占1位,c占2位
typedef struct {
uint32_t a:1;
uint32_t b:1;
uint32_t c:2;
} BitFieldDemo;
// 辅助函数:打印位域内容
void print_bitfield(BitFieldDemo bf) {
printf("BitFieldDemo: a=%u, b=%u, c=%u\n", bf.a, bf.b, bf.c);
}
2.1 编译C代码为静态库
先把C代码编译成静态库,方便Rust调用:
# 生成目标文件
gcc -c bitfield_demo.c -o bitfield_demo.o
# 打包成静态库
ar rcs libbitfield.a bitfield_demo.o
2.2 生成bindings并编写Rust代码
生成bindings:
bindgen bitfield_demo.h --output bindings.rs
Rust示例(main.rs):
// 链接C静态库
#[link(name = "bitfield", kind = "static")]
extern "C" {
fn print_bitfield(bf: bindings::BitFieldDemo);
}
mod bindings;
fn main() {
// 初始化位域结构体
let mut bf = bindings::BitFieldDemo::default();
// bindgen自动生成了set_xxx(设置)和xxx()(读取)方法
bf.set_a(1);
bf.set_b(1);
bf.set_c(3); // c占2位,最大值就是3
// 读取位域值
println!("a: {}", bf.a());
println!("b: {}", bf.b());
println!("c: {}", bf.c());
// 调用C的打印函数(需unsafe)
unsafe {
print_bitfield(bf);
}
// 测试溢出:c占2位,设12会溢出,结果归零(和C行为一致)
bf.set_c(12);
println!("c after overflow: {}", bf.c());
unsafe {
print_bitfield(bf);
}
}
运行代码:
rustc main.rs -L . -o bitfield_demo && ./bitfield_demo
关键注意点:
- bindgen不会让你直接操作位域字段,而是生成
getter(xxx())和setter(set_xxx())方法,不用自己手动算位偏移; - 位域溢出行为和C完全一致(超出位数归零),和Rust普通整数的溢出panic不一样,要注意区分。
三、柔性数组:结构体的“可变尾巴”
C里的柔性数组是结构体最后一个字段为空数组,用来存动态长度的数据(比如数据包、动态字符串),先写C头文件(flex_array_demo.h):
#include <time.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
// C99柔性数组成员:结构体+可变长度尾巴
typedef struct {
time_t timestamp;
unsigned seq;
size_t len;
char payload[]; // 柔性数组,空[]
} MyRecord;
// 创建MyRecord,payload存"hello"
MyRecord* create_record() {
size_t payload_len = 5;
// 分配内存:结构体大小 + payload长度
MyRecord* rec = malloc(sizeof(MyRecord) + payload_len);
rec->timestamp = time(NULL);
rec->seq = 1;
rec->len = payload_len;
memcpy(rec->payload, "hello", payload_len);
return rec;
}
// 打印MyRecord内容
void print_record(MyRecord* rec) {
printf("timestamp: %ld, seq: %u, len: %zu, payload: %.*s\n",
rec->timestamp, rec->seq, rec->len, (int)rec->len, rec->payload);
}
bindgen处理柔性数组有两种方式,咱们逐一说明。
3.1 方式1:默认方式(__IncompleteArrayField)
生成bindings:
bindgen flex_array_demo.h --output bindings.rs
先编译C代码为静态库:
gcc -c flex_array_demo.c -o flex_array_demo.o
ar rcs libflexarray.a flex_array_demo.o
Rust示例(main.rs):
// 链接C静态库和系统libc(用于free释放内存)
#[link(name = "flexarray", kind = "static")]
extern "C" {
fn create_record() -> *mut bindings::MyRecord;
fn print_record(rec: *mut bindings::MyRecord);
}
// 引入libc的free函数
extern "C" {
fn free(ptr: *mut ::std::os::raw::c_void);
}
mod bindings;
fn main() {
// 调用C函数创建record(返回裸指针)
let rec_ptr = unsafe {
create_record() };
let rec = unsafe {
&*rec_ptr };
// 访问柔性数组:用as_slice方法,传入长度
// unsafe原因:Rust无法验证len是否真的对应payload的实际长度
let payload = unsafe {
rec.payload.as_slice(rec.len) };
println!("payload from Rust: {:?}", std::str::from_utf8(payload));
// 调用C打印函数
unsafe {
print_record(rec_ptr);
}
// 释放C分配的内存(必须用C的free)
unsafe {
free(rec_ptr as *mut ::std::os::raw::c_void);
}
}
运行代码:
rustc main.rs -L . -l flexarray -o flex_array_demo && ./flex_array_demo
关键注意点:
- 默认生成的
payload是__IncompleteArrayField类型(零大小),必须用as_slice方法访问; std::mem::size_of::<MyRecord>()只会计算结构体“前缀”的大小,payload的大小需要自己手动加;unsafe的核心原因:Rust无法确认你传入的len是否和实际payload长度匹配,这是C裸内存操作的“遗留问题”。
3.2 方式2:DST方式(动态大小类型)
这种方式更“Rust化”,访问payload时有边界检查,但需要Rust nightly版(依赖不稳定特性)。
生成bindings时添加--flexarray-dst和特性声明:
bindgen flex_array_demo.h \
--flexarray-dst \
--rust-target nightly \
--raw-line '#![feature(ptr_metadata,layout_for_ptr)]' \
--output bindings.rs
Rust示例(main.rs):
#![feature(ptr_metadata,layout_for_ptr)]
#[link(name = "flexarray", kind = "static")]
extern "C" {
fn create_record() -> *mut bindings::MyRecord;
fn print_record(rec: *mut bindings::MyRecord);
}
extern "C" {
fn free(ptr: *mut ::std::os::raw::c_void);
}
mod bindings;
fn main() {
let rec_ptr = unsafe {
create_record() };
let rec_fixed = unsafe {
&*rec_ptr };
// 转换为DST版本的引用(胖指针,自带长度)
let rec_dst = unsafe {
rec_fixed.flex_ref(rec_fixed.len) };
// 直接访问payload切片,自带边界检查
let payload = &rec_dst.payload[..];
println!("DST payload: {:?}", std::str::from_utf8(payload));
// 计算完整大小(包含payload)
let total_size = std::mem::size_of_val(rec_dst);
println!("total size of record: {}", total_size);
// 收尾工作
unsafe {
print_record(rec_ptr);
free(rec_ptr as *mut ::std::os::raw::c_void);
}
}
运行(需nightly版Rust):
rustup run nightly rustc main.rs -L . -l flexarray -o flex_array_dst_demo && ./flex_array_dst_demo
DST方式的优势:
- 胖指针自带长度,访问payload时有边界检查,更安全;
size_of_val能直接计算结构体完整大小(前缀+payload);- 缺点:依赖nightly版,暂时无法用于稳定版Rust项目。
这些都是C/Rust交互的高频场景,掌握后大部分C库的绑定都能搞定。