我正在将Rust接口写入具有以下签名功能的C库:
typedef int (*callback_t)(const int *a, void *user_data);
void execute(callback_t callback);
我想要的是Rust接口的用户可以为T
传递任何类型的user_data
(在C库中不使用user_data
)。换句话说,在Rust方面,我想:
type Callback<T> = fn(a: &mut usize, user_data: &mut T) -> usize;
我尝试将Callback<T>
类型的用户定义的Rust函数转换为
extern "C" fn callback(a: *mut c_int, user_data: *mut c_void) -> c_int
和as
,但是不起作用。我还尝试创建包装闭合。两种尝试均无效。
有人可以帮我吗?
答案 0 :(得分:3)
您不应在不同签名之间强制转换函数指针。这是灾难性的不安全行为,并且会炸毁您的程序(如果幸运的话)。函数指针不可互换,并且编译器无法神奇地使其兼容。
您在这里实际要做的是接受一份用意大利语写的命令,删除“ language = Italian”,将其替换为“ language = Russian”,并希望俄罗斯厨师能够理解,因为,嘿,它说用俄语!
首先,您对原始C类型的翻译可能是错误的。第一个参数是*const c_int
,而不是*mut c_int
。 C确实允许您抛弃const
,但是其他代码很少期望它。
第二,您不应将原始C指针转换为安全的Rust引用。如果C代码使用空指针调用,则您的Rust代码将具有未定义的行为。除非C库使用血液签名并通过程序员的第一个出生的孩子保证两个指针都不会为空,否则不要信任它:首先检查指针。
第三,c_int
和usize
不是同一类型。不要混淆它们。用于Rust接口的正确类型是c_int
。
因此,Rust中的实际C回调类型为:
type CCallback = Option<extern "C" fn(a: *const c_int, user_data: *mut c_void) -> c_int>;
Option
之所以存在,是因为C函数指针可以为null,而在Rust中则不能。
最后,Callback<T>
没有标记为extern "C"
。正确设置调用约定至关重要。
您打算强制转换为C回调类型的任何函数的签名都应该恰好 C回调签名。那就是:
extern "C" fn a_callback(a: *const c_int, user_data: *mut c_void) -> c_int {
::std::process::abort();
}
现在,您可能可以摆脱这种情况:
extern "C" fn a_callback<T>(a: *const c_int, user_data: *mut T) -> c_int {
::std::process::abort();
}
然后将Some(a_callback)
强制为CCallback
。也就是说,我不能保证所有可能的T
都是正确的。
为了安全起见,应将所有Rust回调函数显式包装在转换函数中。最简单的方法是使用一个宏,在指定Rust函数名称的情况下,该宏会生成C填充码。
macro_rules! shim {
($c:ident => $r:ident) => {
extern "C" fn $c(a: *const c_int, user_data: *mut c_void) -> c_int {
if a.is_null() {
::std::process::abort()
}
if user_data.is_null() {
::std::process::abort()
}
// NOTE: You need to make *absolutely certain* that this cast
// of user_data is valid.
let res: i32 = $r(&*a, &mut *(user_data as *mut _));
res as c_int
}
};
}
shim!(another_callback_c => another_callback);
fn another_callback(a: &c_int, user_data: &mut u8) -> i32 {
// Do something...
::std::process::abort()
}