fix(store,ui): export paired free functions for FFI callers

cbm_store_fetch_call_edges, cbm_leiden/cbm_louvain, and
cbm_layout_to_json document caller-owned memory ("caller frees
both" / "the caller frees it" / "Caller must free()") but exported
no matching free function, unlike the rest of the API
(cbm_store_find_nodes -> cbm_store_free_nodes,
cbm_layout_compute -> cbm_layout_free, etc.). From C a bare free()
works via static linking, but a caller across an FFI boundary (e.g.
C# P/Invoke) has no way to reach the DLL's internal free() unless
it is exported by name.

Adds three paired free functions following the existing pattern:
- cbm_store_free_call_edges(src, tgt)
- cbm_leiden_free(result)
- cbm_layout_free_json(json)

Updates the one internal production caller (mcp.c SCC analysis) to
use cbm_store_free_call_edges instead of a bare free(), and the
existing unit tests for cbm_leiden/cbm_louvain/cbm_layout_to_json
to use the new paired frees. Adds fetch_call_edges_basic,
fetch_call_edges_truncated, and fetch_call_edges_empty_project to
test_store_arch.c, covering cbm_store_fetch_call_edges /
cbm_store_free_call_edges, which had no prior test coverage.

Fixes #1762

Signed-off-by: Jumaga2015 <jumaga2015@gmail.com>
This commit is contained in:
Jumaga2015 committed 2026-09-01 21:37:17 +02:00
1 parent 17786374df
commit 5e328814fc
7 files changed
+100 -13

No files matched your search

+2 -4
View File
@@ -5545,12 +5545,10 @@ static int arch_compute_cycles(cbm_store_t *store, const char *project, int64_t
*scanned_edges = ecount;
scc_graph_t g;
if (!scc_build(src, tgt, ecount, &g)) {
free(src);
free(tgt);
cbm_store_free_call_edges(src, tgt);
return CBM_STORE_OK; /* no edges = no cycles, not an error */
}
free(src);
free(tgt);
cbm_store_free_call_edges(src, tgt);
int *comp = malloc((size_t)g.nverts * sizeof(int));
if (!comp) {
+9
View File
@@ -3142,6 +3142,11 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
return CBM_STORE_OK;
}
void cbm_store_free_call_edges(int64_t *src, int64_t *tgt) {
free(src);
free(tgt);
}
int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out,
int *count) {
bind_id_t b = {source_id};
@@ -8234,6 +8239,10 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *
return cbm_leiden(nodes, node_count, edges, edge_count, 1.0, out, out_count);
}
void cbm_leiden_free(cbm_louvain_result_t *result) {
free(result);
}
/* ── Architecture: community clusters via Leiden ───────────────── */
enum {
+6
View File
@@ -574,6 +574,9 @@ int cbm_store_insert_edge_batch(cbm_store_t *s, const cbm_edge_t *edges, int cou
int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edges,
int64_t **out_src, int64_t **out_tgt, int *count, bool *truncated);
/* Free the (source_id, target_id) arrays returned by cbm_store_fetch_call_edges. */
void cbm_store_free_call_edges(int64_t *src, int64_t *tgt);
/* Find edges by source node. */
int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out, int *count);
@@ -996,6 +999,9 @@ int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *e
int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges,
int edge_count, cbm_louvain_result_t **out, int *out_count);
/* Free the result array returned by cbm_leiden / cbm_louvain. */
void cbm_leiden_free(cbm_louvain_result_t *result);
/* ── Memory management helpers ──────────────────────────────────── */
/* Free heap-allocated strings in a stack-allocated node (does NOT free the node itself). */
+4
View File
@@ -867,3 +867,7 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) {
}
return json;
}
void cbm_layout_free_json(char *json) {
free(json);
}
+3
View File
@@ -71,4 +71,7 @@ void cbm_layout_free(cbm_layout_result_t *result);
/* Serialize layout result to JSON string. Caller must free(). */
char *cbm_layout_to_json(const cbm_layout_result_t *result);
/* Free the JSON string returned by cbm_layout_to_json. */
void cbm_layout_free_json(char *json);
#endif /* CBM_UI_LAYOUT3D_H */
+74 -7
View File
@@ -1189,6 +1189,68 @@ TEST(adr_validate_keys_empty) {
PASS();
}
/* ── cbm_store_fetch_call_edges / cbm_store_free_call_edges tests ─ */
TEST(fetch_call_edges_basic) {
cbm_store_t *s = setup_arch_test_store();
ASSERT_NOT_NULL(s);
int64_t *src = NULL;
int64_t *tgt = NULL;
int count = 0;
bool truncated = true;
ASSERT_EQ(cbm_store_fetch_call_edges(s, "test", 100, &src, &tgt, &count, &truncated),
CBM_STORE_OK);
/* setup_arch_test_store() wires 5 CALLS edges among Function nodes. */
ASSERT_EQ(count, 5);
ASSERT_FALSE(truncated);
ASSERT_NOT_NULL(src);
ASSERT_NOT_NULL(tgt);
cbm_store_free_call_edges(src, tgt);
cbm_store_close(s);
PASS();
}
TEST(fetch_call_edges_truncated) {
cbm_store_t *s = setup_arch_test_store();
ASSERT_NOT_NULL(s);
int64_t *src = NULL;
int64_t *tgt = NULL;
int count = 0;
bool truncated = false;
/* Only 5 CALLS edges exist; capping at 2 must set truncated. */
ASSERT_EQ(cbm_store_fetch_call_edges(s, "test", 2, &src, &tgt, &count, &truncated),
CBM_STORE_OK);
ASSERT_EQ(count, 2);
ASSERT_TRUE(truncated);
cbm_store_free_call_edges(src, tgt);
cbm_store_close(s);
PASS();
}
TEST(fetch_call_edges_empty_project) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
cbm_store_upsert_project(s, "empty", "/tmp/empty");
int64_t *src = NULL;
int64_t *tgt = NULL;
int count = 0;
bool truncated = true;
ASSERT_EQ(cbm_store_fetch_call_edges(s, "empty", 100, &src, &tgt, &count, &truncated),
CBM_STORE_OK);
ASSERT_EQ(count, 0);
ASSERT_FALSE(truncated);
/* cbm_store_free_call_edges(NULL, NULL) must not crash. */
cbm_store_free_call_edges(src, tgt);
cbm_store_close(s);
PASS();
}
/* ── Louvain tests ──────────────────────────────────────────────── */
TEST(louvain_basic) {
@@ -1214,7 +1276,7 @@ TEST(louvain_basic) {
/* Triangle and pair different */
ASSERT_TRUE(comm[1] != comm[4]);
free(result);
cbm_leiden_free(result);
PASS();
}
@@ -1223,7 +1285,7 @@ TEST(louvain_empty) {
int count = 0;
ASSERT_EQ(cbm_louvain(NULL, 0, NULL, 0, &result, &count), CBM_STORE_OK);
ASSERT_EQ(count, 0);
free(result);
cbm_leiden_free(result);
PASS();
}
@@ -1234,7 +1296,7 @@ TEST(louvain_single_node) {
ASSERT_EQ(cbm_louvain(nodes, 1, NULL, 0, &result, &count), CBM_STORE_OK);
ASSERT_EQ(count, 1);
ASSERT_EQ(result[0].node_id, 42);
free(result);
cbm_leiden_free(result);
PASS();
}
@@ -1297,7 +1359,7 @@ TEST(louvain_converges) {
}
ASSERT_TRUE(same_count >= 8);
free(result);
cbm_leiden_free(result);
PASS();
}
@@ -1431,7 +1493,7 @@ TEST(leiden_multilevel_collapses_noise) {
}
ASSERT_TRUE(same >= SZ - 1);
}
free(result);
cbm_leiden_free(result);
PASS();
}
@@ -1463,8 +1525,8 @@ TEST(leiden_resolution_controls_granularity) {
ASSERT_TRUE(n_hi > n_lo);
ASSERT_TRUE(leiden_all_communities_connected(lo, N, edges, ne));
ASSERT_TRUE(leiden_all_communities_connected(hi, N, edges, ne));
free(lo);
free(hi);
cbm_leiden_free(lo);
cbm_leiden_free(hi);
PASS();
}
@@ -1727,6 +1789,11 @@ SUITE(store_arch) {
RUN_TEST(adr_splice_refuses_unterminated_fence);
RUN_TEST(adr_validate_keys_empty);
/* Call-edge fetch / free */
RUN_TEST(fetch_call_edges_basic);
RUN_TEST(fetch_call_edges_truncated);
RUN_TEST(fetch_call_edges_empty_project);
/* Louvain */
RUN_TEST(louvain_basic);
RUN_TEST(louvain_empty);
+2 -2
View File
@@ -548,7 +548,7 @@ TEST(layout_to_json) {
ASSERT(strstr(json, "\"hello\"") != NULL);
ASSERT(strstr(json, "\"Function\"") != NULL);
free(json);
cbm_layout_free_json(json);
cbm_layout_free(r);
cbm_store_close(store);
PASS();
@@ -719,7 +719,7 @@ TEST(layout_dead_code_classification) {
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"status\":\"dead\"") != NULL);
ASSERT(strstr(json, "\"in_calls\":2") != NULL);
free(json);
cbm_layout_free_json(json);
cbm_layout_free(r);
cbm_store_close(store);