curiousdannii
curiousdannii

Reputation: 2020

How can I access a global C array of structs from a Rust library?

I am porting a static library to Rust that will be linked with a C application which will provide a global array. Here is the definition of the struct and array:

typedef struct glkunix_argumentlist_struct {
    char *name;
    int argtype;
    char *desc;
} glkunix_argumentlist_t;
extern glkunix_argumentlist_t glkunix_arguments[];

There is no length parameter, instead the last entry in the array will be {NULL, 0, NULL} (example), which you test for when looping through the array.

I think this is the correct Rust representation for the struct:

#[repr(C)]
struct GlkUnixArgument {
    name: *const c_char,
    argtype: c_int,
    desc: *const c_char,
}

The Rust Nomicon shows how to define an extern for a single integer.

I've seen that if you have a length parameter you can use std::slice::from_raw_parts to get a slice, but we don't have a length yet. I could modify the C code to provide one, but I would like to be able to provide a drop-in replacement if I can.

I haven't yet seen how to just create one single Rust struct from a extern. Though I did just have the idea of just std::slice::from_raw_parts with a length of 1.

Is there a better way to more directly define an extern struct in Rust?

Upvotes: 2

Views: 780

Answers (1)

Boiethios
Boiethios

Reputation: 42859

You must calculate the length of your C array before creating the slice with from_raw_parts:

use libc::{c_char, c_int};

#[repr(C)]
pub struct GlkUnixArgument {
    name: *const c_char,
    argtype: c_int,
    desc: *const c_char,
}

pub unsafe fn glkunix_arguments() -> &'static [GlkUnixArgument] {
    extern "C" {
        pub static glkunix_arguments: *const GlkUnixArgument;
    }

    let len = (0..)
        .take_while(|i| {
            let arg = glkunix_arguments.offset(*i);
            (*arg).name != std::ptr::null()
                || (*arg).argtype != 0
                || (*arg).desc != std::ptr::null()
        })
        .count();

    std::slice::from_raw_parts(glkunix_arguments, len)
}

You can even map the C struct to a Rust struct:

use libc::{c_char, c_int, strlen};
use std::{ptr, slice, str};

pub struct GlkUnixArgument {
    pub name: &'static str,
    pub argtype: i32,
    pub desc: &'static str,
}

pub unsafe fn glkunix_arguments() -> Vec<GlkUnixArgument> {
    extern "C" {
        pub static glkunix_arguments: *const GlkUnixArgument_;
    }

    #[repr(C)]
    pub struct GlkUnixArgument_ {
        name: *const c_char,
        argtype: c_int,
        desc: *const c_char,
    }

    impl GlkUnixArgument_ {
        fn is_not_empty(&self) -> bool {
            self.name != ptr::null() || self.argtype != 0 || self.desc != ptr::null()
        }
    }

    unsafe fn c_str_to_rust_str(s: *const i8) -> &'static str {
        str::from_utf8(slice::from_raw_parts(s as *const u8, strlen(s))).unwrap()
    }

    let len = (0..)
        .map(|i| glkunix_arguments.offset(i))
        .take_while(|&arg| (*arg).is_not_empty())
        .count();

    slice::from_raw_parts(glkunix_arguments, len)
        .iter()
        .map(|args| GlkUnixArgument {
            name: c_str_to_rust_str(args.name),
            desc: c_str_to_rust_str(args.desc),
            argtype: args.argtype,
        })
        .collect()
}

Using a lazy construct can ensure that you're doing the operation only once.

Upvotes: 1

Related Questions