Claude-skill-registry convert-c-rust
Convert C code to idiomatic Rust. Use when migrating C projects to Rust, translating C patterns to idiomatic Rust, or refactoring C codebases. Extends meta-convert-dev with C-to-Rust specific patterns covering manual memory management to ownership, pointer safety, type system enhancements, and modernization strategies.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-c-rust" ~/.claude/skills/majiayu000-claude-skill-registry-convert-c-rust && rm -rf "$T"
skills/data/convert-c-rust/SKILL.mdConvert C to Rust
Convert C code to idiomatic Rust. This skill extends
meta-convert-dev with C-to-Rust specific type mappings, idiom translations, and tooling for migrating from manual memory management to Rust's ownership system.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: C types → Rust types with safety guarantees
- Memory model: Manual memory management → Ownership and borrowing
- Pointer safety: Raw pointers → References and smart pointers
- Error handling: Error codes → Result types
- Module system: Header files → Rust modules
- Build system: Makefiles/CMake → Cargo
- Concurrency: pthreads → std::thread and async
- FFI patterns: Interfacing between C and Rust during migration
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - C language fundamentals - see
lang-c-dev - Rust language fundamentals - see
lang-rust-dev - Reverse conversion (Rust → C) - see
convert-rust-c - Advanced C memory engineering - see
lang-c-memory-eng - Advanced Rust memory engineering - see
lang-rust-memory-eng
Quick Reference
| C | Rust | Notes |
|---|---|---|
| | Signed 32-bit |
| | Unsigned 32-bit |
| / | Byte vs Unicode scalar |
| / / | Raw / borrowed / owned |
| / generics | Prefer generics |
| | Similar syntax |
| | Rust enums more powerful |
| (unsafe) / | Prefer tagged enum |
| / | Type-safe nullability |
| / / auto-drop | RAII |
| | Safe file handling |
| | Explicit error handling |
| / | Safe concurrency |
| / macros | Type-safe constants |
| / | Module system |
When Converting Code
- Analyze source thoroughly - Understand memory management patterns
- Map types first - Create type equivalence table
- Identify ownership - Determine who owns each allocation
- Translate pointers - Convert to references or smart pointers
- Handle errors explicitly - Replace error codes with Result
- Preserve semantics - Same behavior, safer implementation
- Test equivalence - Same inputs → same outputs
Type System Mapping
Primitive Types
| C Type | Size (typical) | Rust Type | Size | Notes |
|---|---|---|---|---|
| 1 byte | | 1 byte | Signed byte |
| 1 byte | | 1 byte | Unsigned byte |
| 2 bytes | | 2 bytes | Signed 16-bit |
| 2 bytes | | 2 bytes | Unsigned 16-bit |
| 4 bytes | | 4 bytes | Signed 32-bit (default) |
| 4 bytes | | 4 bytes | Unsigned 32-bit |
| 4/8 bytes | / | 8 bytes / ptr-sized | Platform-dependent in C |
| 4/8 bytes | / | 8 bytes / ptr-sized | Platform-dependent in C |
| 8 bytes | | 8 bytes | Signed 64-bit |
| 4 bytes | | 4 bytes | 32-bit floating point |
| 8 bytes | | 8 bytes | 64-bit floating point (default) |
/ | 1 byte | | 1 byte | Boolean (C99+) |
| ptr-sized | | ptr-sized | Sizes and indices |
| ptr-sized | | ptr-sized | Pointer arithmetic |
| ptr-sized | | ptr-sized | Pointer-sized integer |
| ptr-sized | | ptr-sized | Unsigned pointer-sized |
| - | | 0 bytes | Unit type |
Fixed-width types (C99+ stdint.h → Rust):
| C (stdint.h) | Rust | Notes |
|---|---|---|
| | Exactly 8 bits signed |
| | Exactly 8 bits unsigned |
| | Exactly 16 bits signed |
| | Exactly 16 bits unsigned |
| | Exactly 32 bits signed |
| | Exactly 32 bits unsigned |
| | Exactly 64 bits signed |
| | Exactly 64 bits unsigned |
Pointer Types
| C Pattern | Rust Pattern | When to Use |
|---|---|---|
| | Immutable borrow (read-only access) |
| | Mutable borrow (exclusive access) |
| | Owned heap allocation |
| | Raw pointer (unsafe, FFI) |
| | Mutable raw pointer (unsafe, FFI) |
| / | Pointer to pointer |
| / | Type-erased pointer / generics |
| | or |
Array pointer | / | Slice (borrowed array) |
Array pointer | | Owned dynamic array |
Structure and Union Types
| C | Rust | Notes |
|---|---|---|
| | Similar syntax |
| | No typedef needed |
| (unsafe) | Requires unsafe to access |
| Tagged union | | Safer alternative |
with padding | attribute | Match C layout for FFI |
bit fields | Manual bit manipulation | No direct equivalent |
| Flexible array member | or manual allocation | Safer alternatives |
Enum Types
C:
enum Color { RED, // 0 GREEN, // 1 BLUE // 2 }; enum Status { OK = 0, ERROR = -1, PENDING = 1 };
Rust:
// C-like enum #[repr(i32)] // Use C representation enum Color { Red = 0, Green = 1, Blue = 2, } // Rust idiomatic enum with data enum Status { Ok, Error(String), // Can carry data Pending { progress: f64 }, }
Why this translation:
- Rust enums can carry associated data (algebraic data types)
- Use
or#[repr(C)]
for C compatibility#[repr(i32)] - Rust enums are type-safe and cannot be used as raw integers without explicit conversion
Function Pointers
| C | Rust | Notes |
|---|---|---|
| | Function pointer |
| | Trait object (can capture) |
| | Callback with state |
| Function pointer array | / | Array of function pointers |
Memory Management Translation
Manual Allocation → Ownership
C: Manual Memory Management
#include <stdlib.h> #include <string.h> // Allocate and initialize char *create_string(const char *s) { char *result = malloc(strlen(s) + 1); if (result == NULL) { return NULL; } strcpy(result, s); return result; } // Caller must free int main(void) { char *str = create_string("Hello"); if (str == NULL) { return 1; } printf("%s\n", str); free(str); // Manual cleanup return 0; }
Rust: Automatic Memory Management
// Return owned String fn create_string(s: &str) -> String { s.to_string() // Allocates and returns ownership } fn main() { let s = create_string("Hello"); println!("{}", s); // s automatically dropped at end of scope }
Why this translation:
- Rust's ownership system ensures memory is freed exactly once
- No explicit
needed - Drop trait handles cleanupfree() - Impossible to return dangling pointers
- Compiler enforces memory safety at compile time
malloc/calloc/realloc → Rust Allocation
| C Pattern | Rust Pattern | Notes |
|---|---|---|
| | Single heap allocation |
| | Array allocation |
| | Zero-initialized array |
| | Resize allocation |
| Automatic | Drop trait |
after free | Not needed | Move semantics prevent use-after-free |
C: Array Allocation
int *numbers = malloc(10 * sizeof(int)); if (numbers == NULL) { return -1; } // Use array for (int i = 0; i < 10; i++) { numbers[i] = i * 2; } // Resize int *resized = realloc(numbers, 20 * sizeof(int)); if (resized == NULL) { free(numbers); return -1; } numbers = resized; free(numbers);
Rust: Vec Allocation
let mut numbers = Vec::with_capacity(10); // Use array for i in 0..10 { numbers.push(i * 2); } // Resize numbers.resize(20, 0); // Automatic cleanup when numbers goes out of scope
Pointer Patterns → References and Smart Pointers
Pattern 1: Passing by Pointer
C:
void modify(int *value) { *value += 10; } int x = 5; modify(&x); // x is now 15
Rust:
fn modify(value: &mut i32) { *value += 10; } let mut x = 5; modify(&mut x); // x is now 15
Pattern 2: Optional Pointers (NULL)
C:
int *find_value(int key) { if (key_exists) { return &value; } return NULL; } int *result = find_value(42); if (result != NULL) { printf("%d\n", *result); }
Rust:
fn find_value(key: i32) -> Option<&i32> { if key_exists { Some(&value) } else { None } } if let Some(result) = find_value(42) { println!("{}", result); }
Pattern 3: Shared Ownership
C:
// Reference counting (manual) typedef struct { int ref_count; int value; } RefCounted; RefCounted *acquire(RefCounted *ptr) { ptr->ref_count++; return ptr; } void release(RefCounted *ptr) { if (--ptr->ref_count == 0) { free(ptr); } }
Rust:
use std::rc::Rc; let value = Rc::new(42); let shared = Rc::clone(&value); // Increment ref count // Both `value` and `shared` point to same data // Automatically freed when last Rc is dropped
Thread-safe version:
use std::sync::Arc; let value = Arc::new(42); let shared = Arc::clone(&value); // Thread-safe reference counting
Lifetime and Borrowing
C: Dangling Pointer
int *get_pointer(void) { int x = 42; return &x; // UNDEFINED BEHAVIOR: x is on stack }
Rust: Compile Error
fn get_pointer() -> &i32 { let x = 42; &x // ERROR: x does not live long enough }
Fix: Return Owned Value
fn get_value() -> i32 { let x = 42; x // Move ownership to caller } // Or heap allocate fn get_box() -> Box<i32> { Box::new(42) }
C: Struct with Pointer
typedef struct { char *name; // Who owns this? int age; } Person; // Lifetime unclear - does Person own the name?
Rust: Explicit Lifetime
// Borrowed reference (doesn't own) struct Person<'a> { name: &'a str, age: i32, } // Or owned (owns the name) struct PersonOwned { name: String, age: i32, }
Module System Translation
Header Files → Rust Modules
C: Header/Implementation Split
// point.h #ifndef POINT_H #define POINT_H typedef struct { double x; double y; } Point; Point point_create(double x, double y); double point_distance(const Point *p1, const Point *p2); #endif
// point.c #include "point.h" #include <math.h> Point point_create(double x, double y) { Point p = {x, y}; return p; } double point_distance(const Point *p1, const Point *p2) { double dx = p2->x - p1->x; double dy = p2->y - p1->y; return sqrt(dx*dx + dy*dy); }
Rust: Single File Module
// point.rs pub struct Point { pub x: f64, pub y: f64, } impl Point { pub fn new(x: f64, y: f64) -> Self { Point { x, y } } pub fn distance(&self, other: &Point) -> f64 { let dx = other.x - self.x; let dy = other.y - self.y; (dx*dx + dy*dy).sqrt() } }
Include Guards → Module System
| C Pattern | Rust Pattern | Why |
|---|---|---|
/ / | Not needed | Module system prevents double inclusion |
| Not needed | Same reason |
| | Explicit imports |
| | Standard library imports |
| Forward declarations | Not needed | Compiler resolves order |
Visibility
| C Pattern | Rust Pattern | Notes |
|---|---|---|
(file-local) | (private by default) | Module-private |
| Public function | | Explicitly public |
| Public struct | | Explicitly public |
| Private helper | (no ) | Private by default |
| Opaque type | | Hidden internals |
Error Handling Translation
Error Codes → Result Types
C: Error Code Pattern
#define SUCCESS 0 #define ERROR_NULL_POINTER -1 #define ERROR_OUT_OF_MEMORY -2 #define ERROR_INVALID_INPUT -3 int parse_number(const char *input, int *output) { if (input == NULL || output == NULL) { return ERROR_NULL_POINTER; } char *endptr; long val = strtol(input, &endptr, 10); if (endptr == input) { return ERROR_INVALID_INPUT; } *output = (int)val; return SUCCESS; } // Usage int value; int result = parse_number("42", &value); if (result != SUCCESS) { fprintf(stderr, "Error: %d\n", result); return result; }
Rust: Result Type
#[derive(Debug)] enum ParseError { InvalidInput, OutOfRange, } fn parse_number(input: &str) -> Result<i32, ParseError> { input.parse::<i32>() .map_err(|_| ParseError::InvalidInput) } // Usage match parse_number("42") { Ok(value) => println!("Parsed: {}", value), Err(e) => eprintln!("Error: {:?}", e), } // Or with ? operator fn process() -> Result<(), ParseError> { let value = parse_number("42")?; // Propagates error println!("Value: {}", value); Ok(()) }
Why this translation:
- Result type is explicit in function signature
- Compiler enforces error handling (cannot ignore Result)
operator provides concise error propagation?- Type system prevents mixing error types
errno → Result
C: errno Pattern
#include <stdio.h> #include <errno.h> #include <string.h> FILE *open_file(const char *path) { FILE *file = fopen(path, "r"); if (file == NULL) { fprintf(stderr, "Error opening file: %s\n", strerror(errno)); return NULL; } return file; }
Rust: Result with std::io::Error
use std::fs::File; use std::io; fn open_file(path: &str) -> io::Result<File> { File::open(path) } // Usage match open_file("data.txt") { Ok(file) => { /* use file */ }, Err(e) => eprintln!("Error opening file: {}", e), }
goto cleanup → RAII
C: goto for Cleanup
int process_file(const char *path) { FILE *file = NULL; char *buffer = NULL; int result = -1; file = fopen(path, "r"); if (file == NULL) { goto cleanup; } buffer = malloc(1024); if (buffer == NULL) { goto cleanup; } // Process file... result = 0; cleanup: free(buffer); if (file != NULL) { fclose(file); } return result; }
Rust: Automatic Cleanup with Drop
use std::fs::File; use std::io::{self, Read}; fn process_file(path: &str) -> io::Result<()> { let mut file = File::open(path)?; let mut buffer = String::new(); file.read_to_string(&mut buffer)?; // Process buffer... Ok(()) // file and buffer automatically cleaned up }
Why this translation:
- Drop trait ensures cleanup even on early return
- No need for explicit cleanup labels
- Exception-safe (cleanup happens even if panic occurs)
Concurrency Translation
pthreads → std::thread
C: pthread Creation
#include <pthread.h> #include <stdio.h> void *thread_function(void *arg) { int value = *(int *)arg; printf("Thread received: %d\n", value); return NULL; } int main(void) { pthread_t thread; int input = 42; pthread_create(&thread, NULL, thread_function, &input); pthread_join(thread, NULL); return 0; }
Rust: std::thread
use std::thread; fn main() { let input = 42; let handle = thread::spawn(move || { println!("Thread received: {}", input); }); handle.join().unwrap(); }
Why this translation:
- Rust's type system prevents data races at compile time
closure transfers ownership to threadmove- Cannot accidentally share non-thread-safe data
Mutexes
C: pthread_mutex
#include <pthread.h> typedef struct { int counter; pthread_mutex_t mutex; } SafeCounter; void counter_init(SafeCounter *c) { c->counter = 0; pthread_mutex_init(&c->mutex, NULL); } void counter_increment(SafeCounter *c) { pthread_mutex_lock(&c->mutex); c->counter++; pthread_mutex_unlock(&c->mutex); } void counter_destroy(SafeCounter *c) { pthread_mutex_destroy(&c->mutex); }
Rust: std::sync::Mutex
use std::sync::{Arc, Mutex}; struct SafeCounter { counter: Arc<Mutex<i32>>, } impl SafeCounter { fn new() -> Self { SafeCounter { counter: Arc::new(Mutex::new(0)), } } fn increment(&self) { let mut count = self.counter.lock().unwrap(); *count += 1; // Lock automatically released when `count` goes out of scope } fn get(&self) -> i32 { *self.counter.lock().unwrap() } }
Why this translation:
- Lock guard (RAII) ensures mutex is always unlocked
- Cannot access data without holding lock (compile-time guarantee)
- Arc provides thread-safe reference counting
Atomics
C: C11 Atomics
#include <stdatomic.h> atomic_int counter = ATOMIC_VAR_INIT(0); void increment(void) { atomic_fetch_add(&counter, 1); } int get_value(void) { return atomic_load(&counter); }
Rust: std::sync::atomic
use std::sync::atomic::{AtomicI32, Ordering}; static COUNTER: AtomicI32 = AtomicI32::new(0); fn increment() { COUNTER.fetch_add(1, Ordering::SeqCst); } fn get_value() -> i32 { COUNTER.load(Ordering::SeqCst) }
Memory Ordering Comparison:
| C11 | Rust | Description |
|---|---|---|
| | No synchronization |
| | Load barrier |
| | Store barrier |
| | Both barriers |
| | Sequential consistency (default) |
Serialization Translation
Binary Serialization
C: struct write/read
#include <stdio.h> typedef struct { uint32_t id; char name[50]; float score; } Record; int serialize(const Record *record, const char *filename) { FILE *file = fopen(filename, "wb"); if (file == NULL) return -1; size_t written = fwrite(record, sizeof(Record), 1, file); fclose(file); return written == 1 ? 0 : -1; } int deserialize(Record *record, const char *filename) { FILE *file = fopen(filename, "rb"); if (file == NULL) return -1; size_t read = fread(record, sizeof(Record), 1, file); fclose(file); return read == 1 ? 0 : -1; }
Rust: bincode / serde
use serde::{Serialize, Deserialize}; use std::fs::File; use std::io::{self, Write, Read}; #[derive(Serialize, Deserialize)] struct Record { id: u32, name: String, // Dynamic size score: f32, } fn serialize(record: &Record, filename: &str) -> io::Result<()> { let encoded = bincode::serialize(record).unwrap(); let mut file = File::create(filename)?; file.write_all(&encoded)?; Ok(()) } fn deserialize(filename: &str) -> io::Result<Record> { let mut file = File::open(filename)?; let mut buffer = Vec::new(); file.read_to_end(&mut buffer)?; let record = bincode::deserialize(&buffer).unwrap(); Ok(record) }
JSON Serialization
C: cJSON Library
#include <cJSON.h> typedef struct { char name[50]; int age; } User; char *user_to_json(const User *user) { cJSON *root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "name", user->name); cJSON_AddNumberToObject(root, "age", user->age); char *json = cJSON_Print(root); cJSON_Delete(root); return json; // Caller must free }
Rust: serde_json
use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct User { name: String, age: i32, } fn user_to_json(user: &User) -> String { serde_json::to_string(user).unwrap() } fn user_from_json(json: &str) -> Result<User, serde_json::Error> { serde_json::from_str(json) }
Build System Translation
Makefiles → Cargo
C: Makefile
CC = gcc CFLAGS = -Wall -Wextra -std=c11 -O2 LDFLAGS = -lm SRCS = main.c utils.c parser.c OBJS = $(SRCS:.c=.o) TARGET = myapp all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET) $(LDFLAGS) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(OBJS) $(TARGET) .PHONY: all clean
Rust: Cargo.toml
[package] name = "myapp" version = "0.1.0" edition = "2021" [dependencies] # No math library needed - built-in [profile.release] opt-level = 2
Build commands:
# C make make clean # Rust cargo build cargo build --release cargo clean
CMake → Cargo
C: CMakeLists.txt
cmake_minimum_required(VERSION 3.10) project(MyApp C) set(CMAKE_C_STANDARD 11) add_executable(myapp src/main.c src/utils.c src/parser.c ) target_link_libraries(myapp m)
Rust: Project Structure
myapp/ ├── Cargo.toml └── src/ ├── main.rs ├── utils.rs └── parser.rs
Testing Translation
Unity/CMocka → Rust Tests
C: Unity Framework
#include "unity.h" int add(int a, int b) { return a + b; } void test_add_positive(void) { TEST_ASSERT_EQUAL_INT(5, add(2, 3)); } void test_add_negative(void) { TEST_ASSERT_EQUAL_INT(0, add(-1, 1)); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_add_positive); RUN_TEST(test_add_negative); return UNITY_END(); }
Rust: Built-in Tests
fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_positive() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-1, 1), 0); } }
Run tests:
# C (with Unity) gcc test.c unity.c -o test && ./test # Rust cargo test
Metaprogramming Translation
Preprocessor Macros → Rust Macros
C: Object-like Macros
#define PI 3.14159 #define MAX_SIZE 1024 #define VERSION "1.0.0"
Rust: Constants
const PI: f64 = 3.14159; const MAX_SIZE: usize = 1024; const VERSION: &str = "1.0.0";
C: Function-like Macros
#define SQUARE(x) ((x) * (x)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) int result = SQUARE(5); // 25
Rust: Macros or Inline Functions
// Macro (compile-time) macro_rules! square { ($x:expr) => { $x * $x }; } // Inline function (preferred for simple cases) #[inline] fn max<T: Ord>(a: T, b: T) -> T { if a > b { a } else { b } } let result = square!(5); // 25 let result = max(10, 20); // 20
C: Multi-line Macros
#define SWAP(a, b, type) do { \ type temp = (a); \ (a) = (b); \ (b) = temp; \ } while(0)
Rust: Macro
macro_rules! swap { ($a:expr, $b:expr) => { { let temp = $a; $a = $b; $b = temp; } }; } // Or use std::mem::swap use std::mem; mem::swap(&mut a, &mut b);
C: Conditional Compilation
#ifdef DEBUG #define LOG(msg) printf("DEBUG: %s\n", msg) #else #define LOG(msg) ((void)0) #endif
Rust: Conditional Compilation
#[cfg(debug_assertions)] macro_rules! log { ($msg:expr) => { println!("DEBUG: {}", $msg) }; } #[cfg(not(debug_assertions))] macro_rules! log { ($msg:expr) => {}; } // Or use cfg! macro if cfg!(debug_assertions) { println!("DEBUG: {}", msg); }
Common Idioms
Pattern 1: String Handling
C: Null-Terminated Strings
#include <string.h> #include <stdlib.h> char *concat(const char *s1, const char *s2) { size_t len = strlen(s1) + strlen(s2) + 1; char *result = malloc(len); if (result == NULL) return NULL; strcpy(result, s1); strcat(result, s2); return result; // Caller must free }
Rust: String/&str
fn concat(s1: &str, s2: &str) -> String { format!("{}{}", s1, s2) // Or: s1.to_string() + s2 // Or: [s1, s2].concat() } // No manual memory management needed
Pattern 2: Array Manipulation
C: Manual Bounds Checking
int find_max(const int *arr, size_t len) { if (len == 0) { return -1; // Error indicator } int max = arr[0]; for (size_t i = 1; i < len; i++) { if (arr[i] > max) { max = arr[i]; } } return max; }
Rust: Iterators
fn find_max(arr: &[i32]) -> Option<i32> { arr.iter().max().copied() } // Or with pattern matching fn find_max_explicit(arr: &[i32]) -> Option<i32> { match arr.first() { None => None, Some(&first) => { let max = arr.iter().fold(first, |max, &x| max.max(x)); Some(max) } } }
Pattern 3: Callback Functions
C: Callback with void Context*
typedef void (*callback_t)(int value, void *context); void process_array(const int *arr, size_t len, callback_t cb, void *ctx) { for (size_t i = 0; i < len; i++) { cb(arr[i], ctx); } } void print_cb(int value, void *ctx) { const char *prefix = (const char *)ctx; printf("%s%d\n", prefix, value); } // Usage process_array(arr, len, print_cb, "Value: ");
Rust: Closures
fn process_array<F>(arr: &[i32], mut cb: F) where F: FnMut(i32), { for &value in arr { cb(value); } } // Usage let prefix = "Value: "; process_array(&arr, |value| { println!("{}{}", prefix, value); });
Common Pitfalls
1. Use-After-Free
C: Dangling Pointer
int *ptr = malloc(sizeof(int)); *ptr = 42; free(ptr); printf("%d\n", *ptr); // UNDEFINED BEHAVIOR
Rust: Compile Error
let ptr = Box::new(42); drop(ptr); // Explicit drop // println!("{}", *ptr); // ERROR: use of moved value
Why this matters:
- Rust ownership system prevents use-after-free at compile time
- Cannot access value after it has been moved or dropped
2. Double-Free
C: Double Free
char *str = malloc(100); free(str); free(str); // UNDEFINED BEHAVIOR
Rust: Impossible
let s = Box::new(String::from("hello")); drop(s); // drop(s); // ERROR: use of moved value
3. Buffer Overflow
C: No Bounds Checking
int arr[10]; arr[15] = 42; // UNDEFINED BEHAVIOR
Rust: Panic or Compile Error
let mut arr = [0; 10]; // arr[15] = 42; // PANIC at runtime (in debug mode) // Better: use safe access if let Some(elem) = arr.get_mut(15) { *elem = 42; } else { println!("Index out of bounds"); }
4. NULL Pointer Dereference
C: NULL Check Required
int *ptr = get_value(); if (ptr == NULL) { return -1; } *ptr = 10; // Safe only if check above
Rust: Type-Safe Nullability
let ptr: Option<Box<i32>> = get_value(); match ptr { Some(mut p) => *p = 10, None => return Err("NULL pointer"), } // Or with ? operator let mut p = get_value()?; *p = 10;
5. Integer Overflow
C: Undefined Behavior
int x = INT_MAX; x++; // UNDEFINED BEHAVIOR
Rust: Panic (Debug) or Wrap (Release)
let x = i32::MAX; // let y = x + 1; // PANIC in debug mode // Explicit behavior let y = x.wrapping_add(1); // Wrap around let y = x.checked_add(1); // Returns Option<i32> let y = x.saturating_add(1); // Saturate at MAX
Tooling
c2rust
Automated C to Rust translation tool (produces unsafe Rust as starting point):
# Install cargo install c2rust # Translate C code c2rust transpile compile_commands.json # Produces .rs files with unsafe Rust code # Manual cleanup needed to make code idiomatic
Output characteristics:
- Generates unsafe Rust that matches C behavior
- Preserves C-style pointer usage (raw pointers)
- Good starting point but requires manual refactoring
- Use as scaffolding, not final code
Incremental Migration with FFI
Strategy: Gradually replace C modules with Rust while maintaining C API:
C header (legacy):
// api.h int process_data(const char *input, char *output, size_t output_len);
Rust implementation:
use std::ffi::{CStr, CString}; use std::os::raw::c_char; #[no_mangle] pub extern "C" fn process_data( input: *const c_char, output: *mut c_char, output_len: usize, ) -> i32 { // Convert C string to Rust let input = unsafe { assert!(!input.is_null()); CStr::from_ptr(input) }; let input_str = match input.to_str() { Ok(s) => s, Err(_) => return -1, }; // Pure Rust logic let result = process(input_str); // Convert back to C string let result_cstring = CString::new(result).unwrap(); let bytes = result_cstring.as_bytes_with_nul(); if bytes.len() > output_len { return -2; // Buffer too small } unsafe { std::ptr::copy_nonoverlapping( bytes.as_ptr(), output as *mut u8, bytes.len(), ); } 0 // Success } fn process(input: &str) -> String { // Safe, idiomatic Rust code input.to_uppercase() }
Build Integration
Cargo with C Dependencies:
[build-dependencies] cc = "1.0"
build.rs:
fn main() { cc::Build::new() .file("src/legacy/utils.c") .compile("utils"); }
Migration Strategies
Strategy 1: Clean Slate (Full Rewrite)
When to use:
- Small to medium codebase
- Well-defined requirements
- Time to rewrite from scratch
- Want to modernize architecture
Approach:
- Understand C codebase behavior
- Design Rust architecture
- Implement in idiomatic Rust
- Test against C version (golden tests)
Strategy 2: Incremental (FFI Boundary)
When to use:
- Large codebase
- Need continuous operation
- Limited resources
- Risk-averse
Approach:
- Identify module boundaries
- Create FFI wrappers for C modules
- Rewrite one module at a time in Rust
- Expose Rust module with C-compatible FFI
- Gradually replace C modules
Strategy 3: c2rust Then Refactor
When to use:
- Very large codebase
- Complex pointer logic
- Need mechanical translation first
Approach:
- Run c2rust on codebase
- Get compiling unsafe Rust
- Write comprehensive tests
- Incrementally refactor to safe Rust
- Replace raw pointers with references
- Replace manual memory management with RAII
Examples
Example 1: Simple - Linked List Node
Before (C):
struct Node { int value; struct Node *next; }; struct Node *create_node(int value) { struct Node *node = malloc(sizeof(struct Node)); if (node == NULL) { return NULL; } node->value = value; node->next = NULL; return node; } void free_list(struct Node *head) { while (head != NULL) { struct Node *next = head->next; free(head); head = next; } }
After (Rust):
struct Node { value: i32, next: Option<Box<Node>>, } impl Node { fn new(value: i32) -> Box<Node> { Box::new(Node { value, next: None, }) } } // Automatic cleanup via Drop trait // No manual free_list needed
Example 2: Medium - String Buffer
Before (C):
#include <stdlib.h> #include <string.h> typedef struct { char *data; size_t len; size_t capacity; } StringBuffer; StringBuffer *strbuf_create(size_t capacity) { StringBuffer *buf = malloc(sizeof(StringBuffer)); if (buf == NULL) return NULL; buf->data = malloc(capacity); if (buf->data == NULL) { free(buf); return NULL; } buf->data[0] = '\0'; buf->len = 0; buf->capacity = capacity; return buf; } int strbuf_append(StringBuffer *buf, const char *str) { size_t str_len = strlen(str); if (buf->len + str_len + 1 > buf->capacity) { size_t new_cap = buf->capacity * 2; char *new_data = realloc(buf->data, new_cap); if (new_data == NULL) return -1; buf->data = new_data; buf->capacity = new_cap; } strcpy(buf->data + buf->len, str); buf->len += str_len; return 0; } void strbuf_destroy(StringBuffer *buf) { free(buf->data); free(buf); }
After (Rust):
struct StringBuffer { data: String, } impl StringBuffer { fn new() -> Self { StringBuffer { data: String::new(), } } fn with_capacity(capacity: usize) -> Self { StringBuffer { data: String::with_capacity(capacity), } } fn append(&mut self, s: &str) { self.data.push_str(s); // Automatic reallocation if needed } fn as_str(&self) -> &str { &self.data } } // Automatic cleanup via Drop trait // Or just use String directly: let mut s = String::new(); s.push_str("Hello"); s.push_str(" World");
Example 3: Complex - Thread-Safe Queue
Before (C):
#include <pthread.h> #include <stdlib.h> typedef struct Node { void *data; struct Node *next; } Node; typedef struct { Node *head; Node *tail; pthread_mutex_t mutex; pthread_cond_t cond; } Queue; Queue *queue_create(void) { Queue *q = malloc(sizeof(Queue)); if (q == NULL) return NULL; q->head = q->tail = NULL; pthread_mutex_init(&q->mutex, NULL); pthread_cond_init(&q->cond, NULL); return q; } int queue_push(Queue *q, void *data) { Node *node = malloc(sizeof(Node)); if (node == NULL) return -1; node->data = data; node->next = NULL; pthread_mutex_lock(&q->mutex); if (q->tail == NULL) { q->head = q->tail = node; } else { q->tail->next = node; q->tail = node; } pthread_cond_signal(&q->cond); pthread_mutex_unlock(&q->mutex); return 0; } void *queue_pop(Queue *q) { pthread_mutex_lock(&q->mutex); while (q->head == NULL) { pthread_cond_wait(&q->cond, &q->mutex); } Node *node = q->head; void *data = node->data; q->head = node->next; if (q->head == NULL) { q->tail = NULL; } pthread_mutex_unlock(&q->mutex); free(node); return data; } void queue_destroy(Queue *q) { pthread_mutex_destroy(&q->mutex); pthread_cond_destroy(&q->cond); // ... free remaining nodes ... free(q); }
After (Rust):
use std::sync::{Arc, Mutex, Condvar}; use std::collections::VecDeque; struct Queue<T> { data: Arc<(Mutex<VecDeque<T>>, Condvar)>, } impl<T> Queue<T> { fn new() -> Self { Queue { data: Arc::new((Mutex::new(VecDeque::new()), Condvar::new())), } } fn push(&self, item: T) { let (lock, cvar) = &*self.data; let mut queue = lock.lock().unwrap(); queue.push_back(item); cvar.notify_one(); } fn pop(&self) -> T { let (lock, cvar) = &*self.data; let mut queue = lock.lock().unwrap(); while queue.is_empty() { queue = cvar.wait(queue).unwrap(); } queue.pop_front().unwrap() } } impl<T> Clone for Queue<T> { fn clone(&self) -> Self { Queue { data: Arc::clone(&self.data), } } } // Automatic cleanup via Drop trait // Type-safe: cannot push/pop wrong types // Send + Sync: compiler verifies thread safety
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- High-level to Rust conversion patternsconvert-typescript-rust
- GC to ownership conversion patternsconvert-golang-rust
- C development patternslang-c-dev
- Rust development patternslang-rust-dev
Cross-cutting pattern skills:
- Thread safety patternspatterns-concurrency-dev
- Data serialization patternspatterns-serialization-dev
- Macro patternspatterns-metaprogramming-dev