16.2 Modeling with Python (confclass) 17 Writing Model Tests
Model Builder User's Guide  /  II Device Modeling  /  16 Modeling with Python  / 

16.3 Migrating to confclass

This is a short guide on how to migrate pyobj classes to confclass. Below are pyobj example classes together with a confclass example class that works like the pyobj class when possible.

16.3.1 Basic Class

We start with the simplest example possible, just an empty class named my_basic. The class will have:

16.3.1.1 pyobj

import pyobj
class my_basic(pyobj.ConfObject):
    """Longer description."""
    _class_desc = 'one-line description'

See also section 16.1.3.

16.3.1.2 confclass

An similar class implemented with confclass would be:

import simics
class MyBasic:
    cls = simics.confclass(
        classname="my_basic",
        short_doc="one-line description",
        doc="Longer description.")

See also section 16.2.3.

16.3.2 Overriding Object Callbacks

When creating a Simics class, custom functions can be specified as callbacks that are called during the creation of the class. For examples of callbacks, see section 15.2 for information on callbacks that can be specified when creating a class in C. Some of these callbacks can be specified in pyobj and confclass.

The following table contains a list of the functions available in pyobj and the corresponding namespaced decorators in confclass.

pyobj.ConfObject methodsconfclass decorators
_initialize<confclass>.init (16.2.5)
_finalize<confclass>.finalize (16.2.5)
_pre_delete<confclass>.deinit (16.2.5)
_info<confclass>.command.info (16.2.8)
_status<confclass>.command.status (16.2.8)

If creating and deleting an object with the my_device from one of the below examples, the expected value of calls should be ["init", "finalize", "delete"].

16.3.2.1 pyobj

import pyobj
calls = []
class my_device(pyobj.ConfObject):
    def _initialize(self):
        super()._initialize()
        global calls
        calls.append("init")

    def _finalize(self):
        global calls
        calls.append("finalize")

    def _pre_delete(self):
        global calls
        calls.append("delete")

See also section 16.1.3

16.3.2.2 confclass

import simics
calls = []
class MyDevice:
    cls = simics.confclass(classname="my_device")

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

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

    @cls.deinit
    def deinit(self):
        global calls
        calls.append("delete")

See also section 16.2.5

16.3.3 Hierarchy (_up)

One difference between pyobj and confclass is that pyobj requires custom classes for adding:

This means that when adding any of these properties, it is possible to put the required state in the ConfObject class or in the property sub-class (for example pyobj.Attribute). In confclass, since inheritance is not required, the state is typically put in the main class directly.

For example, using pyobj and and implementing an attribute (see where self.my_attr is set):

  1. State in the custom ConfObject class device1:

    import pyobj
    class device1(pyobj.ConfObject):
        def _initialize(self):
            super()._initialize()
            self.my_attr = 1  # <-- data in main class
    
        class my_attr(pyobj.Attribute):
            attrtype = 'i'
            def getter(self):
                return self._up.my_attr  # use _up to reach main class
            def setter(self, val):
                self._up.my_attr = val  # use _up to reach main class
    
  2. State in the Attribute, my_attr sub-class to device2:

    import pyobj
    class device2(pyobj.ConfObject):
        def _initialize(self):
            super()._initialize()
    
        class my_attr(pyobj.Attribute):
            attrtype = 'i'
            def _initialize(self):
                self.my_attr = 1  # <-- data in attribute class
            def getter(self):
                return self.my_attr
            def setter(self, val):
                self.my_attr = val
    

In confclass, the state must be put in the main class. The corresponding example to device1 would be:

import simics
class MyDevice:
    cls = confclass(classname="device1")
    cls.attr.my_attr("i", default=1)
    @cls.attr.my_attr.getter
    def getter(self):
        return self.my_attr
    @cls.attr.my_attr.setter
    def setter(self, val):
        self.my_attr = val

So the migration action needed when state is needed for attributes, interfaces or port objects, is to move the state to the main class.

16.3.4 Attributes

16.3.4.1 Simple Attribute

The simplest way to add an attribute is an optional attribute with a default value, and no customized getter or setter. For pyobj, the SimpleAttribute is used to create simple attributes with default value, getter and setter, but for confclass the same mechanism is used for all attributes.

16.3.4.1.1 pyobj

This class has an optional attribute named simple which contains a number (i), with the default value 0.

import pyobj
class my_device(pyobj.ConfObject):
    class simple(pyobj.SimpleAttribute(0, "i")):
        pass

See also section 16.1.5

16.3.4.1.2 confclass

In confclass, the attribute name is placed in the cls.attr namespace, and the default argument is used to set a default value and make the attribute optional.

import simics
class MyDevice:
    cls = simics.confclass(classname="my_device")
    cls.attr.simple("i", default=0)

See also section 16.2.7

16.3.4.2 Attribute with Getter and Setter

16.3.4.2.1 pyobj

This is a more complete attribute example containing custom getters and setters. This class has an optional attribute named my_attr which contains a number (i), with the default value 1.

import pyobj
class my_device(pyobj.ConfObject):
    def _initialize(self):
        super()._initialize()
        self.val = 1

    class my_attr(pyobj.Attribute):
        attrtype = 'i'

        def getter(self):
            return self._up.val

        def setter(self, val):
            self._up.val = val

See also section 16.1.5

16.3.4.2.2 confclass

In confclass, the attribute name is placed in the cls.attr namespace, and the default argument is used to set a default value. Further, the getter and setter are specified with decorators, for example cls.attr.my_attr.getter for the getter.

import simics
class MyDevice:
    cls = simics.confclass(classname="my_device")
    cls.attr.my_attr("i", default=1)

    @cls.attr.my_attr.getter
    def my_attr_getter(self):
        return self.my_attr

    @cls.attr.my_attr.setter
    def my_attr_setter(self, new_value):
        self.my_attr = new_value

See also section 16.2.7

16.3.5 Interfaces

Below is demonstrated how to implement the signal interface. The signal interface updates the signal_raised attribute.

16.3.5.1 pyobj

import pyobj
class my_device(pyobj.ConfObject):
    class signal_raised(pyobj.SimpleAttribute(None, "b|n")):
        pass

    class signal(pyobj.Interface):
        def signal_raise(self):
            self._up.signal_raised.val = True

        def signal_lower(self):
            self._up.signal_raised.val = False

See also section 16.1.8

16.3.5.2 confclass

import simics
class MyDevice:
    cls = simics.confclass(classname="my_device")
    cls.attr.signal_raised("b|n", default=None)

    @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

See also section 16.2.10

16.3.6 Port Objects

Below is demonstrated how to implement the signal interface in a port object. The signal interface updates the signal_raised attribute in the main class (my_device).

16.3.6.1 pyobj

import pyobj
class my_device(pyobj.ConfObject):
    class signal_raised(pyobj.SimpleAttribute(None, "b|n")):
        pass

    class RESET(pyobj.PortObject):
        class signal(pyobj.Interface):
            def signal_raise(self):
                self._up._up.signal_raised.val = True

            def signal_lower(self):
                self._up._up.signal_raised.val = False

See also section 16.1.9

16.3.6.2 confclass

import simics
class MyDevice:
    cls = simics.confclass(classname="my_device")
    cls.attr.signal_raised("b|n", default=None)
    reset = cls.o.port.RESET()

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

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

See also section 16.2.11

16.3.7 Events

The class pyobj.Event provides event functionality. Currently confclass has no corresponding functionality, so the confclass example below instead use the Simics API to achieve the same functionality.

16.3.7.1 pyobj

In this example, my_device contains my_event (subclass of pyobj.Event) which can be used to post and cancel events. When my_event triggers, the data is appended to the my_event_data attribute.

import pyobj
class my_device(pyobj.ConfObject):
    class my_event_data(pyobj.SimpleAttribute([], "[i*]")):
        pass

    class my_event(pyobj.Event):
        def callback(self, data):
            simics.SIM_log_info(
                1, self._up.obj, 0, f"my_event triggered with {data}")
            self._up.my_event_data.val.append(data)

        def get_value(self, data):
            return str(data)

        def set_value(self, val):
            return int(val)

        def describe(self, data):
            return 'event with %s' % data

This is an example of how the class can be used to post and cancel events, and also check if the event triggered by checking the my_event_data attribute.

def run_cycles(clock, cycles):
    cli.quiet_run_command(f"continue {cycles} cycles")

# Create objects.
device_pre = simics.pre_conf_object("device", "my_device")
clock_pre = simics.pre_conf_object("clock", "clock", freq_mhz=1)
simics.SIM_add_configuration([device_pre, clock_pre], None)
device = simics.SIM_get_object(device_pre.name)
clock = simics.SIM_get_object(clock_pre.name)
event = device.object_data.my_event

# Test posting on the next cycle, run forward and check that the event triggers.
data_1 = 11
stest.expect_true(len(device.my_event_data) == 0)
event.post(clock, data_1, cycles=1)
run_cycles(clock, 1)
stest.expect_equal(device.my_event_data, [data_1])

# Test posting on the next cycle, cancel the event and run forward to check that
# the event did not trigger.
data_2 = 22
event.post(clock, data_2, cycles=2)
run_cycles(clock, 1)
event.cancel_time(clock, match_fun=lambda d: d == data_2)
run_cycles(clock, 1)
stest.expect_equal(device.my_event_data, [data_1])  # unchanged data

See also section 16.1.10

16.3.7.2 confclass

In my_device, the event my_event is registered along with the event callbacks that are are methods of my_device. The attribute my_event_data contains the data of events that have triggered. The event is registered in @cls.objects_finalized.

import simics
class MyDevice:
    event_name = "my_event"
    cls = simics.confclass(classname="my_device")
    cls.attr.my_event_data("[i*]", default=[])

    @cls.objects_finalized
    def objects_finalized(self):
        self.ev_class = simics.SIM_register_event(
            MyDevice.event_name, MyDevice.cls.classname, 0, self._callback,
            self._destroy, self._get_value, self._set_value, self._describe)

    def _callback(self, ev_obj, data):
        simics.SIM_log_info(
            1, self.obj, 0, f"my_event triggered with {data}")
        self.my_event_data.append(data)

    def _destroy(self, ev_obj, data):
        pass

    def _get_value(self, ev_obj, data):
        return str(data)

    def _set_value(self, ev_obj, val):
        return int(data)

    def _describe(self, ev_obj, data):
        return f"event with {self._get_value(ev_obj, data)}"

    def post_event(self, clock, data, cycles):
        simics.SIM_event_post_cycle(
            clock, self.ev_class, self.obj, cycles, data)

    def cancel_event(self, clock, match_fun=None):
        def pred(data, match_data): return bool(match_fun(data))
        simics.SIM_event_cancel_time(
            clock, self.ev_class, self.obj, pred if match_fun else None, None)

To post and cancel, the post_event, and cancel_event functions can be called from <my_device object>.object_data. This is an example of how the class can be used to post and cancel events, and also check if the event triggered by checking the my_event_data attribute.

def run_cycles(clock, cycles):
    cli.quiet_run_command(f"continue {cycles} cycles")

# Create objects.
device_pre = simics.pre_conf_object("device", "my_device")
clock_pre = simics.pre_conf_object("clock", "clock", freq_mhz=1)
simics.SIM_add_configuration([device_pre, clock_pre], None)
device = simics.SIM_get_object(device_pre.name)
clock = simics.SIM_get_object(clock_pre.name)
event = device.object_data

# Test posting on the next cycle, run forward and check that the event triggers.
data_1 = 11
stest.expect_true(len(device.my_event_data) == 0)
event.post_event(clock, data_1, 1)
run_cycles(clock, 1)
stest.expect_equal(device.my_event_data, [data_1])

# Test posting on the next cycle, cancel the event and run forward to check that
# the event did not trigger.
data_2 = 22
event.post_event(clock, data_2, 2)
run_cycles(clock, 1)
event.cancel_event(clock, match_fun=lambda d: d == data_2)
run_cycles(clock, 1)
stest.expect_equal(device.my_event_data, [data_1])  # unchanged data

See also:

16.2 Modeling with Python (confclass) 17 Writing Model Tests