Working with Device Tree in Rust: Calling `libfdt`

Michael Zhao
3 min readFeb 25, 2023

--

This short article introduces how to work with Device Tree in Rust by calling the API of native libfdt library.

Background

Device Tree is a data structure describing the hardware components of a particular computer so that the operating system’s kernel can use and manage those components, including the CPU or CPUs, the memory, the buses and the integrated peripherals (from Wikipedia).

The core reason for the existence of Device Tree in Linux is to provide a way to describe non-discoverable hardware. This information was previously hard coded in source code. (from eLinux)

To handle the device tree in Rust, you can either:

  • Call the API provided by native library libfdt
  • Or use some kind of Rust crate

In this article I will show how to play in the first way. I will compare some different Rust crates in a later story.

Calling libfdt

DTC is the dominant tool to handle device tree in Linux world. It provides a library libfdt that offers various APIs to manipulate device tree, and a command line program dtc which consumes the libfdt.

Checking the header file libfdt.h, you can see all the API that libfdt library provides.

In a Rust program, to handle device tree, it’s a good idea to call the API of libfdt. After all the libfdt is powerful, commonly used and very stable.

In Rust, libc crate helps when you need to call native API provided by a local library that is written in C language (or others).

The following code example illustrates how to call a libfdt API fdt_create by leveraging libc:

use libc::{c_int, c_void};

// Size of the FDT blob
const FDT_MAX_SIZE: u64 = 0x20_000;

// Link to native `libfdt.so` library
#[link(name = "fdt")]
extern "C" {
fn fdt_create(buf: *mut c_void, bufsize: c_int) -> c_int;
}

fn main() {
// Allocate memory for the holding the blob
let mut fdt = vec![0; FDT_MAX_SIZE as usize];

// Call native C API for creating a FDT
let fdt_ret = unsafe { fdt_create(fdt.as_mut_ptr() as *mut c_void, FDT_MAX_SIZE as c_int) };
if fdt_ret == 0 {
println!("FDT created");
} else {
panic!("Failed to create FDT")
}
}

Some explanation of the code:

  • The library name libfdt is specified with #[link(name = "fdt")]
  • The symbols to reference must be declared in the extern "C" block
  • The call to the native function must be delimited by the unsafe block

In early stage of Cloud Hypervisor, we use this method to build the DTB for the VM. The DTB contains the information of devices that the hypervisor provides. If you are interested in how the VMM was using the libfdt, you can check the history code before this PR got merged. The PR replaced libfdt with a pure Rust crate vm-fdt.

Why we finally replaced libfdt? Because: (these are also the disadvantages of invoking native API)

  • Using a native library makes the Rust program hard to deploy. Because the program depends on something external. To run the Rust program correctly, you have to guarantee the libfdt.so has existed in a folder that LD_LIBRARY_PATH environment variable covers.
  • Another trouble in deployment is the possible difference between the building environment and the running environment. API version mismatch can make the program fail.
  • Using unsafe makes uneasiness, it brings uncertainty in runtime. But runtime certainty is one of the best things of Rust.

Reference

--

--