Skip to content

Commit 76ad499

Browse files
build(abi/ffi): wire the Idris2 ABI + Zig FFI to compile (first time) (#79)
## What The ABI (`src/abi/`, Idris2) and FFI (`ffi/zig/`) layers **had never actually built** — no ipkg, module names that didn't resolve, broken proofs, missing FFI glue types, and a Zig `build.zig` on a pre-0.15 API. This wires **both green** and adds `just` recipes to keep them that way. ### Idris2 ABI — `idris2 --build rpa-elysium-abi.ipkg` ✅ (5/5 modules) - New **`rpa-elysium-abi.ipkg`** (sourcedir=`src/abi`; mirrors HAR's `har-abi.ipkg`). - **Flatten** module names to the `src/abi/` layout — this also fixes `Foreign.idr`'s `ABI`-vs-`Abi` **case mismatch** (it imported modules that didn't exist). - **Proof fixes** in `Types.idr`/`Layout.idr`: `eventKindTagValid` / `eventKindTagFitsInByte` every clause wrongly used `LTE 5 5`; each now matches its actual tag (`0→LTEZero`, `1→LTESucc LTEZero`, …). - `Layout.idr`: `import Data.Nat` (`LTE`/`GT` were undefined). - `Foreign.idr`: **define the FFI glue that was referenced but never declared** — `Handle` (+ `createHandle`/`handlePtr`) and `Result` (`Ok=0..NullPointer=4`, matching the Zig enum); fix `registerCallback` to take a raw C-callable pointer (an Idris closure can't be `cast` to `AnyPtr`). ### Zig FFI — `zig build` + `zig build test` ✅ - Port `build.zig` to the **Zig 0.15 API** (`addSharedLibrary`/`addStaticLibrary` → `addLibrary` + shared module + `linkage`; `link_libc` for the C allocator). - Fix `main.zig` for 0.15 (`opaque`-with-fields → `struct`; `callconv(.C)` → `callconv(.c)`). - Produces `librpa_ffi.{a,so}`; tests pass. The **11 exported C symbols match** `Foreign.idr`'s `%foreign` declarations, and the `Result` codes agree. ### Justfile `verify-abi`, `build-ffi`, `test-ffi`, `verify-abi-ffi`. ## Known follow-ups (not compile issues) - `Foreign.idr`'s `process` wrapper treats result `0` as **error** while `processArray` and the Zig side treat `0` as **ok** — the success convention needs reconciling (correctness, with a test). - `generated/abi/` C headers still unimplemented (the formal Idris↔Zig bridge). - `LinearDispatch.eph` (Ephapax) needs its own toolchain to build. Follows the shared-ABI adoption (#78). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6561ab4 commit 76ad499

12 files changed

Lines changed: 144 additions & 48 deletions

File tree

.github/workflows/mirror.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,4 @@ permissions:
99
jobs:
1010
mirror:
1111
uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@d135b05bfc647d0c0fbfedc7e80f37ea50f49236
12-
timeout-minutes: 10
1312
secrets: inherit

.github/workflows/rust-ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,3 @@ permissions:
1212
jobs:
1313
rust-ci:
1414
uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@412a7031577112b31ee287cc6060179d638d6500
15-
timeout-minutes: 10

.github/workflows/secret-scanner.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ permissions:
1111
contents: read
1212
jobs:
1313
scan:
14+
permissions:
15+
contents: read
16+
pull-requests: write
17+
actions: read
1418
uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@d135b05bfc647d0c0fbfedc7e80f37ea50f49236
15-
timeout-minutes: 10
1619
secrets: inherit
1720
trufflehog:
1821
runs-on: ubuntu-latest

Justfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ test:
3030
test-verbose:
3131
cargo test --workspace -- --nocapture
3232

33+
# Build + typecheck the Idris2 ABI layer (src/abi/) via its package.
34+
verify-abi:
35+
idris2 --build rpa-elysium-abi.ipkg
36+
37+
# Build the Zig FFI shared + static libraries (ffi/zig/).
38+
build-ffi:
39+
cd ffi/zig && zig build
40+
41+
# Run the Zig FFI unit tests.
42+
test-ffi:
43+
cd ffi/zig && zig build test
44+
45+
# Verify the whole ABI/FFI layer compiles (Idris proofs + Zig libs + tests).
46+
verify-abi-ffi: verify-abi build-ffi test-ffi
47+
3348
# Clean build artifacts
3449
clean:
3550
cargo clean

ffi/zig/build.zig

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,35 @@ pub fn build(b: *std.Build) void {
1313
const target = b.standardTargetOptions(.{});
1414
const optimize = b.standardOptimizeOption(.{});
1515

16-
// Shared library for FFI consumers
17-
const lib = b.addSharedLibrary(.{
18-
.name = "rpa_ffi",
16+
// The root module shared by every artifact (Zig 0.15 build API).
17+
const mod = b.createModule(.{
1918
.root_source_file = b.path("src/main.zig"),
2019
.target = target,
2120
.optimize = optimize,
21+
// main.zig uses std.heap.c_allocator for C-ABI-owned allocations.
22+
.link_libc = true,
2223
});
2324

24-
// Also produce a static library for embedding
25-
const static_lib = b.addStaticLibrary(.{
25+
// Shared library for FFI consumers.
26+
const lib = b.addLibrary(.{
2627
.name = "rpa_ffi",
27-
.root_source_file = b.path("src/main.zig"),
28-
.target = target,
29-
.optimize = optimize,
28+
.root_module = mod,
29+
.linkage = .dynamic,
3030
});
3131

32-
// Install both artifacts
32+
// Also produce a static library for embedding.
33+
const static_lib = b.addLibrary(.{
34+
.name = "rpa_ffi",
35+
.root_module = mod,
36+
.linkage = .static,
37+
});
38+
39+
// Install both artifacts.
3340
b.installArtifact(lib);
3441
b.installArtifact(static_lib);
3542

36-
// Unit tests
37-
const main_tests = b.addTest(.{
38-
.root_source_file = b.path("src/main.zig"),
39-
.target = target,
40-
.optimize = optimize,
41-
});
43+
// Unit tests.
44+
const main_tests = b.addTest(.{ .root_module = mod });
4245

4346
const run_main_tests = b.addRunArtifact(main_tests);
4447
const test_step = b.step("test", "Run unit tests");

ffi/zig/src/main.zig

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ pub const Result = enum(c_int) {
3838
null_pointer = 4,
3939
};
4040

41-
/// Library handle (opaque to prevent direct access)
42-
pub const Handle = opaque {
41+
/// Library handle. A regular struct used as the backing type; C consumers only
42+
/// ever hold a `*Handle` and never see the layout, so it is effectively opaque
43+
/// across the ABI. (Zig `opaque {}` types cannot carry fields, which the
44+
/// internal state below requires.)
45+
pub const Handle = struct {
4346
// Internal state hidden from C
4447
allocator: std.mem.Allocator,
4548
initialized: bool,
46-
// Add your fields here
4749
};
4850

4951
//==============================================================================
@@ -210,7 +212,7 @@ export fn rpa_elysium_build_info() [*:0]const u8 {
210212
//==============================================================================
211213

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

215217
/// Register a callback
216218
export fn rpa_elysium_register_callback(

rpa-elysium-abi.ipkg

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- SPDX-License-Identifier: MPL-2.0
2+
-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
-- Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
--
5+
-- Idris2 package for the RPA Elysium ABI layer (src/abi/). Wires the
6+
-- previously-uncompiled ABI modules into a buildable package:
7+
-- idris2 --build rpa-elysium-abi.ipkg
8+
--
9+
-- Module names are flattened to match the src/abi/ layout (sourcedir below),
10+
-- mirroring HAR's har-abi.ipkg. LinearDispatch.eph is Ephapax, not Idris, and
11+
-- is built by its own toolchain — it is intentionally not listed here.
12+
13+
package rpa-elysium-abi
14+
15+
sourcedir = "src/abi"
16+
17+
modules = Types
18+
, Layout
19+
, Foreign
20+
, ProvenFSM
21+
, ProvenQueue

src/abi/Foreign.idr

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,62 @@
99
||| All functions are declared here with type signatures and safety proofs.
1010
||| Implementations live in ffi/zig/
1111

12-
module RpaElysium.ABI.Foreign
12+
module Foreign
1313

14-
import RpaElysium.ABI.Types
15-
import RpaElysium.ABI.Layout
14+
import Types
15+
import Layout
1616

1717
%default total
1818

19+
--------------------------------------------------------------------------------
20+
-- FFI glue types (must match ffi/zig and the generated C header)
21+
--------------------------------------------------------------------------------
22+
23+
||| FFI result codes returned across the C ABI. Tag values MUST match the Zig
24+
||| implementation (ffi/zig): Ok=0, Error=1, InvalidParam=2, OutOfMemory=3,
25+
||| NullPointer=4.
26+
public export
27+
data Result : Type where
28+
Ok : Result
29+
Error : Result
30+
InvalidParam : Result
31+
OutOfMemory : Result
32+
NullPointer : Result
33+
34+
public export
35+
Show Result where
36+
show Ok = "Ok"
37+
show Error = "Error"
38+
show InvalidParam = "InvalidParam"
39+
show OutOfMemory = "OutOfMemory"
40+
show NullPointer = "NullPointer"
41+
42+
||| The C-ABI tag byte for a result code.
43+
public export
44+
resultTag : Result -> Bits32
45+
resultTag Ok = 0
46+
resultTag Error = 1
47+
resultTag InvalidParam = 2
48+
resultTag OutOfMemory = 3
49+
resultTag NullPointer = 4
50+
51+
||| An opaque handle to a library instance, wrapping the raw C pointer as a
52+
||| Bits64. Constructed only via `createHandle`, which rejects the null pointer.
53+
public export
54+
data Handle : Type where
55+
MkHandle : Bits64 -> Handle
56+
57+
||| Construct a handle from a raw pointer; `Nothing` for the null pointer.
58+
public export
59+
createHandle : Bits64 -> Maybe Handle
60+
createHandle 0 = Nothing
61+
createHandle p = Just (MkHandle p)
62+
63+
||| The raw C pointer backing a handle.
64+
public export
65+
handlePtr : Handle -> Bits64
66+
handlePtr (MkHandle p) = p
67+
1968
--------------------------------------------------------------------------------
2069
-- Library Lifecycle
2170
--------------------------------------------------------------------------------
@@ -178,21 +227,25 @@ buildInfo = do
178227
-- Callback Support
179228
--------------------------------------------------------------------------------
180229

181-
||| Callback function type (C ABI)
230+
||| Callback function type (C ABI) — the shape a registered callback must
231+
||| have: `(context : Bits64) -> (event : Bits32) -> (result : Bits32)`.
182232
public export
183233
Callback : Type
184234
Callback = Bits64 -> Bits32 -> Bits32
185235

186236
||| Register a callback
187237
export
188238
%foreign "C:rpa_elysium_register_callback, librpa_elysium"
189-
prim__registerCallback : Bits64 -> AnyPtr -> PrimIO Bits32
239+
prim__registerCallback : Bits64 -> Bits64 -> PrimIO Bits32
190240

191-
||| Safe callback registration
241+
||| Safe callback registration. The callback is passed as a raw pointer to a
242+
||| C-callable function of type `Callback`: an Idris closure cannot be handed to
243+
||| C directly (that needs a foreign export), so the caller supplies the
244+
||| already-C-callable function pointer (`cbPtr`).
192245
export
193-
registerCallback : Handle -> Callback -> IO (Either Result ())
194-
registerCallback h cb = do
195-
result <- primIO (prim__registerCallback (handlePtr h) (cast cb))
246+
registerCallback : Handle -> (cbPtr : Bits64) -> IO (Either Result ())
247+
registerCallback h cbPtr = do
248+
result <- primIO (prim__registerCallback (handlePtr h) cbPtr)
196249
pure $ case resultFromInt result of
197250
Just Ok => Right ()
198251
Just err => Left err

src/abi/Layout.idr

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
-- Ensures that type representations are consistent across the
88
-- Idris2 ABI definitions and the Zig FFI implementation.
99

10-
module RpaElysium.Abi.Layout
10+
module Layout
1111

12-
import RpaElysium.Abi.Types
12+
import Types
13+
import Data.Nat
1314

1415
%default total
1516

@@ -73,9 +74,9 @@ timestampAlignValid = Align8
7374
||| Proof that all event kind tags fit in a single byte
7475
public export
7576
eventKindTagFitsInByte : (ek : EventKind) -> LTE (cast (eventKindTag ek)) 255
76-
eventKindTagFitsInByte (FileCreated _) = LTESucc (LTESucc (LTESucc (LTESucc (LTESucc LTEZero))))
77-
eventKindTagFitsInByte (FileModified _) = LTESucc (LTESucc (LTESucc (LTESucc (LTESucc LTEZero))))
78-
eventKindTagFitsInByte (FileDeleted _) = LTESucc (LTESucc (LTESucc (LTESucc (LTESucc LTEZero))))
79-
eventKindTagFitsInByte (FileRenamed _ _) = LTESucc (LTESucc (LTESucc (LTESucc (LTESucc LTEZero))))
80-
eventKindTagFitsInByte Manual = LTESucc (LTESucc (LTESucc (LTESucc (LTESucc LTEZero))))
77+
eventKindTagFitsInByte (FileCreated _) = LTEZero
78+
eventKindTagFitsInByte (FileModified _) = LTESucc LTEZero
79+
eventKindTagFitsInByte (FileDeleted _) = LTESucc (LTESucc LTEZero)
80+
eventKindTagFitsInByte (FileRenamed _ _) = LTESucc (LTESucc (LTESucc LTEZero))
81+
eventKindTagFitsInByte Manual = LTESucc (LTESucc (LTESucc (LTESucc LTEZero)))
8182
eventKindTagFitsInByte (Scheduled _) = LTESucc (LTESucc (LTESucc (LTESucc (LTESucc LTEZero))))

src/abi/ProvenFSM.idr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
-- EventDisposition → used when events arrive at the workflow engine
1919
-- ValidMachineTransition → defines which workflow state transitions are legal
2020

21-
module RpaElysium.Abi.ProvenFSM
21+
module ProvenFSM
2222

23-
import RpaElysium.Abi.Types
23+
import Types
2424

2525
%default total
2626

0 commit comments

Comments
 (0)