如何通过FFI将Rust crate中的符号重命名后重新导出?
我有一个名为ffizz-string的crate,里面包含多个以fz_string_为前缀的公共FFI函数。在另一个taskchampion-lib crate中,我需要导出这些相同的函数,但改用tc_string_前缀,让通用FFI字符串工具适配当前crate的命名风格。
常规Rust重命名的pub use fz_string_borrow as tc_string_borrow对FFI导出无效,我尝试了多种方法(通过nm ./target/debug/deps/libtaskchampion_lib-....rlib | grep string_borrow验证符号):
#![allow(non_upper_case_globals)] // 尝试0:验证常规FFI函数符号正常导出 #[no_mangle] pub unsafe extern "C" fn tc_string_borrow_0() { todo!() } // 结果:符号显示为`tc_string_borrow_0`(T段) // 尝试1:仅用`as`重命名 pub use ffizz_string::fz_string_borrow as tc_string_borrow_1; // 结果:符号仍为原名字`fz_string_borrow` // 尝试2:用`link_name`重命名 extern "C" { #[link_name = "tc_string_borrow_2"] pub static tc_string_borrow_2: unsafe extern "C" fn(cstr: *const i8) -> ffizz_string::fz_string_t; } // 结果:无效,`link_name`用于指定外部符号的导入名,不是导出名 // 尝试3:在use上用`export_name` /* #[export_name="tc_string_borrow_3"] pub use ffizz_string::fz_string_borrow as tc_string_borrow_3; */ // 结果:编译错误,`export_name`只能直接应用于函数或静态变量 // 尝试4:在静态变量上用`export_name` #[export_name = "tc_string_borrow_4"] pub static tc_string_borrow_4: unsafe extern "C" fn(cstr: *const i8) -> ffizz_string::fz_string_t = ffizz_string::fz_string_borrow; // 结果:编译通过,但导出的是变量符号(D段),不是函数符号(T段),C端调用需解引用,不符合需求 // 尝试5:静态变量加`used`属性 #[used] pub static tc_string_borrow_5: unsafe extern "C" fn(cstr: *const i8) -> ffizz_string::fz_string_t = ffizz_string::fz_string_borrow; // 结果:符号仍为原名字,仅保证变量不被优化
目前唯一有效的方法是用函数包装:
#[inline] pub unsafe extern "C" fn tc_string_borrow(cstr: *const i8) -> ffizz_string::fz_string_t { ffizz_string::fz_string_borrow(cstr) }
但这种方法需要重复函数签名,还依赖编译器跨crate内联,有没有更好的方式?
可行解决方案
1. 用宏自动生成包装函数
手动重复签名确实麻烦,可以写一个宏来批量生成包装函数,既减少重复代码,又保证签名一致:
macro_rules! wrap_ffi_string_fn { // 处理单个函数:原函数名 => 新函数名 ($old_fn:ident => $new_fn:ident) => { #[inline(always)] #[no_mangle] pub unsafe extern "C" fn $new_fn(cstr: *const i8) -> ffizz_string::fz_string_t { ffizz_string::$old_fn(cstr) } }; // 支持批量处理多个函数 ($($old_fn:ident => $new_fn:ident),+ $(,)?) => { $(wrap_ffi_string_fn!($old_fn => $new_fn);)+ }; } // 批量生成包装函数 wrap_ffi_string_fn! { fz_string_borrow => tc_string_borrow, fz_string_free => tc_string_free, fz_string_clone => tc_string_clone }
#[inline(always)]会提示编译器尽可能内联这个包装函数,性能上几乎和直接调用原函数无差异。- 宏自动处理签名,避免手动写错,同时批量生成多个函数也很方便。
2. 平台相关的链接器脚本(不推荐)
如果是特定平台(比如Linux),可以用链接器脚本重命名符号,但这种方法不跨平台,维护成本高,而且需要额外配置构建脚本,不如宏方法通用可靠。比如在Linux下,写一个ld脚本:
OUTPUT_FORMAT(elf64-x86-64) SECTIONS { .text : { *(.text) fz_string_borrow = tc_string_borrow; // 其他符号同理 } }
然后在Cargo.toml中指定链接器脚本:
[build] rustflags = ["-C", "link-arg=-T./rename_symbols.ld"]
但这种方法只能在支持ld脚本的平台使用,Windows和macOS需要不同的处理,所以不推荐作为通用解决方案。
总结
最可靠、跨平台的方案还是宏+包装函数,既保证FFI符号按预期导出,又避免手动重复代码。#[inline(always)]能最大化内联概率,几乎不会带来性能损耗。如果能修改ffizz-string crate,也可以考虑让它支持编译时配置前缀,但如果是第三方库,宏方法就是最优解。
内容的提问来源于stack exchange,提问作者djmitche

