FFI: Providing a `const char*` to a C application from a `cdylib` shared Rust library

Hello,

I'm currently interacting with a C application via a shared library written in Rust. The C application expects a global const char* plugin_name; in the .so file, which I am trying to define as a top level pub static _: &[u8] object in my rust project. However, the C application fails to load the string via dlsym() and only invalid memory is being read out.

An example application would be:

// src/application.c

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

void info(void *handle, const char *name)
{
    const char *value = (const char *)dlsym(handle, name);
    printf("%s: \"%s\"\n", name, value);
}

int main(int argc, char **argv)
{
    const char *target = argv[1];

    // load the shared library
    void *handle = dlopen(target, RTLD_GLOBAL | RTLD_NOW);

    // lookup plugin info
    info(handle, "plugin_name");
    info(handle, "plugin_description");

    return dlclose(handle);
}

The Rust symbols I currently define via:

// src/plugin.rs

#[allow(non_upper_case_globals)]
#[no_mangle]
pub static plugin_name: &[u8] = c"plugin name".to_bytes();

#[allow(non_upper_case_globals)]
#[no_mangle]
pub static plugin_description: &[u8] = c"plugin description".to_bytes();

Compile e.g. via make like:

.PHONY: all
all: libplugin.so application

application: src/application.c Makefile
	gcc -g -o $@ $<

libplugin.so: src/plugin.rs Makefile
	rustc --edition 2021 --crate-type cdylib -C relocation-model=pic -o $@ $<

.PHONY: run
run: all
	./application ./libplugin.so

My rustc version is: 1.82.0 (f6e511eec 2024-10-15)

Running this, the loaded symbols from dlsym just point to empty/random memory:

> make run
./application ./libplugin.so
plugin_name: ""
plugin_description: "
                     0t��|"
> make run
./application ./libplugin.so
plugin_name: ""
plugin_description: "
                     �  r"
> make run
./application ./libplugin.so
plugin_name: ""
plugin_description: "
                     ��3_~"

Looking at it via nm or rust-gdb shows however, that the symbols are well defined in the library:

(gdb) print (char*) plugin_name 
$1 = 0x2000 "plugin name"
(gdb) print (char*) plugin_description 
$2 = 0x200c "plugin description"

Any ideas what I'm doing wrong, what could be the issue here?

I think the main issue is that you are probably printing the pointer to the string, not the string itself, since dlsym returns a pointer to the location of the symbol, which is actually a pointer. You want to cast to char ** and dereference a layer I think.

Also, pub static _: &[u8] is a pair of a pointer and a length, since [u8] is an unsized type. If you want this to be accessible from C, you are best making it a *const std::ffi::c_char, since that corresponds precisely to the type in C, and initialise it by calling as_ptr() on your CStr.

Indeed, with double dereference it works. However, I cannot change the C application in this case. And using .as_ptr() does not work due to missing Sync trait on the std::ptr type:

error[E0277]: `*const i8` cannot be shared between threads safely
 --> src/plugin.rs:5:25
  |
5 | pub static plugin_name: *const c_char = c"plugin name".as_ptr();
  |                         ^^^^^^^^^^^^^ `*const i8` cannot be shared between threads safely
  |
  = help: the trait `Sync` is not implemented for `*const i8`
  = note: shared static variables must have a type that implements `Sync`

Is it possible to disable the Sync requirement for this case with an attribute or something similar?

Ah, I forgot *const isn't Send or Sync, but I guess if you can't change the C code that is moot anyway. If you need the symbol itself to contain the string data, then I think the only option would be to make it an array (and manually include the null terminator). Unfortunately, there isn't a particularly easy way to that other than just listing the characters one by one.

You can declare a Sync wrapper type around a raw pointer:

// Make sure SyncPtr has the exact same representation as a normal pointer
#[repr(transparent)]
struct SyncPtr<T>(*const T);
unsafe impl<T> Sync for SyncPtr<T> {}

#[no_mangle]
pub static plugin_name: SyncPtr<c_char> = SyncPtr(
    c"plugin name".as_ptr()
);

b"" literals are of type &'static [u8; N], which you can dereference to get a [u8; N].

#[unsafe(no_mangle)]
pub static plugin_name: [u8; b"plugin name\0".len()] = *b"plugin name\0";

This doesn’t ensure the '\0 is present like c"" literals do, unfortunately.

3 Likes

That would indeed work. I thought about b"" literals, but missed that they could be copied out of to get the data inline.

Thanks! That looks like it's the best solution.

Edit: Quick macro to make it not half bad:

macro_rules! c_string {
    ( $name:ident, $value:literal ) => {
        #[allow(non_upper_case_globals)]
        #[unsafe(no_mangle)]
        pub static $name: [u8; $value.len()] = *$value;
    };
}

c_string!(plugin_name, b"the plugin\0");
c_string!(plugin_description, b"something interesting!\0");
1 Like