8

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)
chrippa
  • 83
  • 4
  • Have you tried putting `*i8` pointers in the struct instead? – nneonneo Sep 16 '14 at 23:50
  • 1
    Looks like rust-strings are not just `char *`'s. Is there a .h-file that contains a definition of rust strings for linking with C? – EOF Sep 16 '14 at 23:57

3 Answers3

3

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.

Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
  • 1
    Do byte literals (of type `&[u8]`) really coerce to `*const u8`? – huon Sep 17 '14 at 06:11
  • Unfortunately this does not work either. I got the error "expected `*const i8`, found `&'static [u8]` (expected i8, found vector)" but while changing the type to "&'static [u8]" made it compile, it has the same problem as the original code. – chrippa Sep 17 '14 at 08:06
  • @chrippa: Well, there is a `as_ptr` method on slices. But I don't think you're able to call that as part of a static's initialization. The only solution I see is to write a C file, compile that and link it to the rest of your Rust code. – sellibitze Sep 17 '14 at 15:49
  • wut. I had no `main` method when I tested this on play.rust-lang.org and it complained about some other errors, but not on this. With a `main` method, it does complain. I changed my answer, since it seems impossible to do at the moment... – Francis Gagné Sep 18 '14 at 01:51
  • The representation of `Slice` is not guaranteed to not change: like a regular rust struct, the order of the fields is not significant. So Rust does not give you the needed guarantees at this time -- don't pass `&str` across a ffi boundary. – bluss Jul 10 '15 at 15:26
  • @bluss: I added a note to my answer based on your comment. – Francis Gagné Jul 10 '15 at 17:15
  • Thanks! I realized only after commenting that you answer was from the pre-Rust 1.0 era. This came to the surface due to the new answer of course. – bluss Jul 10 '15 at 19:21
2

The short answer is that you can't statically allocate such a struct. Future Rust will probably gain this ability.

What you can do, is statically allocate a struct that contains null pointers, and set those null pointers to something useful when you call the function. Rust has static mut. It requires unsafe code, is not threadsafe at all and is (to the best of my knowledge) considered a code smell.

Right here I consider it a workaround to the fact that there is no way to turn a &[T] into a *const T in a static.

static S: &'static [u8] = b"http://example.org/eg-amp_rust\n\0";
static mut desc: LV2Descriptor = LV2Descriptor {
    amp_uri: 0 as *const libc::c_char, // ptr::null() isn't const fn (yet)
};

#[no_mangle]
pub extern fn lv2_descriptor(index: i32) -> *const LV2Descriptor {
     let ptr = S.as_ptr() as *const libc::c_char;
     unsafe {
        desc.amp_uri = ptr;
        &desc as *const LV2Descriptor
     }
}

answer copied from duplicate question

Community
  • 1
  • 1
oli_obk
  • 28,729
  • 6
  • 82
  • 98
2

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")
};
cg909
  • 2,247
  • 19
  • 23