I am trying to wrap a C function in Rust. The C function struct elem* get_list()
returns the following struct:
struct elem {
char data[5],
struct elem* next
};
In Rust, I have declared the function the following way. The declaration of the C function returns *const c_void
as described in an old version of the Rust documentation, which I could not find at the time of writing. I tried returning *const elem
and working with pointers, achieving the same result:
extern "C" {
pub fn get_list() -> *const c_void;
}
The struct represents a linked list, with next
being a pointer to the next element of the list. Inside Rust, I declared the struct in the following way:
#[repr(C)]
pub struct elem {
pub data: [u8; 5],
pub next: *const c_void,
}
The function returns a *const c_void
pointer to the first element of the linked list (of type elem
). I'm trying to read the elements of the linked list with the following code:
let head = get_list();
while !head.is_null() {
let el: &elem = mem::transmute(head);
let str = el.data;
let str = CStr::from_bytes_with_nul(&str).unwrap();
//do something
head = el.next();
}
This reads junk data - the pointers are not properly aligned, both the string is bad and non-null-terminated, and the next pointer leads to random data (the list had different size when the function is called from C directly).
I tried with the function returning a pointer to elem
and only working with pointers, I tried with transmuting str
from the address of el
- it always reads the same junk data. How do I make it align properly?
I know how to do it with a pointer instead of an array and that's the way it is demonstrated in the Rust documentation, but I cannot change the C code.
After I wrote an example library just for this case, I found out it wasn't an extern problem, rather a CStr
one. As it is fixed in the example, where I slice the buffer to the position of the first NUL terminator, I provide the example I wrote for proper externing.
list.c
#include <stdlib.h>
#include <string.h>
struct elem {
char data[5];
struct elem* next;
};
struct elem* get_list() {
struct elem* head = malloc(sizeof(struct elem));
strcpy(head->data, "1");
struct elem* el = malloc(sizeof(struct elem));
head->next = el;
strcpy(el->data, "2");
el->next = malloc(sizeof(struct elem));
el = el->next;
strcpy(el->data, "3");
el->next = NULL;
return head;
}
main.rs
use std::ffi::CStr;
#[repr(C)]
pub struct elem {
pub data: [u8; 5],
pub next: *const elem
}
#[link(name = "list", kind = "static")]
extern {
pub fn get_list() -> *const elem;
}
fn main() {
unsafe {
let mut list = get_list();
// Note, that, if we call from_bytes_with_nul it will throw
// an NulInternal error, therefore,
// we have to slice the buffer to the first NUL-terminator
while !list.is_null() {
let mut null_pos = (*list).data.len() - 1;
{
for i in 0..(*list).data.len() {
if (*list).data[i] == 0 {
null_pos = i + 1;
break
}
}
}
let str = CStr::from_bytes_with_nul(
(*list).data[..null_pos]
).unwrap();
println!("{:?}", str);
list = (*list).next;
}
}
}
output
"1"
"2"
"3"
Key aspects of the implementation:
Define the same structure, annotated with #[repr(C)]
, so it would be
aligned in the same way as the C one.
Define the extern function to return a const pointer to the structure.
Use pointers instead of std::mem::transmute
Be careful with null pointers and terminators