Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a static C struct containing strings

I'm trying to create a dynamic library in Rust that exports a struct as a symbol that will be loaded into a C program via dlopen().

However, I'm was running into some segfaults when accessing the second string in the struct, so I made a small test program to try figure out what I'm doing wrong.

This is the Rust code (test.rs), compiled with "rustc --crate-type dylib test.rs":

#[repr(C)]
pub struct PluginDesc {
    name: &'static str,
    version: &'static str,
    description: &'static str
}


#[no_mangle]
pub static PLUGIN_DESC: PluginDesc = PluginDesc {
    name: "Test Plugin\0",
    version: "1.0\0",
    description: "Test Rust Plugin\0"
};

and here is the C program that attempts to load the library (test.c), compiled with "gcc test.c -ldl -o test":

#include <dlfcn.h>
#include <stdio.h>


typedef struct {
    const char *name;
    const char *version;
    const char *description;
} plugin_desc;


int main(int argc, char **argv) {
    void *handle;
    plugin_desc *desc;

    handle = dlopen("./libtest.so", RTLD_LOCAL | RTLD_LAZY);
    if (!handle) {
        printf("failed to dlopen: %s\n", dlerror());
        return 1;
    }

    desc = (plugin_desc *) dlsym(handle, "PLUGIN_DESC");
    if (!desc) {
        printf("failed to dlsym: %s\n", dlerror());
        return 1;
    }

    printf("name: %p\n", desc->name);
    printf("version: %p\n", desc->version);
    printf("description: %p\n", desc->description);

    return 0;
}

This is the output:

name: 0x7fa59ef8d750
version: 0xc
description: 0x7fa59ef8d75c

As you can see, the address of desc->version is actually 0xc (12), which is the length of the first string. So it looks like the struct that gets packed into the library also contains the string length after the memory address.

Am I using the wrong string type here? As you can see I had to also make the strings NULL terminated manually. I tried to use the CString wrapper but that does not seem to work in this case ("static items are not allowed to have destructors").

I'm running the latest Rust nightly on Linux:

$ rustc --version
rustc 0.12.0-pre-nightly (f8426e2e2 2014-09-16 02:26:01 +0000)
like image 551
chrippa Avatar asked Sep 16 '14 23:09

chrippa


People also ask

Can you put a string in a struct?

The answer is yes unless you are using an obsolete compiler that does not support initialization of structures with string class members. Make sure that the structure definition has access to the std namespace.

How do you initialize a struct in a string?

You can't initialize any members in a struct declaration. You have to initialize the struct when you create an instance of the struct. struct my_struct { char* str; }; int main(int argc,char *argv[]) { struct my_struct foo = {"string literal"}; ... }

Can we use string in structure in C?

Assigning Values to StringsArrays and strings are second-class citizens in C; they do not support the assignment operator once it is declared. For example, char c[100]; c = "C programming"; // Error!

Is string static in C?

Strings as Static Arrays of CharsEditThere is no string type in C. Instead we have arrays or constants, and we can use them to store sets of characters.


2 Answers

The layout of a slice (&[T] or &str) is a pointer followed by a length, as documented by the Slice struct of the std::raw module. That's why reading the version field from your C code shows the length of the name field's value. (Note, however, that the exact memory layout of slices is not considered stable, so it might change in a later version. In any case, you should not pass Rust-specific data types to C; only pass primitive types – which includes raw pointers – and types annotated with #[repr(C)].)

EDIT: Unfortunately, there seems to be no way to do this in Rust for now. There are functions to get raw pointers from slices, but function calls are not allowed in static initializers. As suggested by sellibitze in the comments, you should define that variable in a C source file.

like image 136
Francis Gagné Avatar answered Sep 21 '22 23:09

Francis Gagné


As already mentioned in the other answers, the main problem is that &str is a reference to a dynamically sized type. Rust represents such references or pointers in memory with a "fat" pointer that also contains a length and not with a simple pointer like C's const char *.

As the memory layout for these references is not (yet) stable, you cannot reliably use &str, &[T] or dyn T for FFI.

Since Rust 1.32 (January 2019) str::as_ptr is usable in constant context, which allows to easily create a raw pointer to a static string. The only remaining problem is that raw pointers are deemed thread-unsafe by default. So you also need to implement Sync on PluginDesc to assert that your struct is thread-safe.

#[repr(C)]
pub struct PluginDesc {
    name: *const u8,
    version: *const u8,
    description: *const u8
}

unsafe impl Sync for PluginDesc {}

#[no_mangle]
pub static PLUGIN_DESC: PluginDesc = PluginDesc {
    name: "Test Plugin\0".as_ptr(),
    version: "1.0\0".as_ptr(),
    description: "Test Rust Plugin\0".as_ptr()
};

Since 2017 there is also the null_terminated crate that makes null terminated strings more readable and safer to use, but currently requires unstable language features that are only usable with a nightly compiler:

use null_terminated::{str0_utf8, NulStr};

#[repr(C)]
pub struct PluginDesc {
    name: &'static NulStr,
    version: &'static NulStr,
    description: &'static NulStr
}

#[no_mangle]
pub static PLUGIN_DESC: PluginDesc = PluginDesc {
    name: str0_utf8!("Test Plugin"),
    version: str0_utf8!("1.0"),
    description: str0_utf8!("Test Rust Plugin")
};
like image 21
cg909 Avatar answered Sep 23 '22 23:09

cg909