-
Notifications
You must be signed in to change notification settings - Fork 144
Feat/memory helpers #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat/memory helpers #209
Changes from all commits
152ce07
1d39640
fd36045
ac8fde6
f6366bc
d229cb6
5d06d9a
7087412
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,14 @@ int cbm_thread_join(cbm_thread_t *t) { | |
| return 0; | ||
| } | ||
|
|
||
| int cbm_thread_detach(cbm_thread_t *t) { | ||
| if (t->handle) { | ||
| CloseHandle(t->handle); | ||
| t->handle = NULL; | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| #else /* POSIX */ | ||
|
|
||
| int cbm_thread_create(cbm_thread_t *t, size_t stack_size, void *(*fn)(void *), void *arg) { | ||
|
|
@@ -77,6 +85,10 @@ int cbm_thread_join(cbm_thread_t *t) { | |
| return pthread_join(t->handle, NULL); | ||
| } | ||
|
|
||
| int cbm_thread_detach(cbm_thread_t *t) { | ||
| return pthread_detach(t->handle); | ||
| } | ||
|
Comment on lines
+88
to
+90
|
||
|
|
||
| #endif | ||
|
|
||
| /* ── Mutex ────────────────────────────────────────────────────── */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,6 +31,48 @@ static inline void *safe_realloc(void *ptr, size_t size) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return tmp; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Safe free: frees and NULLs a pointer to prevent double-free / use-after-free. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Accepts void** so it works with any pointer type via the macro. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static inline void safe_free_impl(void **pp) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (pp && *pp) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| free(*pp); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *pp = NULL; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #define safe_free(ptr) safe_free_impl((void **)(void *)&(ptr)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Safe string free: frees a const char* and NULLs it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Casts away const so callers don't need the (void*) dance. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static inline void safe_str_free(const char **sp) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (sp && *sp) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| free((void *)*sp); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *sp = NULL; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Safe buffer free: frees a heap array and zeros its element count. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Use for dynamic arrays paired with a size_t count. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static inline void safe_buf_free_impl(void **buf, size_t *count) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (buf && *buf) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| free(*buf); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *buf = NULL; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (count) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *count = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #define safe_buf_free(buf, countp) safe_buf_free_impl((void **)(void *)&(buf), (countp)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+65
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Safe grow: doubles capacity and reallocs when count reaches cap. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Usage: safe_grow(arr, count, cap, growth_factor) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Evaluates to the new arr (NULL on OOM — old memory freed by safe_realloc). */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #define safe_grow(arr, n, cap, factor) do { \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ((size_t)(n) >= (size_t)(cap)) { \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (cap) *= (factor); \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (arr) = safe_realloc((arr), (size_t)(cap) * sizeof(*(arr))); \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+66
to
+72
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Safe grow: doubles capacity and reallocs when count reaches cap. | |
| * Usage: safe_grow(arr, count, cap, growth_factor) | |
| * Evaluates to the new arr (NULL on OOM — old memory freed by safe_realloc). */ | |
| #define safe_grow(arr, n, cap, factor) do { \ | |
| if ((size_t)(n) >= (size_t)(cap)) { \ | |
| (cap) *= (factor); \ | |
| (arr) = safe_realloc((arr), (size_t)(cap) * sizeof(*(arr))); \ | |
| /* Safe grow: grows capacity and reallocs when count reaches cap. | |
| * Usage: safe_grow(arr, count, cap, growth_factor) | |
| * On success, updates arr and cap together. On OOM/overflow, arr becomes | |
| * NULL only if safe_realloc freed the old buffer; cap is left unchanged. */ | |
| #define safe_grow(arr, n, cap, factor) do { \ | |
| if ((size_t)(n) >= (size_t)(cap)) { \ | |
| size_t _safe_old_cap = (size_t)(cap); \ | |
| size_t _safe_new_cap = _safe_old_cap ? (_safe_old_cap * (size_t)(factor)) : 1u; \ | |
| if (_safe_new_cap <= _safe_old_cap) { \ | |
| _safe_new_cap = _safe_old_cap + 1u; \ | |
| } \ | |
| if (sizeof(*(arr)) != 0 && _safe_new_cap <= (SIZE_MAX / sizeof(*(arr)))) { \ | |
| void *_safe_tmp = safe_realloc((arr), _safe_new_cap * sizeof(*(arr))); \ | |
| if (_safe_tmp) { \ | |
| (arr) = _safe_tmp; \ | |
| (cap) = _safe_new_cap; \ | |
| } else { \ | |
| (arr) = NULL; \ | |
| } \ | |
| } else { \ | |
| (arr) = NULL; \ | |
| } \ |
Copilot
AI
Apr 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description focuses on adding memory helper functions, but this PR also changes watcher concurrency behavior, Cypher lexing/parsing, SQLite error handling, pipeline edge emission, HTTP server threading, and adds multiple .claude/* workflow files. Consider splitting these into separate PRs (or updating the description) so reviewers can assess each behavioral change independently and reduce merge risk.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -315,6 +315,9 @@ int main(int argc, char **argv) { | |
| } | ||
|
|
||
| /* Create and start watcher in background thread */ | ||
| /* Initialize log mutex before any threads are created */ | ||
| cbm_ui_log_init(); | ||
|
|
||
|
Comment on lines
317
to
+320
|
||
| cbm_store_t *watch_store = cbm_store_open_memory(); | ||
| g_watcher = cbm_watcher_new(watch_store, watcher_index_fn, NULL); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2704,7 +2704,9 @@ int cbm_store_get_schema(cbm_store_t *s, const char *project, cbm_schema_info_t | |
| const char *sql = "SELECT label, COUNT(*) FROM nodes WHERE project = ?1 GROUP BY label " | ||
| "ORDER BY COUNT(*) DESC;"; | ||
| sqlite3_stmt *stmt = NULL; | ||
| sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL); | ||
| if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK || !stmt) { | ||
| return CBM_NOT_FOUND; | ||
| } | ||
| bind_text(stmt, SKIP_ONE, project); | ||
|
|
||
| int cap = ST_INIT_CAP_8; | ||
|
|
@@ -2729,7 +2731,9 @@ int cbm_store_get_schema(cbm_store_t *s, const char *project, cbm_schema_info_t | |
| const char *sql = "SELECT type, COUNT(*) FROM edges WHERE project = ?1 GROUP BY type ORDER " | ||
| "BY COUNT(*) DESC;"; | ||
| sqlite3_stmt *stmt = NULL; | ||
| sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL); | ||
| if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK || !stmt) { | ||
| return CBM_NOT_FOUND; | ||
| } | ||
|
Comment on lines
2731
to
+2736
|
||
| bind_text(stmt, SKIP_ONE, project); | ||
|
|
||
| int cap = ST_INIT_CAP_8; | ||
|
|
@@ -3435,7 +3439,9 @@ static bool pkg_in_list(const char *pkg, char **list, int count) { | |
| static int collect_pkg_names(cbm_store_t *s, const char *sql, const char *project, char **pkgs, | ||
| int max_pkgs) { | ||
| sqlite3_stmt *stmt = NULL; | ||
| sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL); | ||
| if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK || !stmt) { | ||
| return 0; | ||
| } | ||
| bind_text(stmt, SKIP_ONE, project); | ||
| int count = 0; | ||
| while (sqlite3_step(stmt) == SQLITE_ROW && count < max_pkgs) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| #include "foundation/log.h" | ||
| #include "foundation/hash_table.h" | ||
| #include "foundation/compat.h" | ||
| #include "foundation/compat_thread.h" | ||
| #include "foundation/compat_fs.h" | ||
| #include "foundation/str_util.h" | ||
|
|
||
|
|
@@ -50,6 +51,7 @@ struct cbm_watcher { | |
| cbm_index_fn index_fn; | ||
| void *user_data; | ||
| CBMHashTable *projects; /* name → project_state_t* */ | ||
| cbm_mutex_t projects_lock; | ||
| atomic_int stopped; | ||
| }; | ||
|
|
||
|
|
@@ -236,6 +238,7 @@ cbm_watcher_t *cbm_watcher_new(cbm_store_t *store, cbm_index_fn index_fn, void * | |
| w->index_fn = index_fn; | ||
| w->user_data = user_data; | ||
| w->projects = cbm_ht_create(CBM_SZ_32); | ||
| cbm_mutex_init(&w->projects_lock); | ||
| atomic_init(&w->stopped, 0); | ||
| return w; | ||
| } | ||
|
|
@@ -244,8 +247,11 @@ void cbm_watcher_free(cbm_watcher_t *w) { | |
| if (!w) { | ||
| return; | ||
| } | ||
| cbm_mutex_lock(&w->projects_lock); | ||
| cbm_ht_foreach(w->projects, free_state_entry, NULL); | ||
| cbm_ht_free(w->projects); | ||
| cbm_mutex_unlock(&w->projects_lock); | ||
| cbm_mutex_destroy(&w->projects_lock); | ||
| free(w); | ||
| } | ||
|
|
||
|
|
@@ -264,6 +270,7 @@ void cbm_watcher_watch(cbm_watcher_t *w, const char *project_name, const char *r | |
| } | ||
|
|
||
| /* Remove old entry first (key points to state's project_name) */ | ||
| cbm_mutex_lock(&w->projects_lock); | ||
| project_state_t *old = cbm_ht_get(w->projects, project_name); | ||
| if (old) { | ||
| cbm_ht_delete(w->projects, project_name); | ||
|
|
@@ -272,17 +279,22 @@ void cbm_watcher_watch(cbm_watcher_t *w, const char *project_name, const char *r | |
|
|
||
| project_state_t *s = state_new(project_name, root_path); | ||
| cbm_ht_set(w->projects, s->project_name, s); | ||
| cbm_mutex_unlock(&w->projects_lock); | ||
|
Comment on lines
273
to
+282
|
||
| cbm_log_info("watcher.watch", "project", project_name, "path", root_path); | ||
| } | ||
|
|
||
| void cbm_watcher_unwatch(cbm_watcher_t *w, const char *project_name) { | ||
| if (!w || !project_name) { | ||
| return; | ||
| } | ||
| cbm_mutex_lock(&w->projects_lock); | ||
| project_state_t *s = cbm_ht_get(w->projects, project_name); | ||
| if (s) { | ||
| cbm_ht_delete(w->projects, project_name); | ||
| state_free(s); | ||
| } | ||
| cbm_mutex_unlock(&w->projects_lock); | ||
| if (s) { | ||
| cbm_log_info("watcher.unwatch", "project", project_name); | ||
| } | ||
| } | ||
|
|
@@ -421,7 +433,9 @@ int cbm_watcher_poll_once(cbm_watcher_t *w) { | |
| .now = now_ns(), | ||
| .reindexed = 0, | ||
| }; | ||
| cbm_mutex_lock(&w->projects_lock); | ||
| cbm_ht_foreach(w->projects, poll_project, &ctx); | ||
| cbm_mutex_unlock(&w->projects_lock); | ||
| return ctx.reindexed; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -78,6 +78,32 @@ TEST(cypher_lex_single_quote_string) { | |||||||||
| PASS(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| TEST(cypher_lex_string_overflow) { | ||||||||||
| /* Build a string literal longer than 4096 bytes to verify we don't | ||||||||||
| * overflow the stack buffer in lex_string_literal. */ | ||||||||||
| const int big = 5000; | ||||||||||
| /* query: "AAAA...A" (quotes included) */ | ||||||||||
| char *query = malloc(big + 3); /* quote + big chars + quote + NUL */ | ||||||||||
| ASSERT_NOT_NULL(query); | ||||||||||
| query[0] = '"'; | ||||||||||
| memset(query + 1, 'A', big); | ||||||||||
| query[big + 1] = '"'; | ||||||||||
| query[big + 2] = '\0'; | ||||||||||
|
|
||||||||||
| cbm_lex_result_t r = {0}; | ||||||||||
| int rc = cbm_lex(query, &r); | ||||||||||
| ASSERT_EQ(rc, 0); | ||||||||||
| ASSERT_NULL(r.error); | ||||||||||
| ASSERT_GTE(r.count, 1); | ||||||||||
| ASSERT_EQ(r.tokens[0].type, TOK_STRING); | ||||||||||
| /* The string should be truncated to CBM_SZ_4K - 1 (4095) characters. */ | ||||||||||
| ASSERT_EQ((int)strlen(r.tokens[0].text), 4095); | ||||||||||
|
Comment on lines
+99
to
+100
|
||||||||||
| /* The string should be truncated to CBM_SZ_4K - 1 (4095) characters. */ | |
| ASSERT_EQ((int)strlen(r.tokens[0].text), 4095); | |
| /* The string should be truncated to CBM_SZ_4K - 1 characters. */ | |
| ASSERT_EQ((int)strlen(r.tokens[0].text), (CBM_SZ_4K - 1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
lex_string_literal, the overflow truncation check runs before the escape-handling branch. If the string is already at max length and the next bytes are an escape like\", the code will skip only the backslash, then see"on the next iteration and treat it as the closing quote (even though it was escaped), prematurely terminating the string. To preserve correct lexing, handle escape sequences even when truncating (e.g., move the truncation logic after the escape handling, or when truncating and seeing\\skip both bytes when possible).