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 }