The Humble Beginnings of a SystemVerilog Reflection API, Part 3

We've already looked at how to interrogate classes about what variables they have and how to set and get the values of these variables in different instances. Classes are much more than just data containers, though. They also contain methods that can operate on their variables. In this post we'll look at how we can handle tasks and functions inside our reflection API.

Before we start, however, let's take a quick look at the two kinds of methods we can declare in SystemVerilog: tasks and functions. Both allow the caller to pass information to the method via arguments. Functions can also return a value when the call is finished, but must do this without consuming any simulation time. Tasks can, on the other hand, advance time, but they can't return anything.

Because both have some similarities, it makes sense to model them using a common class, rf_method. This class will handle queries regarding a method's arguments:

virtual class rf_method;
extern function string get_name();
extern function method_kind_e get_kind();

extern function array_of_rf_io_declaration get_io_declarations();
extern function rf_io_declaration get_io_declaration_by_name(string name);

// ...
endclass

The VPI section of the standard uses the term "IO declaration" for a method's arguments. This is because, as for module and interface ports, an argument can be an input, an output or an inout. We'll need another class to handle IO declaration:

class rf_io_declaration;
extern function string get_name();
extern function string get_type();
extern function io_direction_e get_direction();

// ...
endclass

Given a vpiHandle that points to an IO declaration, it's easy to extract its direction:

typedef enum { INPUT, OUTPUT, INOUT } io_direction_e;

function io_direction_e rf_io_declaration::get_direction();
case (vpi_get(vpiDirection, io_declaration))
vpiInput : return INPUT;
vpiOutput : return OUTPUT;
vpiInout : return INOUT;
default : $fatal(0, "Direction %s not supported", vpi_get_str(vpiDirection,
io_declaration));
endcase
endfunction

To extract the type of an IO declaration, we can treat it as any normal variable. This means that we can use the rf_variable class's get_type() function to provide us with this information:

function string rf_io_declaration::get_type();
rf_variable v = new(vpi_handle(vpiExpr, io_declaration));
return v.get_type();
endfunction

Looping over a method's IO declaration is done in basically the same way as when going through variables, so we'll skip over that code.

Now that we've handle the similarities between tasks and functions, it's time to look at their differences. In terms of properties we can express through code, tasks don't provide anything more. This means that the rf_task class doesn't have to do anything special:

class rf_task extends rf_method;
function new(vpiHandle method);
super.new(method);
endfunction
endclass

We'll still define this class, because the rf_method class is virtual (meaning that we don't want to instantiate it directly). It also makes our API future-proof, if anything is added to tasks later.

Functions have a return type, which we would like to be able to interrogate. The rf_function class let's us do this:

class rf_function extends rf_method;
extern function string get_return_type();

// ...
endclass

As for method arguments, we can treat the return of a function as a variable and use the rf_variable class to get its type:

function string rf_function::get_return_type();
vpiHandle r = vpi_handle(vpiReturn, method);
rf_variable v;
if (r == null)
return "void";
v = new(r);
return v.get_type();
endfunction

These were the basic things we could extract about tasks and functions. Other interesting properties would be whether they are local, protected or public, whether they are virtual and so on, but we won't look at this here. These could be added to rf_method later.

As we said when we talked about variables, being able to interrogate the structure of declared methods only means that we've implemented introspection. Full reflection in this respect requires us to be able to call the methods of any object. Unfortunately, I couldn't find anything in the VPI chapter about how to do this, so it it seems that this might not be possible. Oh well, we were bound to be disappointed eventually...

Even though it's not possible to call functions, I can imagine how this could have been implemented. For a certain function handle (and by this I mean a vpiHandle) obtained from within an object we could set the contents of its input and inout arguments to our desired values via calls to vpi_set_value(...). Afterwards, we could trigger a call of the function via some VPI function call. An idea would be to simply do a vpi_put_value(...) on the function handle itself. This would be consistent with how VPI code can trigger named events. After the call, we could get the returned value using vpi_get_value(...).

Tasks are more complicated, though, because they can consume simulation time. We would be able to start a task from within an invoke(...) function and have it run in the background. Parallel behavior is modeled by threads and when starting a task, we'd need to specify where it gets started. Do we start a new thread, totally independent of all others? Do we start the task as a child process of the current thread? Regardless of where we would start the task, this could also be done by calling vpi_put_value(...) on the task handle. To be able to wait for a task to finish, we'd need to be able to set a callback for when its execution is completed. This callback could trigger an event which we would use for synchronization.

Implementing method calls via the VPI sounds like a lot of work, especially when handling tasks. It's a shame that the SystemVerilog committee decided to skip it entirely. Maybe we'll be lucky and it will make its way into the next IEEE 1800 release.

With this post I'm going to conclude our reflection series, but not before talking about some future steps. The first and most obvious is to test the library with a wider variety of simulators. I'd appreciate any help from enthusiastic users that have access to tools from other vendors. Another thing I'd like to implement is a more generic way of handling data types (i.e. an rf_type class), to be able to deal with both primitive and user defined types. This should make it easier to implement setting and getting of more than just int variables. A pretty cool thing would also be to be able to traverse the inheritance tree of a class, by getting its super-class and its sub-classes.

While the library is far from being usable in a production environment, you can get it from GitHub and play around with it. If you do want to use it for something and it's missing a feature you need, you can either open an issue or, even better, fork the repository, implement it yourself and send it upstream to be integrated.

Comments