I've been thinking about best practice for exposing Rust code via a C interface ("C calls Rust"). I'm using cbindgen to create a C header from a rust project. There are several ways to write the Rust side of the interface such that, from C, the user can pass a const char *
.
In the examples below, I use a c_char
, but the same question would apply for creating any "slice-like" type from a C pointer.
-
*const c_char
- pro: most straightforward way
- con: must remember to "check for NULL"
-
Option<NonNull<c_char>>
- pro: can't forget NULL check (because of
Option<T>
) - con: forces use of mutable pointer in bindings (since
NonNull<T>
uses*mut T
)
- pro: can't forget NULL check (because of
-
Option<&c_char>
- pro: can't forget NULL check (because of
Option<T>
) - con: it feels weird to construct a slice-like thing from just a pointer-like thing
- pro: can't forget NULL check (because of
As of writing, all methods are fine according to miri
.
use std::{ffi::CStr, os::raw::c_char, ptr::NonNull};
/// Takes a `*const c_char`
#[no_mangle]
pub unsafe extern "C" fn str_length_ptr(s: *const c_char) -> usize {
// Manually check for NULL case (easy to forget)
if s.is_null() {
return 0;
}
let s = CStr::from_ptr(s);
s.to_bytes().len()
}
/// Takes a `Option<NonNull<c_char>>`
///
/// Since `NonNull<T>` acts as a `*mut T`, we don't get "const" in C bindings
#[no_mangle]
pub unsafe extern "C" fn str_length_nonnull(s: Option<NonNull<c_char>>) -> usize {
// Since we have an Option, we can't forget to handle NULL
let s = if let Some(s) = s {
s.as_ptr()
} else {
return 0;
};
let s = CStr::from_ptr(s);
s.to_bytes().len()
}
/// Takes a `Option<&c_char>`
#[no_mangle]
pub unsafe extern "C" fn str_length_option_ref(s: Option<&c_char>) -> usize {
// Since we have an Option, we can't forget to handle NULL
let s = if let Some(s) = s {
s as *const c_char
} else {
return 0;
};
let s = CStr::from_ptr(s);
s.to_bytes().len()
}
/// Pretend to be C
#[cfg(test)]
#[test]
fn pretend_to_be_c_code() {
use std::mem::transmute;
// SAFETY: C strings are NULL or NUL-terminated
let null: *const c_char = std::ptr::null();
let empty = b"\0".as_ptr() as *const c_char;
let hi = b"hello\0".as_ptr() as *const c_char;
let expectations = [(null, 0), (empty, 0), (hi, 5)];
for (input, expected) in expectations {
let acutal = unsafe { str_length_ptr(input) };
assert_eq!(acutal, expected);
let acutal = unsafe {
// CAUTION: do not use transmute() in real code; used here to imitate C FFI boundary
let input = transmute::<*const c_char, Option<NonNull<c_char>>>(input);
str_length_nonnull(input)
};
assert_eq!(acutal, expected);
let acutal = unsafe {
// CAUTION: do not use transmute() in real code; used here to imitate C FFI boundary
let input = transmute::<*const c_char, Option<&c_char>>(input);
str_length_option_ref(input)
};
assert_eq!(acutal, expected);
}
}
Creating bindings with cbindgen --lang c
gives:
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
/**
* Takes a `*const c_char`
*/
uintptr_t str_length_ptr(const char *s);
/**
* Takes a `Option<NonNull<c_char>>`
*
* Since `NonNull<T>` acts as a `*mut T`, we don't get "const" in C bindings
*/
uintptr_t str_length_nonnull(char *s);
/**
* Takes a `Option<&c_char>`
*/
uintptr_t str_length_option_ref(const char *s);
What are folks' thoughts on the best practice?
I would say Option<NonNull<c_char>>
is best, but it feels weird to take a reference to a single c_char
and then infer that it should be treated as an array-like thing.
Updates
- Option 3 above is not valid, as shown by @Hyeonu, @H2CO3.
- I understand that
transmute()
should never be used in real code. I usedtransmut()
because it most closely resembles what calling over an FFI interface actually does. I avoided calling the interface with actual C code in this example since (as of writing) Miri cannot handle code that traverses an FFI boundary. I added comments in the code above to clarify.