C Memory Management
Comprehensive guide to memory management in the Matchy C API.
Overview
The Matchy C API uses opaque handles to manage Rust objects safely from C. Understanding the ownership and lifetime rules is critical for preventing memory leaks and use-after-free bugs.
Core Principles
1. Ownership Model
- Caller owns input strings - Keep them valid for the duration of the function call
- Callee owns output handles - The library manages the underlying memory
- Explicit cleanup required - Always call the matching
_free()
or_close()
function
2. No Double-Free
Once you call a cleanup function, the handle is invalid:
matchy_builder_free(builder);
builder = NULL; // Good practice: prevent use-after-free
3. Memory Lifetime
Handles remain valid until explicitly freed, even if the creating function returns.
Cleanup Functions
Database Handles
void matchy_close(matchy_t *db);
Closes a database and frees associated resources:
- Unmaps the database file
- Releases internal buffers
- Invalidates the handle
When to call: After you're done querying the database
matchy_t *db = NULL;
if (matchy_open("database.mxy", &db) == MATCHY_SUCCESS) {
// Use db for queries...
matchy_close(db);
db = NULL; // Good practice
}
Builder Handles
void matchy_builder_free(matchy_builder_t *builder);
Frees a builder and all associated entries:
- Releases all added entries
- Frees internal build state
- Invalidates the handle
When to call: After building or if build fails
matchy_builder_t *builder = matchy_builder_new();
if (builder) {
matchy_builder_add(builder, "key", NULL);
// ... build or error ...
matchy_builder_free(builder);
builder = NULL;
}
Result Handles
void matchy_free_result(matchy_result_t *result);
Frees a query result:
- Releases match data
- Frees any associated strings
- Invalidates the handle
When to call: Immediately after extracting needed data
matchy_result_t *result = NULL;
int32_t err = matchy_lookup(db, "192.0.2.1", &result);
if (err == MATCHY_SUCCESS && result != NULL) {
// Extract data from result...
matchy_free_result(result);
result = NULL;
}
String Handles
void matchy_free_string(char *string);
Frees strings allocated by the library (e.g., error messages, validation results):
When to call: After using library-allocated strings
char *error_msg = NULL;
int32_t err = matchy_validate("file.mxy", MATCHY_VALIDATION_STRICT, &error_msg);
if (err != MATCHY_SUCCESS && error_msg != NULL) {
fprintf(stderr, "Validation error: %s\n", error_msg);
matchy_free_string(error_msg);
}
Entry Data Lists
void matchy_free_entry_data_list(matchy_entry_data_list_t *list);
Frees structured data query results:
When to call: After processing entry data
matchy_entry_data_list_t *list = NULL;
if (matchy_get_entry_data_list(entry, &list) == MATCHY_SUCCESS) {
// Process list...
matchy_free_entry_data_list(list);
}
Common Patterns
Pattern 1: Single Query
void query_once(const char *db_path, const char *query) {
matchy_t *db = NULL;
// Open database
if (matchy_open(db_path, &db) != MATCHY_SUCCESS) {
return;
}
// Query
matchy_result_t *result = NULL;
if (matchy_lookup(db, query, &result) == MATCHY_SUCCESS) {
if (result != NULL) {
// Use result...
matchy_free_result(result);
}
}
// Cleanup
matchy_close(db);
}
Pattern 2: Multiple Queries
void query_many(const char *db_path, const char **queries, size_t count) {
matchy_t *db = NULL;
if (matchy_open(db_path, &db) != MATCHY_SUCCESS) {
return;
}
// Reuse database handle for multiple queries
for (size_t i = 0; i < count; i++) {
matchy_result_t *result = NULL;
if (matchy_lookup(db, queries[i], &result) == MATCHY_SUCCESS) {
if (result != NULL) {
// Use result...
matchy_free_result(result);
}
}
}
matchy_close(db);
}
Pattern 3: Build and Query
int build_and_query(void) {
matchy_builder_t *builder = matchy_builder_new();
if (!builder) {
return -1;
}
// Build
matchy_builder_add(builder, "key", "{\"value\": 42}");
uint8_t *buffer = NULL;
uintptr_t size = 0;
int32_t err = matchy_builder_build(builder, &buffer, &size);
// Builder no longer needed
matchy_builder_free(builder);
if (err != MATCHY_SUCCESS) {
return -1;
}
// Open from buffer
matchy_t *db = NULL;
err = matchy_open_buffer(buffer, size, &db);
if (err != MATCHY_SUCCESS) {
free(buffer);
return -1;
}
// Query
matchy_result_t *result = NULL;
matchy_lookup(db, "key", &result);
if (result) {
matchy_free_result(result);
}
matchy_close(db);
free(buffer);
return 0;
}
Error Handling
Early Returns
Always cleanup on error paths:
matchy_t *db = NULL;
if (matchy_open(path, &db) != MATCHY_SUCCESS) {
return -1; // Nothing to cleanup
}
matchy_result_t *result = NULL;
if (matchy_lookup(db, query, &result) != MATCHY_SUCCESS) {
matchy_close(db); // Must cleanup db!
return -1;
}
// Use result...
matchy_free_result(result);
matchy_close(db);
return 0;
Goto Cleanup Pattern
For complex functions:
int process(const char *path) {
matchy_t *db = NULL;
matchy_result_t *result = NULL;
int ret = -1;
if (matchy_open(path, &db) != MATCHY_SUCCESS) {
goto cleanup;
}
if (matchy_lookup(db, "query", &result) != MATCHY_SUCCESS) {
goto cleanup;
}
// Success path
ret = 0;
cleanup:
if (result) matchy_free_result(result);
if (db) matchy_close(db);
return ret;
}
Thread Safety
Database Handles
Thread-safe for concurrent reads:
// Thread 1
matchy_result_t *r1 = NULL;
matchy_lookup(db, "query1", &r1); // Safe
matchy_free_result(r1);
// Thread 2 (concurrent, safe)
matchy_result_t *r2 = NULL;
matchy_lookup(db, "query2", &r2); // Safe
matchy_free_result(r2);
Not safe for concurrent close:
// Thread 1: Querying
matchy_lookup(db, "query", &result);
// Thread 2: Closing (UNSAFE!)
matchy_close(db); // Race condition!
Builder Handles
Not thread-safe - use from a single thread:
// UNSAFE:
matchy_builder_t *builder = matchy_builder_new();
// Thread 1
matchy_builder_add(builder, "key1", NULL);
// Thread 2
matchy_builder_add(builder, "key2", NULL); // Race condition!
Result Handles
Not thread-safe - each thread needs its own:
// Safe: Each thread has its own result
void *thread1(void *arg) {
matchy_t *db = arg;
matchy_result_t *result = NULL;
matchy_lookup(db, "query1", &result);
matchy_free_result(result);
return NULL;
}
void *thread2(void *arg) {
matchy_t *db = arg;
matchy_result_t *result = NULL;
matchy_lookup(db, "query2", &result);
matchy_free_result(result);
return NULL;
}
Common Mistakes
Mistake 1: Forgetting to Free
❌ Wrong:
for (int i = 0; i < 1000; i++) {
matchy_result_t *result = NULL;
matchy_lookup(db, queries[i], &result);
// Memory leak! Never freed result
}
✅ Correct:
for (int i = 0; i < 1000; i++) {
matchy_result_t *result = NULL;
matchy_lookup(db, queries[i], &result);
if (result) {
// Use result...
matchy_free_result(result);
}
}
Mistake 2: Use After Free
❌ Wrong:
matchy_result_t *result = NULL;
matchy_lookup(db, "query", &result);
matchy_free_result(result);
// Use after free!
int type = matchy_result_type(result);
✅ Correct:
matchy_result_t *result = NULL;
matchy_lookup(db, "query", &result);
int type = matchy_result_type(result);
matchy_free_result(result);
result = NULL; // Good practice
Mistake 3: Double Free
❌ Wrong:
matchy_free_result(result);
matchy_free_result(result); // Double free! Undefined behavior
✅ Correct:
if (result) {
matchy_free_result(result);
result = NULL;
}
Mistake 4: Missing Cleanup on Error
❌ Wrong:
matchy_t *db = NULL;
matchy_open(path, &db);
matchy_result_t *result = NULL;
if (matchy_lookup(db, query, &result) != MATCHY_SUCCESS) {
return -1; // Leak! Didn't close db
}
✅ Correct:
matchy_t *db = NULL;
matchy_open(path, &db);
matchy_result_t *result = NULL;
if (matchy_lookup(db, query, &result) != MATCHY_SUCCESS) {
matchy_close(db);
return -1;
}
Valgrind Testing
Use Valgrind to detect memory issues:
valgrind --leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
./your_program
A clean run should show:
HEAP SUMMARY:
in use at exit: 0 bytes in 0 blocks
total heap usage: X allocs, X frees, Y bytes allocated
All heap blocks were freed -- no leaks are possible
See Also
- C API Overview - API design and principles
- C Querying - Query operations
- Building with C - Compilation and linking
- Error Handling Reference - Error codes and handling