Skip to content

Commit e3ca6e3

Browse files
committed
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xMKB3T4Vo5FYC7Czx3JSH
1 parent 890972d commit e3ca6e3

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)