Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/zig-ffi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# SPDX-License-Identifier: PMPL-1.0-or-later
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# zig-ffi.yml — builds the verisimdb-data Zig FFI, runs its unit tests,
# and runs the OctadDimension + ProvenanceEntry round-trip consumer that
# links the library (#6, V-L3-N1). Zig is pinned by tarball sha256 (no
# third-party action) so the lane is Scorecard pinned-dependencies clean.
name: Zig FFI

on:
push:
branches: [main, master]
paths:
- 'ffi/zig/**'
- '.github/workflows/zig-ffi.yml'
pull_request:
branches: ['**']
paths:
- 'ffi/zig/**'
- '.github/workflows/zig-ffi.yml'

permissions:
contents: read

jobs:
ffi-roundtrip:
name: Build + round-trip consumer
runs-on: ubuntu-latest
timeout-minutes: 15

env:
ZIG_VERSION: 0.15.2
ZIG_TARBALL: https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz
ZIG_SHA256: 02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239

steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Install Zig ${{ env.ZIG_VERSION }} (sha256-pinned)
run: |
set -euo pipefail
curl -fsSL "$ZIG_TARBALL" -o /tmp/zig.tar.xz
echo "${ZIG_SHA256} /tmp/zig.tar.xz" | sha256sum -c -
mkdir -p "$HOME/zig"
tar -xJf /tmp/zig.tar.xz -C "$HOME/zig" --strip-components=1
echo "$HOME/zig" >> "$GITHUB_PATH"

- name: Show Zig version
run: zig version

- name: Build, unit-test, and run the FFI round-trip consumer
working-directory: ffi/zig
run: zig build check
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,7 @@ deps/
.cache/
build/
dist/

# Zig build cache/artifacts
ffi/zig/.zig-cache/
ffi/zig/zig-out/
120 changes: 46 additions & 74 deletions ffi/zig/build.zig
Original file line number Diff line number Diff line change
@@ -1,94 +1,66 @@
// {{PROJECT}} FFI Build Configuration
// verisimdb-data FFI build configuration
// SPDX-License-Identifier: PMPL-1.0-or-later
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
//
// Zig 0.15.x. De-templated from the {{PROJECT}} skeleton (#6, V-L3-N1):
// the old build referenced a nonexistent header/bench/docs and the
// pre-0.14 addSharedLibrary API. This builds the FFI as a static
// library, runs the in-source unit tests, and builds + runs a consumer
// that links the library and exercises the OctadDimension +
// ProvenanceEntry round-trip.

const std = @import("std");

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

// Shared library (.so, .dylib, .dll)
const lib = b.addSharedLibrary(.{
.name = "{{project}}",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
const lib = b.addLibrary(.{
.name = "verisimdb_data",
.linkage = .static,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});

// Set version
lib.version = .{ .major = 0, .minor = 1, .patch = 0 };

// Static library (.a)
const lib_static = b.addStaticLibrary(.{
.name = "{{project}}",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});

// Install artifacts
b.installArtifact(lib);
b.installArtifact(lib_static);

// Generate header file for C compatibility
const header = b.addInstallHeader(
b.path("include/{{project}}.h"),
"{{project}}.h",
);
b.getInstallStep().dependOn(&header.step);

// Unit tests
// In-source unit tests (src/main.zig `test { ... }` blocks).
const lib_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});

const run_lib_tests = b.addRunArtifact(lib_tests);

const test_step = b.step("test", "Run library tests");
const test_step = b.step("test", "Run FFI unit tests");
test_step.dependOn(&run_lib_tests.step);

// Integration tests
const integration_tests = b.addTest(.{
.root_source_file = b.path("test/integration_test.zig"),
.target = target,
.optimize = optimize,
});

integration_tests.linkLibrary(lib);

const run_integration_tests = b.addRunArtifact(integration_tests);

const integration_test_step = b.step("test-integration", "Run integration tests");
integration_test_step.dependOn(&run_integration_tests.step);

// Documentation
const docs = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = .Debug,
// Consumer demo: a standalone executable that LINKS the FFI library
// (proving the C-ABI contract) and round-trips OctadDimension +
// ProvenanceEntry through the exported encode/decode functions.
const consumer = b.addExecutable(.{
.name = "octad_consumer",
.root_module = b.createModule(.{
.root_source_file = b.path("test/octad_consumer.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
consumer.linkLibrary(lib);
b.installArtifact(consumer);

const docs_step = b.step("docs", "Generate documentation");
docs_step.dependOn(&b.addInstallDirectory(.{
.source_dir = docs.getEmittedDocs(),
.install_dir = .prefix,
.install_subdir = "docs",
}).step);

// Benchmark (if needed)
const bench = b.addExecutable(.{
.name = "{{project}}-bench",
.root_source_file = b.path("bench/bench.zig"),
.target = target,
.optimize = .ReleaseFast,
});

bench.linkLibrary(lib);

const run_bench = b.addRunArtifact(bench);
const run_consumer = b.addRunArtifact(consumer);
const consumer_step = b.step("consumer", "Build + run the FFI consumer round-trip demo");
consumer_step.dependOn(&run_consumer.step);

const bench_step = b.step("bench", "Run benchmarks");
bench_step.dependOn(&run_bench.step);
// `zig build check` = unit tests + consumer (the CI entry point).
const check_step = b.step("check", "Unit tests + consumer round-trip (CI gate)");
check_step.dependOn(&run_lib_tests.step);
check_step.dependOn(&run_consumer.step);
}
160 changes: 155 additions & 5 deletions ffi/zig/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ pub const Result = enum(c_int) {
null_pointer = 4,
};

/// Library handle (opaque to prevent direct access)
pub const Handle = opaque {
// Internal state hidden from C
/// Library handle. A plain struct on the Zig side; C consumers only ever
/// hold it behind `?*Handle` and never dereference it, so it stays opaque
/// across the ABI. (The template declared this `opaque` *with fields*,
/// which is a compile error — fixed as part of de-stubbing for #6.)
pub const Handle = struct {
allocator: std.mem.Allocator,
initialized: bool,
// Add your fields here
};

//==============================================================================
Expand Down Expand Up @@ -209,7 +210,7 @@ export fn verisimdb_data_build_info() [*:0]const u8 {
//==============================================================================

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

/// Register a callback
export fn verisimdb_data_register_callback(
Expand Down Expand Up @@ -248,6 +249,100 @@ export fn verisimdb_data_is_initialized(handle: ?*Handle) u32 {
return if (h.initialized) 1 else 0;
}

//==============================================================================
// Octad ABI (#6, V-L3-N1)
//
// The verisimdb octad has eight dimensions. `OctadDimension` carries the
// per-dimension presence byte; `ProvenanceEntry` mirrors the octad
// `provenance` block (a 64-bit content hash plus NUL-padded tool/version
// identifiers). Both are `extern struct` so the C ABI / layout is stable
// across the FFI boundary, and each has a wire encode/decode pair so a
// consumer can prove a lossless round-trip.
//==============================================================================

/// Eight octad dimension presence bytes (0 = absent, 1 = present).
pub const OctadDimension = extern struct {
data: u8,
metadata: u8,
provenance: u8,
lineage: u8,
constraints: u8,
access_control: u8,
temporal: u8,
simulation: u8,
};

pub const OCTAD_WIRE_LEN: usize = 8;

/// Serialize an `OctadDimension` (8 bytes, dimension order). Returns the
/// number of bytes written, or -1 on a null/short-buffer error.
export fn verisimdb_data_octad_encode(
in: ?*const OctadDimension,
out: ?[*]u8,
cap: usize,
) isize {
const d = in orelse return -1;
const o = out orelse return -1;
if (cap < OCTAD_WIRE_LEN) return -1;
const src = std.mem.asBytes(d);
@memcpy(o[0..OCTAD_WIRE_LEN], src[0..OCTAD_WIRE_LEN]);
return @intCast(OCTAD_WIRE_LEN);
}

/// Deserialize an `OctadDimension` from `buf`.
export fn verisimdb_data_octad_decode(
buf: ?[*]const u8,
len: usize,
out: ?*OctadDimension,
) Result {
const b = buf orelse return .null_pointer;
const o = out orelse return .null_pointer;
if (len < OCTAD_WIRE_LEN) return .invalid_param;
const dst = std.mem.asBytes(o);
@memcpy(dst[0..OCTAD_WIRE_LEN], b[0..OCTAD_WIRE_LEN]);
return .ok;
}

/// One provenance record: a content hash plus NUL-padded identifiers.
pub const ProvenanceEntry = extern struct {
hash: u64,
tool: [32]u8,
version: [16]u8,
};

/// hash (8, little-endian) + tool (32) + version (16).
pub const PROVENANCE_WIRE_LEN: usize = 8 + 32 + 16;

/// Serialize a `ProvenanceEntry`. Returns bytes written or -1.
export fn verisimdb_data_provenance_encode(
in: ?*const ProvenanceEntry,
out: ?[*]u8,
cap: usize,
) isize {
const e = in orelse return -1;
const o = out orelse return -1;
if (cap < PROVENANCE_WIRE_LEN) return -1;
std.mem.writeInt(u64, o[0..8], e.hash, .little);
@memcpy(o[8..40], &e.tool);
@memcpy(o[40..56], &e.version);
return @intCast(PROVENANCE_WIRE_LEN);
}

/// Deserialize a `ProvenanceEntry` from `buf`.
export fn verisimdb_data_provenance_decode(
buf: ?[*]const u8,
len: usize,
out: ?*ProvenanceEntry,
) Result {
const b = buf orelse return .null_pointer;
const e = out orelse return .null_pointer;
if (len < PROVENANCE_WIRE_LEN) return .invalid_param;
e.hash = std.mem.readInt(u64, b[0..8], .little);
@memcpy(&e.tool, b[8..40]);
@memcpy(&e.version, b[40..56]);
return .ok;
}

//==============================================================================
// Tests
//==============================================================================
Expand All @@ -272,3 +367,58 @@ test "version" {
const ver_str = std.mem.span(ver);
try std.testing.expectEqualStrings(VERSION, ver_str);
}

test "octad dimension round-trip is lossless" {
const in = OctadDimension{
.data = 1,
.metadata = 1,
.provenance = 1,
.lineage = 0,
.constraints = 1,
.access_control = 0,
.temporal = 1,
.simulation = 0,
};
var buf: [OCTAD_WIRE_LEN]u8 = undefined;
const n = verisimdb_data_octad_encode(&in, &buf, buf.len);
try std.testing.expectEqual(@as(isize, @intCast(OCTAD_WIRE_LEN)), n);

var out: OctadDimension = undefined;
try std.testing.expectEqual(Result.ok, verisimdb_data_octad_decode(&buf, buf.len, &out));
try std.testing.expectEqual(in, out);
}

test "octad encode rejects a short buffer" {
const in = std.mem.zeroes(OctadDimension);
var small: [3]u8 = undefined;
try std.testing.expectEqual(@as(isize, -1), verisimdb_data_octad_encode(&in, &small, small.len));
}

test "provenance entry round-trip is lossless" {
var in = std.mem.zeroes(ProvenanceEntry);
in.hash = 0xDEAD_BEEF_CAFE_F00D;
@memcpy(in.tool[0..7], "verisim");
@memcpy(in.version[0..5], "0.1.0");

var buf: [PROVENANCE_WIRE_LEN]u8 = undefined;
const n = verisimdb_data_provenance_encode(&in, &buf, buf.len);
try std.testing.expectEqual(@as(isize, @intCast(PROVENANCE_WIRE_LEN)), n);

var out: ProvenanceEntry = undefined;
try std.testing.expectEqual(
Result.ok,
verisimdb_data_provenance_decode(&buf, buf.len, &out),
);
try std.testing.expectEqual(in.hash, out.hash);
try std.testing.expectEqualSlices(u8, &in.tool, &out.tool);
try std.testing.expectEqualSlices(u8, &in.version, &out.version);
}

test "provenance decode rejects a short buffer" {
var out: ProvenanceEntry = undefined;
var short: [10]u8 = undefined;
try std.testing.expectEqual(
Result.invalid_param,
verisimdb_data_provenance_decode(&short, short.len, &out),
);
}
Loading
Loading