Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ipc-codegen/examples/zig/echo/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ pub fn build(b: *std.Build) void {
});
const msgpack_mod = msgpack_dep.module("msgpack");

const ipc_runtime_dep = b.dependency("ipc_runtime", .{
.target = target,
.optimize = optimize,
});
const ipc_runtime_mod = ipc_runtime_dep.module("ipc_runtime");

// Echo server
const server_exe = b.addExecutable(.{
.name = "echo_server",
Expand All @@ -20,6 +26,7 @@ pub fn build(b: *std.Build) void {
}),
});
server_exe.root_module.addImport("msgpack", msgpack_mod);
server_exe.root_module.addImport("ipc_runtime", ipc_runtime_mod);
b.installArtifact(server_exe);

// Echo client
Expand All @@ -32,5 +39,6 @@ pub fn build(b: *std.Build) void {
}),
});
client_exe.root_module.addImport("msgpack", msgpack_mod);
client_exe.root_module.addImport("ipc_runtime", ipc_runtime_mod);
b.installArtifact(client_exe);
}
5 changes: 5 additions & 0 deletions ipc-codegen/examples/zig/echo/build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
.url = "https://github.com/zigcc/zig-msgpack/archive/refs/tags/0.0.17.tar.gz",
.hash = "zig_msgpack-0.0.14-evvueL5SBQACmim6j6klQ9wWIIG_UxGlPvVYdiNy0KT8",
},
// ipc-runtime/zig: UDS + MPSC-SHM transport binding (compiles the
// shared C++ sources via Zig's clang).
.ipc_runtime = .{
.path = "../../../../ipc-runtime/zig",
},
},
.paths = .{
"build.zig",
Expand Down
16 changes: 11 additions & 5 deletions ipc-codegen/examples/zig/echo/echo_client.zig
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/// Echo IPC client (Zig) — uses GENERATED typed client + UDS backend.
/// Echo IPC client (Zig) — uses GENERATED typed client + the ipc-runtime
/// Zig binding for transport. No per-service UDS code in the example.
/// Usage: echo_client --socket /tmp/echo.sock
const std = @import("std");
const ipc_runtime = @import("ipc_runtime");
const echo_client = @import("generated/echo_client.zig");
const uds_backend = @import("generated/uds_backend.zig");
const types = @import("generated/echo_types.zig");

pub fn main() !void {
var args = std.process.args();
_ = args.next();
var socket_path: ?[]const u8 = null;
var socket_path: ?[:0]const u8 = null;
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--socket")) {
socket_path = args.next();
Expand All @@ -19,8 +20,13 @@ pub fn main() !void {
std.process.exit(1);
};

var backend = try uds_backend.UdsBackend.connect(path);
const EchoClient = echo_client.Client(uds_backend.UdsBackend);
// Use page_allocator: the codegen-emitted client frees response buffers
// with std.heap.page_allocator, so the runtime Client must allocate with
// the same one.
var backend = try ipc_runtime.Client.fromPath(std.heap.page_allocator, path);
defer backend.deinit();

const EchoClient = echo_client.Client(ipc_runtime.Client);
var client = EchoClient.init(&backend);

// Test 1: EchoBytes
Expand Down
98 changes: 74 additions & 24 deletions ipc-codegen/examples/zig/echo/echo_server.zig
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
/// Echo IPC server (Zig) — uses GENERATED types + IPC server template.
/// Hand-written dispatch + echo handlers.
/// Echo IPC server (Zig) — uses the ipc-runtime Zig binding for transport
/// and codegen-emitted types for msgpack encode/decode of payloads.
/// Usage: echo_server --socket /tmp/echo.sock
const std = @import("std");
const ipc_runtime = @import("ipc_runtime");
const msgpack = @import("msgpack");
const Payload = msgpack.Payload;
const types = @import("generated/echo_types.zig");
const ipc_server = @import("generated/ipc_server.zig");

const alloc = std.heap.page_allocator;

// Per-request scratch buffer. The runtime expects the handler's returned slice
// to remain valid until the next call, so we keep one buffer that the handler
// reuses each iteration.
var resp_scratch: ?[]u8 = null;

pub fn main() !void {
var args = std.process.args();
_ = args.next();
var socket_path: ?[]const u8 = null;
var socket_path: ?[:0]const u8 = null;
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--socket")) {
socket_path = args.next();
Expand All @@ -23,41 +28,86 @@ pub fn main() !void {
return error.InvalidArgument;
};

try ipc_server.serve(path, dispatch);
var server = try ipc_runtime.Server.fromPath(path);
defer server.deinit();
try server.listen();
std.debug.print("ipc-server(zig): listening on {s}\n", .{path});

server.run(*u8, undefined, handle);
}

fn handle(_: *u8, _: i32, req: []const u8) []u8 {
// Free the previous response (the runtime has already copied it out).
if (resp_scratch) |prev| alloc.free(prev);
resp_scratch = null;

var reader = std.Io.Reader.fixed(req);
var packer = msgpack.PackerIO.init(&reader, undefined);
const request = packer.read(alloc) catch return makeError("decode failed");

const outer_len = request.getArrLen() catch return makeError("expected outer array");
if (outer_len != 1) return makeError("expected outer array of size 1");

const inner = request.getArrElement(0) catch return makeError("expected [name, payload]");
const inner_len = inner.getArrLen() catch return makeError("expected [name, payload]");
if (inner_len != 2) return makeError("expected [name, payload]");

const cmd_name = (inner.getArrElement(0) catch return makeError("missing cmd name")).asStr() catch return makeError("cmd name not a string");
const fields = inner.getArrElement(1) catch return makeError("missing fields");

const resp = dispatch(cmd_name, fields) catch return makeError("dispatch failed");
return resp;
}

fn dispatch(cmd_name: []const u8, fields: Payload) ipc_server.DispatchResult {
// Shutdown
fn dispatch(cmd_name: []const u8, fields: Payload) ![]u8 {
if (std.mem.eql(u8, cmd_name, "EchoShutdown")) {
return .{ .resp_name = "EchoShutdownResponse", .resp_payload = Payload.mapPayload(alloc) };
return try packResponse("EchoShutdownResponse", Payload.mapPayload(alloc));
}

// EchoBytes — echo back
if (std.mem.eql(u8, cmd_name, "EchoBytes")) {
const cmd = types.EchoBytes.fromPayload(fields) catch return makeError("deser failed");
const cmd = try types.EchoBytes.fromPayload(fields);
const resp = types.EchoBytesResponse{ .data = cmd.data };
return .{ .resp_name = "EchoBytesResponse", .resp_payload = resp.toPayload(alloc) };
return try packResponse("EchoBytesResponse", try resp.toPayload(alloc));
}

// EchoFields — echo back
if (std.mem.eql(u8, cmd_name, "EchoFields")) {
const cmd = types.EchoFields.fromPayload(fields) catch return makeError("deser failed");
const cmd = try types.EchoFields.fromPayload(fields);
const resp = types.EchoFieldsResponse{ .a = cmd.a, .b = cmd.b, .name = cmd.name };
return .{ .resp_name = "EchoFieldsResponse", .resp_payload = resp.toPayload(alloc) };
return try packResponse("EchoFieldsResponse", try resp.toPayload(alloc));
}

// EchoNested — echo back
if (std.mem.eql(u8, cmd_name, "EchoNested")) {
const cmd = types.EchoNested.fromPayload(fields) catch return makeError("deser failed");
const cmd = try types.EchoNested.fromPayload(fields);
const resp = types.EchoNestedResponse{ .inner = cmd.inner };
return .{ .resp_name = "EchoNestedResponse", .resp_payload = resp.toPayload(alloc) };
return try packResponse("EchoNestedResponse", try resp.toPayload(alloc));
}
return makeErrorBytes("unknown command");
}

return makeError("unknown command");
fn packResponse(name: []const u8, payload: Payload) ![]u8 {
// Wire format: [responseName, {payload}]
var arr = try Payload.arrPayload(2, alloc);
try arr.setArrElement(0, try Payload.strToPayload(name, alloc));
try arr.setArrElement(1, payload);

var writer = std.Io.Writer.Allocating.init(alloc);
defer writer.deinit();
var packer = msgpack.PackerIO.init(undefined, &writer.writer);
try packer.write(arr);
const bytes = try writer.toOwnedSlice();
resp_scratch = bytes;
return bytes;
}

fn makeError(message: []const u8) []u8 {
return makeErrorBytes(message) catch {
// Last-ditch: return a fixed empty bytes (the runtime treats len=0 as
// an empty response; that's acceptable in this catastrophic path).
const empty = alloc.alloc(u8, 0) catch unreachable;
resp_scratch = empty;
return empty;
};
}

fn makeError(message: []const u8) ipc_server.DispatchResult {
fn makeErrorBytes(message: []const u8) ![]u8 {
var err_map = Payload.mapPayload(alloc);
err_map.mapPut("message", Payload.strToPayload(message, alloc) catch return .{ .resp_name = "EchoErrorResponse", .resp_payload = Payload.mapPayload(alloc) }) catch {};
return .{ .resp_name = "EchoErrorResponse", .resp_payload = err_map };
try err_map.mapPut("message", try Payload.strToPayload(message, alloc));
return try packResponse("EchoErrorResponse", err_map);
}
11 changes: 6 additions & 5 deletions ipc-codegen/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,21 +384,22 @@ function generate(args: Args) {
`${toSnakeCase(prefix)}_server.zig`,
gen.generateServer(compiled),
);
copyTemplate("zig", "ipc_server.zig", absOut);
// No transport template copy — consumers wire @import("ipc_runtime")
// (the Zig binding shipped from ipc-runtime/zig/) and use its
// Server.fromPath / listen / run loop directly.
}
if (args.client) {
writeFile(
`${toSnakeCase(prefix)}_client.zig`,
gen.generateClient(compiled),
);
}
// Backend trait template (kept; FFI backend still references it).
// Backend trait — keep so FFI consumers can plug in their own
// implementation. ipc_runtime.Client satisfies the same contract,
// so UDS/SHM consumers don't need a separate backend file.
if (args.uds || args.ffi) {
copyTemplateOnce("zig", "backend.zig", absOut);
}
if (args.uds) {
copyTemplateOnce("zig", "uds_backend.zig", absOut);
}
if (args.ffi) {
copyTemplateOnce("zig", "ffi_backend.zig", absOut);
}
Expand Down
Loading
Loading