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
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table (Java primitives have exact C equivalents)
- Preserve semantics over syntax similarity
- Plan memory management - who owns what, when to free
- Adopt C idioms - don't write "Java code in C syntax"
- Handle edge cases - null, exceptions, array bounds
- 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
mallocreturn 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 errnois 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 *implstores 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) andcapacity(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
synchronizedis 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
errnofor 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
synchronizedkeywordwait()/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→ explicitpthread_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:
- 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
- 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
- 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
-
Forgetting to free memory
- Issue: Java GC handles cleanup; C requires manual
free() - Solution: Every
mallocshould have a correspondingfree; use goto cleanup pattern
- Issue: Java GC handles cleanup; C requires manual
-
Null pointer dereference
- Issue: Java throws NullPointerException; C crashes with segfault
- Solution: Check pointers before dereferencing:
if (ptr != NULL) { ... }
-
Buffer overflow
- Issue: Java arrays are bounds-checked; C arrays are not
- Solution: Use
strncpy,snprintf, track array sizes, validate indices
-
Use-after-free
- Issue: Java GC prevents this; C allows accessing freed memory
- Solution: Set pointers to NULL after freeing; use tools like valgrind
-
String encoding mismatch
- Issue: Java strings are UTF-16; C strings are typically UTF-8 or ASCII
- Solution: Convert encoding explicitly; use
iconvlibrary or manual conversion
-
Integer overflow
- Issue: Java detects overflow (throws exception or wraps); C wraps silently
- Solution: Use safe math libraries or check before operations
-
Thread safety
- Issue: Java
synchronizedis implicit; C mutexes must be explicit - Solution: Document thread-safety requirements; use mutexes consistently
- Issue: Java
-
Error handling
- Issue: Java exceptions propagate automatically; C error codes must be checked
- Solution: Check every function return value; use goto for cleanup
-
Missing initialization
- Issue: Java initializes fields to default values; C leaves memory uninitialized
- Solution: Always initialize variables:
int x = 0;or usecallocfor zero-init
-
Type safety
- Issue: Java has strong typing; C allows dangerous casts with
void* - Solution: Minimize
void*usage; use typed pointers; document ownership
- Issue: Java has strong typing; C allows dangerous casts with
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 examplesconvert-python-c- Similar high-level to low-level conversionlang-java-dev- Java development patternslang-c-dev- C development patterns
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
patterns-concurrency-dev- Threads, mutexes, atomics across languagespatterns-serialization-dev- JSON, binary formats across languagespatterns-memory-eng- Manual memory management patterns