如何正确分配数据以传递给FFI调用?

5
学习了如何正确地从Rust中调用Windows API的FFI之后,我进一步尝试并想要确认我的理解。
我有一个被调用两次的Windows API。在第一次调用时,它会返回实际输出参数所需的缓冲区大小。然后,使用具有足够大小的缓冲区再次调用它。我目前使用Vec作为此缓冲区的数据类型(请参见下面的示例)。
代码可以运行,但我想知道是否这是正确的做法,或者是否更好地利用像alloc::heap::allocate这样的函数直接保留一些内存,然后使用transmute将FFI的结果转换回来。同样,我的代码可以工作,但我正在尝试看一下背后的情况。
extern crate advapi32;
extern crate winapi;
extern crate widestring;
use widestring::WideCString;
use std::io::Error as IOError;
use winapi::winnt;

fn main() {
    let mut lp_buffer: Vec<winnt::WCHAR> = Vec::new();
    let mut pcb_buffer: winapi::DWORD = 0;

    let rtrn_bool = unsafe {
        advapi32::GetUserNameW(lp_buffer.as_mut_ptr(),
                               &mut pcb_buffer )
    };

    if rtrn_bool == 0 {

        match IOError::last_os_error().raw_os_error() {
            Some(122) => {
                // Resizing the buffers sizes so that the data fits in after 2nd 
                lp_buffer.resize(pcb_buffer as usize, 0 as winnt::WCHAR);
            } // This error is to be expected
            Some(e) => panic!("Unknown OS error {}", e),
            None => panic!("That should not happen"),
        }
    }


    let rtrn_bool2 = unsafe {
        advapi32::GetUserNameW(lp_buffer.as_mut_ptr(), 
                               &mut pcb_buffer )
    };

    if rtrn_bool2 == 0 {
        match IOError::last_os_error().raw_os_error() {
            Some(e) => panic!("Unknown OS error {}", e),
            None => panic!("That should not happen"),
        }
    }

    let widestr: WideCString = unsafe { WideCString::from_ptr_str(lp_buffer.as_ptr()) };

    println!("The owner of the file is {:?}", widestr.to_string_lossy());
}

依赖项:

[dependencies]
advapi32-sys = "0.2"
winapi = "0.2"
widestring = "*"

相关但不是主题:为C / FFI库调用分配对象 - Shepmaster
1个回答

4
理想情况下,您应该使用 std::alloc::alloc,因为您可以在 layout 的一部分中指定所需的对齐方式:
pub unsafe fn alloc(layout: Layout) -> *mut u8

主要的缺点是你需要知道对齐方式,即使你释放了分配。
通常使用Vec作为一种简单的分配机制是常见的做法,但是在使用它时需要小心。
  1. Make sure that your units are correct — is the "length" parameter the number of items or the number of bytes?
  2. If you dissolve the Vec into component parts, you need to
    1. track the length and the capacity. Some people use shrink_to_fit to ensure those two values are the same.
    2. Avoid crossing the streams - that memory was allocated by Rust and must be freed by Rust. Convert it back into a Vec to be dropped.
  3. Beware that an empty Vec does not have a NULL pointer!:

    fn main() {
        let v: Vec<u8> = Vec::new();
        println!("{:p}", v.as_ptr());
        // => 0x1
    }
    

针对你的特定情况,我建议使用 Veccapacity 而不是自己追踪第二个变量。你会注意到,在第一次调用后,你忘记更新 pcb_buffer,所以我很确定代码总是会失败。这很烦人,因为它需要可变引用,所以你不能完全摆脱它。
此外,你可以通过 reserve 空间,而不是扩展 Vec
还有没有保证第一次调用所需的大小与第二次调用所需的大小相同。你可以做一些循环,但那样你就必须担心无限循环的发生。

谢谢回答。所以,我想我最好坚持使用Vec的稳定解决方案。我将更改Vec的内容为winnt::CHAR,以便项目数量和长度之间没有区别。我想这会使它更容易。 - Norbert
我有两个问题关于你的回答:
  • 你说我必须将内存转换回Vec才能被删除,这是什么意思?
  • 我不确定是否可以使用容量代替我的第二个变量作为输出参数传递给FFI。Windows API将覆盖此变量的内容,我想用我的Vec容量来做这件事可能不是一个好主意。或者我在这方面误解了你的意思?
- Norbert
@Norbert 我稍微澄清一下。如果您一开始就将Vec分解,那么转换回Vec才是必须的;但您的情况好像并不需要这样做。对于后者,我想用capacity的值来代替0,但您必须准备另一个变量作为引用传递。 - Shepmaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接