lib/choir/src/composition/invocation.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const composition = @import("root.zig");
  3 const abi = composition.abi;
  4 const loaded = composition.loaded;
  5 
  6 const Allocator = std.mem.Allocator;
  7 
  8 pub const ResourceSpec = struct {
  9     bytes: []u8,
 10     access: composition.Access,
 11 };
 12 
 13 pub const State = enum {
 14     prepared,
 15     running,
 16     completed,
 17     failed,
 18 };
 19 
 20 /// One call's owned ABI resources and state, bound to a live `LoadedComposition`.
 21 /// Resource byte slices remain caller-owned and must outlive invocation completion.
 22 pub const Invocation = struct {
 23     allocator: Allocator,
 24     owner: *loaded.LoadedComposition,
 25     resources: []abi.Resource,
 26     frame: abi.Frame,
 27     state: State = .prepared,
 28 
 29     pub fn init(
 30         allocator: Allocator,
 31         owner: *loaded.LoadedComposition,
 32         specs: []const ResourceSpec,
 33     ) (Allocator.Error || error{ CompositionClosing, InvalidResource, TooManyResources })!*Invocation {
 34         if (specs.len > abi.max_resources) return error.TooManyResources;
 35         for (specs) |spec| {
 36             if (spec.bytes.len == 0) return error.InvalidResource;
 37         }
 38         const invocation_id = owner.reserveInvocation() orelse return error.CompositionClosing;
 39         errdefer owner.releaseInvocation(invocation_id);
 40 
 41         const resources = try allocator.alloc(abi.Resource, specs.len);
 42         errdefer allocator.free(resources);
 43         for (specs, 0..) |spec, index| {
 44             resources[index] = abi.Resource.init(owner.contextPointer().owner_cookie, invocation_id, spec.bytes, spec.access);
 45         }
 46 
 47         const invocation = try allocator.create(Invocation);
 48         invocation.* = .{
 49             .allocator = allocator,
 50             .owner = owner,
 51             .resources = resources,
 52             .frame = abi.Frame.init(owner.contextPointer().owner_cookie, invocation_id, resources),
 53         };
 54         return invocation;
 55     }
 56 
 57     pub fn deinit(self: *Invocation) void {
 58         if (self.state == .running) @panic("invocation destroyed while running");
 59         const allocator = self.allocator;
 60         const owner = self.owner;
 61         const invocation_id = self.frame.invocation_id;
 62         allocator.free(self.resources);
 63         self.* = undefined;
 64         allocator.destroy(self);
 65         owner.releaseInvocation(invocation_id);
 66     }
 67 
 68     pub fn id(self: *const Invocation) u64 {
 69         return self.frame.invocation_id;
 70     }
 71 
 72     pub fn framePointer(self: *Invocation) *abi.Frame {
 73         return &self.frame;
 74     }
 75 
 76     pub fn status(self: *const Invocation) abi.Status {
 77         return self.frame.statusValue();
 78     }
 79 };
 80 
 81 /// A context-bound wrapper around one process-local raw fragment address.
 82 /// `address` is public for callers that supply the composition's ABI context
 83 /// and frame directly; `invoke` additionally requires a prepared invocation
 84 /// owned by the same loaded composition.
 85 pub const Entry = struct {
 86     owner: *loaded.LoadedComposition,
 87     address: usize,
 88 
 89     pub fn init(
 90         owner: *loaded.LoadedComposition,
 91         fragment_id: composition.FragmentId,
 92         export_name: []const u8,
 93     ) loaded.EntryError!Entry {
 94         return .{
 95             .owner = owner,
 96             .address = try owner.entryAddress(fragment_id, export_name),
 97         };
 98     }
 99 
100     pub fn invoke(self: Entry, invocation: *Invocation) abi.Status {
101         if (invocation.owner != self.owner or invocation.state != .prepared) {
102             invocation.frame.setStatus(.invalid_resource);
103             invocation.state = .failed;
104             return .invalid_resource;
105         }
106         invocation.state = .running;
107         const function: *const fn (i64, i64) callconv(.c) i64 = @ptrFromInt(self.address);
108         const raw_status = function(
109             @bitCast(@as(u64, @intFromPtr(self.owner.contextPointer()))),
110             @bitCast(@as(u64, @intFromPtr(invocation.framePointer()))),
111         );
112         const status = abi.decodeStatus(raw_status);
113         invocation.frame.setStatus(status);
114         invocation.state = if (status == .ok) .completed else .failed;
115         return status;
116     }
117 };
118 
119 test "composition invocation resources carry one owner and identity" {
120     try std.testing.expectEqual(abi.Status.fragment_failure, abi.decodeStatus(-1));
121     try std.testing.expectEqual(abi.Status.ok, abi.decodeStatus(@backingInt(abi.Status.ok)));
122     try std.testing.expectEqual(abi.Status.fragment_failure, abi.decodeStatus(9000));
123 }
124 
125 test "composition entries require a durable fragment export" {
126     const init_info = @typeInfo(@TypeOf(Entry.init)).@"fn";
127     try std.testing.expectEqual(@as(usize, 3), init_info.param_types.len);
128     try std.testing.expect(init_info.param_types[1].? == composition.FragmentId);
129     try std.testing.expect(init_info.param_types[2].? == []const u8);
130 }