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.
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.
Convert 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.
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
char
1 byte
i8
1 byte
Signed byte
unsigned char
1 byte
u8
1 byte
Unsigned byte
short
2 bytes
i16
2 bytes
Signed 16-bit
unsigned short
2 bytes
u16
2 bytes
Unsigned 16-bit
int
4 bytes
i32
4 bytes
Signed 32-bit (default)
unsigned int
4 bytes
u32
4 bytes
Unsigned 32-bit
long
4/8 bytes
i64 / isize
8 bytes / ptr-sized
Platform-dependent in C
unsigned long
4/8 bytes
u64 / usize
8 bytes / ptr-sized
Platform-dependent in C
long long
8 bytes
i64
8 bytes
Signed 64-bit
float
4 bytes
f32
4 bytes
32-bit floating point
double
8 bytes
f64
8 bytes
64-bit floating point (default)
_Bool / bool
1 byte
bool
1 byte
Boolean (C99+)
size_t
ptr-sized
usize
ptr-sized
Sizes and indices
ptrdiff_t
ptr-sized
isize
ptr-sized
Pointer arithmetic
intptr_t
ptr-sized
isize
ptr-sized
Pointer-sized integer
uintptr_t
ptr-sized
usize
ptr-sized
Unsigned pointer-sized
void
-
()
0 bytes
Unit type
Fixed-width types (C99+ stdint.h → Rust):
C (stdint.h)
Rust
Notes
int8_t
i8
Exactly 8 bits signed
uint8_t
u8
Exactly 8 bits unsigned
int16_t
i16
Exactly 16 bits signed
uint16_t
u16
Exactly 16 bits unsigned
int32_t
i32
Exactly 32 bits signed
uint32_t
u32
Exactly 32 bits unsigned
int64_t
i64
Exactly 64 bits signed
uint64_t
u64
Exactly 64 bits unsigned
Pointer Types
C Pattern
Rust Pattern
When to Use
const T*
&T
Immutable borrow (read-only access)
T*
&mut T
Mutable borrow (exclusive access)
T*
Box<T>
Owned heap allocation
T*
*const T
Raw pointer (unsafe, FFI)
T*
*mut T
Mutable raw pointer (unsafe, FFI)
T**
&mut &T / Box<Box<T>>
Pointer to pointer
void*
*mut c_void / T: ?Sized
Type-erased pointer / generics
NULL
None
Option<&T> or Option<Box<T>>
Array pointer T*
&[T] / &mut [T]
Slice (borrowed array)
Array pointer T*
Vec<T>
Owned dynamic array
Structure and Union Types
C
Rust
Notes
struct Point { int x; int y; }
struct Point { x: i32, y: i32 }
Similar syntax
typedef struct { ... } Name;
struct Name { ... }
No typedef needed
union Data { ... }
union Data { ... } (unsafe)
Requires unsafe to access
Tagged union
enum Data { Int(i32), Float(f64) }
Safer alternative
struct with padding
#[repr(C)] attribute
Match C layout for FFI
struct bit fields
Manual bit manipulation
No direct equivalent
Flexible array member
Vec<T> 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 #[repr(C)] or #[repr(i32)] for C compatibility
Rust enums are type-safe and cannot be used as raw integers without explicit conversion
// 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 free() needed - Drop trait handles cleanup
Impossible to return dangling pointers
Compiler enforces memory safety at compile time
malloc/calloc/realloc → Rust Allocation
C Pattern
Rust Pattern
Notes
malloc(size)
Box::new(value)
Single heap allocation
malloc(n * sizeof(T))
Vec::with_capacity(n)
Array allocation
calloc(n, sizeof(T))
vec![0; n]
Zero-initialized array
realloc(ptr, new_size)
vec.resize(new_len, default)
Resize allocation
free(ptr)
Automatic
Drop trait
ptr = NULL 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);
}
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?
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
#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
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)
}
}
}
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()
}