Ivan Bratoev
Ivan Bratoev

Reputation: 245

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.

Upvotes: 1

Views: 896

Answers (1)

Ivan Bratoev
Ivan Bratoev

Reputation: 245

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

Upvotes: 1

Related Questions