How to Add a New CDP Domain to Lightpanda: A Step-by-Step Guide
To add a new CDP domain to Lightpanda, create a domain module in src/cdp/domains/<Domain>.zig implementing the processMessage function, then register it in src/cdp/cdp.zig by extending the length-based switch dispatcher using the asUint helper.
Lightpanda implements the Chrome DevTools Protocol (CDP) by routing JSON-RPC commands to specialized domain modules. Adding a new CDP domain requires implementing the message processing logic and wiring it into the central dispatcher located in src/cdp/cdp.zig.
Creating the Domain Module
Start by creating a new file in src/cdp/domains/<YourDomain>.zig. This file must export a processMessage function that parses the action, validates parameters, and dispatches to the appropriate handler.
The standard pattern uses std.meta.stringToEnum to match the action name, cmd.params to deserialize JSON parameters, and cmd.sendResult to return responses:
const std = @import("std");
const log = @import("../log.zig");
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { echo }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.echo => {
const params = (try cmd.params(struct {
message: []const u8,
})) orelse return error.InvalidParams;
try cmd.sendResult(.{ .message = params.message }, .{});
},
}
}
Key implementation details from the Lightpanda source code:
- Action parsing: Use an anonymous enum (e.g.,
enum { echo }) withstd.meta.stringToEnumto convert thecmd.input.actionstring into a typed value. - Parameter extraction: The
cmd.paramsmethod takes a struct type reflecting the expected JSON schema and returns an optional pointer. - Response handling: Use
cmd.sendResultfor success responses andcmd.sendErrorfor protocol errors.
Registering the Domain in the Dispatcher
Edit src/cdp/cdp.zig and locate the dispatchCommand function. Lightpanda uses a compile-time optimized dispatcher that matches domain names by their byte length to avoid runtime string comparisons.
For a domain named "Example" (7 characters), extend the existing case 7 block using the asUint helper to convert the string into a u56 integer:
7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
asUint(u56, "Runtime") => return @import("domains/runtime.zig").processMessage(command),
asUint(u56, "Example") => return @import("domains/example.zig").processMessage(command),
else => {},
},
The asUint macro (defined at lines 13-15 in src/cdp/cdp.zig) enables compile-time constant generation. The integer width must match the domain length (e.g., u56 for 7 bytes, u24 for 3 bytes).
Testing the New Domain
Add unit tests in src/cdp/testing.zig to validate the domain's behavior. The testing harness provides testing.context() and assertion methods like expectSentResult:
test "cdp: Example.echo" {
var ctx = testing.context();
defer ctx.deinit();
try ctx.processMessage(.{
.id = 1,
.method = "Example.echo",
.params = .{ .message = "hello world" },
});
try ctx.expectSentResult(.{ .message = "hello world" }, .{ .id = 1, .index = 0 });
}
Run the full test suite to verify integration:
zig build test
Architectural Details
The CDP implementation in src/cdp/cdp.zig (lines 22-38) extracts the domain name from the method string ("<Domain>.<Action>") and routes commands via the dispatchCommand function. Each domain module follows a consistent contract:
- File location:
src/cdp/domains/<name>.zig - Entry point:
processMessage(cmd: anytype) !void - Error handling: Return
error.UnknownMethodfor unrecognized actions orerror.InvalidParamsfor malformed input
Existing domains like Runtime (src/cdp/domains/runtime.zig) and Network serve as reference implementations for the pattern.
Summary
- Create a new domain file in
src/cdp/domains/<Domain>.zigimplementingprocessMessagewith action enum parsing and parameter validation. - Register the domain in
src/cdp/cdp.zigby adding a case to the length-basedswitchindispatchCommandusing the appropriateasUintwidth. - Write tests in
src/cdp/testing.zigusing thetesting.context()harness and runzig build testto verify. - Document the domain's methods and parameters for downstream developers.
Frequently Asked Questions
What is the maximum length for a CDP domain name in Lightpanda?
The dispatcher supports domain names up to the maximum integer width used in the switch cases (typically u56 for 7 characters or u64 for 8 characters). For longer domain names, you would need to extend the dispatchCommand switch with a new case block using a wider integer type (e.g., u64 for 8 bytes) following the existing pattern in src/cdp/cdp.zig.
Why does Lightpanda use length-based matching instead of string comparison?
The dispatcher converts domain name bytes into integers (e.g., u56 for 7-character names) and matches them at compile time using the asUint helper. This eliminates runtime string hashing and comparison overhead, resulting in faster command dispatch with zero-allocation parsing for the domain routing logic.
How do I handle errors in a new CDP domain?
Return error.UnknownMethod when std.meta.stringToEnum fails to match the action, and error.InvalidParams when cmd.params returns null (indicating missing or malformed JSON parameters). The dispatcher automatically converts these into proper CDP error responses via cmd.sendError.
Is it required to add tests in src/cdp/testing.zig?
While not strictly enforced by the compiler, the Lightpanda repository requires comprehensive test coverage for all CDP domains. You should add tests in src/cdp/testing.zig or create a dedicated test file that imports the testing harness. Use testing.context() to simulate CDP sessions and expectSentResult to verify JSON responses.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →