返回 Skill 列表
extension
分类: 开发与工程无需 API Key

convert-java-c

将Java代码转换为惯用的C代码。在将Java项目迁移到C、将Java模式转换为惯用的C模式,或为了性能、最小运行时依赖以及系统编程而重构Java代码库时使用。扩展了meta-convert-dev,并添加了Java到C特定的模式。

person作者: jakexiaohubgithub

Convert Java to C

Convert Java code to idiomatic C. This skill extends meta-convert-dev with Java-to-C specific type mappings, idiom translations, and tooling for transforming managed, object-oriented Java code into manual, procedural C.

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: Java types → C types (managed → manual memory)
  • Idiom translations: Java OOP patterns → C procedural/opaque pointer patterns
  • Error handling: Exceptions → error codes + errno
  • Concurrency: Java threads/synchronized → pthreads/mutexes
  • Memory/Ownership: GC + automatic cleanup → malloc/free + manual management
  • Object-Oriented → Procedural: Classes/interfaces → structs + function pointers

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • Java language fundamentals - see lang-java-dev
  • C language fundamentals - see lang-c-dev
  • Reverse conversion (C → Java) - see convert-c-java

Quick Reference

| Java | C | Notes | |------|---|-------| | int | int32_t | Guaranteed 32-bit | | long | int64_t | Guaranteed 64-bit | | byte | int8_t | Signed 8-bit | | short | int16_t | Signed 16-bit | | char | uint16_t or wchar_t | Java char is UTF-16 | | float | float | 32-bit IEEE 754 | | double | double | 64-bit IEEE 754 | | boolean | bool (C99+) or int | Use stdbool.h | | String | char* or custom struct | Null-terminated or length-tracked | | ArrayList<T> | T* + size/capacity | Manual dynamic array | | HashMap<K,V> | Custom hash table | Use uthash or implement | | null | NULL | Pointer null | | Object | void* | Type-erased pointer | | interface | struct with function pointers | Vtable pattern | | Exception | int error code + errno | Manual error propagation | | synchronized | pthread_mutex_t | Manual locking | | Thread | pthread_t | POSIX threads |

When Converting Code

  1. Analyze source thoroughly before writing target
  2. Map types first - create type equivalence table (Java primitives have exact C equivalents)
  3. Preserve semantics over syntax similarity
  4. Plan memory management - who owns what, when to free
  5. Adopt C idioms - don't write "Java code in C syntax"
  6. Handle edge cases - null, exceptions, array bounds
  7. Test equivalence - same inputs → same outputs

Type System Mapping

Primitive Types

| Java | C | Notes | |------|---|-------| | byte | int8_t | Signed 8-bit (-128 to 127) | | short | int16_t | Signed 16-bit (-32,768 to 32,767) | | int | int32_t | Signed 32-bit | | long | int64_t | Signed 64-bit | | char | uint16_t or wchar_t | Java char is UTF-16 (2 bytes) | | float | float | 32-bit IEEE 754 | | double | double | 64-bit IEEE 754 | | boolean | bool (C99+) or int | Use #include <stdbool.h> | | void | void | No return value |

Critical Note on char: Java's char is a UTF-16 code unit (unsigned 16-bit). C's char is an ASCII character (8-bit). For Java String → C conversion, use UTF-8 char* or wide characters (wchar_t*).

String Types

| Java | C | Notes | |------|---|-------| | String | const char* | Immutable, null-terminated | | String | char* | Mutable, null-terminated | | StringBuilder | char* + manual realloc | Dynamic buffer | | char[] | char* or uint16_t* | Depends on encoding |

String Encoding: Java strings are UTF-16. C typically uses UTF-8. Convert using iconv or manual UTF-16 → UTF-8 conversion.

Collection Types

| Java | C | Notes | |------|---|-------| | ArrayList<T> | T* + size_t size, capacity | Manual dynamic array | | LinkedList<T> | struct Node { T data; struct Node* next; } | Manual linked list | | HashMap<K,V> | struct Entry { K key; V value; }* + hash | Use uthash library or custom | | HashSet<T> | HashMap<T, bool> or custom | Hash table with presence | | T[] | T* + size_t length | Fixed-size or dynamic | | Queue<T> | T* + head/tail pointers | Circular buffer or linked list | | Stack<T> | T* + top pointer | Array-based stack |

Object Types

| Java | C | Notes | |------|---|-------| | class | struct | Data + opaque pointer pattern | | interface | struct with function pointers | Vtable pattern | | abstract class | struct + function pointer vtable | Partial implementation | | enum | enum or #define constants | C enums are just ints | | Object | void* | Type-erased pointer (avoid) | | null | NULL | Null pointer |

Generic Types → Type Erasure or Macros

| Java | C | Notes | |------|---|-------| | List<T> | Separate type per T or void* | No generics; use macros or code generation | | <T extends Comparable> | Function pointer for comparison | Pass comparator explicitly | | Map<K, V> | Separate hash table per type | Or use void* with casting |

Approach 1: Type-specific implementations

// int_list.h
typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} IntList;

// string_list.h
typedef struct {
    char **data;
    size_t size;
    size_t capacity;
} StringList;

Approach 2: Generic with void (less type-safe)*

typedef struct {
    void **data;
    size_t size;
    size_t capacity;
} GenericList;

Approach 3: Macros (type-safe, compile-time)

#define DEFINE_LIST(T, PREFIX) \
typedef struct { \
    T *data; \
    size_t size, capacity; \
} PREFIX##List;

Idiom Translation

Pattern 1: Object Creation and Destruction

Java:

// Constructor
public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Destructor (finalize - rarely used)
    @Override
    protected void finalize() {
        // Cleanup
    }
}

// Usage
User user = new User("Alice", 30);
// GC handles cleanup automatically

C:

// Opaque pointer pattern
typedef struct User User;

// Constructor
User* user_create(const char *name, int age) {
    User *user = malloc(sizeof(User));
    if (user == NULL) {
        return NULL;
    }
    user->name = strdup(name);  // Allocate copy
    user->age = age;
    return user;
}

// Destructor
void user_destroy(User *user) {
    if (user != NULL) {
        free(user->name);
        free(user);
    }
}

// Usage
User *user = user_create("Alice", 30);
if (user != NULL) {
    // Use user...
    user_destroy(user);
}

Why this translation:

  • Java handles memory automatically with GC; C requires manual malloc/free
  • Opaque pointers hide implementation details (similar to Java private fields)
  • Constructor/destructor pattern mimics Java's object lifecycle
  • Always check malloc return for NULL (Java throws OutOfMemoryError)

Pattern 2: Null Handling

Java:

// Nullable reference
String name = getUser(id);
if (name != null) {
    System.out.println(name.toUpperCase());
} else {
    System.out.println("Unknown");
}

// Optional (Java 8+)
Optional<String> nameOpt = Optional.ofNullable(getUser(id));
String upper = nameOpt.map(String::toUpperCase).orElse("Unknown");

C:

// Null pointer check
char *name = get_user(id);
if (name != NULL) {
    // Create uppercase copy
    char upper[256];
    size_t i;
    for (i = 0; i < sizeof(upper) - 1 && name[i] != '\0'; i++) {
        upper[i] = toupper(name[i]);
    }
    upper[i] = '\0';
    printf("%s\n", upper);
} else {
    printf("Unknown\n");
}
free(name);

// Optional-like pattern (C11+)
typedef struct {
    bool present;
    char value[256];
} OptionalString;

OptionalString get_user_optional(int id) {
    OptionalString opt = {0};
    char *name = get_user(id);
    if (name != NULL) {
        opt.present = true;
        strncpy(opt.value, name, sizeof(opt.value) - 1);
        free(name);
    }
    return opt;
}

Why this translation:

  • C has no built-in Optional type; must check NULL explicitly
  • Manual memory management: know when to free returned pointers
  • C strings are mutable; modifications require copying

Pattern 3: Exception Handling → Error Codes

Java:

public void processFile(String path) throws IOException {
    File file = new File(path);
    if (!file.exists()) {
        throw new FileNotFoundException(path);
    }

    try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
        String line = reader.readLine();
        // Process...
    } catch (IOException e) {
        throw new IOException("Failed to read " + path, e);
    }
}

C:

#include <errno.h>

// Error codes
#define SUCCESS 0
#define ERR_FILE_NOT_FOUND -1
#define ERR_IO_ERROR -2

int process_file(const char *path) {
    // Check file exists
    if (access(path, F_OK) != 0) {
        errno = ENOENT;
        return ERR_FILE_NOT_FOUND;
    }

    FILE *file = fopen(path, "r");
    if (file == NULL) {
        // errno set by fopen
        return ERR_IO_ERROR;
    }

    char buffer[256];
    if (fgets(buffer, sizeof(buffer), file) == NULL) {
        fclose(file);
        return ERR_IO_ERROR;
    }

    // Process...

    fclose(file);
    return SUCCESS;
}

// Usage
int result = process_file("data.txt");
if (result != SUCCESS) {
    fprintf(stderr, "Error: %s (code %d)\n", strerror(errno), result);
}

Why this translation:

  • C has no exceptions; use return codes and errno
  • errno is a thread-local global for system error details
  • Check every I/O operation return value
  • Manual resource cleanup (no try-with-resources)

Pattern 4: Interfaces → Function Pointer Vtables

Java:

interface Drawable {
    void draw();
    int getWidth();
}

class Circle implements Drawable {
    private int radius;

    public Circle(int radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing circle");
    }

    @Override
    public int getWidth() {
        return radius * 2;
    }
}

C:

// Interface as vtable
typedef struct Drawable Drawable;

typedef struct {
    void (*draw)(Drawable *self);
    int (*get_width)(Drawable *self);
} DrawableVTable;

struct Drawable {
    const DrawableVTable *vtable;
    void *impl;  // Opaque implementation
};

// Circle implementation
typedef struct {
    int radius;
} Circle;

static void circle_draw(Drawable *self) {
    Circle *circle = (Circle *)self->impl;
    printf("Drawing circle with radius %d\n", circle->radius);
}

static int circle_get_width(Drawable *self) {
    Circle *circle = (Circle *)self->impl;
    return circle->radius * 2;
}

static const DrawableVTable circle_vtable = {
    .draw = circle_draw,
    .get_width = circle_get_width,
};

// Constructor
Drawable* circle_create(int radius) {
    Circle *circle = malloc(sizeof(Circle));
    if (circle == NULL) return NULL;

    circle->radius = radius;

    Drawable *drawable = malloc(sizeof(Drawable));
    if (drawable == NULL) {
        free(circle);
        return NULL;
    }

    drawable->vtable = &circle_vtable;
    drawable->impl = circle;
    return drawable;
}

// Usage
Drawable *obj = circle_create(10);
obj->vtable->draw(obj);           // Polymorphic call
int width = obj->vtable->get_width(obj);

Why this translation:

  • C has no built-in polymorphism; simulate with function pointer tables (vtables)
  • void *impl stores the concrete type (requires casting)
  • Vtable stores function pointers for interface methods
  • Manual vtable initialization (Java does this automatically)

Pattern 5: ArrayList → Dynamic Array

Java:

ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);

for (int num : numbers) {
    System.out.println(num);
}

numbers.remove(1);  // Remove at index 1

C:

typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} IntList;

IntList* int_list_create(void) {
    IntList *list = malloc(sizeof(IntList));
    if (list == NULL) return NULL;

    list->data = malloc(10 * sizeof(int));  // Initial capacity
    if (list->data == NULL) {
        free(list);
        return NULL;
    }

    list->size = 0;
    list->capacity = 10;
    return list;
}

int int_list_add(IntList *list, int value) {
    if (list->size >= list->capacity) {
        // Resize (double capacity)
        size_t new_capacity = list->capacity * 2;
        int *new_data = realloc(list->data, new_capacity * sizeof(int));
        if (new_data == NULL) {
            return -1;  // Failed to resize
        }
        list->data = new_data;
        list->capacity = new_capacity;
    }

    list->data[list->size++] = value;
    return 0;
}

void int_list_remove_at(IntList *list, size_t index) {
    if (index >= list->size) return;

    // Shift elements left
    for (size_t i = index; i < list->size - 1; i++) {
        list->data[i] = list->data[i + 1];
    }
    list->size--;
}

void int_list_destroy(IntList *list) {
    if (list != NULL) {
        free(list->data);
        free(list);
    }
}

// Usage
IntList *numbers = int_list_create();
int_list_add(numbers, 1);
int_list_add(numbers, 2);
int_list_add(numbers, 3);

for (size_t i = 0; i < numbers->size; i++) {
    printf("%d\n", numbers->data[i]);
}

int_list_remove_at(numbers, 1);
int_list_destroy(numbers);

Why this translation:

  • Java ArrayList auto-grows; C requires manual realloc
  • Track both size (current elements) and capacity (allocated space)
  • Manual bounds checking (Java throws IndexOutOfBoundsException)
  • Explicit destroy function to free memory

Pattern 6: HashMap → Custom Hash Table

Java:

HashMap<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);

Integer age = ages.get("Alice");
if (age != null) {
    System.out.println(age);
}

ages.remove("Bob");

C:

// Using uthash library: https://troydhanson.github.io/uthash/
#include "uthash.h"

typedef struct {
    char *key;
    int value;
    UT_hash_handle hh;
} Entry;

Entry *ages = NULL;  // Hash table head

// Put
void put(const char *key, int value) {
    Entry *entry;
    HASH_FIND_STR(ages, key, entry);

    if (entry == NULL) {
        entry = malloc(sizeof(Entry));
        entry->key = strdup(key);
        entry->value = value;
        HASH_ADD_KEYPTR(hh, ages, entry->key, strlen(entry->key), entry);
    } else {
        entry->value = value;  // Update existing
    }
}

// Get
int get(const char *key, int *out_value) {
    Entry *entry;
    HASH_FIND_STR(ages, key, entry);

    if (entry != NULL) {
        *out_value = entry->value;
        return 1;  // Found
    }
    return 0;  // Not found
}

// Remove
void remove_key(const char *key) {
    Entry *entry;
    HASH_FIND_STR(ages, key, entry);

    if (entry != NULL) {
        HASH_DEL(ages, entry);
        free(entry->key);
        free(entry);
    }
}

// Cleanup
void destroy_all(void) {
    Entry *entry, *tmp;
    HASH_ITER(hh, ages, entry, tmp) {
        HASH_DEL(ages, entry);
        free(entry->key);
        free(entry);
    }
}

// Usage
put("Alice", 30);
put("Bob", 25);

int age;
if (get("Alice", &age)) {
    printf("%d\n", age);
}

remove_key("Bob");
destroy_all();

Why this translation:

  • C has no built-in hash table; use libraries (uthash, glib) or implement manually
  • Manual key copying (strdup) and freeing
  • Return values indicate success/failure (no null to indicate missing)
  • Iteration requires library-specific macros

Pattern 7: Synchronized → Mutexes

Java:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

C:

#include <pthread.h>

typedef struct {
    int count;
    pthread_mutex_t mutex;
} Counter;

Counter* counter_create(void) {
    Counter *counter = malloc(sizeof(Counter));
    if (counter == NULL) return NULL;

    counter->count = 0;
    pthread_mutex_init(&counter->mutex, NULL);
    return counter;
}

void counter_increment(Counter *counter) {
    pthread_mutex_lock(&counter->mutex);
    counter->count++;
    pthread_mutex_unlock(&counter->mutex);
}

int counter_get(Counter *counter) {
    pthread_mutex_lock(&counter->mutex);
    int value = counter->count;
    pthread_mutex_unlock(&counter->mutex);
    return value;
}

void counter_destroy(Counter *counter) {
    if (counter != NULL) {
        pthread_mutex_destroy(&counter->mutex);
        free(counter);
    }
}

Why this translation:

  • Java synchronized is automatic; C requires explicit lock/unlock
  • Must remember to unlock (Java does this automatically even on exception)
  • Consider using atomics (atomic_int) for simple counters (C11+)
  • Initialize and destroy mutexes explicitly

Pattern 8: Thread Creation

Java:

Thread thread = new Thread(() -> {
    System.out.println("Running in thread");
});
thread.start();
thread.join();

C:

#include <pthread.h>
#include <stdio.h>

void* thread_function(void *arg) {
    printf("Running in thread\n");
    return NULL;
}

int main(void) {
    pthread_t thread;

    if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
        perror("pthread_create");
        return 1;
    }

    if (pthread_join(thread, NULL) != 0) {
        perror("pthread_join");
        return 1;
    }

    return 0;
}

Why this translation:

  • Java Thread wraps OS threads; C uses POSIX threads directly
  • Function pointers instead of lambda/Runnable
  • Manual error checking (Java throws exceptions)
  • No automatic thread pooling (Java ExecutorService)

Paradigm Translation

Mental Model Shift: Object-Oriented → Procedural

| Java Concept | C Approach | Key Insight | |--------------|------------|-------------| | Class with state | struct + opaque pointer | Data separated from functions | | Method | Function with self pointer | Explicit this as first parameter | | Inheritance | Struct embedding + casting | Manual vtable for polymorphism | | Interface | Function pointer table | Manual dispatch | | Constructor | create() function | Explicit allocation with malloc | | Destructor/finalize | destroy() function | Manual cleanup with free | | new keyword | malloc + initialization | Manual memory allocation | | GC | Manual free | Explicit deallocation | | Access modifiers | Opaque pointers + header/impl split | Module-level visibility |

Memory Management Mental Model

| Java Model | C Model | Conceptual Translation | |------------|---------|------------------------| | Automatic GC | Manual malloc/free | Ownership tracking is manual | | References | Pointers | Explicit addresses | | Array bounds checking | Manual checks or buffer overflow | No automatic safety | | String immutability | Mutable char arrays | Manual copying for safety | | No dangling references | Dangling pointers possible | Use-after-free bugs possible |

Error Handling Mental Model

| Java Model | C Model | Conceptual Translation | |------------|---------|------------------------| | Exceptions | Error codes | No stack unwinding | | try-catch | if (error) { handle } | Manual propagation | | finally | goto cleanup pattern | Manual resource release | | Checked exceptions | Function documentation | Compiler doesn't enforce | | Stack traces | Manual logging | No automatic context |


Error Handling

Java Exception Model → C Error Codes

Java Approach:

  • Exceptions for error conditions
  • Stack unwinding for cleanup
  • Checked vs unchecked exceptions
  • Try-catch-finally blocks

C Approach:

  • Return codes for error conditions
  • Manual cleanup with goto or careful ordering
  • errno for system call errors
  • Explicit error checking at every call

Pattern: Error Code + errno

// Error codes
typedef enum {
    SUCCESS = 0,
    ERR_NULL_POINTER = -1,
    ERR_OUT_OF_MEMORY = -2,
    ERR_FILE_NOT_FOUND = -3,
    ERR_IO_ERROR = -4,
} ErrorCode;

// Function with error handling
ErrorCode read_config(const char *path, Config **out) {
    if (path == NULL || out == NULL) {
        return ERR_NULL_POINTER;
    }

    FILE *file = fopen(path, "r");
    if (file == NULL) {
        errno = ENOENT;
        return ERR_FILE_NOT_FOUND;
    }

    Config *config = malloc(sizeof(Config));
    if (config == NULL) {
        fclose(file);
        return ERR_OUT_OF_MEMORY;
    }

    // Read data...
    if (fgets(config->buffer, sizeof(config->buffer), file) == NULL) {
        free(config);
        fclose(file);
        return ERR_IO_ERROR;
    }

    fclose(file);
    *out = config;
    return SUCCESS;
}

// Usage with goto cleanup pattern
ErrorCode process(void) {
    Config *config = NULL;
    Data *data = NULL;
    ErrorCode result = SUCCESS;

    result = read_config("app.conf", &config);
    if (result != SUCCESS) {
        goto cleanup;
    }

    data = malloc(sizeof(Data));
    if (data == NULL) {
        result = ERR_OUT_OF_MEMORY;
        goto cleanup;
    }

    // Process...

cleanup:
    free(data);
    free(config);
    return result;
}

Concurrency Patterns

Java Concurrency → POSIX Threads

Java Model:

  • Thread class + Runnable interface
  • synchronized keyword
  • wait() / notify()
  • ExecutorService thread pools
  • CompletableFuture

C Model:

  • pthread_t + function pointers
  • pthread_mutex_t explicit locks
  • pthread_cond_t condition variables
  • Manual thread pool implementation
  • No built-in async/await

Pattern: Producer-Consumer with Condition Variables

Java:

class Queue {
    private LinkedList<Integer> items = new LinkedList<>();
    private final int MAX = 10;

    public synchronized void put(int item) throws InterruptedException {
        while (items.size() >= MAX) {
            wait();
        }
        items.add(item);
        notifyAll();
    }

    public synchronized int take() throws InterruptedException {
        while (items.isEmpty()) {
            wait();
        }
        int item = items.removeFirst();
        notifyAll();
        return item;
    }
}

C:

#include <pthread.h>

#define MAX_QUEUE 10

typedef struct {
    int items[MAX_QUEUE];
    int head, tail, count;
    pthread_mutex_t mutex;
    pthread_cond_t not_full;
    pthread_cond_t not_empty;
} Queue;

Queue* queue_create(void) {
    Queue *q = malloc(sizeof(Queue));
    if (q == NULL) return NULL;

    q->head = q->tail = q->count = 0;
    pthread_mutex_init(&q->mutex, NULL);
    pthread_cond_init(&q->not_full, NULL);
    pthread_cond_init(&q->not_empty, NULL);
    return q;
}

void queue_put(Queue *q, int item) {
    pthread_mutex_lock(&q->mutex);

    while (q->count >= MAX_QUEUE) {
        pthread_cond_wait(&q->not_full, &q->mutex);
    }

    q->items[q->tail] = item;
    q->tail = (q->tail + 1) % MAX_QUEUE;
    q->count++;

    pthread_cond_signal(&q->not_empty);
    pthread_mutex_unlock(&q->mutex);
}

int queue_take(Queue *q) {
    pthread_mutex_lock(&q->mutex);

    while (q->count == 0) {
        pthread_cond_wait(&q->not_empty, &q->mutex);
    }

    int item = q->items[q->head];
    q->head = (q->head + 1) % MAX_QUEUE;
    q->count--;

    pthread_cond_signal(&q->not_full);
    pthread_mutex_unlock(&q->mutex);

    return item;
}

void queue_destroy(Queue *q) {
    if (q != NULL) {
        pthread_mutex_destroy(&q->mutex);
        pthread_cond_destroy(&q->not_full);
        pthread_cond_destroy(&q->not_empty);
        free(q);
    }
}

Why this translation:

  • Java wait() / notifyAll()pthread_cond_wait() / pthread_cond_signal()
  • Java synchronized → explicit pthread_mutex_lock/unlock
  • Manual mutex management (Java does it automatically)
  • Condition variables require associated mutex

Memory & Ownership

Java GC → C Manual Memory Management

Key Differences:

| Aspect | Java | C | |--------|------|---| | Allocation | new Object() | malloc(sizeof(Object)) | | Deallocation | Automatic (GC) | Manual (free()) | | Dangling references | Impossible | Possible (use-after-free) | | Memory leaks | Possible (unreachable but referenced) | Possible (forgot to free) | | Null checks | NullPointerException at runtime | Segmentation fault | | Initialization | Guaranteed (default values) | Uninitialized (garbage) |

Ownership Patterns in C:

  1. Caller owns, callee borrows (most common)
void process_user(const User *user) {
    // user is borrowed, don't free
    printf("%s\n", user->name);
}

User *user = user_create("Alice", 30);
process_user(user);
user_destroy(user);  // Caller frees
  1. Callee owns, caller receives
char* create_greeting(const char *name) {
    char *greeting = malloc(100);
    snprintf(greeting, 100, "Hello, %s!", name);
    return greeting;  // Caller must free
}

char *msg = create_greeting("Alice");
printf("%s\n", msg);
free(msg);  // Caller frees
  1. Shared ownership (ref counting)
typedef struct {
    Data data;
    int ref_count;
} RefCounted;

RefCounted* ref_create(void) {
    RefCounted *r = malloc(sizeof(RefCounted));
    r->ref_count = 1;
    return r;
}

void ref_retain(RefCounted *r) {
    r->ref_count++;
}

void ref_release(RefCounted *r) {
    r->ref_count--;
    if (r->ref_count == 0) {
        free(r);
    }
}

Common Pitfalls

  1. Forgetting to free memory

    • Issue: Java GC handles cleanup; C requires manual free()
    • Solution: Every malloc should have a corresponding free; use goto cleanup pattern
  2. Null pointer dereference

    • Issue: Java throws NullPointerException; C crashes with segfault
    • Solution: Check pointers before dereferencing: if (ptr != NULL) { ... }
  3. Buffer overflow

    • Issue: Java arrays are bounds-checked; C arrays are not
    • Solution: Use strncpy, snprintf, track array sizes, validate indices
  4. Use-after-free

    • Issue: Java GC prevents this; C allows accessing freed memory
    • Solution: Set pointers to NULL after freeing; use tools like valgrind
  5. String encoding mismatch

    • Issue: Java strings are UTF-16; C strings are typically UTF-8 or ASCII
    • Solution: Convert encoding explicitly; use iconv library or manual conversion
  6. Integer overflow

    • Issue: Java detects overflow (throws exception or wraps); C wraps silently
    • Solution: Use safe math libraries or check before operations
  7. Thread safety

    • Issue: Java synchronized is implicit; C mutexes must be explicit
    • Solution: Document thread-safety requirements; use mutexes consistently
  8. Error handling

    • Issue: Java exceptions propagate automatically; C error codes must be checked
    • Solution: Check every function return value; use goto for cleanup
  9. Missing initialization

    • Issue: Java initializes fields to default values; C leaves memory uninitialized
    • Solution: Always initialize variables: int x = 0; or use calloc for zero-init
  10. Type safety

    • Issue: Java has strong typing; C allows dangerous casts with void*
    • Solution: Minimize void* usage; use typed pointers; document ownership

Tooling

| Tool | Purpose | Notes | |------|---------|-------| | GCC / Clang | C compiler | Use -Wall -Wextra -Werror for warnings | | Valgrind | Memory leak detection | Detects leaks, use-after-free, uninitialized memory | | AddressSanitizer | Memory error detection | Built into GCC/Clang with -fsanitize=address | | gdb | Debugger | Debug segfaults and logic errors | | uthash | Hash table library | Header-only hash table for C | | cJSON | JSON parsing | Parse/generate JSON in C | | Unity / CMocka | Unit testing | Testing frameworks for C | | pthread | POSIX threads | Standard threading library | | Make / CMake | Build system | Compile multi-file C projects |


Examples

Example 1: Simple - Data Class with Methods

Before (Java):

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }

    public double distance(Point other) {
        int dx = this.x - other.x;
        int dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

After (C):

// point.h
#ifndef POINT_H
#define POINT_H

typedef struct {
    int x;
    int y;
} Point;

Point point_create(int x, int y);
int point_get_x(const Point *p);
int point_get_y(const Point *p);
double point_distance(const Point *p1, const Point *p2);

#endif

// point.c
#include "point.h"
#include <math.h>

Point point_create(int x, int y) {
    Point p = {x, y};
    return p;
}

int point_get_x(const Point *p) {
    return p->x;
}

int point_get_y(const Point *p) {
    return p->y;
}

double point_distance(const Point *p1, const Point *p2) {
    int dx = p1->x - p2->x;
    int dy = p1->y - p2->y;
    return sqrt(dx * dx + dy * dy);
}

// Usage
Point p1 = point_create(0, 0);
Point p2 = point_create(3, 4);
double dist = point_distance(&p1, &p2);  // 5.0

Example 2: Medium - Error Handling with Resources

Before (Java):

public String readFirstLine(String path) throws IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
        String line = reader.readLine();
        if (line == null) {
            throw new IOException("File is empty");
        }
        return line;
    }
}

After (C):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define SUCCESS 0
#define ERR_FILE_OPEN -1
#define ERR_FILE_EMPTY -2
#define ERR_OUT_OF_MEMORY -3

int read_first_line(const char *path, char **out_line) {
    if (path == NULL || out_line == NULL) {
        return ERR_FILE_OPEN;
    }

    FILE *file = fopen(path, "r");
    if (file == NULL) {
        return ERR_FILE_OPEN;
    }

    char buffer[256];
    if (fgets(buffer, sizeof(buffer), file) == NULL) {
        fclose(file);
        return ERR_FILE_EMPTY;
    }

    // Remove trailing newline
    size_t len = strlen(buffer);
    if (len > 0 && buffer[len - 1] == '\n') {
        buffer[len - 1] = '\0';
    }

    *out_line = strdup(buffer);
    if (*out_line == NULL) {
        fclose(file);
        return ERR_OUT_OF_MEMORY;
    }

    fclose(file);
    return SUCCESS;
}

// Usage
char *line = NULL;
int result = read_first_line("data.txt", &line);
if (result == SUCCESS) {
    printf("First line: %s\n", line);
    free(line);
} else {
    fprintf(stderr, "Error reading file: %d\n", result);
}

Example 3: Complex - Thread-Safe Counter with Interface

Before (Java):

interface Counter {
    void increment();
    int get();
}

class AtomicCounter implements Counter {
    private final AtomicInteger count = new AtomicInteger(0);

    @Override
    public void increment() {
        count.incrementAndGet();
    }

    @Override
    public int get() {
        return count.get();
    }
}

class SynchronizedCounter implements Counter {
    private int count = 0;

    @Override
    public synchronized void increment() {
        count++;
    }

    @Override
    public synchronized int get() {
        return count;
    }
}

After (C):

// counter.h
#ifndef COUNTER_H
#define COUNTER_H

#include <pthread.h>
#include <stdatomic.h>

typedef struct Counter Counter;

typedef struct {
    void (*increment)(Counter *self);
    int (*get)(Counter *self);
    void (*destroy)(Counter *self);
} CounterVTable;

struct Counter {
    const CounterVTable *vtable;
    void *impl;
};

// Atomic counter
Counter* atomic_counter_create(void);

// Synchronized counter
Counter* synchronized_counter_create(void);

#endif

// counter.c
#include "counter.h"
#include <stdlib.h>

// Atomic implementation
typedef struct {
    atomic_int count;
} AtomicCounterImpl;

static void atomic_counter_increment(Counter *self) {
    AtomicCounterImpl *impl = (AtomicCounterImpl *)self->impl;
    atomic_fetch_add(&impl->count, 1);
}

static int atomic_counter_get(Counter *self) {
    AtomicCounterImpl *impl = (AtomicCounterImpl *)self->impl;
    return atomic_load(&impl->count);
}

static void atomic_counter_destroy_impl(Counter *self) {
    free(self->impl);
    free(self);
}

static const CounterVTable atomic_vtable = {
    .increment = atomic_counter_increment,
    .get = atomic_counter_get,
    .destroy = atomic_counter_destroy_impl,
};

Counter* atomic_counter_create(void) {
    AtomicCounterImpl *impl = malloc(sizeof(AtomicCounterImpl));
    if (impl == NULL) return NULL;

    atomic_init(&impl->count, 0);

    Counter *counter = malloc(sizeof(Counter));
    if (counter == NULL) {
        free(impl);
        return NULL;
    }

    counter->vtable = &atomic_vtable;
    counter->impl = impl;
    return counter;
}

// Synchronized implementation
typedef struct {
    int count;
    pthread_mutex_t mutex;
} SyncCounterImpl;

static void sync_counter_increment(Counter *self) {
    SyncCounterImpl *impl = (SyncCounterImpl *)self->impl;
    pthread_mutex_lock(&impl->mutex);
    impl->count++;
    pthread_mutex_unlock(&impl->mutex);
}

static int sync_counter_get(Counter *self) {
    SyncCounterImpl *impl = (SyncCounterImpl *)self->impl;
    pthread_mutex_lock(&impl->mutex);
    int value = impl->count;
    pthread_mutex_unlock(&impl->mutex);
    return value;
}

static void sync_counter_destroy_impl(Counter *self) {
    SyncCounterImpl *impl = (SyncCounterImpl *)self->impl;
    pthread_mutex_destroy(&impl->mutex);
    free(impl);
    free(self);
}

static const CounterVTable sync_vtable = {
    .increment = sync_counter_increment,
    .get = sync_counter_get,
    .destroy = sync_counter_destroy_impl,
};

Counter* synchronized_counter_create(void) {
    SyncCounterImpl *impl = malloc(sizeof(SyncCounterImpl));
    if (impl == NULL) return NULL;

    impl->count = 0;
    pthread_mutex_init(&impl->mutex, NULL);

    Counter *counter = malloc(sizeof(Counter));
    if (counter == NULL) {
        pthread_mutex_destroy(&impl->mutex);
        free(impl);
        return NULL;
    }

    counter->vtable = &sync_vtable;
    counter->impl = impl;
    return counter;
}

// Usage
Counter *counter = atomic_counter_create();
counter->vtable->increment(counter);
int count = counter->vtable->get(counter);
counter->vtable->destroy(counter);

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-python-c - Similar high-level to low-level conversion
  • lang-java-dev - Java development patterns
  • lang-c-dev - C development patterns

Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):

  • patterns-concurrency-dev - Threads, mutexes, atomics across languages
  • patterns-serialization-dev - JSON, binary formats across languages
  • patterns-memory-eng - Manual memory management patterns