We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 25 of 28
C strings and ownership across FFI
Implement string conversion and an owned-handle API for calling C from Rust.
Fix the dangling pointer in the starter, validate incoming bytes, and ensure
every handle created by lifecycle is released.
pub fn c_byte_len(s: &str) -> Option<usize>
pub fn decode(bytes: Vec<u8>) -> Result<String, String>
pub fn make_handle(s: &str) -> *mut c_char
pub unsafe fn handle_len(p: *const c_char) -> usize
pub unsafe fn free_handle(p: *mut c_char)
pub fn lifecycle(items: Vec<String>) -> Vec<usize>
Required behavior
c_byte_lencreates a C string and calls the suppliedstrlen. Return its byte count, excluding the terminator, orNoneifscontains any zero byte. Empty text givesSome(0).decodevalidates a complete byte buffer using the rules below, then returns an owned UTF-8 string without the terminator.make_handletransfers ownership of a newly createdCStringthroughinto_raw. Return null if the input contains any zero byte.handle_lenreturns zero for null; otherwise callstrlenon a valid live handle. It does not consume or free the handle.free_handledoes nothing for null. Otherwise reclaim the original pointer withCString::from_rawand drop the resulting owner once.- Keep the supplied
lifecycle: create, measure and free each handle in input order. Preserve duplicates. Empty input returns an empty vector; an item containing a zero byte produces a null handle and a zero length.
decode checks errors in this order:
| Buffer | Result |
|---|---|
| Contains no zero byte, including an empty buffer | Err("missing_terminator") |
| Its first zero byte is not its last byte | Err("interior_nul") |
| Has exactly one final zero, but preceding bytes are not valid UTF-8 | Err("not_utf8") |
| Has exactly one final zero and valid UTF-8 before it | Ok(decoded_text) |
For example, [0] decodes to an empty string, [255, 0] is not_utf8,
and [255, 0, 0] is interior_nul. The harness displays errors with an
err: prefix; your function returns the error name without that prefix.
Bytes, text and terminators
Rust's String and &str contain valid UTF-8 and have a byte length. They
may contain zero bytes. A C string ends at its first zero byte and does not
by itself specify an encoding. strlen counts bytes before that zero.
Thus "héllo" occupies six UTF-8 bytes even though it has five Unicode
scalar values.
CString owns C-compatible bytes and ensures one final zero with none
before it. CString::new adds the terminator and rejects any zero already
in the input, including a trailing one. &CStr borrows a C string.
CStr::from_bytes_with_nul validates a buffer's terminator placement;
.to_str() separately validates UTF-8. Do not use lossy decoding here,
because the required result for invalid UTF-8 is an error.
Keep the owner alive during a borrowed call
The starter saves the pointer from a temporary CString in one statement
and uses it in the next. The owner has already been dropped, so calling
strlen through that pointer is undefined behavior. A dangling-pointer
lint can flag this pattern, but lint coverage is not a lifetime proof.
Give the owner a name and keep it alive until the call returns. This complete example uses a borrowed pointer without transferring ownership:
use std::ffi::{CString, c_char};
unsafe extern "C" {
fn strlen(s: *const c_char) -> usize;
}
fn main() -> Result<(), std::ffi::NulError> {
let owned = CString::new("héllo")?;
// SAFETY: owned supplies live, immutable, nul-terminated bytes for this call.
let length = unsafe { strlen(owned.as_ptr()) };
assert_eq!(length, 6);
Ok(())
}
A temporary can remain alive through a call in the same statement. The requirement is that the owner outlive every use of its pointer, not that every temporary is invalid. A named owner usually makes that reasoning easier. If C retains the pointer after returning, a local owner that is about to be dropped is insufficient.
Transfer and reclaim ownership deliberately
into_raw keeps the allocation alive after the CString value is consumed.
The caller must eventually return the original pointer for reclamation.
Do not pass an offset pointer, an already-freed pointer or a foreign
allocation to CString::from_raw. The position of the terminating zero
must still match the original string length when reclaiming it.
This exercise treats handles as read-only until freed. Do not remove or move the terminator, access a freed handle, or access the bytes while they are being freed. Repeated reads are allowed while the handle remains valid; reclamation happens exactly once.
Use the deallocation API specified by the allocator or library that created
an object. In particular, do not use C's free for these CString handles.
Allocator compatibility is an API contract, not something to infer from
both pointers referring to heap memory.
Declaring a C-facing API
Rust String, &str and slices have no directly supported C ABI in these
signatures. Use an agreed pointer/length representation or a terminated
string, and document ownership. A #[repr(C)] aggregate still needs
appropriate field types; a function pointer needs the correct calling
convention. Neither an attribute nor a raw pointer makes an interface safe
on its own.
handle_len and free_handle are unsafe because callers must establish
pointer validity and ownership conditions that the functions cannot check.
Their # Safety documentation states those obligations. Compiler and
Clippy checks catch some mistakes, but cannot prove that foreign callers
obey the contract or that allocations are eventually freed.
Submissions must compile, pass the tests and pass clippy -D warnings.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.