Skip to content

Commit ce8cdc7

Browse files
P1: implement real Zig FFI matching the Idris2 ABI (#30)
## Problem The Idris2 ABI is the source of truth for this `-iser` project. Its `%foreign "C:eclexiaiser_*"` declarations live in **two** places: - `src/interface/abi/Eclexiaiser/ABI/Foreign.idr` — 18 symbols - the `namespace Foreign` block inside `src/interface/abi/Eclexiaiser/ABI/Types.idr` — **3 more symbols**: `eclexiaiser_measure_energy`, `eclexiaiser_query_carbon`, `eclexiaiser_enforce_budget` The Zig FFI (`src/interface/ffi/src/main.zig`) exported the 18 from `Foreign.idr` but was **missing all 3** declared in `Types.idr`. Those symbols have no matching `export fn`, so they would fail to link when the ABI is compiled against `libeclexiaiser`. The Zig FFI therefore did not fully match its ABI. ## Fix Added the three missing C-ABI exports, with C signatures matching their `%foreign` types exactly (Idris `Bits64`/`Bits32` ↔ Zig `u64`/`u32`; opaque handle ↔ `?*anyopaque`; result `Bits32` ↔ the `Result` enum): | Idris `%foreign` (Types.idr `namespace Foreign`) | Zig export | | --- | --- | | `eclexiaiser_measure_energy : Bits64 -> PrimIO Bits64` | `eclexiaiser_measure_energy(handle) u64` | | `eclexiaiser_query_carbon : Bits32 -> PrimIO Bits32` | `eclexiaiser_query_carbon(zone_id: u32) u32` (handle-free) | | `eclexiaiser_enforce_budget : Bits64 -> Bits64 -> PrimIO Bits32` | `eclexiaiser_enforce_budget(budget_uj, measured_uj: u64) Result` (handle-free) | The handle-free forms mirror the safe wrappers `measureEnergy` / `queryCarbon` / `enforceBudget` in the `Types.idr` `namespace Foreign` block. `eclexiaiser_enforce_budget` returns the `Result` enum, whose values already equal `Types.idr` `resultToInt` (`Ok=0 .. CounterUnavailable=7`). Three `test` blocks were added covering the new symbols: `measure_energy` main path + null-handle rejection, handle-free `query_carbon`, and `enforce_budget` result codes (ok / budget_exceeded). Only `src/interface/ffi/` is touched. ## Verification - `zig test src/main.zig -lc` → **11/11 pass**, no errors/warnings - `idris2 --build eclexiaiser-abi.ipkg` → **exit 0**, clean (build dir removed afterward) - All **21** `C:eclexiaiser_*` ABI symbols now have a matching `export fn` - `Result` enum values equal `resultToInt` ## CI note Any rust-ci / Hypatia / governance reds are pre-existing estate-infra checks unrelated to this Zig-only change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH --- _Generated by [Claude Code](https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 890972d commit ce8cdc7

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

src/interface/ffi/src/main.zig

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,28 @@ export fn eclexiaiser_read_power_mw(raw_handle: ?*anyopaque) u64 {
247247
return estimatePowerMw();
248248
}
249249

250+
/// Measure energy consumption for a handle, in microjoules.
251+
/// Matches the ABI declaration in Types.idr (namespace Foreign):
252+
/// %foreign "C:eclexiaiser_measure_energy" : Bits64 -> PrimIO Bits64
253+
/// Returns the current counter reading (uJ), or 0 if no counter is available
254+
/// or the handle is null.
255+
export fn eclexiaiser_measure_energy(raw_handle: ?*anyopaque) u64 {
256+
const handle = getHandle(raw_handle) orelse return 0;
257+
258+
if (!handle.initialized) {
259+
setError("Handle not initialized");
260+
return 0;
261+
}
262+
263+
const reading = readEnergyCounter(handle.counter_type) orelse {
264+
setError("Energy counter not available");
265+
return 0;
266+
};
267+
268+
clearError();
269+
return reading;
270+
}
271+
250272
//==============================================================================
251273
// Carbon Intensity API
252274
//==============================================================================
@@ -280,6 +302,14 @@ export fn eclexiaiser_query_renewable_pct(raw_handle: ?*anyopaque, zone_id: u32)
280302
return getStaticRenewablePct(zone_id);
281303
}
282304

305+
/// Query carbon intensity for a grid zone without a library handle.
306+
/// Matches the ABI declaration in Types.idr (namespace Foreign):
307+
/// %foreign "C:eclexiaiser_query_carbon" : Bits32 -> PrimIO Bits32
308+
/// Returns milligrams CO2 per kWh from the static dataset.
309+
export fn eclexiaiser_query_carbon(zone_id: u32) u32 {
310+
return getStaticCarbonIntensity(zone_id);
311+
}
312+
283313
/// Set the carbon API provider.
284314
export fn eclexiaiser_set_carbon_api(raw_handle: ?*anyopaque, api_source: u32) Result {
285315
const handle = getHandle(raw_handle) orelse return .null_pointer;
@@ -317,6 +347,19 @@ export fn eclexiaiser_enforce_energy_budget(raw_handle: ?*anyopaque, budget_uj:
317347
return .ok;
318348
}
319349

350+
/// Enforce an energy budget without a library handle.
351+
/// Matches the ABI declaration in Types.idr (namespace Foreign):
352+
/// %foreign "C:eclexiaiser_enforce_budget" : Bits64 -> Bits64 -> PrimIO Bits32
353+
/// Returns .ok (0) if within budget, .budget_exceeded (5) otherwise.
354+
export fn eclexiaiser_enforce_budget(budget_uj: u64, measured_uj: u64) Result {
355+
if (measured_uj > budget_uj) {
356+
setError("Energy budget exceeded");
357+
return .budget_exceeded;
358+
}
359+
clearError();
360+
return .ok;
361+
}
362+
320363
/// Enforce a carbon limit against a measurement.
321364
export fn eclexiaiser_enforce_carbon_limit(raw_handle: ?*anyopaque, limit_mg_co2: u64, measured_mg_co2: u64) Result {
322365
const handle = getHandle(raw_handle) orelse return .null_pointer;
@@ -639,3 +682,23 @@ test "struct layout sizes" {
639682
try std.testing.expectEqual(@as(usize, 40), @sizeOf(BudgetEnforcement));
640683
try std.testing.expectEqual(@as(usize, 40), @sizeOf(SustainabilityReport));
641684
}
685+
686+
test "Types.idr Foreign: measure_energy and null handle" {
687+
// Null handle must yield 0 (not a crash).
688+
try std.testing.expectEqual(@as(u64, 0), eclexiaiser_measure_energy(null));
689+
690+
const raw_handle = eclexiaiser_init() orelse return error.InitFailed;
691+
defer eclexiaiser_free(raw_handle);
692+
// Estimate counter is always available, so a reading is produced.
693+
try std.testing.expect(eclexiaiser_measure_energy(raw_handle) > 0);
694+
}
695+
696+
test "Types.idr Foreign: query_carbon (handle-free)" {
697+
try std.testing.expectEqual(@as(u32, 200_000), eclexiaiser_query_carbon(0x4742)); // GB
698+
try std.testing.expectEqual(@as(u32, 20_000), eclexiaiser_query_carbon(0x4E4F)); // NO
699+
}
700+
701+
test "Types.idr Foreign: enforce_budget (handle-free) result codes" {
702+
try std.testing.expectEqual(Result.ok, eclexiaiser_enforce_budget(1000, 500));
703+
try std.testing.expectEqual(Result.budget_exceeded, eclexiaiser_enforce_budget(500, 1000));
704+
}

0 commit comments

Comments
 (0)