Link Library Programming Guide
The link library lets users write Simics link models.
A link is used to connect devices in different cells, even in different processes, so that they can exchange non-instantaneous messages in a deterministic way.
Typical links include Ethernet and other network connections, but also simpler signal links and more abstract communication channels.
Much of the functionality of links is the same for different kinds of links; that commonality is encapsulated in the link library.
This makes it simpler to create new link types, since a minimum of new code is required for each new link type.
Before going further, it is important to realize when not to use the link library:
- If the devices are expected to all be located in the same configuration cell, most of the link library support is unnecessary.
It will probably be much easier to create a simple broadcast bus object that distribute messages where they should go via interface calls.
- If the requirement is for a simple point-to-point protocol to exchange messages between two devices in different configuration cells, the
datagram_link might well suffice.
It allows simple byte strings to be passed from one sender to one or multiple receivers via the simple datagram_link interface.
- If the requirement is to broadcast messages between a set of devices, once again, the
datagram_link might be the easiest way forward, as it provides just that.
The link library, however, is available for more complex usage: multiple message types across the link (such as link speed, link status, etc.), special addressing (unicast or multicast groups, switching, etc.), special types of devices (such as sender and receiver for the signal-link).
All of these requirements will be more easily expressed, and provide better simulation performance, by creating a special-purpose link based on the link library.
The rest of this document describes how to create a new link type, using the provided datagram_link as an example.
The reader is expected to know how to develop Simics modules in C.
Further information can be found in the Model Builder User’s Guide.
A link consists of two Simics configuration classes: link and endpoint.
In a running simulation, there will exist one link object per link, and one endpoint object per device connected to the link.
The link and endpoint classes use the link library to handle many of the details of link operation.
The link library keeps its state in opaque data structures attached to the link and endpoint objects.
Some of this information can be queried via the link library API.
When devices connect to the link, they really contact a freshly created endpoint object, usually provided by the link component.
Any interaction between the device and the link goes through that endpoint object.
When running in a distributed simulation, a link is represented by an identically named instance in each process of the simulation.
Link objects may have configuration parameters, which are represented as Simics attributes in the link class.
These parameters should only be changed when the simulation is not running, to avoid indeterminism.
In a distributed setup, configuration parameters should also be shared with the other instances of the same link: the link library provides the SIMLINK_config_update_value() and SIMLINK_config_remove_value() functions for that purpose.
For example, the goal_latency of the link is implemented as a global configuration parameter.
More information is available in the Link Library API reference.
Endpoint objects may contain configuration parameters, as well as simulation state that can be changed at runtime, since they are only involved with one device on the link.
For example, the ethernet_switch implementation keeps a translation table from MAC addresses to endpoint IDs in each endpoint on the link, allowing them to be updated as endpoints learn about the connected devices.
Linking the link library to your module is simply a matter of adding the following line to the module’s Makefile (for GNU Make builds):
MODULE_LDFLAGS = -llink
For CMake-based builds, use target_link_libraries to link against the link library instead.
The init_local() function in the link module needs to call the SIMLINK_init_library() function:
void
init_local()
{
/* The link library must always be initialised first. */
SIMLINK_init_library();
/* The rest of the module initialization */
}
Each class defines a C struct type to store an instance of the class.
The struct must start with a conf_object_t, and like any Simics configuration object it may also contain any additional configuration parameters required by the specific link.
As usual, if this information needs to be saved in checkpoints, you as the class author are responsible for registering attributes that save and restore it.
typedef struct {
conf_object_t obj;
/* Any link-specific parameters would go here. */
} datagram_link_t;
typedef struct {
conf_object_t obj;
/* Any endpoint-specific state would go here. */
} datagram_link_endpoint_t;
Each link type needs to declare a variable of type link_type_t and fill it with information used by the link library to access functionality that is specific to the link type.
Each of the link_type_t member functions is described in the Link Library API reference.
The link class’s init() method needs to call SIMLINK_init() to attach all link library’s internal information to the link object.
The finalize() method needs to call SIMLINK_finalize().
static conf_object_t *
datagram_link_alloc(conf_class_t *cls)
{
datagram_link_t *dl = MM_ZALLOC(1, datagram_link_t);
return &dl->obj;
}
static void *
datagram_link_init(conf_object_t *obj)
{
datagram_link_t *dl = (datagram_link_t *)obj;
static const link_type_t link_methods = {
.msg_to_attr = msg_to_attr,
.msg_from_attr = msg_from_attr,
.free_msg = free_msg,
.marshal = marshal,
.unmarshal = unmarshal,
.deliver = deliver,
.update_config_value = link_config_value_updated,
.remove_config_value = link_config_value_removed,
};
SIMLINK_init(&dl->obj, &link_methods);
return &dl->obj;
}
static void
datagram_link_finalize(conf_object_t *obj)
{
SIMLINK_finalize(obj);
}
Similarly, the endpoint class’s init() and finalize() methods need to call SIMLINK_endpoint_init() and SIMLINK_endpoint_finalize(), respectively:
static void *
datagram_link_endpoint_init(conf_object_t *obj)
{
datagram_link_endpoint_t *dlep =
(datagram_link_endpoint_t *)obj;
SIMLINK_endpoint_init(&dlep->obj, false);
return dlep;
}
static void
datagram_link_endpoint_finalize(conf_object_t *ep)
{
SIMLINK_endpoint_finalize(ep);
}
Depending on the current configuration, endpoint objects might be created while no device is yet connected.
For that reason, device related operations, such as caching an interface, are expected to be done in the device_changed() callback rather than in the finalize() function.
See also link_type_t documentation for more information and an example.
The endpoint class also has to have a deinit() function, which has to be, or at least call, SIMLINK_endpoint_disconnect().
After declaring the link class with SIM_create_class(), SIMLINK_register_class() must also be called, with the class as argument.
Similarly, for the endpoint class, SIMLINK_register_endpoint_class() must be called.
It takes two parameters: the endpoint class and the attribute type of a checkpointed in-flight link message.
void
init_local()
{
/* The link library must always be initialised first. */
SIMLINK_init_library();
const class_info_t cl_methods = {
.alloc = datagram_link_alloc,
.init = datagram_link_init,
.finalize = datagram_link_finalize,
.deinit = datagram_link_deinit,
.dealloc = datagram_link_dealloc,
.short_desc = "link that broadcasts byte strings",
.description = "A link that broadcasts byte strings."
};
conf_class_t *cl = SIM_create_class("datagram_link_impl",
&cl_methods);
/* Tell the link library what class represents the link */
SIMLINK_register_class(cl);
const class_info_t epcl_methods = {
.alloc = datagram_link_endpoint_alloc,
.init = datagram_link_endpoint_init,
.finalize = datagram_link_endpoint_finalize,
.deinit = SIMLINK_endpoint_disconnect,
.dealloc = datagram_link_endpoint_dealloc,
.short_desc = "endpoint for datagram links",
.description = "Endpoint for datagram link objects."
};
conf_class_t *epcl = SIM_create_class("datagram_link_endpoint",
&epcl_methods);
static const datagram_link_interface_t dgram_link_if = {
.receive = receive
};
SIM_REGISTER_INTERFACE(epcl, datagram_link, &dgram_link_if);
/* Tell the link library what class we use for endpoints */
SIMLINK_register_endpoint_class(epcl, "d");
}
Endpoints always need to implement interfaces specific to the link type, so they can accept messages from the connected devices.
Here, datagram_link registers a datagram_link interface for its endpoint class.
void
init_local()
{
/* The link library must always be initialised first. */
SIMLINK_init_library();
const class_info_t cl_methods = {
.alloc = datagram_link_alloc,
.init = datagram_link_init,
.finalize = datagram_link_finalize,
.deinit = datagram_link_deinit,
.dealloc = datagram_link_dealloc,
.short_desc = "link that broadcasts byte strings",
.description = "A link that broadcasts byte strings."
};
conf_class_t *cl = SIM_create_class("datagram_link_impl",
&cl_methods);
/* Tell the link library what class represents the link */
SIMLINK_register_class(cl);
const class_info_t epcl_methods = {
.alloc = datagram_link_endpoint_alloc,
.init = datagram_link_endpoint_init,
.finalize = datagram_link_endpoint_finalize,
.deinit = SIMLINK_endpoint_disconnect,
.dealloc = datagram_link_endpoint_dealloc,
.short_desc = "endpoint for datagram links",
.description = "Endpoint for datagram link objects."
};
conf_class_t *epcl = SIM_create_class("datagram_link_endpoint",
&epcl_methods);
static const datagram_link_interface_t dgram_link_if = {
.receive = receive
};
SIM_REGISTER_INTERFACE(epcl, datagram_link, &dgram_link_if);
/* Tell the link library what class we use for endpoints */
SIMLINK_register_endpoint_class(epcl, "d");
}
The link model needs to define a structure that represents all kinds of messages that can be sent over the link.
This structure should “inherit” from link_message_t, by placing it first in the struct (so that a pointer to a datagram_link_message_t can be cast to a link_message_t pointer).
typedef struct {
link_message_t common; /* should always be first */
/* The actual data in the message - in our case an allocated
byte string owned by this structure. */
bytes_t payload;
} datagram_link_message_t;
New messages are allocated with MM_MALLOC(), and SIMLINK_init_message() is used to fill in the “common” part:
static link_message_t *
new_datagram_message(const uint8 *data, size_t len)
{
datagram_link_message_t *m = MM_MALLOC(1, datagram_link_message_t);
SIMLINK_init_message(&m->common);
uint8 *d = MM_MALLOC(len, uint8);
memcpy(d, data, len);
m->payload = (bytes_t){.data = d, .len = len};
return &m->common;
}
Finally, the message is dispatched using the SIMLINK_send_message() or SIMLINK_send_message_multi() functions.
static void
receive(conf_object_t *NOTNULL ep, bytes_t msg)
{
SIMLINK_send_message(ep, LINK_BROADCAST_ID,
new_datagram_message(msg.data, msg.len));
}
Messages are delivered to the endpoints’ deliver() callback registered by the link.
The callback is responsible for translating the message into something the receiving device will understand.
static void
deliver(conf_object_t *ep, const link_message_t *lm)
{
const datagram_link_message_t *m =
(const datagram_link_message_t *)lm;
conf_object_t *dev = SIMLINK_endpoint_device(ep);
const char *port = SIMLINK_endpoint_port(ep);
const datagram_link_interface_t *dli =
SIM_c_get_port_interface(dev, "datagram_link", port);
if (dli)
dli->receive(dev, m->payload);
else
SIM_LOG_ERROR(ep, 0, "Device does not implement"
" datagram_link interface");
}
To be part of a standard, component-based configuration, new links have to provide a component that will wrap the link implementation and handle connections.
An important role of the link component is to create and destroy endpoint objects as the connections are being made.
Links tend to share a lot of common functionality, and link components are no exceptions.
The link library provides two interfaces to easily create link components.
The simplest way to create a link component is to use the link_components.create_simple() function. Here is the component definition for datagram_link:
from link_components import create_simple
class datagram_link(
create_simple(link_class = 'datagram_link_impl',
endpoint_class = 'datagram_link_endpoint',
connector_type = 'datagram-link',
class_desc = "datagram link",
basename = 'datagram_link')):
"""The datagram link component creates a datagram-link, which is a simple
broadcast bus forwarding messages (as sequences of bytes) from a sender
device to all other devices present of the link. The datagram-link is both
an example of how to build a link with the Simics Link Library, and a
simple broadcast link that can be reused when multi-cell communication
between devices is necessary. Refer to the <cite>Link Library Programming
Guide</cite> for more information."""
Using this simplified method assumes that the link fulfills a number of requirements:
- The link object can be created without additional configuration.
- There is no restriction on the number of connections to the link, beyond the connector type check that is done at the component level.
The link connector takes either an object or an [object, port] two-element list as argument.
Device connectors should accept an endpoint object as argument.
- The link has only one endpoint class. An endpoint can be created without additional configuration.
Further information is available in the link_components.create_simple() API description, in the Link Library API reference.
If the new link type does not comply with the limitations above, a slightly more complex interface is available.
When more flexibility is required, it is possible to create a new component class inheriting from link_components.link_component.
Two functions should be overridden to control how the component handles connections:
class ethernet_switch(link_components.link_component):
"""Ethernet switch: this component represents a switched Ethernet network,
allowing any number of devices to connect and optimizing the packet routing
according to what is learned about the MAC addresses talking on the link."""
_class_desc = 'an Ethernet switch component'
_help_categories = ['Networking']
class basename(link_components.link_component.basename):
val = 'ethernet_switch'
def create_unconnected_endpoint(self, cnt):
return create_vlan_switch_endpoint(self.get_slot('link'), None,
None, True)
def register_connector_templates(self):
self.eth_tmpl = self.add_link_connector_template(
name = 'ethernet-link-connector',
type = 'ethernet-link',
growing = True,
create_unconnected_endpoint = self.create_unconnected_endpoint)
def add_objects(self):
self.add_pre_obj_with_name('link', 'eth-switch-link',
self.get_link_object_name(),
goal_latency = self.goal_latency.val,
global_id = self.global_id.val)
self.add_link_connector('device', self.eth_tmpl)
def get_free_connector_cmd(self):
c = self.get_unconnected_connector_object('device')
if not c:
raise cli.CliError('Internal error: no connectors found')
return c.name
register_connector_templates() is responsible for registering the description—called template here—of the connectors that will be allowed in the link.
Each connector template can be associated with a number of functions that will be called when a connection is attempted, is successful, or is destroyed.
Each connector template can also be defined as growing, meaning that new connectors will be added dynamically to make sure there is always one connector free.
add_objects() is responsible for creating the necessary link object and the initial connectors.
A more complete explanation of these functions is available in the component API in the Link Library API reference.