Skip to content

Revise the FFI chapters #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
54 changes: 36 additions & 18 deletions idioms/ffi-accepting-strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,25 @@ When accepting strings via FFI through pointers, there are two principles that
should be followed:

1. Keep foreign strings "borrowed", rather than copying them directly.
2. Minimize `unsafe` code during the conversion.
2. Minimize the amount of complexity and `unsafe` code involved in converting
from a C-style string to native Rust strings.

## Motivation

Rust has built-in support for C-style strings with its `CString` and `CStr`
types. However, there are different approaches one can take with strings that
are being accepted from a foreign caller of a Rust function.
The strings used in C have different behaviours to those used in Rust, namely:

The best practice is simple: use `CStr` in such a way as to minimize unsafe
code, and create a borrowed slice. If an owned String is needed, call
`to_string()` on the string slice.
- C strings are null-terminated while Rust strings store their length
- C strings can contain any arbitrary non-zero byte while Rust strings must be
UTF-8
- C strings are accessed and manipulated using `unsafe` pointer operations
while interactions with Rust strings go through safe methods

The Rust standard library comes with C equivalents of Rust's `String` and `&str`
called `CString` and `&CStr`, that allow us to avoid a lot of the complexity
and `unsafe` code involved in converting between C strings and Rust strings.

The `&CStr` type also allows us to work with borrowed data, meaning passing
strings between Rust and C is a zero-cost operation.

## Code Example

Expand All @@ -25,20 +33,30 @@ pub mod unsafe_module {

// other module content

/// Log a message at the specified level.
///
/// # Safety
///
/// It is the caller's guarantee to ensure `msg`:
///
/// - is not a null pointer
/// - points to valid, initialized data
/// - points to memory ending in a null byte
/// - won't be mutated for the duration of this function call
#[no_mangle]
pub extern "C" fn mylib_log(msg: *const libc::c_char, level: libc::c_int) {
pub unsafe extern "C" fn mylib_log(
msg: *const libc::c_char,
level: libc::c_int
) {
let level: crate::LogLevel = match level { /* ... */ };

let msg_str: &str = unsafe {
// SAFETY: accessing raw pointers expected to live for the call,
// and creating a shared reference that does not outlive the current
// stack frame.
match std::ffi::CStr::from_ptr(msg).to_str() {
Ok(s) => s,
Err(e) => {
crate::log_error("FFI string conversion failed");
return;
}
// SAFETY: The caller has already guaranteed this is okay (see the
// `# Safety` section of the doc-comment).
let msg_str: &str = match std::ffi::CStr::from_ptr(msg).to_str() {
Ok(s) => s,
Err(e) => {
crate::log_error("FFI string conversion failed");
return;
}
};

Expand Down