Skip to content

Commit 43db2a4

Browse files
build(zig): migrate FFI to Zig 0.15 + real linked integration tests (#67)
- build.zig: 0.11 addStaticLibrary/addSharedLibrary API -> 0.15 addLibrary/root_module; same artifacts (libochrance.a + libochrance.so, soname unchanged for the Idris %foreign contract) - main.zig: callconv(.C) -> callconv(.c); Ed25519.KeyPair.create -> generateDeterministic (0.15 renames) - test/integration_test.zig: replace never-compiling {{project}} template debris with a real C-ABI integration suite (17 tests: lifecycle, error paths, strings, BLAKE3 KAT, thread smoke) linked against libochrance.a and wired into 'zig build test'; drop the unguarded double-free assert - test-ed25519*.zig: fix for 0.15 (fromBytes infallible; comptime Type printing) - CI zig-ffi.yml + session-start hook + docs: Zig pin 0.11.0 -> 0.15.2 Verified locally: zig build test 24/24; dlopen link test PASS; tests/ffi/run_ffi_test.sh 6/6 (production Merkle root = real BLAKE3 across the Idris->Zig boundary, != XOR spec root); tests/e2e_test.sh 27/27. Progresses #39. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e4baf13 commit 43db2a4

9 files changed

Lines changed: 158 additions & 130 deletions

File tree

.claude/hooks/session-start.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# The web container is ephemeral and cloned fresh, so the toolchains this repo
88
# needs are reinstalled at session start:
99
# - Idris2 v0.8.0 (core framework, proofs, parser, verification)
10-
# - Zig 0.11.0 (cryptographic FFI in ffi/zig)
10+
# - Zig 0.15.2 (cryptographic FFI in ffi/zig)
1111
# - just (build/recipe runner; `just build`, `just check`, ...)
1212
# - Agda 2.6.3 (estate formal-verification toolchain)
1313
#
@@ -41,10 +41,10 @@ if ! { have just && have agda && have idris2; }; then
4141
|| log "WARN: some apt packages failed to install"
4242
fi
4343

44-
# --- Zig 0.11.0 via the PyPI ziglang package (ziglang.org is blocked) -------
44+
# --- Zig 0.15.2 via the PyPI ziglang package (ziglang.org is blocked) -------
4545
if ! have zig; then
46-
log "installing Zig 0.11.0 via PyPI ziglang ..."
47-
if pip3 install --break-system-packages --quiet "ziglang==0.11.0"; then
46+
log "installing Zig 0.15.2 via PyPI ziglang ..."
47+
if pip3 install --break-system-packages --quiet "ziglang==0.15.2"; then
4848
cat > /usr/local/bin/zig <<'SH'
4949
#!/bin/sh
5050
# Thin shim exposing the PyPI `ziglang` package as a `zig` command.

.github/workflows/zig-ffi.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ jobs:
2626
runs-on: ubuntu-latest
2727
steps:
2828
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
29-
- name: Set up Zig 0.11.0
29+
- name: Set up Zig 0.15.2
3030
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
3131
with:
32-
version: 0.11.0
32+
version: 0.15.2
3333
- name: Build (libochrance.so + libochrance.a)
3434
working-directory: ffi/zig
3535
run: zig build

ROADMAP.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Target: Production-ready filesystem verification for OSTree deployments
151151
=== Required
152152

153153
* **Idris2** 0.8.0+ (dependent types, totality checking)
154-
* **Zig** 0.11+ (FFI implementation)
154+
* **Zig** 0.15+ (FFI implementation)
155155
* **BLAKE3** (cryptographic hashing)
156156

157157
=== Optional (Phase 2+)

ffi/zig/build.zig

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// SPDX-License-Identifier: MPL-2.0
22
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
33
//
4-
// Minimum Zig version: 0.11.0
4+
// Minimum Zig version: 0.15.0 (Build.addLibrary / root_module API; for the
5+
// old 0.11 addStaticLibrary/addSharedLibrary form see git history)
56
// Required for: std.crypto.hash.Blake3, C ABI export, @memcpy builtin
67
//
78
const std = @import("std");
@@ -10,44 +11,58 @@ pub fn build(b: *std.Build) void {
1011
const target = b.standardTargetOptions(.{});
1112
const optimize = b.standardOptimizeOption(.{});
1213

13-
// Build libochrance.so shared library
14-
const lib = b.addStaticLibrary(.{
15-
.name = "ochrance",
16-
.root_source_file = .{ .cwd_relative = "src/main.zig" },
14+
// One module definition shared by every artifact. libc is required by
15+
// std.heap.c_allocator (used by the handle lifecycle) and is the natural
16+
// choice for a C-ABI FFI library.
17+
const mod = b.createModule(.{
18+
.root_source_file = b.path("src/main.zig"),
1719
.target = target,
1820
.optimize = optimize,
21+
.link_libc = true,
1922
});
2023

21-
// libc is required by std.heap.c_allocator (used by the handle lifecycle)
22-
// and is the natural choice for a C-ABI FFI library.
23-
lib.linkLibC();
24-
b.installArtifact(lib);
24+
// Static libochrance.a
25+
const static_lib = b.addLibrary(.{
26+
.name = "ochrance",
27+
.linkage = .static,
28+
.root_module = mod,
29+
});
30+
b.installArtifact(static_lib);
2531

26-
// Also create a shared library version (libochrance.so) — this is what the
27-
// Idris2 FFI loads at runtime. The name MUST be "ochrance" so the artifact is
28-
// libochrance.so: the Idris bindings declare `%foreign "C:blake3_hash,
29-
// libochrance"` (ochrance-core/Ochrance/FFI/Crypto.idr), so the runtime loader
30-
// resolves the soname libochrance.so. Any other shared-library name would never
31-
// be found. A static libochrance.a and a shared libochrance.so share the base
32+
// Shared libochrance.so — this is what the Idris2 FFI loads at runtime.
33+
// The name MUST be "ochrance" so the artifact is libochrance.so: the Idris
34+
// bindings declare `%foreign "C:blake3_hash, libochrance"`
35+
// (ochrance-core/Ochrance/FFI/Crypto.idr), so the runtime loader resolves
36+
// the soname libochrance.so. Any other shared-library name would never be
37+
// found. A static libochrance.a and a shared libochrance.so share the base
3238
// name without clashing (distinct extensions).
33-
const shared_lib = b.addSharedLibrary(.{
39+
const shared_lib = b.addLibrary(.{
3440
.name = "ochrance",
35-
.root_source_file = .{ .cwd_relative = "src/main.zig" },
36-
.target = target,
37-
.optimize = optimize,
41+
.linkage = .dynamic,
42+
.root_module = mod,
3843
});
39-
shared_lib.linkLibC();
4044
b.installArtifact(shared_lib);
4145

42-
// Tests
46+
// Unit tests (in-module test blocks: KAT vectors, Ed25519 round-trip)
4347
const tests = b.addTest(.{
44-
.root_source_file = .{ .cwd_relative = "src/main.zig" },
45-
.target = target,
46-
.optimize = optimize,
48+
.root_module = mod,
49+
});
50+
51+
// Integration tests: extern declarations linked against libochrance.a,
52+
// exercising the C ABI exactly as an external consumer (Idris2) does.
53+
const integration_tests = b.addTest(.{
54+
.root_module = b.createModule(.{
55+
.root_source_file = b.path("test/integration_test.zig"),
56+
.target = target,
57+
.optimize = optimize,
58+
.link_libc = true,
59+
}),
4760
});
48-
tests.linkLibC();
61+
integration_tests.linkLibrary(static_lib);
4962

5063
const run_tests = b.addRunArtifact(tests);
51-
const test_step = b.step("test", "Run unit tests");
64+
const run_integration_tests = b.addRunArtifact(integration_tests);
65+
const test_step = b.step("test", "Run unit + integration tests");
5266
test_step.dependOn(&run_tests.step);
67+
test_step.dependOn(&run_integration_tests.step);
5368
}

ffi/zig/src/main.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export fn ochrance_build_info() [*:0]const u8 {
210210
//==============================================================================
211211

212212
/// Callback function type (C ABI)
213-
pub const Callback = *const fn (u64, u32) callconv(.C) u32;
213+
pub const Callback = *const fn (u64, u32) callconv(.c) u32;
214214

215215
/// Register a callback
216216
export fn ochrance_register_callback(
@@ -260,7 +260,7 @@ export fn ochrance_is_initialized(handle: ?*Handle) u32 {
260260
// int ed25519_verify(const uint8_t sig[64], const uint8_t pk[32],
261261
// const uint8_t* msg, size_t msg_len); // 1=valid, 0=invalid
262262
//
263-
// Backed by Zig's audited std.crypto (Zig 0.11.0). These replace the XOR/zero
263+
// Backed by Zig's audited std.crypto (Zig 0.15+). These replace the XOR/zero
264264
// placeholder hashes; see ROADMAP Phase 1 and docs/VALENCE_SHELL_BRIDGE.adoc,
265265
// which gate cryptographic-integrity claims on closing exactly this gap.
266266
//==============================================================================
@@ -368,7 +368,7 @@ test "sha3_256_hash known-answer vectors" {
368368

369369
test "ed25519_verify accepts valid and rejects tampered" {
370370
const seed = [_]u8{42} ** 32;
371-
const kp = try Ed25519.KeyPair.create(seed);
371+
const kp = try Ed25519.KeyPair.generateDeterministic(seed);
372372
const message: []const u8 = "ochrance attestation";
373373
const signature = try kp.sign(message, null);
374374
const sig_bytes = signature.toBytes();

ffi/zig/test-ed25519-api.zig

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ const std = @import("std");
44
const Ed25519 = std.crypto.sign.Ed25519;
55

66
pub fn main() !void {
7-
// Check what fields Signature has
8-
const T = @typeInfo(Ed25519.Signature);
9-
std.debug.print("Signature type: {}\n", .{T});
7+
// Check what fields Signature has (std.builtin.Type is comptime-only, so
8+
// enumerate field names rather than printing the Type value itself)
9+
inline for (@typeInfo(Ed25519.Signature).@"struct".fields) |field| {
10+
std.debug.print("Signature field: {s}: {s}\n", .{ field.name, @typeName(field.type) });
11+
}
1012
}

ffi/zig/test-ed25519.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const Ed25519 = std.crypto.sign.Ed25519;
66
pub fn main() !void {
77
// Create a keypair
88
const seed: [32]u8 = [_]u8{1} ** 32;
9-
const keypair = try Ed25519.KeyPair.create(seed);
9+
const keypair = try Ed25519.KeyPair.generateDeterministic(seed);
1010

1111
// Sign something
1212
const message = "test";
@@ -19,8 +19,8 @@ pub fn main() !void {
1919
const sig_bytes = sig.toBytes();
2020
std.debug.print("Signature bytes length: {}\n", .{sig_bytes.len});
2121

22-
// Try fromBytes
23-
const sig2 = try Ed25519.Signature.fromBytes(sig_bytes);
22+
// Try fromBytes (infallible since Zig 0.12: returns Signature directly)
23+
const sig2 = Ed25519.Signature.fromBytes(sig_bytes);
2424
_ = sig2;
2525

2626
std.debug.print("Success!\n", .{});

0 commit comments

Comments
 (0)