I recently generated Rust bindings to some C functionality and ran into an issue where the bindgen generated a function definition that took a *mut *mut c_void. However, passing a *mut *mut c_void returned an error from the function. After some testing I found that passing a *mut c_void did not (I had to change the definition from the bindings). I have attempted to recreate an MVC of the issue below. The following is a bit long, please bear with me.
A representation of the C code. This is not the exact interface as only the API call is documented but the internals are hidden. The following is an approximation of the API functionality. In C you provide a PVOID * and it fills out that pointer with some memory.
// test.h
#pragma once
#include <windows.h>
EXTERN_C DWORD DoublePointerTest(IN OUT PVOID * Address);
EXTERN_C DWORD PointerTest(IN OUT PVOID * Address);
// test.c
#include "test.h"
DWORD DoublePointerTest(IN OUT PVOID * Address) {
if ( *(DWORD *)Address == 0x0 ) {
*Address = VirtualAlloc(NULL, 1024, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
if( *Address != NULL ) {
return 0;
}
else {
return GetLastError();
}
}
printf("Error, Address is not NULL: %x\n", *(DWORD *)Address);
return 0x99000099;
}
DWORD PointerTest(IN OUT PVOID * Address) {
if ( *(DWORD *)Address == 0x0 ) {
*Address = VirtualAlloc(NULL, 1024, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
if( *Address != NULL ) {
return 0;
}
else {
return GetLastError();
}
}
printf("Error, Address is not NULL: %x\n", *(DWORD *)Address);
return 0x99000099;
}
Rust bindgen generated the following bindings.
/* automatically generated by rust-bindgen 0.59.1 */
pub type DWORD = ::std::os::raw::c_ulong;
pub type PVOID = *mut ::std::os::raw::c_void;
extern "C" {
pub fn DoublePointerTest(Address: *mut PVOID) -> DWORD;
}
extern "C" {
pub fn PointerTest(Address: PVOID) -> DWORD; // this has been changed manually from *mut PVOID to just PVOID after the bindings were generated
}
So, in DoublePointerTest, bindgen generated *mut PVOID, which itself is *mut c_void, so its really *mut *mut c_void. I changed the second function, PointerTest, to just take *mut c_void to see if that would work.
Heres the rust code and the test output.
include!("./bindings.rs");
#[cfg(test)]
mod tests {
use core::ffi::c_void;
use crate::{PointerTest, DoublePointerTest};
#[test]
fn double_pointer_test() {
let mut x: usize = 0;
let mut x_ptr = &mut x as *mut usize as *mut c_void; // cast &mut x to usize ptr (which it is) then cast that to a void *
let res = unsafe { DoublePointerTest(&mut x_ptr as *mut *mut c_void)}; // cast to *mut *mut x_ptr
assert_eq!(res, 0, "Result: {:x}", res);
}
#[test]
fn pointer_test() {
let mut x: usize = 0;
let x_ptr = &mut x as *mut usize as *mut c_void; // cast &mut x to usize ptr (which it is) then cast that to a void *
let res = unsafe { PointerTest(x_ptr) };
assert_eq!(res, 0, "Result: {:x}", res); // x_ptr = *mut c_void
}
}
Running unittests (target\debug\deps\test_sys-c05258ef0066ffdb.exe)
running 2 tests
Error, Address is not NULL: 23fff340
test tests::pointer_test ... ok
test tests::double_pointer_test ... FAILED
failures:
---- tests::double_pointer_test stdout ----
thread 'tests::double_pointer_test' panicked at 'assertion failed: `(left == right)`
left: `2566914201`,
right: `0`: Result: 99000099', src\lib.rs:13:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::double_pointer_test
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
In DoublePointerTest I passed in *mut *mut c_void as the bindings expected and received an error. In PointerTest I passed just *mut c_void and it works as expected, no errors. In C you would call the function using something like the following:
void *x = NULL;
DWORD res = PointerTest(&x); // &x = void **x
which does work.
double_pointer_test fails because C tries to dereference the memory and sees that it is not NULL so it returns an error. The real API attempts to allocate memory at this address and then fails (since it is already allocated) and returns an error.
So my questions are:
- Did bindgen generate *mut *mut just to be safe because it can't know the implementation details?
- Why does *mut *mut behave differently than void**. Is it because Rust passes by reference already and so you dont need an extra layer of inderection?
- Would it be better to rewrite the bindings so that it takes a Rust reference to a usize e.g. PointerTest(&mut usize)?
Thanks for reading, I know it was long. And thanks for any help that you might provide.