你所描述的API看起来非常可疑。请记住,C中实际上没有“数组” - 数组只是指向同一类型多个值的开头的指针,这些值在内存中连续布置。因此,在C中不可能只是“分配”一个数组。有两个概念可以理解为“分配”到一个数组中:首先,将指向某个位置的指针分配给数组的开头:
const char *s1 = "hello";
const char *s2 = "world";
const char *s = s1;
s = s2;
其次,它是将某些数据从一个指针复制到另一个指针,通常使用memcpy()
或类似的函数来完成:
const char *s1 = "hello";
char s2[5];
memcpy(s2, s1, 5);
你现在可能已经明白我所说的你的API为何是可疑的了。你的回调函数会被给予一个
void *
,但是并没有指示应该使用哪种“数组复制”方法。
如果是第一种情况,即将指针复制到数组的开头,那么
void *
类型就极其无用了。它并没有说明这个指针应该如何表示。看起来你正在尝试做这件事,然而,它不会像你想象的那样工作。下面是你代码的编译变体(请注意,它是错误的,很可能会导致你的程序崩溃;请参见下文):
#![feature(libc)]
extern crate libc;
use libc::c_void;
pub extern fn demo(data: *mut *mut c_void) {
let mut vec = vec!(1, 2, 3);
unsafe {
*data = vec.as_mut_ptr() as *mut c_void;
}
}
(请注意,由于自动引用,您可以直接在包含向量的mut变量上调用as_mut_ptr())
参数类型现在不仅是*mut c_void,而是*mut *mut c_void,也就是指向*mut c_void的指针。这样调用此函数的程序可以将类型为void *的局部变量的指针传递给此函数,并获得指向实际数组的指针,类似于
void *data;
some_struct.callback_fn(&data);
请注意,您不能简单地让
demo
仅接受
*mut c_void
,因为您能做的唯一事情就是重新分配参数本身,但重新分配参数只会重新分配该参数值,即此参数所代表的局部变量。这在函数外部无法观察到。换句话说,以下代码(也是您提供的代码的变体):
pub extern fn demo(mut data: *mut c_void) {
let mut vec = vec!(1, 2, 3);
data = vec.as_mut_ptr() as *mut c_void;
}
这段代码什么也不做,Rust很高兴指出这一点:
<anon>:6:20: 6:28 warning: variable `data` is assigned to, but never used, #[warn(unused_variables)] on by default
<anon>:6 pub extern fn demo(mut data: *mut c_void) {
^~~~~~~~
<anon>:8:5: 8:9 warning: value assigned to `data` is never read, #[warn(unused_assignments)] on by default
<anon>:8 data = vec.as_mut_ptr() as *mut c_void;
^~~~
我说
*mut *mut c_void
的代码是错误的原因是它实际上违反了内存安全性。如果你创建一个
Vec
实例并将其存储到本地变量中,当此变量超出作用域时,向量本身将被销毁,并释放其包装的内存。因此,使用
as_ptr()
或
as_mut_ptr()
从中获取的每个指针都将变为无效。
有几种方法可以解决这个问题,最简单的方法是只需
forget()
向量:
use std::mem;
let mut vec = vec![1, 2, 3];
*data = vec.as_mut_ptr() as *mut c_void;
mem::forget(vec);
这种方法会“遗忘”向量-其析构函数不会被调用。然而,这样会在程序中引入内存泄漏。每次调用
demo()
都会分配一些内存,但不会释放,因此最终您的程序将使用所有可用内存,并可能在之后崩溃。在某些情况下,这是明智的做法,特别是在低级代码中。例如,您的API可能指定它只会调用此函数一次。
这种方法的另一个问题是与上述问题的逻辑结果有关。您的API可以指定应该由谁释放提供给它的指针的内存。例如,它可能要求传递使用
malloc()
分配的内存,因此它将自己使用
free()
来释放它。或者它可能指定您应定义另一个函数,在释放所有分配的内存时将调用该函数。无论哪种方式实现起来都有点不方便,我不会详细介绍如何实现,除非确实是您的情况。无论如何,您的API必须清楚地指定内存的所有者,并且您应该考虑这一点,因为Rust对所有权更加明确。
另一个可能性是,您的API要求您将某些数据复制到由
void *
指针指定的内存中。换句话说,它的实现包含像这样的代码:
char buffer[256];
some_struct.callback_fn(buffer);
该API期望在callback_fn
调用后,buffer
中填充了数据。
如果是这种情况,API必须自然地指定您的程序可以使用的缓冲区中的最大字节数,您的demo
函数可能如下所示:
use std::ptr;
use libc::c_void;
pub extern fn demo(data: *mut c_void) {
let vec: Vec<u8> = vec!(1, 2, 3);
unsafe {
ptr::copy_nonoverlapping(vec.as_ptr(), data as *mut u8, vec.len());
}
}
如果需要,你还可以使用std::slice::from_raw_parts_mut()
将data
转换为&mut [u8]
,并使用clone_from_slice()
方法或bytes::copy_memory()
函数,但它们都不稳定,所以不能在稳定版本的Rust中使用。
在这种情况下,您应该特别注意不要溢出由调用程序提供给您的缓冲区。其最大大小应在API中指定。
另一个问题是仅对于字节数组(C
端的char *
,Rust端的&[u8]/&mut [u8]
)才能简单地复制数组。当您开始使用更大的类型时,例如i32
,您将面临可移植性问题的可能性。例如,在C
中,int
没有定义大小,因此您无法盲目将&[i32]
转换为具有四倍原始大小的&[u8]
,并从中将字节复制到*mut u8
。这些问题应该非常谨慎地处理。