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:

Windows
project> bin\project-setup.bat --py-device-confclass my-confclass-device
Linux
project$ ./bin/project-setup --py-device-confclass my-confclass-device

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 Lifetime 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 be 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 it is more convenient to set 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:

default
Set a default value. Also set kind to Sim_Attr_Optional in kind if the current value is Sim_Attr_Required. See also additional information on attribute default values.
optional
Make attribute optional. Set to true to set Sim_Attr_Optional in kind.
pseudo
Make attribute pseudo. Set to true to set Sim_Attr_Pseudo in kind.
read_only
Make attribute read-only. If set to true, pseudo must also be set to true. (see note below)
write_only
Make attribute write-only. If set to true, pseudo must also be set to true. (see note below)

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.

16.2.7.1 Examples

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.7.2 Default Values

If an attribute is of container type, the default value cannot be an instance of that type. The reason for this is that the default value would otherwise be shared among all instances of that class. So if the attribute was a list, and obj1 appended an element to the list, that would also affect obj2, assuming these are instances of the same class.

For example, if having an attribute attr1 that has the type list of integers, having [] as default value as in the class wrong_default_value will raise an exception.

class WrongDefaultValue:
    cls = simics.confclass(classname="wrong_default_value")
    cls.attr.attr1("[i*]", default=[])

As a workaround, instead use a callable that requires no argument, as in the class right_default_value. This will create a unique list for each instance as default value.

class RightDefaultValue:
    cls = simics.confclass(classname="right_default_value")
    cls.attr.attr1("[i*]", default=list)

Allowed default values are:

Forbidden default values are (container types):

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. For each method in the interface, there will be a decorator that is used to specify custom methods in the interface implementation.

In this example we use the signal interface, and use the cls.iface.signal.signal_raise and cls.iface.signal.signal_lower decorators to specify the custom methods in the interface implementation.

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=list)

    @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=list)

    @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=list)

    @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 Events

Simics Events classes can be registered by appending an event name to the <confclass>.event namespace, and this will create an object with decorators that can be used to specify the event callbacks.

When creating a Simics object from a confclass that has declared events, there will be an entry for each event class on Python object, and this entry can be used to post/cancel/find events for this class. For example, this would post the my_event event to trigger in 123 cycles, using the clock conf.clk:

obj.object_data.my_event.post(clock=conf.clk, cycles=123)

The clock object (requirements depends on the delay metrics), can either be specified with the clock key-word argument, but alternatively, if the object has an object assigned to the queue attribute, this will be used (SIM_object_clock is used internally).

If the object has a clock (queue set), and clock is specified in a call, the object from clock argument is used.

16.2.13.1 Declaration

Events are added by appending an event name to the <confclass>.event namespace. The only requirement is that a callback function (see SIM_register_event) is implemented and decorated with <confclass><event name>.callback. This is the shortest possible event declaration:

class MyDevice1:
    cls = simics.confclass(classname="my_device1")

    @cls.event.my_event.callback
    def callback(self, data):
        pass

The complete list of decorator is:

With a call to the event declaration, it is possible to customize:

Here is an example of declaring an non-saved event class with a static description.

class MyDevice2:
    cls = simics.confclass(classname="my_device2")
    cls.event.my_event(saved=False, description="nice weather")

    @cls.event.my_event.callback
    def callback(self, data):
        assert data is None

Even though events are per default saved to checkpoints, because of the default flags being Sim_EC_No_Flags, the data used when posting events will not be handled properly unless the get_value and set_value callbacks are implemented and assigned (decorated) to handle the data. If get_value and set_value are not implemented, a default implementation will be provided that always defaults the data argument to the callback as being None.

16.2.13.2 Posting/Cancelling/Finding

As mentioned earlier, a member of Python object will be set for each declared event, and this member allows using the event.

class MyDevice3:
    cls = simics.confclass(classname="my_device3")
    cls.attr.callback_args(
        "[s*]", default=list, doc="arguments to each call to 'callback'")
    cls.event.my_event(saved=False, description="nice weather")

    @cls.event.my_event.callback
    def callback(self, data):
        self.callback_args.append(data)

Create an object and a clock. Set ‘event’ to the member used to access the event run-time functionality.

simics.SIM_add_configuration([
    simics.pre_conf_object("device", MyDevice3.cls.classname),
    simics.pre_conf_object("clock", "clock", freq_mhz=100)], None)
device = conf.device
clock = conf.clock
event = device.object_data.my_event

Post an event to trigger after 1 cycles and data “1”. Here the clock is specified as an argument.

event.post(clock=clock, cycles=1, data="1")

Set the ‘queue’ attribute in the object. After this, we don’t have to specify the clock argument in each event operation.

device.queue = clock

Post another event to trigger after 2 cycles and data “2”. Here we make use of that ‘device’ has a clock, and we don’t need to specify this.

event.post(cycles=2, data="2")

Now we will list the posted events, identifying them with the data we used when posting.

stest.expect_equal(event.find_next_cycles(data="1"), 1)
stest.expect_equal(event.find_next_cycles(data="2"), 2)

Cancel the first event posted at cycle 1, and check that it is gone.

event.cancel_time(data="1")
stest.expect_equal(event.find_next_cycles(data="1"), None)

Run forward two cycles, and check that the “2” event has triggered.

cli.global_cmds.continue_cycles(count=2)
stest.expect_equal(event.find_next_cycles(data="2"), None)
stest.expect_equal(device.callback_args, ["2"])

16.2.13.3 See also

16.2.14 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