16.1 Modeling with Python (pyobj) 16.3 Migrating to confclass
Model Builder User's Guide  /  II Device Modeling  /  16 Modeling with Python  / 

16.2 Modeling with Python (confclass)

This section describes how to model devices using Python and the confclass class.

16.2.1 Overview

A custom class always starts by creating a confclass object. The confclass class is available in the simics module. This is the shortest possible custom class based on confclass:

class MyDevice:
    cls = simics.confclass(classname="my_device")

This will create a conf class named my_device, and allow creation of Simics objects, for example by using SIM_set_configuration or SIM_create_object.

To further develop a custom class, the confclass object (cls) is then used to:

  1. create hierarchical items like:
  2. specify special functions using decorators:

Note that hierarchical items also contains decorators, which express hierarchical significance, as will be explained in the attributes and the port objects sections.

16.2.2 Creating an example Device Class

A complete Simics module with a device class based on confclass can be created using the project-setup script:

This will create skeleton code for a new device in the [project]/modules/my-confclass-device/ directory, with all files needed to build it as a Simics module. The entry point for a module written in Python is the file [project]/modules/module_load.py, which is executed when the Simics module is loaded. This file is normally small, containing something like:

from . import my_device_confclass

The rest of the implementation is given in another file, in this case [project]/modules/my_device_confclass.py.

16.2.3 Creating a Device Class

The confclass class is available from the simics module. Here is the shortest possible Python device created with confclass:

class MyDevice:
    cls = simics.confclass(classname="my_device")

The complete list of confclass creation arguments is:

ArgumentDesc
classnamethe name of the device class
parentused to specify parent class when using inheritance (see the Inheritance section)
pseudoset to True to make a Sim_Class_Kind_Pseudo class (see class_info_t)
registerset to False to prevent registration (see the Prevent registration section)
short_docshort documentation, see next section
doclong documentation, see next section

16.2.4 Class Documentation

This is code for my_class class, but this time with added documentation.

class MyDevice:
    cls = simics.confclass(
        classname="my_device",
        short_doc="one-line documentation",
        doc="Long documentation.")

The added optional arguments in the example are:

16.2.5 Overriding Conf Object Methods

Most functions in the class_info_t struct that is passed to the SIM_create_class (see also the Object Initialization section) can be optionally be overridden using decorators provided by the cls:

class MyDevice:
    cls = simics.confclass(classname="my_device")

    def __init__(self):
        self.calls = []

    @cls.init
    def initialize(self):
        self.calls.append("initialize")

    @cls.finalize
    def finalize(self):
        self.calls.append("finalize")

    @cls.objects_finalized
    def objects_finalized(self):
        self.calls.append("objects_finalized")

    @cls.deinit
    def deinit(self):
        global deinit_called
        deinit_called = True

16.2.6 Accessing the Python Object

Inside the class, the self argument to object methods contains an instance of the Python class, which is expected since it is a Python object. This is also true for Simics callback methods, such as the object init callback in this example:

class MyDevice:
    cls = simics.confclass(classname="my_device")
    def __init__(self):
        assert isinstance(self, MyDevice)

    @cls.init
    def init(self):
        assert isinstance(self, MyDevice)

On an instance of the Simics class, the instance of MyDevice is available as <device>.object_data:

device = simics.SIM_create_object("my_device", "device")
assert isinstance(device.object_data, MyDevice)

This is another example showing access to the variable local from the Simics object device, and object_data.

class MyDevice:
    cls = simics.confclass(classname="my_device")
    @cls.init
    def init(self):
        self.local = 1

device = simics.SIM_create_object("my_device", "device")
assert device.object_data.local == 1

Confclass automatically sets the obj-member of the Python object to the corresponding configuration object (conf_object_t). In this example, self.obj is used with SIM_object_name, which takes a configuration object / conf_object_t as argument, to print the object name.

class MyDevice:
    cls = simics.confclass(classname="my_device")
    @cls.objects_finalized
    def objects_finalized(self):
        print(simics.SIM_object_name(self.obj))
device = simics.SIM_create_object("my_device", "device")

16.2.7 Attributes

Attributes are created by adding them to the namespace under <confclass obj>.attr, for example <confclass obj>.attr.<my_attribute>. In this example, we create an attribute v1 with the attribute type "i":

class MyDevice:
    cls = simics.confclass(classname="my_device")
    cls.attr.v1("i")
# Create an object named 'device', with default value 1 for v1.
device = simics.SIM_create_object(
    MyDevice.cls.classname, "device", [["v1", 1]])
stest.expect_equal(device.v1, 1)

To specify custom attribute getters and setters, decorate the custom function with the getter or setter decorators in the attribute object. Using the previous example, an overridden getter function should then be decorated with @cls.attr.v1.getter, and a setter method would decorated with @cls.attr.v1.setter

    @cls.attr.v1.getter
    def get_v1(self):
        return self.v1
    @cls.attr.v1.setter
    def set_v1(self, new_value):
        self.v1 = new_value

Note that the decorators are taken hierarchically from cls.attr.v1, and that the backing value can be accessed with self.v1.

To document the attribute, set the doc argument. This corresponds to the desc argument in SIM_register_attribute.

As is documented in the Modelling with C section and in the API Reference Manual (SIM_register_attribute), attributes can have different properties that can be controlled with the attr_attr_t attr in SIM_register_attribute. For confclass, the corresponding argument is kind, but there are more convenient to setting this to combinations of attr_attr_t.

As has been mentioned before, kind defaults to Sim_Attr_Required, but when using the optional arguments, the mutually exclusive (bits in 0x7) parts of kind (attr_attr_t) will be updated according to the arguments used:

The reason why pseudo must be set to true when setting either read_only or write_only to true is that attributes that are saved to checkpoints (for example if Sim_Attr_Optional is set) must have be both readable and writeable.

In the following example various attributes are created:

class MyDevice:
    cls = simics.confclass(classname="my_device")

    # Add required attribute.
    cls.attr.attr0("i")

    # Add an optional attribute by setting 'default' with documentation.
    cls.attr.attr1("i", default=1, doc="documentation")

    # Add a read-only attribute.
    cls.attr.attr2("i", read_only=True, default=2, pseudo=True)

    # Add a write-only attribute.
    cls.attr.attr3("i", write_only=True, pseudo=True)

    # Add a pseudo attribute.
    cls.attr.attr4("i", pseudo=True)

    # Create a required attribute with custom access methods below.
    # The access methods use the decorators from the attribute:
    # - @cls.attr.attr5.getter
    # - @cls.attr.attr5.setter
    cls.attr.attr5("i")

    # Use the getter decorator for 'attr5'
    @cls.attr.attr5.getter
    def get_attr5(self):
        return self.attr5

    # Use the setter decorator for 'attr5'
    @cls.attr.attr5.setter
    def set_attr5(self, new_value):
        self.attr5 = new_value

16.2.8 Info and Status Commands

The info and status commands can easily be added by using a decorator to the method that provides the info or status data. In the below example:

class MyDevice:
    cls = simics.confclass(classname="my_device")

    @cls.init
    def init(self):
        self.info_value = 1
        self.status_value = 2

    @cls.command.info
    def get_info(self):
        return [("Info", [("value", self.info_value)])]

    @cls.command.status
    def get_status(self):
        return [("Status", [("value", self.status_value)])]

16.2.9 Prevent Registration

By default a class created using confclass is automatically registered as a Simics class. To prevent the registration, the argument register can be set to False. At a later stage, use the register function to register the class.

class MyDevice:
    cls = simics.confclass(classname="my_device", register=False)

16.2.10 Interfaces

Interfaces are implemented using cls.iface.<name> where <name> is the name of the Simics interface. In this example we use the signal interface, and use the cls.iface.signal.signal_raise and cls.iface.signal.signal_lower to implement the required methods.

class MyDevice:
    cls = simics.confclass(classname="my_device")
    cls.attr.signal_raised("b", default=False)

    @cls.iface.signal.signal_raise
    def signal_raise(self):
        self.signal_raised = True

    @cls.iface.signal.signal_lower
    def signal_lower(self):
        self.signal_raised = False

16.2.11 Port Objects

Port objects allow a class to have several implementations of the same interface (see example). Port objects are added using the <parent confclass>.o namespace, for example <parent confclass>.o.port.port1(). This gives another confclass instance which can be used to add attributes and interfaces to the port object, just like it is done with the parent's confclass.

16.2.11.1 Basic Examples

When adding the confclass for the port object, the classname argument can be used or not used to achieve different results:

  1. By not specifying classname, a class name will be assigned based on the parent class name and the last component of the hierarchical name.

    class Parent1:
        parent = simics.confclass(classname='parent1')  # the parent confclass
        child = parent.o.port.child()  # default classname
    

    Since the child/port objects lacks a classname, it will automatically be assigned the name parent1.child.

    obj1 = SIM_create_object('parent1', 'obj1', [])
    stest.expect_equal(obj1.port.child.classname, 'parent1.child')
    
  2. It is also possible to specify the last part of a custom class name, by setting classname to a string that starts with a dot, followed by a valid class name.

    class Parent2:
        parent = simics.confclass(classname='parent2')  # the parent confclass
        child = parent.o.port.child('.custom_child')  # name starting with dot
    

    The parent class name will automatically be prepended to the custom class name.

    obj2 = SIM_create_object('parent2', 'obj2', [])
    stest.expect_equal(obj2.port.child.classname, 'parent2.custom_child')
    
  3. Finally, it is also possible to specify an existing class.

    class Child3:
        cls = simics.confclass(classname='child3')
    
    class Parent3:
        parent = simics.confclass(classname='parent3')  # the parent confclass
        child = parent.o.port.child(classname='child3')  # child3 defined above
    

    The child port object will have the child3 class, and will get an instance of the Child3 as object_data.

    obj3 = SIM_create_object('parent3', 'obj3', [])
    stest.expect_equal(obj3.port.child.classname, 'child3')
    stest.expect_true(isinstance(obj3.port.child.object_data, Child3))
    

An important difference between these examples is the object_data of the port object. In the first two examples, the object_data will be set to the parent objects object_data, since there is no custom Python class, while in the last example, the object_data will be set to an instance of the Python class Class3, associated with child3. This is important, since the state for the port object will be separate from the parent object. The storage for attributes will be separate, and interface implementations will not get direct access to the parent's object_data and must use SIM_port_object_parent(self.obj).object_data to access the parent's state.

16.2.11.2 Add an Interface

To add an interface, use the reference to the port objects confobject. The history member can be accessed with self.history from methods decorated with both parent and child. The reason for this is that the child class does not have a custom Python class and will hence share object_data with the parent class.

class Parent:
    parent = simics.confclass(classname="parent")
    child = parent.o.port.child()

    @parent.init
    def init(self):
        self.history = []

    @child.iface.signal.signal_raise
    def signal_raise(self):
        self.history.append("raise")

    @child.iface.signal.signal_lower
    def signal_lower(self):
        self.history.append("lower")

Call the signal interface on the child/port interface and verify the contents of history.

obj = SIM_create_object("parent", "obj")
obj.port.child.iface.signal.signal_lower()
obj.port.child.iface.signal.signal_raise()
stest.expect_equal(obj.object_data.history, ["lower", "raise"])

16.2.11.3 Add an Attribute

Replace the member variable history in the previous example with an attribute.

class Parent:
    parent = simics.confclass(classname="parent")
    child = parent.o.port.child()
    child.attr.history("[s*]", default=[])

    @child.iface.signal.signal_raise
    def signal_raise(self):
        self.history.append("raise")

    @child.iface.signal.signal_lower
    def signal_lower(self):
        self.history.append("lower")

Call the signal interface on the child/port interface and verify the contents of the history variable.

obj = SIM_create_object("parent", "obj")
obj.port.child.iface.signal.signal_lower()
obj.port.child.iface.signal.signal_raise()
stest.expect_equal(obj.port.child.history, ["lower", "raise"])

Because the object_data is shared between parent and child, history is accessible from the parent object as well.

stest.expect_equal(obj.object_data.history,  ["lower", "raise"])

16.2.11.4 Attribute Conflicts

As mentioned above, the object_data (self) of the port object is set to either:

If the object_data is shared with the parents, the attributes are required to be unique. In this example attribute a exists in both the parent and port object, which will result in a raised exception during class definition.

try:
    class Parent1:
        parent = simics.confclass(classname="parent1")  # the parent confclass
        parent.attr.a("i", default=1)
        child = parent.o.port.child()  # the child/port confclass
        child.attr.a("i", default=2)  # name collision for attribute "a"
except TypeError:
    pass  # expected
else:
    stest.fail("Did not get TypeError for colliding attributes.")

To correct the situation, add a Python class to the port object. This will result in that the parent and the port objects will get unique object_data.

class Child2:
    cls = simics.confclass(classname="child2")
    cls.attr.a("i", default=2)

class Parent2:
    parent = simics.confclass(classname="parent2")
    parent.attr.a("i", default=1)
    child = parent.o.port.child(classname=Child2.cls.classname)

By providing a custom Python class for the port object, the port object will get a non-shared unique object_data (an instance of Child), thus eliminating the attribute collision between the parent and the port object.

parent = simics.SIM_create_object("parent2", "parent")
stest.expect_different(parent.object_data, parent.port.child.object_data)
stest.expect_equal(parent.a, 1)
stest.expect_equal(parent.port.child.a, 2)

16.2.11.5 Advanced Port Object Examples

16.2.11.5.1 Two Interfaces

In the below example, my_device has two port objects with different implementations of the signal interface.

class MyDevice:
    """This class contains two implementations of the 'signal' interface
    implemented in two different port objects."""
    RAISE = "raise"
    LOWER = "lower"

    cls = simics.confclass(classname="my_device")
    reset_1 = cls.o.port.RESET_1()
    reset_2 = cls.o.port.RESET_2()
    cls.attr.history("[[ss]*]", default=[])

    @reset_1.iface.signal.signal_raise
    def reset_1_raise(self):
        self.signal_raise_common("RESET_1")

    @reset_1.iface.signal.signal_lower
    def reset_1_lower(self):
        self.signal_lower_common("RESET_1")

    @reset_2.iface.signal.signal_raise
    def reset_2_raise(self):
        self.signal_raise_common("RESET_2")

    @reset_2.iface.signal.signal_lower
    def reset_2_lower(self):
        self.signal_lower_common("RESET_2")

    def signal_raise_common(self, port):
        self.history.append([port, self.RAISE])

    def signal_lower_common(self, port):
        self.history.append([port, self.LOWER])

16.2.11.6 Using an existing class

To specify an existing class, set classname to an existing class. This way, an interface implementation can be added from another class.

class Resetter:
    """A class implementing the 'signal' interface."""
    cls = simics.confclass(classname="resetter")
    cls.attr.raised("b", default=False)
    cls.iface.signal()

    @cls.iface.signal.signal_raise
    def signal_raise(self):
        self.raised = True

class MyDevice:
    """Set the class 'resetter' as port object class to get the 'signal'
    interface (implemented by 'resetter')."""
    cls = simics.confclass(classname="my_device")
    reset = cls.o.port.RESET(classname="resetter")

16.2.12 Port Interfaces

Port interfaces is a legacy feature that allows a class to have several implementations of the same interface. The recommended way to do this is to use Port Objects.

When a port interface has been defined, it can be retrieved from the object with SIM_get_port_interface. Below is an example of how port objects are registered in Python using the confclass framework:

class MyDevice:
    RAISE = "raise"
    LOWER = "lower"

    cls = simics.confclass(classname="my_device")
    reset_1_iface = cls.ports.RESET_1.signal
    reset_2_iface = cls.ports.RESET_2.signal
    cls.attr.history("[[ss]*]", default=[])

    @reset_1_iface.signal_raise
    def reset_1_raise(self):
        self.signal_raise_common("RESET_1")

    @reset_1_iface.signal_lower
    def reset_1_lower(self):
        self.signal_lower_common("RESET_1")

    @reset_2_iface.signal_raise
    def reset_2_raise(self):
        self.signal_raise_common("RESET_2")

    @reset_2_iface.signal_lower
    def reset_2_lower(self):
        self.signal_lower_common("RESET_2")

    def signal_raise_common(self, port):
        self.history.append([port, self.RAISE])

    def signal_lower_common(self, port):
        self.history.append([port, self.LOWER])

16.2.13 Inheritance

Inheritance works as dictated by Python. However, just using inheritance will not result in Simics (confclass) specific properties, such as attributes and interfaces being inherited. For this, the parent parameter must be set to the confclass class of the parent class.

class Parent:
    """Class with attribute 'a1' and method 'foo'."""
    cls = simics.confclass(classname="parent")
    cls.attr.a1("i", default=1)
    def foo(self):
        pass

class InheritPython(Parent):
    """Inherits from 'Parent', but not the Simics parts."""
    @classmethod
    def check(cls):
        stest.expect_true(hasattr(cls, "foo"))
        stest.expect_false(hasattr(cls, "a1"))

class InheritPythonAndSimics(Parent):
    """Inherit from 'Parent', including the Simics parts."""
    cls = simics.confclass(classname="inherit_py_and_sim", parent=Parent.cls)
    @classmethod
    def check(cls):
        stest.expect_true(hasattr(cls, "foo"))
        stest.expect_true(hasattr(cls, "a1"))

class InheritPythonOverrideSimics1(Parent):
    """Inherit from Parent (not Simics parts) and create a Simics class."""
    cls = simics.confclass(classname="inherit_py_override_sim")
    @classmethod
    def check(cls):
        stest.expect_true(hasattr(cls, "foo"))
        stest.expect_false(hasattr(cls, "a1"))
16.1 Modeling with Python (pyobj) 16.3 Migrating to confclass