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