I’m a robotics masters student, and I use ROS 2 (the “Robot Operating System”) constantly. I’m dogfooding a Zig wrapper for RCL (the “ROS Client Library”), along with some surrounding core libraries for a few reasons:
- The Zig dev experience makes writing reliable, performant code (the kind needed in robotics) much easier than C++. Zig is a solid foundation, most of what is missing are libraries.
- Sidestepping CMake to build packages with Zig instead. The Zig build system is preferable for dependency management compared to centralized package managers. Pip and CMake have burned me too many times when using third-party code.
- Zig will be big for robotics when the matrix and GPU features are mature. An RCL library should be ready when that time comes.
Compare Zig to C++ code for an (extremely) basic service and client:
C++ Service vs Zig
C++ Code
//! Demonstrates usage of high-level RCL APIs: executor with service and client
const std = @import("std");
const rcl = @import("rclzig");
const example_interfaces = @import("example_interfaces");
const AddTwoInts = example_interfaces.srv.AddTwoInts;
const AddService = rcl.Service(AddTwoInts);
fn serveAddTwoInts(userdata: ?*anyopaque, request: *const AddTwoInts.Request, response: *AddTwoInts.Response) anyerror!void {
const node: *rcl.Node = @ptrCast(@alignCast(userdata));
response.sum = request.a +% request.b;
node.log(@src(), .info, "Serving request {} + {}", .{request.a, request.b});
}
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
try rcl.init(gpa, init.io, init.minimal.args);
defer rcl.shutdown();
var node = try rcl.initNode("sumservice", "", .{});
defer node.deinit();
var service = try AddService.init(&node, "add", .{
.callback = &serveAddTwoInts,
.userdata = &node,
});
defer service.deinit(&node);
const executor = try rcl.Executor.create(node.getContext(), .{.threads = null}); // single-threaded
defer executor.destroy();
try executor.spinObjects(.{&service}, .{});
}
C++ Client vs Zig
C++ Code
//! Demonstrates usage of high-level RCL APIs: executor with service and client
const std = @import("std");
const rcl = @import("rclzig");
const example_interfaces = @import("example_interfaces");
const AddTwoInts = example_interfaces.srv.AddTwoInts;
const AddClient = rcl.Client(AddTwoInts);
var random = std.Random.DefaultPrng.init(0);
fn requestAddTwoInts(userdata: ?*anyopaque) anyerror!void {
const client: *AddClient = @ptrCast(@alignCast(userdata));
var req: AddTwoInts.Request = try client.initRequest();
req.a = @bitCast(random.next() % 32);
req.b = @bitCast(random.next() % 32);
_ = try client.sendRequestOwned(req);
}
fn responseAddTwoInts(userdata: ?*anyopaque, query: *const AddClient.Query) anyerror!void {
const node: *rcl.Node = @ptrCast(@alignCast(userdata));
node.log(@src(), .info, "Got sum: {} + {} = {}", .{query.request.a, query.request.b, query.response.sum});
}
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
try rcl.init(gpa, init.io, init.minimal.args);
defer rcl.shutdown();
var node = try rcl.initNode("sumservice", "", .{});
defer node.deinit();
var client = try AddClient.init(&node, "add", .{
.callback = &responseAddTwoInts,
.userdata = &node,
});
defer client.deinit(&node);
var clock = try rcl.Clock.init(.steady, .{});
defer clock.deinit();
var timer = try rcl.Timer.init(node.getContext(), &clock, rcl.time.hz(100), .{
.callback = &requestAddTwoInts,
.userdata = &client,
});
defer timer.deinit();
const executor = try rcl.Executor.create(node.getContext(), .{.threads = null}); // single-threaded
defer executor.destroy();
try executor.spinObjects(.{&client, &timer}, .{});
}
CMake vs build.zig
CMake Code
const std = @import("std");
const ros = @import("rclzig"); // import rclzig dependency build script
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const rclzig_dep = b.dependency("rclzig", .{});
const rclzig = rclzig_dep.module("rclzig");
// EXAMPLE PACKAGE SETUP
const sumservice = b.addExecutable(.{
.name = "sumservice",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("sumservice.zig"),
})
});
sumservice.root_module.addImport("rclzig", rclzig);
const sumclient = b.addExecutable(.{
.name = "sumclient",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("sumclient.zig"),
})
});
sumclient.root_module.addImport("rclzig", rclzig);
// NOTE: ROS dependency management is still in-progress
const example_pkg = ros.Package{
.target = target,
.optimize = optimize,
.artifacts = &.{sumservice, sumclient},
.dependencies = &.{ros.Dependency{
.package = "example_interfaces",
.srvs = &.{"AddTwoInts"},
}},
};
ros.installPackage(b, example_pkg);
// interface with CMake to be compatible with ROS-native build tools
_ = ros.generateCMakeStep(b, .{});
}
I’ll publish the codebase when my build system and rcl_action interface is more mature, and when my migration to 0.16 is done. I’d love to hear your thoughts on where Zig fits into robotics!
P.S.
I’ve seen several other “rclzig” or “ROS+Zig” libraries online, but they don’t fit my needs:
jacobperron’s rclzig: untouched for a few years, missing integration with key parts of the ROS software ecosystem.
zig-robotics’ zigros: attempts to replace the Colcon+CMake build system for C/C++ with Zig. I see the value, but this will ultimately compete with Colcon instead of working with it. Also, my goal is to write ROS nodes in Zig, which has a different set of requirements.