Encapsulation patterns for unsafe code
Safe abstractions encapsulate unsafe code behind safe interfaces, ensuring that invariants are maintained and undefined behavior cannot be triggered through the public API. This pattern allows leveraging low-level operations while maintaining Rust's safety guarantees at the interface boundary.
The key principle: unsafe code is an implementation detail, not part of the API contract.
Sometimes you need unsafe operations for:
However, exposing unsafe directly leads to:
// Example 1: Safe wrapper around raw pointer
pub struct Buffer {
ptr: *mut u8,
len: usize,
capacity: usize,
}
impl Buffer {
/// Safe: Allocates with proper alignment and capacity
pub fn new(capacity: usize) -> Self {
let layout = std::alloc::Layout::array::<u8>(capacity).unwrap();
let ptr = unsafe { std::alloc::alloc(layout) };
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
Buffer {
ptr,
len: 0,
capacity,
}
}
/// Safe: Bounds checking prevents invalid access
pub fn push(&mut self, value: u8) {
if self.len >= self.capacity {
panic!("Buffer is full");
}
unsafe {
// SAFETY: We checked bounds above, so this is safe
*self.ptr.add(self.len) = value;
}
self.len += 1;
}
/// Safe: Returns Option instead of raw pointer
pub fn get(&self, index: usize) -> Option<u8> {
if index < self.len {
unsafe {
// SAFETY: index < len, so this is valid
Some(*self.ptr.add(index))
}
} else {
None
}
}
/// Safe: Returns slice with valid lifetime
pub fn as_slice(&self) -> &[u8] {
unsafe {
// SAFETY: ptr is valid for len bytes
std::slice::from_raw_parts(self.ptr, self.len)
}
}
}
impl Drop for Buffer {
fn drop(&mut self) {
unsafe {
// SAFETY: ptr was allocated with same layout
let layout = std::alloc::Layout::array::<u8>(self.capacity).unwrap();
std::alloc::dealloc(self.ptr, layout);
}
}
}
// Buffer is safe to use without unsafe blocks!
fn example_buffer() {
let mut buf = Buffer::new(10);
buf.push(42);
buf.push(100);
println!("Value at 0: {:?}", buf.get(0));
println!("Slice: {:?}", buf.as_slice());
}
// Example 2: Safe abstraction over uninitialized memory
pub struct UninitVec<T> {
ptr: *mut T,
len: usize,
capacity: usize,
}
impl<T> UninitVec<T> {
pub fn with_capacity(capacity: usize) -> Self {
let layout = std::alloc::Layout::array::<T>(capacity).unwrap();
let ptr = unsafe { std::alloc::alloc(layout) as *mut T };
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
UninitVec {
ptr,
len: 0,
capacity,
}
}
/// Safe: Takes ownership and initializes memory
pub fn push(&mut self, value: T) {
assert!(self.len < self.capacity, "Vec is full");
unsafe {
// SAFETY: Capacity checked, writing to uninitialized memory
std::ptr::write(self.ptr.add(self.len), value);
}
self.len += 1;
}
/// Safe: Only returns initialized elements
pub fn get(&self, index: usize) -> Option<&T> {
if index < self.len {
unsafe {
// SAFETY: index < len, so element is initialized
Some(&*self.ptr.add(index))
}
} else {
None
}
}
pub fn len(&self) -> usize {
self.len
}
}
impl<T> Drop for UninitVec<T> {
fn drop(&mut self) {
// Drop all initialized elements
for i in 0..self.len {
unsafe {
// SAFETY: Elements 0..len are initialized
std::ptr::drop_in_place(self.ptr.add(i));
}
}
// Deallocate memory
unsafe {
let layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
std::alloc::dealloc(self.ptr as *mut u8, layout);
}
}
}
// Example 3: Safe abstraction for transmutation
pub struct TransmuteView<T, U> {
data: T,
_marker: std::marker::PhantomData<U>,
}
impl<T, U> TransmuteView<T, U> {
/// Safe: Compile-time size and alignment checks
pub fn new(data: T) -> Option<Self> {
// Ensure sizes and alignments are compatible
if std::mem::size_of::<T>() == std::mem::size_of::<U>()
&& std::mem::align_of::<T>() >= std::mem::align_of::<U>()
{
Some(TransmuteView {
data,
_marker: std::marker::PhantomData,
})
} else {
None
}
}
/// Safe: Returns reference with correct lifetime
pub fn as_ref(&self) -> &U {
unsafe {
// SAFETY: Sizes and alignments checked in new()
&*((&self.data) as *const T as *const U)
}
}
}
fn main() {
println!("=== Buffer Example ===");
example_buffer();
println!("\n=== UninitVec Example ===");
let mut vec = UninitVec::with_capacity(5);
vec.push(10);
vec.push(20);
vec.push(30);
for i in 0..vec.len() {
println!("Element {}: {:?}", i, vec.get(i));
}
}
unsafe {
// SAFETY: Explain why this is safe
// - What invariants are upheld
// - What preconditions are met
// - What could go wrong if violated
}
// ❌ DON'T: Expose raw pointers in public API
pub struct BadVec<T> {
pub ptr: *mut T, // Public! Users can violate invariants
pub len: usize,
pub capacity: usize,
}
// ✅ DO: Keep invariants private
pub struct GoodVec<T> {
ptr: *mut T, // Private
len: usize,
capacity: usize,
}
impl<T> GoodVec<T> {
pub fn as_ptr(&self) -> *const T {
self.ptr // Const pointer is safer
}
}
// ❌ DON'T: Assume preconditions without checking
pub unsafe fn get_unchecked<T>(slice: &[T], index: usize) -> &T {
// No bounds check! Caller must ensure index < len
&*slice.as_ptr().add(index)
}
// ✅ DO: Provide safe alternative with checks
pub fn get<T>(slice: &[T], index: usize) -> Option<&T> {
if index < slice.len() {
Some(&slice[index])
} else {
None
}
}
// Unsafe version for when performance is critical
pub unsafe fn get_unchecked<T>(slice: &[T], index: usize) -> &T {
debug_assert!(index < slice.len(), "Index out of bounds");
&*slice.as_ptr().add(index)
}
// ❌ DON'T: Undocumented unsafe code
unsafe {
*self.ptr.add(index) = value;
}
// ✅ DO: Explain safety invariants
unsafe {
// SAFETY:
// - index < self.len (checked above)
// - self.ptr is valid for self.len elements (maintained by struct)
// - No other mutable references exist (guaranteed by &mut self)
*self.ptr.add(index) = value;
}
use std::ptr::NonNull;
use std::marker::PhantomData;
use std::ops::Deref;
use std::alloc::{alloc, dealloc, Layout};
struct RcBox<T> {
ref_count: usize,
value: T,
}
pub struct Rc<T> {
ptr: NonNull<RcBox<T>>,
_marker: PhantomData<RcBox<T>>,
}
impl<T> Rc<T> {
pub fn new(value: T) -> Self {
let layout = Layout::new::<RcBox<T>>();
let ptr = unsafe { alloc(layout) as *mut RcBox<T> };
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
unsafe {
// SAFETY: ptr is valid and aligned for RcBox<T>
std::ptr::write(
ptr,
RcBox {
ref_count: 1,
value,
},
);
}
Rc {
ptr: unsafe { NonNull::new_unchecked(ptr) },
_marker: PhantomData,
}
}
fn ref_count(&self) -> usize {
unsafe {
// SAFETY: ptr is always valid while Rc exists
(*self.ptr.as_ptr()).ref_count
}
}
fn inc_ref(&self) {
unsafe {
// SAFETY: We have a valid reference, so ptr is valid
let rc = &mut (*self.ptr.as_ptr()).ref_count;
*rc = rc.checked_add(1).expect("Reference count overflow");
}
}
fn dec_ref(&mut self) -> bool {
unsafe {
// SAFETY: ptr is valid
let rc = &mut (*self.ptr.as_ptr()).ref_count;
*rc -= 1;
*rc == 0
}
}
}
impl<T> Clone for Rc<T> {
fn clone(&self) -> Self {
self.inc_ref();
Rc {
ptr: self.ptr,
_marker: PhantomData,
}
}
}
impl<T> Deref for Rc<T> {
type Target = T;
fn deref(&self) -> &T {
unsafe {
// SAFETY: ptr is valid for the lifetime of self
&(*self.ptr.as_ptr()).value
}
}
}
impl<T> Drop for Rc<T> {
fn drop(&mut self) {
if self.dec_ref() {
unsafe {
// SAFETY: Last reference, safe to drop
let layout = Layout::new::<RcBox<T>>();
std::ptr::drop_in_place(self.ptr.as_ptr());
dealloc(self.ptr.as_ptr() as *mut u8, layout);
}
}
}
}
// Safe to use!
fn rc_example() {
let rc1 = Rc::new(vec![1, 2, 3]);
let rc2 = rc1.clone();
let rc3 = rc1.clone();
println!("rc1: {:?}", *rc1);
println!("rc2: {:?}", *rc2);
println!("rc3: {:?}", *rc3);
}
_unchecked variantsget and set methods for an arraylibc::malloc and free// Vec is a safe abstraction over raw pointers
let mut v = Vec::new();
v.push(1); // No unsafe needed!
use bytes::Bytes;
// Safe abstraction over raw memory
let bytes = Bytes::from(&b"hello"[..]);
// Safe wrappers around OS primitives
use tokio::net::TcpListener;
// No unsafe in user code
let listener = TcpListener::bind("127.0.0.1:8080").await?;
Run this code in the official Rust Playground