Bug in FFI when passing CString followed by an int

100 views Asked by At

My Rust test code

extern "C" {
fn test_int_only(n : libc::c_int);
fn test_int_and_str(s : CString , n : libc::c_int);
}

pub fn test1() { 
unsafe {
    test_int_only(0);
    test_int_only(1);
    test_int_only(2);
    test_int_only(4);
    test_int_only(-12);
    }
}


pub fn test2() { 
unsafe {
    test_int_and_str(CString::new("Foo").unwrap(),0);
    test_int_and_str(CString::new("Bar").unwrap(),1);
    test_int_and_str(CString::new("Baz").unwrap(),2);
    test_int_and_str(CString::new("Fub").unwrap(),4);
    test_int_and_str(CString::new("Bub").unwrap(),-12);
    }
}

My C code

void test_int_only(int abc){
    printf("%d\n", abc);
}

void test_int_and_str(const char* name,int abc) {
    printf("%s %d\n", name, abc);
}

When testing for test_int_only()

1
2
4
-12

When testing for test_int_and_str()

Foo 4
Bar 4
Baz 4
Fub 4
Bub 4

It seems that the 2nd arg is being interpreted (either in rust or c) as sizeof string, rather than the value passed from the Rust code. I'm guessing it's related to either a calling convention or null termination not working correctly. It's a C dll, with _cdecl (windows 32bit dll) calling convention. Interestingly, passing a (opaque) pointer and an int (in another test) works fine so I don't think it's a calling convention issue.

1

There are 1 answers

7
oli_obk On BEST ANSWER

It seems that the 2nd arg is being interpreted (either in rust or c) as sizeof string, rather than the value passed from the Rust code.

Correct. You are experiencing undefined behavior here.

Your C-Function has a different signature from the extern function you declared in Rust-Code. First of all, passing types that are not #[repr(C)] to an extern function is undefined behavior and there used to be a lint for that afaik. Second, a CString is not a char*, it is a struct with internal data. If you want to pass a const char*, you have to pass a *const u8. You can get such a pointer from a CString through the into_ptr function.

Note that into_ptr leaks the memory, you need to use from_ptr again to get a CString object that can get deallocated. If you just want to lend out the CString object you can do something along the following lines:

// ensure c_str lives longer than the function call
let c_str = CString::new("Foo").unwrap();
unsafe { test_int_and_str(c_str.as_ptr(), 0); }
// let the destructor of c_str free the memory