Driver Debugging Framework

Fuchsia provides a productionized driver debugging framework that allows developers to author, expose, and interactively execute driver-specific diagnostic and debug commands.

Architecture

The debugging framework consists of four primary components:

  1. fuchsia.driver.debug.Debug FIDL Protocol: Defines the standardized interface for listing and executing debug commands across drivers.
  2. Helper Libraries (sdk/lib/driver/debug/rust & sdk/lib/driver/debug/cpp): Simplifies the authoring of debug subcommands in Rust (via argh::SubCommands and driver_debug) and C++ (via fdf_debug::DebugServer) with argument parsing, automatic --help generation, command discovery, and request dispatching.
  3. debug CLI Utility (src/devices/bin/driver_debug): A host/target CLI tool packaged for interactive execution inside component explore.
  4. Driver Integration (driver/debug.shard.cml): DFv2 drivers include driver/debug.shard.cml in their component manifest and export fuchsia.driver.debug.Debug via ServiceFs or OutgoingDirectory in their outgoing directory (/out/svc/fuchsia.driver.debug.Debug).

FIDL Protocol (fuchsia.driver.debug)

The fuchsia.driver.debug protocol exposes two methods:

library fuchsia.driver.debug;

using zx;

const MAX_COMMAND_NAME_LENGTH uint32 = 256;
const MAX_ARG_LENGTH uint32 = 1024;
const MAX_ARG_COUNT uint32 = 128;
const MAX_DESCRIPTION_LENGTH uint32 = 1024;
const MAX_COMMAND_COUNT uint32 = 64;

type CommandInfo = table {
    1: name string:MAX_COMMAND_NAME_LENGTH;
    2: description string:MAX_DESCRIPTION_LENGTH;
};

@discoverable
open protocol Debug {
    flexible Execute(resource struct {
        args vector<string:MAX_ARG_LENGTH>:MAX_ARG_COUNT;
        stdout zx.Handle:SOCKET;
        stderr zx.Handle:SOCKET;
    }) -> (struct {
        exit_code int32;
    }) error zx.Status;

    flexible ListCommands() -> (struct {
        commands vector<CommandInfo>:MAX_COMMAND_COUNT;
    }) error zx.Status;
};

Implementing Debug Commands in a Rust Driver

1. Include the CML Shard in the Driver Manifest

driver/debug.shard.cml is not included implicitly for all drivers. Any driver that implements the fuchsia.driver.debug.Debug protocol must explicitly include "driver/debug.shard.cml" in its .cml component manifest.

This shard:

  • Declares and exposes the fuchsia.driver.debug.Debug protocol capability from self.
  • Configures the fuchsia.dash.launcher-tool-urls facet ("fuchsia-pkg://fuchsia.com/driver_debug") so the debug CLI tool is automatically available in ffx component explore.
{
    include: [
        "driver/debug.shard.cml",
        "driver_component/driver.shard.cml",
        "inspect/client.shard.cml",
        "syslog/client.shard.cml",
    ],
    program: {
        runner: "driver",
        binary: "driver/my_driver.so",
        bind: "meta/bind/my_driver.bindbc",
    },
}

2. Define Subcommands with argh

Define your subcommand structs and top-level subcommand enum with #[derive(FromArgs)] and #[argh(subcommand)]:

use argh::FromArgs;

#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "ping")]
/// Ping the driver to verify connectivity.
pub struct PingArgs {
    #[argh(option, short = 'c', default = "1", description = "number of pings")]
    pub count: u32,
}

#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "reset")]
/// Reset the hardware device state.
pub struct ResetArgs {
    #[argh(switch, description = "perform a hard reset")]
    pub hard: bool,
}

#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum MyDriverCommands {
    Ping(PingArgs),
    Reset(ResetArgs),
}

3. Handle Debug Requests

Use driver_debug::next_command in a while let loop to read parsed subcommands from a DebugRequestStream:

use fidl_fuchsia_driver_debug::DebugRequestStream;

async fn handle_debug(mut stream: DebugRequestStream) -> Result<(), fidl::Error> {
    while let Some((cmd, responder)) = driver_debug::next_command(&mut stream).await? {
        let result: Result<String, anyhow::Error> = match cmd {
            MyDriverCommands::Ping(args) => {
                let mut out = String::new();
                for i in 1..=args.count {
                    out.push_str(&format!("ping {i}\n"));
                }
                Ok(out)
            }
            MyDriverCommands::Reset(args) => {
                if args.hard {
                    // perform hardware reset
                    Ok("hard reset completed\n".to_string())
                } else {
                    Ok("soft reset completed\n".to_string())
                }
            }
        };
        responder.send(result)?;
    }
    Ok(())
}

driver_debug::next_command automatically:

  • Responds to ListCommands requests using argh::SubCommands metadata (command names and doc comments).
  • Handles --help flags and syntax/argument parsing errors on Execute requests, writing output to stdout/stderr and returning the appropriate exit code (0 for help, 2 for syntax errors).

Alternatively, you can use driver_debug::serve(stream, handler) with an async closure when you don't need custom stream loop control.

4. Serve the Protocol in the Driver

In your driver's start method:

use fidl_fuchsia_driver_debug::DebugRequestStream;
use fuchsia_component::server::ServiceFs;

let mut service_fs = ServiceFs::new();

service_fs.dir("svc").add_fidl_service(move |stream: DebugRequestStream| {
    fuchsia_async::Scope::current().spawn(async move {
        let _ = handle_debug(stream).await;
    });
});

context.serve_outgoing(&mut service_fs)?;

Implementing Debug Commands in a C++ Driver (fdf_debug::DebugServer)

C++ DFv2 drivers can use the //sdk/lib/driver/debug/cpp library to register debug commands and serve fuchsia.driver.debug.Debug.

1. Add Build Dependency

Add //sdk/lib/driver/debug/cpp to your driver's target dependencies in GN (BUILD.gn) or Bazel (BUILD.bazel):

GN (BUILD.gn):

fuchsia_cc_driver("my_driver") {
  deps = [
    "//sdk/lib/driver/component/cpp",
    "//sdk/lib/driver/debug/cpp",
  ]
}

Bazel (BUILD.bazel):

fuchsia_cc_driver(
    name = "my_driver",
    deps = [
        "//sdk/lib/driver/debug/cpp",
        "@fuchsia_sdk//pkg/driver_component_cpp",
    ],
)

2. Include Header and Declare DebugServer

In your driver class, include <lib/driver/debug/cpp/debug_server.h> and add fdf_debug::DebugServer as a member:

#include <lib/driver/component/cpp/driver_base2.h>
#include <lib/driver/component/cpp/driver_export2.h>
#include <lib/driver/debug/cpp/debug_server.h>

class MyDriver : public fdf::DriverBase2 {
 public:
  MyDriver() : fdf::DriverBase2("my_driver") {}

  zx::result<> Start(fdf::DriverContext context) override;

 private:
  fdf_debug::DebugServer debug_server_;
};

3. Register Commands and Serve in Start()

Register synchronous, asynchronous, or string-returning commands and call Serve(dispatcher(), outgoing()):

zx::result<> MyDriver::Start(fdf::DriverContext context) {
  // Register synchronous string command:
  debug_server_.RegisterCommand(
      "ping", "Ping the driver.", "[-c <count>]",
      [](cpp20::span<const std::string> args) -> fit::result<std::string, std::string> {
        int count = 1;
        for (size_t i = 0; i < args.size(); ++i) {
          if (args[i] == "-c" && i + 1 < args.size()) {
            count = std::stoi(std::string(args[++i]));
          } else {
            return fit::error("Unknown option: " + std::string(args[i]));
          }
        }
        std::string out;
        for (int i = 1; i <= count; ++i) {
          out += "ping " + std::to_string(i) + "\n";
        }
        return fit::ok(out);
      });

  // Register full ExecutionResult command:
  debug_server_.RegisterCommand(
      "reset", "Reset the driver state.", "[--hard]",
      [](cpp20::span<const std::string> args) -> fdf_debug::ExecutionResult {
        bool hard = false;
        for (const auto& arg : args) {
          if (arg == "--hard") {
            hard = true;
          }
        }
        fdf_debug::ExecutionResult res;
        res.exit_code() = 0;
        res.stdout_() = hard ? "hard reset\n" : "soft reset\n";
        res.stderr_() = "";
        return res;
      });

  // Register asynchronous command:
  debug_server_.RegisterCommand(
      "async_action", "Run async diagnostic test.",
      [this](cpp20::span<const std::string> args,
             fit::callback<void(fdf_debug::ExecutionResult)> completer) {
        // Run asynchronous workload and invoke completer when done
      });

  // Serve Debug protocol on the driver's outgoing directory:
  zx::result result = debug_server_.Serve(dispatcher(), outgoing());
  if (result.is_error()) {
    return result.take_error();
  }

  return zx::ok();
}

FUCHSIA_DRIVER_EXPORT2(MyDriver);

Using the debug CLI in component explore

When debugging a running system, use ffx component explore to drop into the driver component namespace:

$ ffx component explore /bootstrap/boot-drivers:my-driver

List Available Commands

$ debug list-commands
COMMAND              DESCRIPTION
-------              -----------
ping                 Ping the driver to verify connectivity.
reset                Reset the hardware device state.

Execute Commands

$ debug ping -c 3
ping 1
ping 2
ping 3
$ debug reset --hard
hard reset completed

Help and Diagnostics

$ debug --help
Usage: debug <command> [<args>]

Driver debug commands.

Options:
  --help            display usage information

Commands:
  ping              Ping the driver to verify connectivity.
  reset             Reset the hardware device state.

Return Codes

  • 0: Success (or --help output printed to standard output).
  • 1: Driver internal execution error / handler failure (details in standard error).
  • 2: Syntax or argument parsing error (details in standard error).