I'm having some troubles understanding how I can call this function from Rust.
If I go with let mut s: *const c_char = std::mem::uninitialized(); and call it like foo(&mut s); the compiler complains that the types differ in mutability:
expected raw pointer `*mut *mut i8` found mutable reference `&mut *const i8`
How should I do this? Also, where in the Rust book (or other docs) is this explained in more details? I'm struggling to find examples of something similar.
Fortunately while the actual interface is more complex than my contrived example I don't have to free the string I get from this call (it is a static C string).
You're right. I am not allowed to write to that string in any way, but this is not expressed by the C interface (yet). This also changes the Rust definition to *mut *const ::std::os::raw::c_char which in turn means that I should use *const c_char instead of *mut c_char.
For real code I would normally use std::mem::MaybeUninit to make it clear that foo() is initializing your s string instead of initializing s with std::ptr::null_mut() (or std::mem::uninitialized() which is almost always broken).
// Create an uninitialized *mut c_char
let mut s: MaybeUninit<*mut c_char> = MaybeUninit::uninit();
// Give foo() a pointer to our "s" so it can be initialized
foo(s.as_mut_ptr());
// The string pointer has now been initialized
let s: *mut c_char = s.assume_init();
Also, as you've mentioned foo()'s signature should really take a const char **. String literals are normally loaded into readonly memory and accidentally modifying them is UB. Using a *mut *const c_char means downstream code will need to explicitly cast away the const if they want to mutate it, which should trigger alarm bells.