Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 75

QEMU Object Model (QOM) for Device Management

Recommended resources

Introduction

In the last lesson, we explored the low-level mechanisms of device emulation: how QEMU intercepts guest I/O through Memory-Mapped I/O (MMIO) and Port I/O (PIO), dispatches these events to C functions, and uses interrupt injection to signal back to the guest. This raises a crucial question for any complex system: how do you manage and organize the hundreds of emulated devices, their states, their interconnections, and their lifecycles in a structured way? A flat collection of callback functions would be unmanageable.

This lesson answers that question by introducing the QEMU Object Model (QOM). We will fulfill the learning outcome: Describe the QEMU Object Model (QOM) for device representation and lifecycle management. Think of QOM as the architectural blueprint or framework that brings order to the device emulation primitives we've already discussed. It's the final piece of theory we need before you implement your first device in the next lesson.


1. What is the QEMU Object Model (QOM)?

At its heart, QEMU is a large C project. C does not natively support object-oriented concepts like classes, inheritance, or polymorphism. To manage the complexity of emulating entire computer systems, the QEMU developers created their own object-oriented framework within C, known as QOM.

[PDF] Tame the (q)emu: debug firmware on custom emulated board

To start, let's get a high-level overview of QOM's purpose from the paper '[PDF] Tame the (q)emu'. This paper provides an excellent practical guide to adding a new board to QEMU, and its introduction to QOM is very clear.

Please read the section 'The QEMU Object Model' (Section 2) on page 3. Focus on how it describes QOM as a system of 'pseudo-classes' that provides modularity for representing components like CPUs, peripherals, and boards.

As the paper states, QOM provides "pseudo-classes" that can be instantiated at runtime. This allows QEMU to be built from modular, reusable components. A virt machine, for example, is composed of QOM objects representing a CPU, a memory controller, a PCIe bus, and various devices attached to that bus.

How QOM Implements "Objects" in C

QOM cleverly simulates inheritance and polymorphism using C structs and function pointers.

  • Inheritance via Nested Structs: A "subclass" is a struct whose first member is an instance of its parent "class" struct. Because C guarantees that the first member of a struct is located at offset 0, a pointer to the child struct can be safely cast to a pointer to the parent struct. This allows generic functions that operate on a parent type to also work on any child type.

  • Polymorphism via Function Pointers: Class-specific behavior (like what to do when a device is "realized" or "reset") is implemented via function pointers stored in the class struct.

Let's look at how this is structured in the code.

[PDF] Tame the (q)emu: debug firmware on custom emulated board

The same paper, '[PDF] Tame the (q)emu', has an excellent section with code listings that illustrate the nested struct technique.

Please read 'Definition of object classes in QEMU' (Section 2.2) on pages 4-7. Pay close attention to: Listing 2 and 3: See how SSIPeripheral contains a DeviceState parent, which in turn contains an ObjectClass parent. Figure 1 and 2: These diagrams are key. They visualize the nested hierarchy and the resulting 'unfolded' memory layout of the final state structure. This is the core mechanism of QOM inheritance.

To provide a more abstract view, here is a diagram illustrating the relationships between the fundamental QOM types.

QEMU Object Model (QOM) Core Class Diagram
This diagram shows the core structures of QOM. `TypeInfo` is used to register a new type. This registration process creates a `TypeImpl` structure. When a class is first used, its `class_init` function is called to create a singleton `ObjectClass` that holds class-wide data, including pointers to methods and properties. When an instance is created, an `Object` is allocated, containing a pointer to its class and a pointer to its parent in the object hierarchy.

2. Device Representation in QOM

So, how do you define a new device using this model? It involves three key components:

  1. The State Struct: This is your device's "instance data." It's a C struct that holds all the state for a single instance of your device, such as register values, a pointer to its memory region, and crucially, an instance of its parent's state struct as its first member.
    Example: struct LilboardMachineState from Listing 5 in the tame_the_qemu paper.

  2. The Class Struct and class_init Function: This holds data shared across all instances of a class. More importantly, its initialization function, _class_init(), is where you "register" your device's methods (like the realize function) and define its properties. This function is called only once, the first time the type is used.
    Example: lilboard_machine_class_init() from Listing 8, which sets the realize function pointer mc->init.

  3. The TypeInfo Struct: This is a static struct that acts as the "registration form" for your new type. You provide QEMU with the type's name, its parent's name, the size of its instance state struct, and pointers to its initialization functions (class_init and instance_init). QEMU's type_register_static() uses this to add your type to its internal type system.
    Example: lilboard_info from Listing 7.


3. Device Lifecycle Management

One of QOM's most important roles is managing the lifecycle of a device. A device isn't just allocated and ready to go. It goes through a specific sequence of states, giving the creator of the device (e.g., the machine initialization code) a chance to configure it before it becomes "live".

The primary lifecycle stages are:

  1. Instantiation: The memory for the object's state struct is allocated, and its instance_init callback is run.
  2. Configuration: The code that created the object now sets its properties (e.g., what size of RAM to use, which character device to connect to a serial port).
  3. Realization: The device's realize callback is run. This is where the device "comes to life." It acquires resources like memory regions, connects itself to buses, and registers for I/O callbacks. This step can fail.
  4. Unrealization: For hot-unpluggable devices, the unrealize callback is run to release resources and disconnect the device.

Deep Dive: instance_init() vs. realize()

A common point of confusion for new QEMU developers is what code should go in instance_init versus realize. Getting this wrong leads to subtle bugs. This distinction is also a frequent topic in advanced technical interviews.

QEMU's instance_init() vs. realize() - Red Hat People

Thomas Huth's blog post 'QEMU's instance_init() vs. realize()' is the definitive explanation of this topic. It clarifies the purpose of each function and the reasoning behind their separation.

Please read the entire blog post. It's short but dense with critical information. Focus on the core reason for the split: device introspection. The command qemu-system-x86_64 --device xyz,help instantiates a device to query its properties but does not realize it. Understanding this will make the rules for each function clear.

To summarize the critical rules from the article:

  • instance_init():

    • Purpose: Perform initial, non-failable setup. This is the place to create child objects and define dynamic properties.
    • Context: Runs before properties are set. Must not assume it's running in any particular machine or that any other devices exist.
    • Constraints: Must not fail (no abort() or exit()). Crashing here would mean a user couldn't even ask for help on a device.
    • Cleanup: Anything allocated or registered here should be cleaned up in a corresponding instance_finalize() function, because the object might be temporary.
  • realize():

    • Purpose: Make the device "live." This is where you connect to buses, map MMIO regions, request interrupts, and perform any setup that might fail.
    • Context: Runs after properties have been set. The device has been attached to a machine and can be wired to other devices.
    • Constraints: Can and should fail by returning an Error object if configuration is invalid or resources are unavailable.

Interview Scenario

An interviewer poses this problem:

"I've written a new QEMU device that connects to the host's serial port /dev/ttyS0 to communicate with a physical modem. In my device's instance_init function, I have fd = open(\"/dev/ttyS0\", O_RDWR); if (fd < 0) { exit(1); }. The device works perfectly when I boot a VM. However, my colleague reports that just running qemu-system-aarch64 -M virt --device my-modem,help crashes QEMU. What is the bug and how would you fix it?"

Model Response

  1. Identify the Root Cause: The problem is a fundamental misunderstanding of the QOM lifecycle. The command --device my-modem,help performs introspection. It creates a temporary instance of the device to query its properties (like its name, description, and available options) but it does not realize the device. The crash occurs because the failing code is in instance_init, which is executed during this introspection phase.

  2. Explain the Specific Errors: There are two errors in the instance_init function:

    • It performs a failable operation: Opening a device file can fail for many reasons (e.g., permissions, device not present). instance_init must not perform operations that can fail.
    • It calls exit(1): This violates the rule that instance_init must not fail. It terminates the entire QEMU process when the user was only asking for information.
  3. Propose the Solution: The fix is to move the logic into the correct lifecycle function.

    • The open() call and its error handling should be moved from instance_init to the device's realize function.
    • If the open() call fails within realize, it should not call exit(). Instead, it should use QEMU's error reporting mechanism to return an Error object to the caller (e.g., error_setg(errp, "Failed to open /dev/ttyS0: %s", strerror(errno));). This allows QEMU to gracefully fail the machine's startup with a clear error message, rather than crashing.
    • The instance_init function should be cleaned up to only contain non-failable setup, if any is needed.

Conclusion

In this lesson, we demystified the QEMU Object Model, the foundational framework that provides structure and scalability to QEMU's C codebase.

Key Takeaways:

  • QOM is an object-oriented framework for C that provides modularity and reusability by representing system components as objects.
  • Device Representation is achieved through a combination of a TypeInfo struct for registration, a per-instance State struct, and a per-class Class struct. Inheritance is simulated with nested structs.
  • Device Lifecycle follows a strict path: instantiation (instance_init), property configuration, and then activation (realize).
  • The distinction between instance_init (for non-failable, context-free setup) and realize (for failable, machine-specific activation) is critical and driven by QEMU's need to support device introspection without a full machine boot.

Preview of the Next Lesson:

You now understand both the low-level primitives of emulation (MMIO, interrupts) and the high-level framework for organizing devices (QOM). You are fully equipped to put theory into practice. In our next lesson, we will implement a simple emulated memory-mapped (MMIO) device in QEMU and make it accessible to a guest. You will write the TypeInfo, state struct, and lifecycle functions for a basic hardware device from scratch.