From e3ca6e312dfadef436969ac698b60041bc6cc7e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 22:03:49 +0000 Subject: [PATCH] P1: implement real Zig FFI matching the Idris2 ABI The Idris2 ABI is the source of truth. Its %foreign declarations live in two places: Foreign.idr (18 symbols) and the `namespace Foreign` block in Types.idr (3 symbols: eclexiaiser_measure_energy, eclexiaiser_query_carbon, eclexiaiser_enforce_budget). The Zig FFI exported the 18 from Foreign.idr but was missing all 3 from Types.idr, so those symbols would fail to link when the ABI is compiled against libeclexiaiser. This adds the three missing C-ABI exports with signatures matching their %foreign types exactly: eclexiaiser_measure_energy : Bits64 -> PrimIO Bits64 (handle: opaque ptr) -> u64 microjoules eclexiaiser_query_carbon : Bits32 -> PrimIO Bits32 (zone_id: u32) -> u32 mgCO2/kWh (handle-free) eclexiaiser_enforce_budget : Bits64 -> Bits64 -> PrimIO Bits32 (budget_uj, measured_uj: u64) -> Result (handle-free) eclexiaiser_enforce_budget returns the Result enum, whose values already match Types.idr resultToInt (Ok=0 .. CounterUnavailable=7); the handle-free forms mirror the safe wrappers measureEnergy/queryCarbon/enforceBudget in the Types.idr namespace Foreign block. Adds three `test` blocks covering the new symbols: measure_energy main path plus null-handle rejection, handle-free query_carbon, and enforce_budget result codes (ok / budget_exceeded). Verification: - zig test src/main.zig -lc : 11/11 pass, no warnings - idris2 --build eclexiaiser-abi.ipkg : exit 0, clean - all 21 C:eclexiaiser_* ABI symbols now have a matching export fn - Result enum values equal resultToInt Only src/interface/ffi/ is touched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH --- src/interface/ffi/src/main.zig | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/interface/ffi/src/main.zig b/src/interface/ffi/src/main.zig index a8739aa..20d0612 100644 --- a/src/interface/ffi/src/main.zig +++ b/src/interface/ffi/src/main.zig @@ -247,6 +247,28 @@ export fn eclexiaiser_read_power_mw(raw_handle: ?*anyopaque) u64 { return estimatePowerMw(); } +/// Measure energy consumption for a handle, in microjoules. +/// Matches the ABI declaration in Types.idr (namespace Foreign): +/// %foreign "C:eclexiaiser_measure_energy" : Bits64 -> PrimIO Bits64 +/// Returns the current counter reading (uJ), or 0 if no counter is available +/// or the handle is null. +export fn eclexiaiser_measure_energy(raw_handle: ?*anyopaque) u64 { + const handle = getHandle(raw_handle) orelse return 0; + + if (!handle.initialized) { + setError("Handle not initialized"); + return 0; + } + + const reading = readEnergyCounter(handle.counter_type) orelse { + setError("Energy counter not available"); + return 0; + }; + + clearError(); + return reading; +} + //============================================================================== // Carbon Intensity API //============================================================================== @@ -280,6 +302,14 @@ export fn eclexiaiser_query_renewable_pct(raw_handle: ?*anyopaque, zone_id: u32) return getStaticRenewablePct(zone_id); } +/// Query carbon intensity for a grid zone without a library handle. +/// Matches the ABI declaration in Types.idr (namespace Foreign): +/// %foreign "C:eclexiaiser_query_carbon" : Bits32 -> PrimIO Bits32 +/// Returns milligrams CO2 per kWh from the static dataset. +export fn eclexiaiser_query_carbon(zone_id: u32) u32 { + return getStaticCarbonIntensity(zone_id); +} + /// Set the carbon API provider. export fn eclexiaiser_set_carbon_api(raw_handle: ?*anyopaque, api_source: u32) Result { const handle = getHandle(raw_handle) orelse return .null_pointer; @@ -317,6 +347,19 @@ export fn eclexiaiser_enforce_energy_budget(raw_handle: ?*anyopaque, budget_uj: return .ok; } +/// Enforce an energy budget without a library handle. +/// Matches the ABI declaration in Types.idr (namespace Foreign): +/// %foreign "C:eclexiaiser_enforce_budget" : Bits64 -> Bits64 -> PrimIO Bits32 +/// Returns .ok (0) if within budget, .budget_exceeded (5) otherwise. +export fn eclexiaiser_enforce_budget(budget_uj: u64, measured_uj: u64) Result { + if (measured_uj > budget_uj) { + setError("Energy budget exceeded"); + return .budget_exceeded; + } + clearError(); + return .ok; +} + /// Enforce a carbon limit against a measurement. export fn eclexiaiser_enforce_carbon_limit(raw_handle: ?*anyopaque, limit_mg_co2: u64, measured_mg_co2: u64) Result { const handle = getHandle(raw_handle) orelse return .null_pointer; @@ -639,3 +682,23 @@ test "struct layout sizes" { try std.testing.expectEqual(@as(usize, 40), @sizeOf(BudgetEnforcement)); try std.testing.expectEqual(@as(usize, 40), @sizeOf(SustainabilityReport)); } + +test "Types.idr Foreign: measure_energy and null handle" { + // Null handle must yield 0 (not a crash). + try std.testing.expectEqual(@as(u64, 0), eclexiaiser_measure_energy(null)); + + const raw_handle = eclexiaiser_init() orelse return error.InitFailed; + defer eclexiaiser_free(raw_handle); + // Estimate counter is always available, so a reading is produced. + try std.testing.expect(eclexiaiser_measure_energy(raw_handle) > 0); +} + +test "Types.idr Foreign: query_carbon (handle-free)" { + try std.testing.expectEqual(@as(u32, 200_000), eclexiaiser_query_carbon(0x4742)); // GB + try std.testing.expectEqual(@as(u32, 20_000), eclexiaiser_query_carbon(0x4E4F)); // NO +} + +test "Types.idr Foreign: enforce_budget (handle-free) result codes" { + try std.testing.expectEqual(Result.ok, eclexiaiser_enforce_budget(1000, 500)); + try std.testing.expectEqual(Result.budget_exceeded, eclexiaiser_enforce_budget(500, 1000)); +}