Claude-skill-registry convert-java-c

Convert Java code to idiomatic C. Use when migrating Java projects to C, translating Java patterns to idiomatic C, or refactoring Java codebases for performance, minimal runtime dependencies, and systems programming. Extends meta-convert-dev with Java-to-C specific patterns.

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-java-c" ~/.claude/skills/majiayu000-claude-skill-registry-convert-java-c && rm -rf "$T"
manifest: skills/data/convert-java-c/SKILL.md
source content

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

JavaCNotes
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 tableUse 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

JavaCNotes
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

JavaCNotes
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

JavaCNotes
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

JavaCNotes
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

JavaCNotes
List<T>
Separate type per
T
or
void*
No generics; use macros or code generation
<T extends Comparable>
Function pointer for comparisonPass comparator explicitly
Map<K, V>
Separate hash table per typeOr 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 ConceptC ApproachKey Insight
Class with state
struct
+ opaque pointer
Data separated from functions
MethodFunction with
self
pointer
Explicit
this
as first parameter
InheritanceStruct embedding + castingManual vtable for polymorphism
InterfaceFunction pointer tableManual dispatch
Constructor
create()
function
Explicit allocation with
malloc
Destructor/finalize
destroy()
function
Manual cleanup with
free
new
keyword
malloc
+ initialization
Manual memory allocation
GCManual
free
Explicit deallocation
Access modifiersOpaque pointers + header/impl splitModule-level visibility

Memory Management Mental Model

Java ModelC ModelConceptual Translation
Automatic GCManual malloc/freeOwnership tracking is manual
ReferencesPointersExplicit addresses
Array bounds checkingManual checks or buffer overflowNo automatic safety
String immutabilityMutable char arraysManual copying for safety
No dangling referencesDangling pointers possibleUse-after-free bugs possible

Error Handling Mental Model

Java ModelC ModelConceptual Translation
ExceptionsError codesNo stack unwinding
try-catchif (error) { handle }Manual propagation
finallygoto cleanup patternManual resource release
Checked exceptionsFunction documentationCompiler doesn't enforce
Stack tracesManual loggingNo 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:

AspectJavaC
Allocation
new Object()
malloc(sizeof(Object))
DeallocationAutomatic (GC)Manual (
free()
)
Dangling referencesImpossiblePossible (use-after-free)
Memory leaksPossible (unreachable but referenced)Possible (forgot to free)
Null checksNullPointerException at runtimeSegmentation fault
InitializationGuaranteed (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

ToolPurposeNotes
GCC / ClangC compilerUse
-Wall -Wextra -Werror
for warnings
ValgrindMemory leak detectionDetects leaks, use-after-free, uninitialized memory
AddressSanitizerMemory error detectionBuilt into GCC/Clang with
-fsanitize=address
gdbDebuggerDebug segfaults and logic errors
uthashHash table libraryHeader-only hash table for C
cJSONJSON parsingParse/generate JSON in C
Unity / CMockaUnit testingTesting frameworks for C
pthreadPOSIX threadsStandard threading library
Make / CMakeBuild systemCompile 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