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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/data/convert-c-rust/SKILL.md
source content

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

CRustNotes
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

  1. Analyze source thoroughly - Understand memory management patterns
  2. Map types first - Create type equivalence table
  3. Identify ownership - Determine who owns each allocation
  4. Translate pointers - Convert to references or smart pointers
  5. Handle errors explicitly - Replace error codes with Result
  6. Preserve semantics - Same behavior, safer implementation
  7. Test equivalence - Same inputs → same outputs

Type System Mapping

Primitive Types

C TypeSize (typical)Rust TypeSizeNotes
char
1 byte
i8
1 byteSigned byte
unsigned char
1 byte
u8
1 byteUnsigned byte
short
2 bytes
i16
2 bytesSigned 16-bit
unsigned short
2 bytes
u16
2 bytesUnsigned 16-bit
int
4 bytes
i32
4 bytesSigned 32-bit (default)
unsigned int
4 bytes
u32
4 bytesUnsigned 32-bit
long
4/8 bytes
i64
/
isize
8 bytes / ptr-sizedPlatform-dependent in C
unsigned long
4/8 bytes
u64
/
usize
8 bytes / ptr-sizedPlatform-dependent in C
long long
8 bytes
i64
8 bytesSigned 64-bit
float
4 bytes
f32
4 bytes32-bit floating point
double
8 bytes
f64
8 bytes64-bit floating point (default)
_Bool
/
bool
1 byte
bool
1 byteBoolean (C99+)
size_t
ptr-sized
usize
ptr-sizedSizes and indices
ptrdiff_t
ptr-sized
isize
ptr-sizedPointer arithmetic
intptr_t
ptr-sized
isize
ptr-sizedPointer-sized integer
uintptr_t
ptr-sized
usize
ptr-sizedUnsigned pointer-sized
void
-
()
0 bytesUnit type

Fixed-width types (C99+ stdint.h → Rust):

C (stdint.h)RustNotes
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 PatternRust PatternWhen 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

CRustNotes
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 manipulationNo 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

CRustNotes
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 PatternRust PatternNotes
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)
AutomaticDrop trait
ptr = NULL
after free
Not neededMove 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 PatternRust PatternWhy
#ifndef
/
#define
/
#endif
Not neededModule system prevents double inclusion
#pragma once
Not neededSame reason
#include "header.h"
use crate::module;
Explicit imports
#include <stdlib.h>
use std::collections::HashMap;
Standard library imports
Forward declarationsNot neededCompiler resolves order

Visibility

C PatternRust PatternNotes
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
  • move
    closure 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:

C11RustDescription
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:

  1. Understand C codebase behavior
  2. Design Rust architecture
  3. Implement in idiomatic Rust
  4. Test against C version (golden tests)

Strategy 2: Incremental (FFI Boundary)

When to use:

  • Large codebase
  • Need continuous operation
  • Limited resources
  • Risk-averse

Approach:

  1. Identify module boundaries
  2. Create FFI wrappers for C modules
  3. Rewrite one module at a time in Rust
  4. Expose Rust module with C-compatible FFI
  5. Gradually replace C modules

Strategy 3: c2rust Then Refactor

When to use:

  • Very large codebase
  • Complex pointer logic
  • Need mechanical translation first

Approach:

  1. Run c2rust on codebase
  2. Get compiling unsafe Rust
  3. Write comprehensive tests
  4. Incrementally refactor to safe Rust
  5. Replace raw pointers with references
  6. 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 examples
  • convert-typescript-rust
    - High-level to Rust conversion patterns
  • convert-golang-rust
    - GC to ownership conversion patterns
  • lang-c-dev
    - C development patterns
  • lang-rust-dev
    - Rust development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev
    - Thread safety patterns
  • patterns-serialization-dev
    - Data serialization patterns
  • patterns-metaprogramming-dev
    - Macro patterns