c_abi.zig (15707B)
1 const std = @import("std"); 2 const wasm = @import("builtin").target.cpu.arch == .wasm32; 3 const tree = @import("tree.zig"); 4 const Arena = @import("arena.zig").Arena; 5 const reduce = @import("reduce.zig"); 6 const codecs = @import("codecs.zig"); 7 const kernel = @import("kernel.zig"); 8 const bundle = @import("bundle.zig"); 9 const handles = @import("handles.zig"); 10 const Handle = handles.Handle; 11 12 // Keep these values synchronized with include/arboricx.h. 13 const Status = enum(c_int) { 14 ok = 0, 15 invalid_handle = 1, 16 out_of_memory = 2, 17 fuel_exhausted = 3, 18 type_mismatch = 4, 19 invalid_data = 5, 20 io_error = 6, 21 handle_exhausted = 7, 22 }; 23 fn status(err: anyerror) Status { 24 return switch (err) { 25 error.InvalidHandle => .invalid_handle, 26 error.OutOfMemory => .out_of_memory, 27 error.FuelExhausted => .fuel_exhausted, 28 error.HandleExhausted => .handle_exhausted, 29 else => .invalid_data, 30 }; 31 } 32 33 pub const ArbCtx = struct { 34 gpa: std.mem.Allocator, 35 arena: Arena, 36 kernel_root: u32, 37 handles: handles.Table = .{}, 38 39 fn prepare(self: *ArbCtx) !void { 40 self.arena.roots.clearRetainingCapacity(); 41 try self.arena.roots.append(self.gpa, self.kernel_root); 42 try self.handles.appendRoots(&self.arena); 43 } 44 fn publish(self: *ArbCtx, node: u32, out: *Handle) Status { 45 const handle = self.handles.create(self.gpa, node) catch |err| return status(err); 46 out.* = handle; 47 return .ok; 48 } 49 // Multi-output operations publish atomically: never leak half a pair. 50 fn publishPair(self: *ArbCtx, a: u32, b: u32, out_a: *Handle, out_b: *Handle) Status { 51 const ha = self.handles.create(self.gpa, a) catch |err| return status(err); 52 const hb = self.handles.create(self.gpa, b) catch |err| { 53 self.handles.release(ha) catch unreachable; 54 return status(err); 55 }; 56 out_a.* = ha; 57 out_b.* = hb; 58 return .ok; 59 } 60 }; 61 62 export fn arb_abi_version() u32 { 63 return 3; 64 } 65 export fn arboricx_init() ?*ArbCtx { 66 const gpa = if (wasm) std.heap.wasm_allocator else std.heap.smp_allocator; 67 const ptr = gpa.create(ArbCtx) catch return null; 68 ptr.* = .{ .gpa = gpa, .arena = Arena.init(gpa), .kernel_root = undefined }; 69 ptr.kernel_root = kernel.loadKernel(&ptr.arena) catch { 70 ptr.arena.deinit(); 71 gpa.destroy(ptr); 72 return null; 73 }; 74 ptr.arena.gc_enabled = true; 75 return ptr; 76 } 77 export fn arboricx_free(ctx: *ArbCtx) void { 78 ctx.handles.deinit(ctx.gpa); 79 ctx.arena.deinit(); 80 ctx.gpa.destroy(ctx); 81 } 82 export fn arboricx_free_buf(ctx: *ArbCtx, ptr: [*]u8, len: usize) void { 83 ctx.gpa.free(ptr[0..len]); 84 } 85 export fn arb_release(ctx: *ArbCtx, handle: Handle) Status { 86 ctx.handles.release(handle) catch |err| return status(err); 87 return .ok; 88 } 89 export fn arb_retain(ctx: *ArbCtx, handle: Handle, out: *Handle) Status { 90 const node = ctx.handles.get(handle) catch |err| return status(err); 91 return ctx.publish(node, out); 92 } 93 export fn arb_set_gc_policy(ctx: *ArbCtx, minimum: u32, multiplier: u32) Status { 94 ctx.arena.setGcPolicy(minimum, multiplier) catch |err| return status(err); 95 return .ok; 96 } 97 export fn arb_collect(ctx: *ArbCtx) Status { 98 ctx.prepare() catch |err| return status(err); 99 ctx.arena.collect(&.{}) catch |err| return status(err); 100 return .ok; 101 } 102 export fn arb_leaf(ctx: *ArbCtx, out: *Handle) Status { 103 return ctx.publish(ctx.arena.alloc(.leaf) catch |err| return status(err), out); 104 } 105 export fn arb_stem(ctx: *ArbCtx, child: Handle, out: *Handle) Status { 106 const child_node = ctx.handles.get(child) catch |err| return status(err); 107 const node = ctx.arena.alloc(.{ .stem = .{ .child = child_node } }) catch |err| return status(err); 108 return ctx.publish(node, out); 109 } 110 export fn arb_fork(ctx: *ArbCtx, left: Handle, right: Handle, out: *Handle) Status { 111 const left_node = ctx.handles.get(left) catch |err| return status(err); 112 const right_node = ctx.handles.get(right) catch |err| return status(err); 113 const node = ctx.arena.alloc(.{ .fork = .{ .left = left_node, .right = right_node } }) catch |err| return status(err); 114 return ctx.publish(node, out); 115 } 116 export fn arb_app(ctx: *ArbCtx, func: Handle, arg: Handle, out: *Handle) Status { 117 const func_node = ctx.handles.get(func) catch |err| return status(err); 118 const arg_node = ctx.handles.get(arg) catch |err| return status(err); 119 const node = ctx.arena.alloc(.{ .app = .{ .func = func_node, .arg = arg_node } }) catch |err| return status(err); 120 return ctx.publish(node, out); 121 } 122 export fn arb_is_leaf(ctx: *ArbCtx, root: Handle, out: *c_int) Status { 123 const node = ctx.handles.get(root) catch |err| return status(err); 124 out.* = if (ctx.arena.get(node).* == .leaf) 1 else 0; 125 return .ok; 126 } 127 export fn arb_is_stem(ctx: *ArbCtx, root: Handle, out: *c_int) Status { 128 const node = ctx.handles.get(root) catch |err| return status(err); 129 out.* = if (ctx.arena.get(node).* == .stem) 1 else 0; 130 return .ok; 131 } 132 export fn arb_is_fork(ctx: *ArbCtx, root: Handle, out: *c_int) Status { 133 const node = ctx.handles.get(root) catch |err| return status(err); 134 out.* = if (ctx.arena.get(node).* == .fork) 1 else 0; 135 return .ok; 136 } 137 export fn arb_is_app(ctx: *ArbCtx, root: Handle, out: *c_int) Status { 138 const node = ctx.handles.get(root) catch |err| return status(err); 139 out.* = if (ctx.arena.get(node).* == .app) 1 else 0; 140 return .ok; 141 } 142 export fn arb_get_stem_child(ctx: *ArbCtx, root: Handle, out_child: *Handle) Status { 143 const idx = ctx.handles.get(root) catch |err| return status(err); 144 const node = ctx.arena.get(idx).*; 145 if (node != .stem) return .type_mismatch; 146 return ctx.publish(node.stem.child, out_child); 147 } 148 export fn arb_get_fork_children(ctx: *ArbCtx, root: Handle, out_left: *Handle, out_right: *Handle) Status { 149 const idx = ctx.handles.get(root) catch |err| return status(err); 150 const node = ctx.arena.get(idx).*; 151 if (node != .fork) return .type_mismatch; 152 return ctx.publishPair(node.fork.left, node.fork.right, out_left, out_right); 153 } 154 export fn arb_get_app_func_arg(ctx: *ArbCtx, root: Handle, out_func: *Handle, out_arg: *Handle) Status { 155 const idx = ctx.handles.get(root) catch |err| return status(err); 156 const node = ctx.arena.get(idx).*; 157 if (node != .app) return .type_mismatch; 158 return ctx.publishPair(node.app.func, node.app.arg, out_func, out_arg); 159 } 160 export fn arb_reduce(ctx: *ArbCtx, root: Handle, fuel: u64, out: *Handle) Status { 161 const node = ctx.handles.get(root) catch |err| return status(err); 162 ctx.prepare() catch |err| return status(err); 163 const result = reduce.reduce(node, &ctx.arena, fuel) catch |err| return status(err); 164 return ctx.publish(result, out); 165 } 166 export fn arb_of_number(ctx: *ArbCtx, n: u64, out: *Handle) Status { 167 return ctx.publish(codecs.ofNumber(&ctx.arena, n) catch |err| return status(err), out); 168 } 169 export fn arb_of_string(ctx: *ArbCtx, s: [*:0]const u8, out: *Handle) Status { 170 return ctx.publish(codecs.ofString(&ctx.arena, std.mem.sliceTo(s, 0)) catch |err| return status(err), out); 171 } 172 export fn arb_of_bytes(ctx: *ArbCtx, bytes: [*]const u8, len: usize, out: *Handle) Status { 173 return ctx.publish(codecs.ofBytes(&ctx.arena, bytes[0..len]) catch |err| return status(err), out); 174 } 175 export fn arb_of_list(ctx: *ArbCtx, items: [*]const Handle, len: usize, out: *Handle) Status { 176 const nodes = ctx.gpa.alloc(u32, len) catch |err| return status(err); 177 defer ctx.gpa.free(nodes); 178 for (items[0..len], nodes) |h, *node| node.* = ctx.handles.get(h) catch |err| return status(err); 179 return ctx.publish(codecs.ofList(&ctx.arena, nodes) catch |err| return status(err), out); 180 } 181 export fn arb_to_number(ctx: *ArbCtx, root: Handle, out: *u64) Status { 182 const node = ctx.handles.get(root) catch |err| return status(err); 183 ctx.prepare() catch |err| return status(err); 184 const value = (codecs.toNumber(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch; 185 out.* = value; 186 return .ok; 187 } 188 export fn arb_to_bool(ctx: *ArbCtx, root: Handle, out: *c_int) Status { 189 const node = ctx.handles.get(root) catch |err| return status(err); 190 ctx.prepare() catch |err| return status(err); 191 const value = (codecs.toBool(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch; 192 out.* = if (value) 1 else 0; 193 return .ok; 194 } 195 export fn arb_to_string(ctx: *ArbCtx, root: Handle, out_ptr: **u8, out_len: *usize) Status { 196 const node = ctx.handles.get(root) catch |err| return status(err); 197 ctx.prepare() catch |err| return status(err); 198 const value = (codecs.toString(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch; 199 out_ptr.* = @ptrCast(value.ptr); 200 out_len.* = value.len; 201 return .ok; 202 } 203 export fn arb_to_bytes(ctx: *ArbCtx, root: Handle, out_ptr: **u8, out_len: *usize) Status { 204 return arb_to_string(ctx, root, out_ptr, out_len); 205 } 206 export fn arb_unwrap_result(ctx: *ArbCtx, root: Handle, out_ok: *c_int, out_value: *Handle, out_rest: *Handle) Status { 207 const node = ctx.handles.get(root) catch |err| return status(err); 208 ctx.prepare() catch |err| return status(err); 209 const r = (codecs.unwrapResult(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch; 210 const result = ctx.publishPair(r.value, r.rest, out_value, out_rest); 211 if (result == .ok) out_ok.* = if (r.ok) 1 else 0; 212 return result; 213 } 214 export fn arb_unwrap_host_value(ctx: *ArbCtx, root: Handle, out_tag: *u64, out_payload: *Handle) Status { 215 const node = ctx.handles.get(root) catch |err| return status(err); 216 ctx.prepare() catch |err| return status(err); 217 const hv = (codecs.unwrapHostValue(&ctx.arena, node) catch |err| return status(err)) orelse return .type_mismatch; 218 const result = ctx.publish(hv.payload, out_payload); 219 if (result == .ok) out_tag.* = hv.tag; 220 return result; 221 } 222 // Explicit version-specific native entrypoint; ABI v3 is NOT an IO selector. 223 // No legacy grant struct is accepted. Failed calls never publish an output handle. 224 export fn arb_run_io_v2(ctx: *ArbCtx, program: Handle, out: *Handle) Status { 225 if (wasm) return .io_error; 226 const node = ctx.handles.get(program) catch |err| return status(err); 227 ctx.prepare() catch |err| return status(err); 228 const result = @import("io_v2_native.zig").run(&ctx.arena, node, std.math.maxInt(u64)) catch |err| return switch (err) { 229 error.OutOfMemory => .out_of_memory, 230 error.FuelExhausted => .fuel_exhausted, 231 else => .io_error, 232 }; 233 return ctx.publish(result, out); 234 } 235 export fn arb_kernel_root(ctx: *ArbCtx, out: *Handle) Status { 236 return ctx.publish(ctx.kernel_root, out); 237 } 238 export fn arb_load_bundle(ctx: *ArbCtx, bytes: [*]const u8, len: usize, name: [*:0]const u8, out: *Handle) Status { 239 return ctx.publish(bundle.loadBundleExport(&ctx.arena, bytes[0..len], std.mem.sliceTo(name, 0)) catch |err| return status(err), out); 240 } 241 export fn arb_load_bundle_default(ctx: *ArbCtx, bytes: [*]const u8, len: usize, out: *Handle) Status { 242 return ctx.publish(bundle.loadBundleDefaultRoot(&ctx.arena, bytes[0..len]) catch |err| return status(err), out); 243 } 244 245 // WASM-only host scratch buffers; not additions to the native C ABI. 246 comptime { 247 if (wasm) { 248 @export(&hostAlloc, .{ .name = "arb_host_alloc" }); 249 @export(&hostFree, .{ .name = "arb_host_free" }); 250 @export(&hostOfText, .{ .name = "arb_host_of_text" }); 251 @export(&hostToText, .{ .name = "arb_host_to_text" }); 252 @export(&hostPreview, .{ .name = "arb_host_preview" }); 253 @export(&hostReduceUnbounded, .{ .name = "arb_host_reduce_unbounded" }); 254 @export(&hostSnapshotUnbounded, .{ .name = "arb_host_snapshot_unbounded" }); 255 @export(&hostPreviewUnbounded, .{ .name = "arb_host_preview_unbounded" }); 256 @export(&hostStats, .{ .name = "arb_host_stats" }); 257 } 258 } 259 // 8-byte alignment permits u64 ABI output parameters. 260 fn hostAlloc(len: usize) callconv(.c) ?[*]align(8) u8 { 261 const allocator = if (wasm) std.heap.wasm_allocator else std.heap.smp_allocator; 262 const bytes = allocator.alignedAlloc(u8, .@"8", len) catch return null; 263 return bytes.ptr; 264 } 265 fn hostFree(ptr: [*]align(8) u8, len: usize) callconv(.c) void { 266 const allocator = if (wasm) std.heap.wasm_allocator else std.heap.smp_allocator; 267 allocator.free(ptr[0..len]); 268 } 269 270 // Browser text is a list of Unicode scalars, never a list of UTF-8 bytes. 271 fn hostOfText(ctx: *ArbCtx, ptr: [*]const u8, len: usize, limit: usize, out: *Handle) callconv(.c) Status { 272 const view = std.unicode.Utf8View.init(ptr[0..len]) catch return .invalid_data; 273 var iter = view.iterator(); 274 var count: usize = 0; 275 while (iter.nextCodepoint()) |_| { 276 if (count == limit) return .invalid_data; 277 count += 1; 278 } 279 return ctx.publish(@import("source_text.zig").fromUtf8(&ctx.arena, ptr[0..len]) catch |err| return status(err), out); 280 } 281 fn hostToText(ctx: *ArbCtx, root: Handle, limit: usize, out_ptr: **u8, out_len: *usize) callconv(.c) Status { 282 const node = ctx.handles.get(root) catch |err| return status(err); 283 ctx.prepare() catch |err| return status(err); 284 const bytes = @import("source_text.zig").toUtf8UnboundedWork(&ctx.arena, node, limit) catch |err| return status(err); 285 out_ptr.* = @ptrCast(bytes.ptr); 286 out_len.* = bytes.len; 287 return .ok; 288 } 289 // Diagnostic counts, not byte/RSS limits; useful for repeated-session checks. 290 fn hostStats(ctx: *ArbCtx, slots: *u32, owned: *u32, nodes: *u32, live: *u32) callconv(.c) Status { 291 slots.* = @intCast(ctx.handles.slots.items.len); 292 owned.* = 0; 293 for (ctx.handles.slots.items) |slot| { 294 if (slot.node != null) owned.* += 1; 295 } 296 nodes.* = ctx.arena.len(); 297 live.* = @intCast(ctx.arena.last_live); 298 return .ok; 299 } 300 301 fn hostPreview(ctx: *ArbCtx, root: Handle, limit: u32, fuel: u64, out_ptr: **u8, out_len: *usize) callconv(.c) Status { 302 const node = ctx.handles.get(root) catch |err| return status(err); 303 ctx.prepare() catch |err| return status(err); 304 const bytes = @import("preview.zig").capture(&ctx.arena, node, limit, fuel) catch |err| return status(err); 305 out_ptr.* = @ptrCast(bytes.ptr); 306 out_len.* = bytes.len; 307 return .ok; 308 } 309 310 // Browser work is cancellable by Worker termination, not an implicit fuel cap. 311 fn hostReduceUnbounded(ctx: *ArbCtx, root: Handle, out: *Handle) callconv(.c) Status { 312 const node = ctx.handles.get(root) catch |err| return status(err); 313 ctx.prepare() catch |err| return status(err); 314 const result = reduce.reduceUnbounded(node, &ctx.arena) catch |err| return status(err); 315 return ctx.publish(result, out); 316 } 317 fn hostPreviewUnbounded(ctx: *ArbCtx, root: Handle, limit: u32, out_ptr: **u8, out_len: *usize) callconv(.c) Status { 318 const node = ctx.handles.get(root) catch |err| return status(err); 319 ctx.prepare() catch |err| return status(err); 320 const bytes = @import("preview.zig").captureUnboundedWork(&ctx.arena, node, limit) catch |err| return status(err); 321 out_ptr.* = @ptrCast(bytes.ptr); 322 out_len.* = bytes.len; 323 return .ok; 324 } 325 326 // Full bounded result transport, distinct from the 256-node preview API. 327 fn hostSnapshotUnbounded(ctx: *ArbCtx, root: Handle, limit: u32, out_ptr: **u8, out_len: *usize) callconv(.c) Status { 328 const node = ctx.handles.get(root) catch |err| return status(err); 329 ctx.prepare() catch |err| return status(err); 330 const bytes = @import("preview.zig").snapshotUnboundedWork(&ctx.arena, node, limit) catch |err| return status(err); 331 out_ptr.* = @ptrCast(bytes.ptr); 332 out_len.* = bytes.len; 333 return .ok; 334 }