Call to DLL to fill struct incl. char*

Hi there,

I am a Rust beginner.... but need to call a function in a DLL which needs to fill data into a struct. So I build to small test to try out, which this:

struct _TestStruct {
	char* cp;
	size_t size_cp;
	float f;
	int   i;
};

typedef struct _TestStruct TestStruct;

    void test_func(TestStruct& ts) {
    	ts.cp = new char[15];
    	ts.size_cp = 15;
    	memset(ts.cp, 0, 15);
    	strncpy_s(ts.cp, 15, "Hello world!", 13);
    	ts.i = 42;
    	ts.f = (float) 3.14;
    	return;
    }

That the function I am trying to call, I could, if necessary allocate the memory in the Rust code as well, but there I do not know, how much I will need.

This is what I have... not compiling at the moment, but I am also not sure, whether it is the right approach:

    use std::os::raw::c_char;
    #[repr(C)]
    pub struct TestStruct {
        pub cp: *const c_char,
        pub size: u8,
        pub float: f32,
        pub num: i32
    }

    #[link(name="test_dll_for_rust")]
    extern {
        fn showVarSizes();
        fn test_func(data: &mut TestStruct);
        fn multiply(a: i32, b: i32) -> i32;
    }

    fn main() {
        println!("Hello, world!");
        unsafe{showVarSizes();}
        let mut data = TestStruct{cp: 0, size: 0, float: 0.0, num: 0};
        unsafe{
            println!("3 * 4 = {}", multiply(3,4));
            test_func(&mut data);
        };
    }

Any pointer into the right direction?

Thanks!

Please sourround your code with three backticks (```) so that your code will formatted as such.
It will make it easier to read for all of us.

Thanks for the hint, was actually looking for something like that, but was late last night :wink:

The only error I see is this:

error[E0308]: mismatched types
  --> src/lib.rs:20:39
   |
20 |         let mut data = TestStruct{cp: 0, size: 0, float: 0.0, num: 0};
   |                                       ^ expected *-ptr, found usize
   |
   = note: expected type `*const i8`
              found type `usize`

because 0 is not a pointer, but you can call std::ptr::null() instead (null_mut() for a *mut pointer).

Is that the only issue?

Be aware that only C++ can safely free the allocated buffer because it was created with new[]. You need to pass the pointer back to C++ so it can be deleted appropriately (or just leak it).

That was actually an important hint ;), I was not sure how close I was. With that I got that part working, my data types for the numbers do not fit yet, need to have a second look at it.

I was hoping that when my Struct in Rust goes out of scope, that the memory will be freed, leaking it, will not be an option in the real scenario I am going to have, as that code will be executed quite often.

In case I would want to allocate the memory in Rust, what would be the correct type for cp, so I can use it in C++?

Thanks so far!

Ok, so the above I got working (the number issues was, size should be a u64 instead of u8). So now I wanted to address the memory allocation issue and find a solution that the memory gets allocated in Rust. Here is my thought (not working):

use std::os::raw::c_char;
use std::ffi::CStr;

#[repr(C)]
struct TestStruct {
    //pub cp: *const c_char,
    pub cp: [c_char; 15],
    pub size: u64,
    pub float: f32,
    pub num: i32
}

#[link(name="test_dll_for_rust")]
extern {
    fn showVarSizes();
    fn test_func(data: &mut TestStruct);
}



fn main() {
    println!("Hello, world!");
    //let mut data = TestStruct{cp: std::ptr::null(), size: 0, float: 0.0, num: 0};
    let mut data = TestStruct{cp: [0;15], size: 15, float: 0.0, num: 0};
    unsafe{
        showVarSizes();
        test_func(&mut data);
        let c_str = CStr::from_ptr(data.cp.as_ptr());
        println!("{} {} {} {}", c_str.to_str().unwrap(), data.size, data.float, data.num);
    };
}

And the C++ code look like this now:

struct _TestStruct {
	char* cp;
	size_t size_cp;
	float f;
	int   i;
};

typedef struct _TestStruct TestStruct;

void test_func(TestStruct& ts) {
	//ts.cp = new char[15];
	//ts.size_cp = 15;
	//memset(ts.cp, 0, ts.size_cp);
	strncpy(ts.cp,"Hello world!", 13);
	ts.i = 42;
	ts.f = (float) 3.14;
	return;
}

The code compiles without errors, but when I execute the programm I get:
error: process didn't exit successfully: target\debug\test-dll-data.exe (exit code: 0xc0000005, STATUS_ACCESS_VIOLATION)
By trying around, I know the violation is happening in the DLL code. So something is wrong with my data type, but at the moment I do not know what.

The C++ code expects a pointer, but you are providing an array of 15 zeros. I assume the access violation happens when the zero pointer is dereferenced. You need to pass a pointer instead. You could obtain a pointer by calling as_ptr(), but that requires that you make sure the array stays alive for the duration of the function. In particular, you wouldn't be able to declare the array inline.

2 Likes

Tangentially: you should use type aliases that are guaranteed to work for C interop, rather than guess at sizes, which is error-prone as you have discovered. std::os::raw contains c_float and c_int as well as c_char. The only one that's missing is size_t which is usize on all platforms. You can also use the libc crate, which contains size_t along with a lot of other useful things, and you might want to use it anyway if you're doing C FFI.

Also tangentially: strncpy is pretty tricky to use right, and while what you have works, it would be wrong if the destination buffer were shorter than the source string. Be more than the usual amount of careful when using strncpy.

Now, as for your problem: just because Rust can't deallocate the memory itself doesn't mean it can't deallocate it indirectly. It seems you need to call some C++ to figure out how much to allocate. There are a couple ways of dealing with this:

  1. Define a function in Rust that can be called from C++ but returns a pointer to memory that can be released by Rust. Then inside test_func use that function instead of new char[15]. test_func will then allocate memory that can be freed by Rust (by turning it into a Vec, Box or String, and letting it go out of scope, for example). I'm sure there's some way to override the allocator that new[] uses; maybe you could do that. In this solution, Rust calls C++ which calls Rust.

  2. Allocate the space in C++ using new, but also define a function in C++ that can be called from Rust and can be used to delete it. Call this function inside drop for TestStruct and pass it the pointer.

Which one works best for you basically depends on which parts of the interface you can control.

2 Likes

You are absolutely right! With that I got that approach working, looks like this:

use std::os::raw::{c_char,c_float, c_int};
use std::ffi::CStr;


#[repr(C)]
pub struct TestStruct {
    pub cp: *const c_char,
    pub size: u64,
    pub float: c_float,
    pub num: c_int
}

#[link(name="test_dll_for_rust")]
extern {
    fn showVarSizes();
    fn test_func(data: &mut TestStruct);
    fn multiply(a: i32, b: i32) -> i32;
}

fn main() {
    let cp : [c_char;15] = [0; 15];
    let mut data = TestStruct{cp: cp.as_ptr(), size: 15, float: 0.0, num: 0};
    unsafe{
        test_func(&mut data);
        let c_str = CStr::from_ptr(data.cp);
        println!("{} {} {} {}", c_str.to_str().unwrap(), data.size, data.float, data.num);
    };
}

Thanks also for your tips, I actually was already considering the libc crate. Your first alternative looks a little complicate.... the second one might actually work in my case. With the working solution I would indeed make an educated guess about the maximal size, which would be ok. But for the learning purpose I will look into the your second approach as well :slight_smile:!