Memory and Errors¶
This page covers memory ownership, error handling, and thread safety. These are critical for using libwlite correctly from any language.
Memory ownership¶
The rule¶
Objects returned by _create or _load functions belong to the caller. Borrowed objects (returned by functions that do not allocate) are owned by their parent.
Owned objects¶
These are allocated by libwlite and must be freed by the caller:
| Object | Free function | Notes |
|---|---|---|
wlite_db |
wlite_close(db) |
Closes the SQLite connection |
wlite_model |
wlite_model_free(model) |
Frees all tables, fields, indexes |
wlite_stmt |
wlite_stmt_finalize(stmt) |
Finalizes the prepared statement |
wlite_tx |
wlite_tx_free(tx) |
Frees the transaction handle |
wlite_plan |
wlite_plan_free(plan) |
Frees the migration plan |
wlite_error |
wlite_error_free(err) |
Frees the error object |
Borrowed objects¶
These are owned by their parent and must NOT be freed separately:
| Object | Parent | Lifetime |
|---|---|---|
wlite_table |
wlite_model |
Valid until model is freed |
wlite_field |
wlite_model |
Valid until model is freed |
wlite_index |
wlite_model |
Valid until model is freed |
| Column text pointer | wlite_stmt |
Valid until next step or finalize |
Example: correct ownership¶
wlite_model *model = NULL;
wlite_db *db = NULL;
wlite_stmt *stmt = NULL;
// Allocate
wlite_model_load_file("schema.wlite", &model); // caller owns model
wlite_open("app.db", &db); // caller owns db
wlite_prepare(db, "SELECT * FROM users", &stmt); // caller owns stmt
// Use borrowed objects
const wlite_table *table = wlite_model_table(model, "users");
const wlite_field *field = wlite_table_field(table, "name");
// Do NOT free table or field
// Free in reverse order
wlite_stmt_finalize(stmt); // free stmt
wlite_close(db); // free db
wlite_model_free(model); // free model (also frees all tables and fields)
Example: incorrect ownership (double free)¶
wlite_model *model = NULL;
wlite_model_load_file("schema.wlite", &model);
const wlite_table *table = wlite_model_table(model, "users");
// WRONG: do not free borrowed objects
// wlite_table_free(table); // BUG: double free
wlite_model_free(model); // this already frees the table
Example: use after free¶
wlite_model *model = NULL;
wlite_model_load_file("schema.wlite", &model);
const wlite_table *table = wlite_model_table(model, "users");
wlite_model_free(model);
// WRONG: table is no longer valid
// const char *name = wlite_table_name(table); // BUG: use after free
Error handling¶
Return codes¶
All libwlite functions return wlite_result. The value WLITE_OK (0) indicates success. Any other value indicates an error.
wlite_result r = wlite_open("app.db", &db);
if (r != WLITE_OK) {
fprintf(stderr, "Error: %s\n", wlite_strerror(r));
return 1;
}
Error codes¶
| Code | Value | Meaning |
|---|---|---|
WLITE_OK |
0 | Success |
WLITE_ERROR |
1 | General or unexpected error |
WLITE_INVALID_ARGUMENT |
2 | Null pointer or invalid parameter |
WLITE_OUT_OF_MEMORY |
3 | Memory allocation failed |
WLITE_IO_ERROR |
4 | I/O error (disk full, permission denied, file not found) |
WLITE_PARSE_ERROR |
5 | Schema parse error (malformed .wlite source) |
WLITE_MODEL_ERROR |
6 | Schema model error (invalid table, missing field, etc.) |
WLITE_SQLITE_ERROR |
7 | SQLite returned an error |
WLITE_CONSTRAINT_ERROR |
8 | UNIQUE, CHECK, or FOREIGN KEY constraint violation |
WLITE_NOT_FOUND |
9 | Requested table, column, or resource not found |
WLITE_BUSY |
10 | Database is locked by another connection |
WLITE_TRANSACTION_ERROR |
11 | Transaction failed or is in an invalid state |
Error messages¶
Use wlite_strerror to convert an error code to a human-readable string:
wlite_result r = wlite_open("missing.db", &db);
if (r != WLITE_OK) {
fprintf(stderr, "Error %d: %s\n", r, wlite_strerror(r));
}
Error objects¶
For detailed error information, libwlite populates a wlite_error struct directly. Access its fields:
wlite_error *err = NULL;
wlite_result r = wlite_migrate(db, model);
if (r != WLITE_OK) {
fprintf(stderr, "Migration failed: %s\n", wlite_strerror(r));
}
Some functions also accept an optional error pointer for more detailed diagnostics:
wlite_error *err = NULL;
WlSchema *schema = wl_schema_load("schema.wlite", &err);
if (!schema) {
if (err) {
fprintf(stderr, "Parse error: %s\n", err->message);
wlite_error_free(err);
}
return 1;
}
Checking return values¶
Always check the return value of allocation functions:
wlite_model *model = NULL;
wlite_result r = wlite_model_load_file("schema.wlite", &model);
if (r != WLITE_OK) {
// model is NULL, do not use it
return 1;
}
// model is valid, use it
Do not check return values of void functions or functions that cannot fail:
// These cannot fail, no need to check
wlite_close(db);
wlite_model_free(model);
wlite_stmt_finalize(stmt);
Panic on error¶
For simple programs where any error is fatal:
#define CHECK(r) do { \
if ((r) != WLITE_OK) { \
fprintf(stderr, "Fatal: %s at %s:%d\n", wlite_strerror(r), __FILE__, __LINE__); \
abort(); \
} \
} while(0)
CHECK(wlite_open("app.db", &db));
CHECK(wlite_migrate(db, model));
Thread safety¶
Models¶
Models are immutable after loading. They can be shared across threads safely. Use wlite_model_load_file once, then pass the model pointer to any thread.
// Thread-safe: sharing a model
wlite_model *model = NULL;
wlite_model_load_file("schema.wlite", &model);
// Thread 1
wlite_db *db1 = NULL;
wlite_open("db1.db", &db1);
wlite_migrate(db1, model); // OK: model is immutable
// Thread 2
wlite_db *db2 = NULL;
wlite_open("db2.db", &db2);
wlite_migrate(db2, model); // OK: model is immutable
Database connections¶
Database connections are NOT thread-safe. Each thread must have its own connection. Do not share a wlite_db across threads.
// WRONG: sharing a connection across threads
// Thread 1: wlite_prepare(db, "SELECT ...", &stmt1);
// Thread 2: wlite_prepare(db, "INSERT ...", &stmt2); // DATA RACE
// CORRECT: each thread gets its own connection
// Thread 1: wlite_open("app.db", &db1); wlite_prepare(db1, ...);
// Thread 2: wlite_open("app.db", &db2); wlite_prepare(db2, ...);
Statements¶
Statements belong to a database connection. Do not share a wlite_stmt across threads.
Transactions¶
Transactions belong to a database connection. Do not share a wlite_tx across threads.
Summary¶
| Object | Thread-safe? | Notes |
|---|---|---|
wlite_model |
Yes | Immutable after loading |
wlite_db |
No | One connection per thread |
wlite_stmt |
No | Belongs to a connection |
wlite_tx |
No | Belongs to a connection |
wlite_table |
Yes | Borrowed from model, immutable |
wlite_field |
Yes | Borrowed from model, immutable |
Memory allocation¶
Default allocator¶
libwlite uses the C standard library allocator (malloc, realloc, free) by default.
Custom allocator¶
You can provide a custom allocator by calling wlite_set_allocator before any other function:
void *my_alloc(size_t size) {
return my_custom_malloc(size);
}
void *my_realloc(void *ptr, size_t size) {
return my_custom_realloc(ptr, size);
}
void my_free(void *ptr) {
my_custom_free(ptr);
}
int main(void) {
wlite_set_allocator(my_alloc, my_realloc, my_free);
// ... use libwlite
}
This is useful for embedded systems, games, or applications with custom memory pools.
Memory reporting¶
Use wlite_memory_usage to check how much memory libwlite is currently using:
size_t current, peak;
wlite_memory_usage(¤t, &peak);
printf("Current: %zu bytes, Peak: %zu bytes\n", current, peak);
Cleanup patterns¶
C: goto cleanup¶
The idiomatic C pattern for cleanup on error is goto:
int main(void) {
wlite_model *model = NULL;
wlite_db *db = NULL;
wlite_stmt *stmt = NULL;
int exit_code = 0;
if (wlite_model_load_file("schema.wlite", &model) != WLITE_OK) {
exit_code = 1;
goto done;
}
if (wlite_open("app.db", &db) != WLITE_OK) {
exit_code = 1;
goto done;
}
if (wlite_prepare(db, "SELECT * FROM users", &stmt) != WLITE_OK) {
exit_code = 1;
goto done;
}
while (wlite_step(stmt) == WLITE_OK) {
printf("%s\n", wlite_column_text(stmt, 0));
}
done:
if (stmt) wlite_stmt_finalize(stmt);
if (db) wlite_close(db);
if (model) wlite_model_free(model);
return exit_code;
}
C: defer-like macros¶
For less boilerplate, use a scope-guard macro:
#define DEFER(fn, ptr) __attribute__((cleanup(cleanup_##fn))) typeof(ptr) cleanup_##ptr = (ptr)
static void cleanup_close(wlite_db **p) { if (*p) wlite_close(*p); }
static void cleanup_model(wlite_model **p) { if (*p) wlite_model_free(*p); }
static void cleanup_stmt(wlite_stmt **p) { if (*p) wlite_stmt_finalize(*p); }
Python: context managers¶
Python uses context managers for automatic cleanup:
with wlite.Database.open("app.db") as db:
with db.prepare("SELECT * FROM users") as stmt:
while stmt.step():
print(stmt.column_text(0))
# stmt finalized, db closed automatically
Rust: Drop trait¶
Rust uses Drop for automatic cleanup:
{
let db = Database::open("app.db")?;
let stmt = db.prepare("SELECT * FROM users")?;
// ...
} // stmt dropped, then db dropped, both finalized automatically
Go: defer¶
Go uses defer for cleanup:
db, _ := wlite.Open("app.db")
defer db.Close()
stmt, _ := db.Prepare("SELECT * FROM users")
defer stmt.Finalize()
C#: using statements¶
C# uses using for automatic disposal:
using var db = Database.Open("app.db");
using var stmt = db.Prepare("SELECT * FROM users");
// both disposed at end of scope
Zig: defer¶
Zig uses defer for cleanup: