Search code examples
rustffi

Why is a C structure returning misaligned junk data when read from Rust?


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.


Solution

  • 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