Skip to content

Commit afbab20

Browse files
hyperpolymathclaude
andcommitted
fix(zig): cartridge adapter builds clean on Zig 0.15.2
The adapter at cartridges/echidna-llm-mcp/adapter/ was unbuildable on Zig 0.15.2 — three independent issues, all addressed: 1. **adapter/build.zig** — Zig 0.15 dropped `.root_source_file` from `Build.ExecutableOptions`. Switched to the module-first pattern: create `adapter_mod` via `b.createModule(...)`, then pass it to `b.addExecutable(.{ .root_module = adapter_mod })`. Mirrors the sibling ffi/build.zig already on the new API. 2. **adapter/build.zig** — Added `.link_libc = true` to both modules (ffi_mod + adapter_mod). The FFI uses C allocator + libc memory functions; without explicit libc linkage Zig 0.15 errors out at `std.heap.c_allocator` and `std.c.malloc` references. 3. **ffi/echidna_llm_ffi.zig** — All 16 `export fn` declarations (echidna_llm_init/.../boj_cartridge_*) needed `pub` visibility too. Pure C-ABI export is fine for FFI consumers via the symbol table, but the adapter imports the FFI as a Zig module (`@import("echidna_llm_ffi")`) and Zig 0.15 requires `pub` for cross-module access. 4. **adapter/echidna_llm_adapter.zig:121** — `ffi.echidna_llm_init()` was called with zero arguments but the FFI signature takes `endpoint: [*:0]const u8`. The adapter never compiled even before the Zig version migration. Read endpoint from `BOJ_ENDPOINT` env var with a `http://127.0.0.1:7700` fallback (BoJ's default port). Verification: - `cd adapter && zig build` → exit 0, produces zig-out/bin/echidna_llm_adapter - `cd ffi && zig build test` → exit 0 Note: the cartridge's primary surface is the Deno `mod.js` runtime; this Zig adapter is the optional native path. Restoring its build is what closes the audit-flagged "build.zig API drift" item. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ac44ece commit afbab20

3 files changed

Lines changed: 42 additions & 20 deletions

File tree

cartridges/echidna-llm-mcp/adapter/build.zig

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,29 @@ pub fn build(b: *std.Build) void {
99
const target = b.standardTargetOptions(.{});
1010
const optimize = b.standardOptimizeOption(.{});
1111

12+
// The FFI module uses C allocator + libc memory functions, so its
13+
// module needs `link_libc = true`; that propagates to consumers.
1214
const ffi_mod = b.createModule(.{
1315
.root_source_file = b.path("../ffi/echidna_llm_ffi.zig"),
1416
.target = target,
1517
.optimize = optimize,
18+
.link_libc = true,
1619
});
1720

18-
const adapter = b.addExecutable(.{
19-
.name = "echidna_llm_adapter",
21+
// Zig 0.15 dropped `.root_source_file` from ExecutableOptions; the
22+
// canonical pattern is now to build the executable's root module first
23+
// and pass it via `.root_module`. Mirrors the sibling ffi/build.zig.
24+
const adapter_mod = b.createModule(.{
2025
.root_source_file = b.path("echidna_llm_adapter.zig"),
2126
.target = target,
2227
.optimize = optimize,
28+
.link_libc = true,
29+
});
30+
adapter_mod.addImport("echidna_llm_ffi", ffi_mod);
31+
32+
const adapter = b.addExecutable(.{
33+
.name = "echidna_llm_adapter",
34+
.root_module = adapter_mod,
2335
});
24-
adapter.root_module.addImport("echidna_llm_ffi", ffi_mod);
2536
b.installArtifact(adapter);
2637
}

cartridges/echidna-llm-mcp/adapter/echidna_llm_adapter.zig

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,18 @@ fn listenLoop(port: u16) !void {
118118
}
119119

120120
pub fn main() !void {
121-
ffi.echidna_llm_init();
121+
// FFI signature is `echidna_llm_init(endpoint: [*:0]const u8)`. Read the
122+
// endpoint from BOJ_ENDPOINT env var, falling back to BoJ's default port.
123+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
124+
defer arena.deinit();
125+
const allocator = arena.allocator();
126+
const endpoint_z = std.process.getEnvVarOwned(allocator, "BOJ_ENDPOINT") catch
127+
try allocator.dupeZ(u8, "http://127.0.0.1:7700");
128+
const endpoint_cstr: [*:0]const u8 = if (@TypeOf(endpoint_z) == [:0]u8)
129+
endpoint_z.ptr
130+
else
131+
try allocator.dupeZ(u8, endpoint_z);
132+
_ = ffi.echidna_llm_init(endpoint_cstr);
122133
const t1 = try std.Thread.spawn(.{}, listenLoop, .{REST_PORT});
123134
const t2 = try std.Thread.spawn(.{}, listenLoop, .{GRPC_PORT});
124135
const t3 = try std.Thread.spawn(.{}, listenLoop, .{GQL_PORT});

cartridges/echidna-llm-mcp/ffi/echidna_llm_ffi.zig

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ var boj_endpoint_len: usize = 0;
8989
// ═══════════════════════════════════════════════════════════════════════
9090

9191
/// Initialise the cartridge with BoJ endpoint URL
92-
export fn echidna_llm_init(endpoint: [*:0]const u8) c_int {
92+
pub export fn echidna_llm_init(endpoint: [*:0]const u8) c_int {
9393
state_mutex.lock();
9494
defer state_mutex.unlock();
9595

@@ -107,7 +107,7 @@ export fn echidna_llm_init(endpoint: [*:0]const u8) c_int {
107107

108108
/// Create an ephemeral session token
109109
/// Returns 0 on success, -1 on invalid transition
110-
export fn echidna_llm_authenticate(
110+
pub export fn echidna_llm_authenticate(
111111
token_ptr: [*]const u8,
112112
token_len: c_int,
113113
max_calls: c_int,
@@ -137,7 +137,7 @@ export fn echidna_llm_authenticate(
137137
}
138138

139139
/// Transition to operating state
140-
export fn echidna_llm_start_operating() c_int {
140+
pub export fn echidna_llm_start_operating() c_int {
141141
state_mutex.lock();
142142
defer state_mutex.unlock();
143143

@@ -147,7 +147,7 @@ export fn echidna_llm_start_operating() c_int {
147147
}
148148

149149
/// Close the session (from authenticated or operating)
150-
export fn echidna_llm_close() c_int {
150+
pub export fn echidna_llm_close() c_int {
151151
state_mutex.lock();
152152
defer state_mutex.unlock();
153153

@@ -160,14 +160,14 @@ export fn echidna_llm_close() c_int {
160160
}
161161

162162
/// Get current session state
163-
export fn echidna_llm_get_state() c_int {
163+
pub export fn echidna_llm_get_state() c_int {
164164
state_mutex.lock();
165165
defer state_mutex.unlock();
166166
return @intFromEnum(current_session.state);
167167
}
168168

169169
/// Check if the session is still valid (not expired, not over call limit)
170-
export fn echidna_llm_session_valid() c_int {
170+
pub export fn echidna_llm_session_valid() c_int {
171171
state_mutex.lock();
172172
defer state_mutex.unlock();
173173

@@ -199,7 +199,7 @@ export fn echidna_llm_session_valid() c_int {
199199
/// model: Model tier (0=haiku, 1=sonnet, 2=opus)
200200
///
201201
/// Returns: null-terminated JSON string, or NULL on error
202-
export fn echidna_llm_suggest_tactics(
202+
pub export fn echidna_llm_suggest_tactics(
203203
goal_ptr: [*]const u8,
204204
goal_len: c_int,
205205
hypotheses_ptr: [*]const u8,
@@ -259,7 +259,7 @@ export fn echidna_llm_suggest_tactics(
259259
}
260260

261261
/// Rank provers for a goal. Returns heap-allocated JSON. Caller MUST free.
262-
export fn echidna_llm_rank_provers(
262+
pub export fn echidna_llm_rank_provers(
263263
goal_ptr: [*]const u8,
264264
goal_len: c_int,
265265
model: c_int,
@@ -286,7 +286,7 @@ export fn echidna_llm_rank_provers(
286286
// ═══════════════════════════════════════════════════════════════════════
287287

288288
/// Free a string returned by any echidna_llm_* function
289-
export fn echidna_llm_free(ptr: ?[*:0]u8) void {
289+
pub export fn echidna_llm_free(ptr: ?[*:0]u8) void {
290290
const p = ptr orelse return;
291291
const allocator = std.heap.c_allocator;
292292
const slice = std.mem.span(p);
@@ -299,7 +299,7 @@ export fn echidna_llm_free(ptr: ?[*:0]u8) void {
299299

300300
/// Check if a state transition is valid (C ABI)
301301
/// Mirrors llm_can_transition from Protocol.idr
302-
export fn echidna_llm_can_transition(from: c_int, to: c_int) c_int {
302+
pub export fn echidna_llm_can_transition(from: c_int, to: c_int) c_int {
303303
return switch (from) {
304304
0 => if (to == 1) @as(c_int, 1) else 0, // Unauth → Auth
305305
1 => if (to == 2 or to == 3) @as(c_int, 1) else 0, // Auth → Op or Closed
@@ -309,7 +309,7 @@ export fn echidna_llm_can_transition(from: c_int, to: c_int) c_int {
309309
}
310310

311311
/// Check if an operation is advisory (always 1, proven in Idris2)
312-
export fn echidna_llm_is_advisory(op: c_int) c_int {
312+
pub export fn echidna_llm_is_advisory(op: c_int) c_int {
313313
_ = op;
314314
return 1; // All operations advisory, by construction
315315
}
@@ -323,22 +323,22 @@ const shim = @import("cartridge_shim.zig");
323323
const CARTRIDGE_NAME_PTR: [*:0]const u8 = "echidna-llm-mcp";
324324
const CARTRIDGE_VERSION_PTR: [*:0]const u8 = "0.1.0";
325325

326-
export fn boj_cartridge_init() callconv(.c) c_int {
326+
pub export fn boj_cartridge_init() callconv(.c) c_int {
327327
return 0;
328328
}
329329

330-
export fn boj_cartridge_deinit() callconv(.c) void {}
330+
pub export fn boj_cartridge_deinit() callconv(.c) void {}
331331

332-
export fn boj_cartridge_name() callconv(.c) [*:0]const u8 {
332+
pub export fn boj_cartridge_name() callconv(.c) [*:0]const u8 {
333333
return CARTRIDGE_NAME_PTR;
334334
}
335335

336-
export fn boj_cartridge_version() callconv(.c) [*:0]const u8 {
336+
pub export fn boj_cartridge_version() callconv(.c) [*:0]const u8 {
337337
return CARTRIDGE_VERSION_PTR;
338338
}
339339

340340
/// Dispatch the 4 cartridge.json MCP tools. Grade D Alpha stubs.
341-
export fn boj_cartridge_invoke(
341+
pub export fn boj_cartridge_invoke(
342342
tool_name: [*c]const u8,
343343
json_args: [*c]const u8,
344344
out_buf: [*c]u8,

0 commit comments

Comments
 (0)