13 Adding New Commands 15 Modeling with Python
Model Builder User's Guide  /  II Device Modeling  / 

14 Modeling with C

While the majority of this guide focuses on DML, there is also support for writing modules in C and Python. DML is recommended for device models as DML allows the author to focus on the functionality of the device without having to worry about the mechanics of interacting with Simics. Components must be written in Python which is also useful for high level modules which do not model devices directly. C is useful for integrating with legacy code.

This chapter describes how to create modules written in C focusing on the mechanics of interacting with Simics. The general guidelines presented earlier apply to C as well as to DML. Each section focuses on one concept and describes how to implement it in C. It may be useful to review section Classes and Objects in API Reference Manual to clarify the differences between objects, classes, and modules.

14.1 Module Loading

Most modules need to do some work when initially loaded into Simics. Typically this work includes registering the classes implemented by the module, and their attributes, with Simics.

A module written in C/C++ must implement the function init_local(). It must exist, even if it is empty. The function will be run by Simics when the module is loaded into Simics. The function is defined as:

void
init_local(void)
{
}

If the module is written in C++, this function must be declared extern "C" for C linkage.

14.2 Classes

Each Simics class implemented by a module must be registered with Simics. Remember that classes registered in a module should be listed in the MODULE_CLASSES variable in the module's Makefile. This allows Simics to automatically load the required modules when reading a configuration file. Similarly, components registered in the module should be listed in MODULE_COMPONENTS.

Registering a class is done by creating and filling a class_info_t structure, and then calling the function SIM_create_class with the new class name and the class_info_t structure. The members in the class_info_t structure are:

In C/C++, registration of classes is usually done from within the mandatory init_local() function. The C definition of class_info_t and SIM_create_class() is the following:

    typedef struct class_info {
        conf_object_t *(*alloc)(conf_class_t *cls);
        lang_void *(*init)(conf_object_t *obj);
        void (*finalize)(conf_object_t *obj);
        void (*objects_finalized)(conf_object_t *obj);
        void (*deinit)(conf_object_t *obj);
        void (*dealloc)(conf_object_t *obj);
        const char *description;
        const char *short_desc;
        class_kind_t kind;
    } class_info_t;

conf_class_t *SIM_create_class(
        const char *NOTNULL name, const class_info_t *NOTNULL class_info);

SIM_create_class() returns a pointer to a conf_class_t structure which is used internally by Simics to keep track of the class information. This pointer can be used when referring to the class in calls to other functions.

A simple init_local() initialization function could look like this:

void
init_local(void)
{
        class_info_t cdata;
        conf_class_t *my_class;

        memset(&cdata, 0, sizeof(cdata));
        cdata.init = my_init;
        cdata.kind = Sim_Class_Kind_Pseudo;
        cdata.short_desc = "One-liner";
        cdata.description = "This is my class";

        my_class = SIM_create_class("my-class", &cdata);

        // Other initializations...
}

14.3 Objects

A configuration class implemented in C typically maintains the state of each object in a structure that is co-allocated with the object's conf_object_t structure, which is used both as a general reference to the object and to keep track of information for the configuration system.

This is done by putting the conf_object_t first in a C struct that also contains the object state:

  typedef struct {
          conf_object_t obj;  // Always first!
          // Object-specific state follows.
          float temperature;
  } my_object_t;
  

Converting between a pointer to my_object_t and its corresponding conf_object_t then becomes a cast or member dereference:

          // my_object_t -> conf_object_t
          conf_object_t *obj = &myobj->obj;
  
          // conf_object_t -> my_object_t
          my_object_t *myobj = (my_object_t *)obj;

Of course, the code casting from a conf_object_t pointer must know for certain that it really is an object of the right type. This knowledge is usually implicit in the function interfaces.

The allocation of the combined structure is done in the alloc method in the class_info_t value passed to SIM_create_class:

  static conf_object_t *
  my_alloc(conf_class_t *cls)
  {
          my_object_t *myobj = MM_ZALLOC(1, my_object_t);
          return &myobj->obj;
  }

The method init should be defined to initialise the structure:

  static void *
  my_init(conf_object_t *obj)
  {
          my_object_t *myobj = (conf_object_t *)obj;
          myobj->temperature = 20.0;
          return myobj;
  }

Configuration classes written in C++ may use the same techniques as in C, or simpler by subclassing simics::SimicsObject; see the C++ Device API programming guide for details.

14.4 Attributes

Attributes are linked to the class definition, usually just after the class has been declared, with the SIM_register_attribute() function. It has the following declaration in C:

int SIM_register_attribute(
           conf_class_t *cls, const char *name,
           attr_value_t (*get_attr)(conf_object_t *),
           set_error_t (*set_attr)(conf_object_t *, attr_value_t *),
           attr_attr_t attr,
           const char *type,
           const char *doc);

The parameters of SIM_register_attribute() are:

14.4.1 A Simple Example

Let us use a simple counter attribute as an example.

In C, we'll have an object declared as:

typedef struct my_object {
        conf_object_t obj;
        int foo;
} my_object_t;

We want to implement an attribute called counter, thus we need a pair of set/get functions. counter will internally use foo to keep its value. The pair of get/set functions could be defined as:

static attr_value_t
get_counter(conf_object_t *obj)
{
        my_object_t *mo = (my_object_t *)obj;

        return SIM_make_attr_uint64(mo->foo);
}

static set_error_t
set_counter(conf_object_t *obj, attr_value_t *val)
{
        my_object_t *mo = (my_object_t *)obj;

        mo->foo = SIM_attr_integer(*val);
        return Sim_Set_Ok;
}

In the get_counter() function, obj is the object that owns the attribute and arg is the user information that was registered along with the attribute. Note that obj can be safely cast to my_object_t (conf_object_t is used as a "base type" here). The function creates an attr_value_t variable that will be of integer type and contain the value foo. It then returns this attribute value.

The set_counter() function on the other hand takes a val argument which contains the value to be written. The return value is of type set_error_t, which is defined as below. Descriptions of the values can be found in the API Reference Manual.

typedef enum {
        Sim_Set_Ok,
        Sim_Set_Object_Not_Found,
        Sim_Set_Interface_Not_Found,
        Sim_Set_Illegal_Value,
        Sim_Set_Illegal_Type,
        Sim_Set_Illegal_Index,
        Sim_Set_Attribute_Not_Found,
        Sim_Set_Not_Writable
} set_error_t;

Registering the counter attribute is just a matter of calling SIM_register_attribute():

SIM_register_attribute(my_class, 
                       "counter",
                       get_counter,
                       set_counter,
                       Sim_Attr_Required,
                       "i",
                       "A counter");

14.4.2 A Pseudo Attribute

In the previous example, the attribute counter provides a direct representation of the value foo inside the object. Now let us add an attribute called add_counter that will increase foo by a given value when the attribute is set, and do nothing when the attribute is read. This would give us the following code:

static set_error_t
set_add_counter(conf_object_t *obj, attr_value_t *val)
{
        my_object_t *mo = (my_object_t *)obj;

        mo->foo += SIM_attr_integer(*val);
        return Sim_Set_Ok;
}

There is no need for a get function since this attribute only can be written. The semantics of set_add_counter() are also slightly different, since the function actually adds a value to foo.

It is thus possible to create real attributes whose value corresponds to a real variable in an object, and pseudo attributes which are only used as object "methods".

Registering the add_counter attribute is straightforward:

SIM_register_attribute(class_name, "add_counter",
                       NULL,
                       set_add_counter,
                       Sim_Attr_Pseudo,
                       "i",
                       "A sample pseudo attribute.");

14.4.3 Attribute Errors

Errors in attribute getter methods are signalled by calling SIM_attribute_error with an error description, and then returning an arbitrary value. An explicitly "invalid" value is usually returned in these cases, as generated by SIM_make_attr_invalid, but any value will do. In Python, None is a good choice.

Errors in attribute setter methods are signalled by returning a set_error_t value other than Sim_Set_Ok. In most cases, returning Sim_Set_Illegal_Value is the appropriate way of indicating that the given value is not valid for the attribute. Optionally, SIM_attribute_error may be called in order to provide a more detailed error description, prior to returning.

Attribute errors generally give rise to frontend exceptions. If the error occurred during the reading or writing of a checkpoint, that activity is aborted; any partially written checkpoint or created objects are removed.

14.5 Interfaces

14.5.1 Using Interfaces

An object that wants to interact with another through an interface uses the SIM_get_interface() function to retrieve the interface structure. It can then call the other object using the functions defined in the structure.

conf_object_t *obj = SIM_get_object("phys_mem");
const memory_space_interface_t *ifc;
attr_value_t val;

ifc = SIM_get_interface(obj, "memory_space");
val = ifc->read(obj, SIM_get_object("cpu0"), 0x1234, 4, 0);

When you are using interfaces inside an object, you will often need to define which object you want to talk to via an attribute. Typically this is done by defining an attribute with type o|n, and checking if the object passed as argument implements the necessary interface:

static attr_value_t
get_an_object(conf_object_t *obj)
{
        my_object_t *mo = (my_object_t *)obj;
        return SIM_make_attr_object(mo->an_object);
}

static set_error_t
set_an_object(conf_object_t *obj, attr_value_t *val)
{
        my_object_t *mo = (my_object_t *)obj;

        if (SIM_attr_is_nil(*val)) {
                mo->an_object = NULL;
        } else {
                foo_interface_t *foo_ifc =
                    (foo_interface_t *)SIM_get_interface(
                                           SIM_attr_object(*val), "foo");
                if (SIM_clear_exception() != SimExc_No_Exception)
                        return Sim_Set_Interface_Not_Found;
                mo->an_object = SIM_attr_object(*val);                
        }

        return Sim_Set_Ok;
}

[…]

SIM_register_attribute(class_name, "an_object",
                       get_an_object,
                       set_an_object,
                       Sim_Attr_Optional,
                       "o|n",
                       "An object implementing the 'foo' interface");

14.5.2 Using Port Interfaces

A limitation of interfaces as described above is that each class can only implement each interface once, but you may need to implement an interface several times in the same device model. For example, consider an interrupt controller that has several interrupt inputs having to implement the signal interface once for each input.

To get around this a device will usually use port objects. These are sub objects of the main device object that can be used to specify a particular interface instance when connecting objects, see 34. In this case using a port interface is identical to using a regular interface. You use SIM_get_interface() to get the interface implementation and do not have to care whether the object is a regular object or port object.

However, if your model is required to interact with old models implemented in C or Python you may find that you also have to support an older port mechanism. In this case a port is identified by an object and a port name string.

Your attribute then has to accept either an object or a list with an object and a name string. If a port name is specified you have to use SIM_get_port_interface() instead of SIM_get_interface() to get the interface implementation.

The example code from 14.5.1 below is extended with support for the old port mechanism:

static attr_value_t
get_an_object(conf_object_t *obj)
{
        my_object_t *mo = (my_object_t *)obj;
        if (mo->a_portname != NULL)
                return SIM_make_attr_list(
                        2,
                        SIM_make_attr_object(mo->an_object),
                        SIM_make_attr_object(mo->n_portname));
        return SIM_make_attr_object(mo->an_object);
}

static set_error_t
set_an_object(conf_object_t *obj, attr_value_t *val)
{
        my_object_t *mo = (my_object_t *)obj;

        if (SIM_attr_is_nil(*val)) {
                mo->an_object = NULL;
        } else if (SIM_attr_is_obj(*val)) {
                foo_interface_t *foo_ifc =
                    (foo_interface_t *)SIM_get_interface(
                                           SIM_attr_object(*val), "foo");
                if (SIM_clear_exception() != SimExc_No_Exception)
                        return Sim_Set_Interface_Not_Found;
                mo->an_object = SIM_attr_object(*val);
        } else if (SIM_attr_is_list(*val)) {
                conf_object_t *foo_obj =
                    SIM_attr_object(SIM_attr_list_item(*val, 0));
                const char *foo_port =
                    SIM_attr_string(SIM_attr_list_item(*val, 1));
                foo_interface_t *foo_ifc =
                    (foo_interface_t *)SIM_get_port_interface(
                                           foo_obj, "foo", foo_port);
                if (SIM_clear_exception() != SimExc_No_Exception)
                        return Sim_Set_Interface_Not_Found;
                mo->an_object = foo_obj;
        }
        MM_FREE(mo->a_portname);
        mo->a_portname = foo_port ? MM_STRDUP(PORT) : NULL;

        return Sim_Set_Ok;
}

[…]

SIM_register_attribute(class_name, "an_object",
                       get_an_object,
                       set_an_object,
                       Sim_Attr_Optional,
                       "o|n|[os]",
                       "An object implementing the 'foo' interface");

14.5.3 Implementing an Interface

The implementation of an existing interface requires the population of all the function pointers that are listed in the interface definition with the functions that should be called. The interface should then be registered using the SIM_register_interface() function:

SIM_register_interface(conf_class_t *class, char *name, void *iface)

Where the parameters are:

This gives us the following code:

static cycles_t
my_operate(conf_object_t *mem_hier, conf_object_t *space,
           map_list_t *map, generic_transaction_t *mem_op)
{
        // do something
}

static conf_class_t *my_class;
static timing_model_interface_t ifc;

void
init_local(void)
{
        […]
        ifc.operate = my_operate;
        SIM_register_interface(my_class, "timing_model", (void *) &ifc);
        […]
}

If there is no predefined interface that suits your needs, you will need to define your own interface type. This is described in section 11.

14.5.4 Implementing Interfaces on Port Objects

If your model needs to implement the same interface more than once, for example, consider the case of an interrupt controller with multiple interrupt inputs, it should do that by implementing the interface on multiple port objects.

Port objects and how to use them from C are described in 34.

14.6 Logging

Logging in C is handled by SIM_log_register_group() and the SIM_LOG_* macros.

A single call to SIM_log_register_groups() registers all groups for the class. The function is used as:

SIM_log_register_groups(conf_class_t *cls, const char **gnames)

where the parameters are:

An example:

static char *groupnames[] = { "config", "request", "response", NULL };
SIM_log_register_groups(my_class, &groupnames);

The log group values will be defined by the order of the strings in the tuple as a power of 2 series, so in the example above config corresponds to 1, request corresponds to 2 and response corresponds to 4.

Log outputs are handled with SIM_LOG_INFO() and corresponding macros. They take the following parameters:

SIM_LOG_INFO(int level, conf_object_t *obj, uint64 groups, const char *msg);

with the parameters meaning:

Logging from a Simics module written in C/C++ should be done with the following macros: SIM_LOG_INFO(), SIM_LOG_ERROR(), SIM_LOG_UNDEFINED(), SIM_LOG_SPEC_VIOLATION(), and SIM_LOG_UNIMPLEMENTED(). These macros use the corresponding SIM_log_<type>() function internally, and should always be used instead for performance reasons.

Note that the macros take a variable number of arguments to allow you to write printf()-like strings.

A small example:

static attr_value_t
get_counter_array(conf_object_t *obj, attr_value_t *idx)
{
        my_object_t *mo = (my_object_t *)obj;

        SIM_LOG_INFO(4, obj, 0, "get_counter_array");
        if (!SIM_att_is_nil(*idx)) {
                if (!SIM_attr_is_integer(*idx))
                        SIM_LOG_ERROR(obj, 0,
                                      "Index must be integer");
                        return SIM_make_attr_invalid();

                return SIM_make_attr_uint64(
                        mo->foo[SIM_attr_integer(*idx)]);
        }
        else {
                attr_value_t ret = SIM_alloc_attr_list(10);
                int i;
                for (i = 0; i < 10; i++) {
                        SIM_attr_list_set_item(ret, i,
                                SIM_make_attr_uint64(mo->foo[i]);
                }
                return ret;
        }
}

14.7 Events

Event classes are created with the SIM_register_event() function which should be called at module initialization time. The event class holds a pointer to the callback function. Here is an example from an UART model:

static event_class_t *transmit_event;

static void
uart_transmit(conf_object_t *obj, void *param)
{
        uart_device_t *uart = (uart_device_t *)obj;
        SIM_LOG_INFO(4, &uart->log, 0, "event called");
}

void
init_local(void)
{
        conf_class_t *uart_class;
        […]
        uart_class = SIM_create_class(…);
        […]
        transmit_event = SIM_register_event(
                "transmit character", uart_class, 0, uart_transmit,
                0, 0, 0, 0);
        […]
}

To post an event in the future, based on time, the SIM_event_post_time() and SIM_event_post_cycle() function is used. An example:

        SIM_event_post_time(&uart->log.obj, transmit_event,
                            &uart->log.obj, 0.00001, NULL);

It is possible to specify an argument to be passed to the callback function, as the sixth parameter of SIM_event_post_time. In order for checkpointing to work correctly, you must in this case also specify user-data to attr_value_t conversion functions in the call to SIM_register_event.

If for some reason you do want to remove a posted but not yet handled event, you can cancel it with a call to SIM_event_cancel_time(), specifying the object, event class and optionally a parameter.

        SIM_event_cancel_time(obj, transmit_event, obj, 0, NULL);

You can also check how long time is left until an event occurs using SIM_event_find_next_cycle(), again specifying the object, event class and parameter. The time left to the event is returned in cycles.

If you want to post an event a number of simulated CPU steps into the future it should instead post to the step queue. Posting to the step queue is very similar to posting to the time queue, but the functions SIM_event_post_step(), SIM_event_cancel_step() and SIM_event_find_next_step() should be used instead.

Refer to the API Reference Manual for more information on the function prototypes and their parameters.

14.8 Haps

14.8.1 Providing Haps

As the Simics profiling and event viewing systems are based on listening to haps it can be useful for a device to directly trigger haps rather than relying on haps built into the memory, link, and processor models. In these situations the model has to choose between a standard Simics hap and a user defined hap. Standard haps have the benefit of documentation and predefined hap handlers. User defined haps have the advantage of specificity.

14.8.1.1 Adding a New Type

Before handlers can be notified of a new hap, the hap must be known. A new hap type is made known through registration. Registering a new hap type is done with the function SIM_hap_add_type(). The signature is:

hap_type_t
SIM_hap_add_type(const char *hap,
                 const char *params,
                 const char *param_desc,
                 const char *index,
                 const char *desc,
                 int old_hap_obj);

where the parameters are:

The return value is a handle that must be saved for operations on the hap.

Example:

void
init_local()
{
        […]
        hap_handle = SIM_hap_add_type("My_Special_Hap",
                                      "ii",
                                      "val1 val2",
                                      NULL,
                                      "Triggered when something special"
                                      " happens in my module.",
                                      0);
        if (hap_handle <= 0) {
                /× error handling ×/
                […]
        }
}

14.8.1.2 Triggering a Hap

Whenever the condition for the hap is met, the handlers for the hap should be notified. Triggering a hap incurs some overhead; if it occurs in performance-sensitive code, it may be beneficial to use one of the SIM_hap_is_active_obj or SIM_hap_is_active_obj_idx functions to check if there are any handlers prior to calling the notification function.

bool SIM_hap_is_active_obj(hap_type_t hap, conf_object_t *NOTNULL obj);

bool SIM_hap_is_active_obj_idx(hap_type_t hap, conf_object_t *NOTNULL obj,
                               int64 index);

where the parameter hap is the value returned from SIM_hap_add_type() or from SIM_hap_get_number() if using a standard hap type. These predicates are approximate, but if they return false, there is no need to trigger the hap since no installed functions would be called.

The notification to handlers is normally done by calling one of SIM_hap_occurred(), SIM_hap_occurred_vararg(), SIM_hap_occurred_always(), and SIM_hap_occurred_always_vararg(). See the API Reference Manual for information about the differences.

int
SIM_c_hap_occurred_always(hap_type_t     hap,
                          conf_object_t *obj,
                          int64          value,
                          ...);

The parameters are:

In C, hap parameters will be provided as additional parameters to the function. A short example:

static void
some_func(conf_object_t *obj, int v1, int v2)
{
        if (some_condition) {
                SIM_c_hap_occurred(hap_handle, obj, 0, v1, v2)
        }
}

14.9 Using the Python/C API

If you want to interact with the Python interpreter built into Simics from C, you will need to use the Python/C API. The Python/C API is defined by the simics/python-header.h header file. This file needs to be included before any other Simics API header files.

In order to use the header file, you will need to include the following two statements in your module's Makefile:

  MODULE_CFLAGS = $(PYTHON_INCLUDE)
  MODULE_LDFLAGS = $(PYTHON_LDFLAGS)

These need to be set before the include $(MODULE_MAKEFILE) line and must use the lazy evaluation assignment operator (=) rather than := or +=.

See http://docs.python.org for more information about the Python/C API.

You should not include the standard Python.h header file directly as it contains errors which simics/python-header.h works around.

13 Adding New Commands 15 Modeling with Python