Skip to content

Commit f7d483d

Browse files
P1: fix Zig FFI to build and match the Idris2 ABI (#33)
* Auto-install pinned Zig toolchain in setup + devcontainer The Zig FFI bridge is half of the ABI-FFI standard, but nothing installed Zig: .tool-versions only lists it (commented), setup.sh stops at `just`, and the devcontainer's `postCreateCommand: just deps` referenced a `deps` recipe that did not exist. Unlike the other toolchain pieces, Zig is not distributed via GitHub releases, so it must come from ziglang.org. Add scripts/install-zig.sh: an idempotent, fail-soft installer for the pinned Zig 0.14.0 (arch/OS-aware, uses the system CA store the agent proxy populates, never --insecure). If ziglang.org is not on the session's egress allowlist the download 403s and the script exits 0 with an actionable message, so it never blocks setup or a session. Wire it in via the two paths the project already uses: a "Step 1b" in setup.sh (where the template exposes that step), and a new `deps` Justfile recipe backing the devcontainer postCreateCommand. Once ziglang.org is allowlisted, future setups and dev containers install Zig automatically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH * P1: fix Zig FFI to build and match the Idris2 ABI `zig test src/main.zig -lc` crashed with "free(): double free detected" on the second test (create supervisor and worker). Root cause: dual ownership of tree nodes. The handle's flat `nodes` ArrayList owns every node created by otpiser_create_supervisor / otpiser_create_worker, while otpiser_add_child also links a child into its parent supervisor's `children` list. TreeNode.deinit then recursively freed each child (child.deinit() + allocator.destroy(child)), so any node that was added as a child was freed twice on otpiser_free: once via the parent's recursive deinit and once via the handle's flat `nodes` sweep. Fix: make `children` a non-owning reference list. TreeNode.deinit now releases only its own `children` container and no longer frees the child nodes. The handle's `nodes` list remains the single owner and frees every node exactly once. This matches the alloyiser reference idiom (single, flat ownership; handles are plain structs behind ?*T). No exported C symbol names or Result-enum integer values were changed. All 17 C:otpiser_* symbols in Foreign.idr keep their matching export fn, and the Result enum still mirrors resultToInt (Ok=0 .. MalformedTree=6). Verification: - src/interface/ffi: `zig test src/main.zig -lc` -> all 7 tests pass, 0 errors/warnings - src/interface/abi: `idris2 --build otpiser-abi.ipkg` -> exit 0 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5ee4413 commit f7d483d

1 file changed

Lines changed: 6 additions & 5 deletions

File tree

src/interface/ffi/src/main.zig

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,16 @@ const TreeNode = struct {
7373
module_name: []const u8,
7474
restart_type: ChildRestartType,
7575
shutdown_ms: u32,
76-
// Tree structure
76+
// Tree structure.
77+
// `children` holds non-owning references to other nodes; every node is
78+
// owned exactly once by the handle's flat `nodes` list (see OtpiserHandle).
7779
children: std.ArrayList(*TreeNode),
7880
allocator: std.mem.Allocator,
7981

82+
/// Release only this node's own resources. Children are NOT freed here:
83+
/// they are owned by the handle's `nodes` list and freed there exactly
84+
/// once, avoiding the double-free that arises from dual ownership.
8085
fn deinit(self: *TreeNode) void {
81-
for (self.children.items) |child| {
82-
child.deinit();
83-
self.allocator.destroy(child);
84-
}
8586
self.children.deinit();
8687
}
8788
};

0 commit comments

Comments
 (0)