| name | convert-c-rust |
| description | 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.
This Skill Extends
meta-convert-dev- Foundational conversion patterns (APTV workflow, testing strategies)
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 |
|---|---|---|
int |
i32 |
Signed 32-bit |
unsigned int |
u32 |
Unsigned 32-bit |
char |
u8 / char |
Byte vs Unicode scalar |
char* |
*const u8 / &str / String |
Raw / borrowed / owned |
void* |
*mut c_void / generics |
Prefer generics |
struct |
struct |
Similar syntax |
enum |
enum |
Rust enums more powerful |
union |
union (unsafe) / enum |
Prefer tagged enum |
NULL |
Option<T> / null() |
Type-safe nullability |
malloc/free |
Box::new / Vec / auto-drop |
RAII |
FILE* |
std::fs::File |
Safe file handling |
errno |
Result<T, E> |
Explicit error handling |
pthread_t |
std::thread / tokio |
Safe concurrency |
#define |
const / macros |
Type-safe constants |
#include |
use / mod |
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 |
|---|---|---|---|---|
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
Function Pointers
| C | Rust | Notes |
|---|---|---|
int (*fn)(int, int) |
fn(i32, i32) -> i32 |
Function pointer |
int (*fn)(int, int) |
Fn(i32, i32) -> i32 |
Trait object (can capture) |
void (*callback)(void*) |
Box<dyn Fn(*mut c_void)> |
Callback with state |
| Function pointer array | [fn(); N] / Vec<fn()> |
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
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);
}
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 |
|---|---|---|
#ifndef / #define / #endif |
Not needed | Module system prevents double inclusion |
#pragma once |
Not needed | Same reason |
#include "header.h" |
use crate::module; |
Explicit imports |
#include <stdlib.h> |
use std::collections::HashMap; |
Standard library imports |
| Forward declarations | Not needed | Compiler resolves order |
Visibility
| C Pattern | Rust Pattern | Notes |
|---|---|---|
static int x; (file-local) |
static X: i32 (private by default) |
Module-private |
| Public function | pub fn name() |
Explicitly public |
| Public struct | pub struct Name |
Explicitly public |
| Private helper | fn helper() (no pub) |
Private by default |
| Opaque type | pub struct Handle { _private: () } |
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
moveclosure transfers ownership to thread- 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 |
|---|---|---|
memory_order_relaxed |
Ordering::Relaxed |
No synchronization |
memory_order_acquire |
Ordering::Acquire |
Load barrier |
memory_order_release |
Ordering::Release |
Store barrier |
memory_order_acq_rel |
Ordering::AcqRel |
Both barriers |
memory_order_seq_cst |
Ordering::SeqCst |
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:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-typescript-rust- High-level to Rust conversion patternsconvert-golang-rust- GC to ownership conversion patternslang-c-dev- C development patternslang-rust-dev- Rust development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Thread safety patternspatterns-serialization-dev- Data serialization patternspatterns-metaprogramming-dev- Macro patterns