Back to list
aRustyDev

convert-c-rust

by aRustyDev

AI Contexts, Commands, Plugins, Docs, Examples, Specs, Etc

1🍴 1📅 Jan 16, 2026

SKILL.md


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

CRustNotes
inti32Signed 32-bit
unsigned intu32Unsigned 32-bit
charu8 / charByte vs Unicode scalar
char**const u8 / &str / StringRaw / borrowed / owned
void**mut c_void / genericsPrefer generics
structstructSimilar syntax
enumenumRust enums more powerful
unionunion (unsafe) / enumPrefer tagged enum
NULLOption<T> / null()Type-safe nullability
malloc/freeBox::new / Vec / auto-dropRAII
FILE*std::fs::FileSafe file handling
errnoResult<T, E>Explicit error handling
pthread_tstd::thread / tokioSafe concurrency
#defineconst / macrosType-safe constants
#includeuse / modModule 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
char1 bytei81 byteSigned byte
unsigned char1 byteu81 byteUnsigned byte
short2 bytesi162 bytesSigned 16-bit
unsigned short2 bytesu162 bytesUnsigned 16-bit
int4 bytesi324 bytesSigned 32-bit (default)
unsigned int4 bytesu324 bytesUnsigned 32-bit
long4/8 bytesi64 / isize8 bytes / ptr-sizedPlatform-dependent in C
unsigned long4/8 bytesu64 / usize8 bytes / ptr-sizedPlatform-dependent in C
long long8 bytesi648 bytesSigned 64-bit
float4 bytesf324 bytes32-bit floating point
double8 bytesf648 bytes64-bit floating point (default)
_Bool / bool1 bytebool1 byteBoolean (C99+)
size_tptr-sizedusizeptr-sizedSizes and indices
ptrdiff_tptr-sizedisizeptr-sizedPointer arithmetic
intptr_tptr-sizedisizeptr-sizedPointer-sized integer
uintptr_tptr-sizedusizeptr-sizedUnsigned pointer-sized
void-()0 bytesUnit type

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

C (stdint.h)RustNotes
int8_ti8Exactly 8 bits signed
uint8_tu8Exactly 8 bits unsigned
int16_ti16Exactly 16 bits signed
uint16_tu16Exactly 16 bits unsigned
int32_ti32Exactly 32 bits signed
uint32_tu32Exactly 32 bits unsigned
int64_ti64Exactly 64 bits signed
uint64_tu64Exactly 64 bits unsigned

Pointer Types

C PatternRust PatternWhen to Use
const T*&TImmutable borrow (read-only access)
T*&mut TMutable borrow (exclusive access)
T*Box<T>Owned heap allocation
T**const TRaw pointer (unsafe, FFI)
T**mut TMutable raw pointer (unsafe, FFI)
T**&mut &T / Box<Box<T>>Pointer to pointer
void**mut c_void / T: ?SizedType-erased pointer / generics
NULLNoneOption<&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 unionenum Data { Int(i32), Float(f64) }Safer alternative
struct with padding#[repr(C)] attributeMatch C layout for FFI
struct bit fieldsManual bit manipulationNo direct equivalent
Flexible array memberVec<T> or manual allocationSafer 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) -> i32Function pointer
int (*fn)(int, int)Fn(i32, i32) -> i32Trait 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 freeNot 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 / #endifNot neededModule system prevents double inclusion
#pragma onceNot 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 functionpub fn name()Explicitly public
Public structpub struct NameExplicitly public
Private helperfn helper() (no pub)Private by default
Opaque typepub 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_relaxedOrdering::RelaxedNo synchronization
memory_order_acquireOrdering::AcquireLoad barrier
memory_order_releaseOrdering::ReleaseStore barrier
memory_order_acq_relOrdering::AcqRelBoth barriers
memory_order_seq_cstOrdering::SeqCstSequential 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

Score

Total Score

55/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

0/5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon