The Humble Beginnings of a SystemVerilog Reflection API, Part 2
In the previous post we saw that it's possible to use the Verilog Programming Interface (VPI) to programmatically get information about classes. For example, we can "ask" a class what variables it has. We've wrapped the calls to the C interface in a nice SystemVerilog library by using the Direct Programming Interface (DPI). While being able to mine the code for information about its structure can prove very useful, what this gives use is merely introspection. True reflection requires that we're able to manipulate values stored inside objects, by getting or setting them.
As a template on how this part of the API should look, we're going to take a peek at how Java handles reflection. The first thing to note is that in Java everything is a class (kind of). All classes extend the Object class implicitly. Even primitive types, such as int and char, are wrapped in classes (i.e. Int and Char, respectively). The primitive types and their class wrappers are also pretty much interchangeable via the mechanisms of autoboxing and unboxing.
The java.lang.reflect.Field class (the counterpart to our rf_variable class) contains a set(...) method that allows it to change the value of the respective field for a given object instance. Since everything is an Object, the signature for this method is:
public class Field {
public void set(Object obj, Object value) {
// ...
}
// ...
}
Things aren't as easy in SystemVerilog. There isn't any root class from which all other classes branch out. There's also a clear separation between primitive types and classes. We're going to have to find a way to get around this.
Let's first look at what we can do about the obj argument of the aforementioned function. Since we don't have a built-in Object class, we're going to have to define our own. Our set(...) function will accept an instance of a class called rf_object_instance_base. This class will store a handle to the object as it is returned by the VPI (more details on this in a bit). We need to (somehow) pass references to objects of different types down to the VPI layer. These objects don't have any lowest common denominator, in the form of a common base class.
We have a similar problem with the value argument. Our set(...) function could take an object of a container class, rf_value_base, which stores the value in an "abstract" way. The problem here is compounded by the fact that values can be any SystemVerilog type, built-in or user defined.
Based on the discussion above, we can enhance the rf_variable class to contain the following function:
class rf_variable;
function void set(rf_object_instance_base object, rf_value_base value);
// ...
endclass
Now it's time to take this fuzzy idea and put it into practice. Let's start with the container for object instances. To be able to manipulate an object via the VPI, we need to know how to access it via a vpiHandle. The type of the object isn't really that important. The rf_object_instance_base class only needs to store this handle:
virtual class rf_object_instance_base;
protected vpiHandle class_obj;
function vpiHandle get_class_obj();
return class_obj;
endfunction
endclass
The difficult part is being able to get this handle from objects of different types. To do this without violating the language syntax we'll have to use a parameterized class, rf_object_instance #(...), which takes the object type as a parameter. This parameterized class only exists to make the compiler happy. It traverses the VPI model to find the vpiHandle of the object passed to its get(...) function:
class rf_object_instance #(type T = int) extends rf_object_instance_base;
static function rf_object_instance #(T) get(T object);
vpiHandle obj;
// set 'obj' using the VPI
get = new(obj);
return get;
endfunction
protected function new(vpiHandle class_obj);
this.class_obj = class_obj;
endfunction
endclass
The exact steps required to do the traversal aren't important for this discussion. One very important thing I learned after quite a bit of digging is that it's very very very important to make sure that any simulator optimizations get turned off when trying to work with objects. Normally, for signals and variables the simulator will tell you when you're trying to perform an illegal operation (such as reading a signal that doesn't have read access). When trying to traverse class object relationships, you might not get any such messages. The exact symptom I was seeing was that I was getting a null vpiHandle for an existing object. I've added some sanity checks inside the traversal code to check for this situation, but there may be more pitfalls that I haven't covered.
Here's an example how we can get the instance container for an object:
rf_object_instance_base o = rf_object_instance #(some_class)::get(some_obj);
We can implement the same pattern to deal with values. As before, we need a base class. In this case, this base class doesn't need to do anything, except sit there and look pretty for the compiler:
virtual class rf_value_base;
endclass
When we want to manipulate a concrete value, we can use a parameterized class, rf_value #(...). For those of you familiar with e's reflection capabilities, this concept is similar to using the rf_value_holder struct, together with the unsafe() operator. The parameterized class is going to do the heavy lifting:
class rf_value #(type T = int) extends rf_value_base;
local T value;
local static T def_value;
function new(T value = def_value);
this.value = value;
endfunction
function T get();
return value;
endfunction
function void set(T value);
this.value = value;
endfunction
endclass
The value stored in it can be set using the constructor:
rf_value #(int) v = new(5);
Notice, though, that the get(...) and set(...) functions only exist in the parameterized class. This means that the rf_variable class, which is going to use the value passed to it, needs to cast the container to it's appropriate type.
Before we dive into the implementation of rf_variable::set(...), we need a bit of background information. Getting and setting values is handled in a different way by the VPI than getting handles to simulation objects. Without turning this post into a VPI tutorial, let's have a quick look at how this is done. There are two functions: vpi_get_value(...) and vpi_put_value(...). The prototype for vpi_get_value(...) is:
void vpi_get_value(vpiHandle obj, p_vpi_value value_p);
The obj argument is a handle to the simulation object (signal or variable) that's being interrogated and p is a pointer to a structure in which to store the value information. The type definition for this p_vpi_value struct is:
typedef struct t_vpi_value {
PLI_INT32 format;
union {
PLI_BYTE8 *str;
PLI_INT32 scalar;
PLI_INT32 integer;
double real;
struct t_vpi_time *time;
struct t_vpi_vecval *vector;
struct t_vpi_strengthval *strength;
PLI_BYTE8 *misc;
} value;
} s_vpi_value, *p_vpi_value;
This data type allows all kinds of values to be described. The exact contents of this structure and how they're supposed to be used aren't important. What is important to note is that passing this information across the language boundary (between C and SystemVerilog) isn't going to be easy. First of all, this C structure just can't be translated into an equivalent SystemVerilog representation, because the latter doesn't have any concept of pointers. Secondly, even if we didn't have the problem with pointers, most simulators are only going to support primitive types in DPI declarations (even though the standard allows for more complicated aggregate types).
Since we can't work with any kind of value in a generic fashion, we're going to have the break the problem down. If we can't import the vpi_get_value(...) function directly, we can at least import a stripped down version of it that can only get integer values:
int vpi_get_value_int(vpiHandle obj) {
s_vpi_value v;
v.format = vpiIntVal;
vpi_get_value(obj, &v);
return v.value.integer;
}
We can do the same for its set(...) counterpart:
void vpi_put_value_int(vpiHandle obj, int value) {
s_vpi_value v;
v.format = vpiIntVal;
v.value.integer = value;
vpi_put_value(obj, &v, NULL, vpiNoDelay);
}
Since these functions only use primitive types for their arguments and return values, we can import them using the DPI:
import "DPI-C" context
function int vpi_get_value_int(vpiHandle obj);
import "DPI-C" context
function void vpi_put_value_int(vpiHandle obj, int value);
After this long detour, let's get back to the task at hand. Because we'll have multiple variants of the get_*(...) and put_*(...) functions in the VPI layer, the rf_variable class needs to know which one of them to call, based on the type of variable we're operating on. Since we can only operate on integers at the moment, let's limit the discussion to this type.
The only way I can currently envision implementing this is by writing a big case statement, which dispatches different functions depending on the variable's type:
function void rf_variable::set(rf_object_instance_base object, rf_value_base value);
vpiHandle var_ = get_var(object);
case (vpi_get_str(vpiType, var_))
"vpiIntVar" : set_value_int(var_, value);
default : $fatal(0, "Type '%s' not implemented", vpi_get_str(vpiType,
var_));
endcase
endfunction
The case is quite boring now, but you get the idea. We'd add a new item of each type we want to support. User defined types are for sure going to be a blast to implement...
The get_var(...) function traverses the VPI model to get the handle for the chosen variable inside the given object instance. It's implementation isn't that important for this discussion. More interesting is the set_value_int(...) function:
class rf_variable;
local function void set_value_int(vpiHandle var_, rf_value_base value);
rf_value #(int) val;
if (!$cast(val, value))
$fatal(0, "Internal error");
vpi_put_value_int(var_, val.get());
endfunction
// ...
endclass
As we saw above, to be able to access the value stored in the container, we need to cast it to its parameterization. Afterwards we can get use this value as an argument to vpi_put_value(...).
Getting the value of an object's variable can be done in the same way:
function rf_value_base rf_variable::get(rf_object_instance_base object);
vpiHandle var_ = get_var(object);
case (vpi_get_str(vpiType, var_))
"vpiIntVar" : return get_value_int(var_);
default : $fatal(0, "Type '%s' not implemented", vpi_get_str(vpiType,
var_));
endcase
endfunction
Here, the get_value_int(...) gets the value via the VPI:
class rf_variable;
local function rf_value_base get_value_int(vpiHandle var_);
rf_value #(int) ret = new();
ret.set(vpi_get_value_int(var_));
return ret;
endfunction
// ...
endclass
Using these two functions, it's possible to manipulate and interrogate the values of class variables using reflection:
rf_value #(int) v = new(5);
some_var.set(rf_object_instance #(some_class)::get(some_obj), v);
void'(!$cast(v, some_var.set(rf_object_instance #(some_class)::get(some_obj)));
I'm not particularly thrilled by having to implement a new pair of get_value_*(...)/set_value_*(...) functions for each new type that we want to support, but at least this is an internal implementation detail that doesn't affect the API. It can always be changed if something better comes along. Using a case statement and casting are also frowned upon in the OOP community, because they can usually be avoided by using polymorphism. If anyone has a cleaner implementation, this is also something that can be changed without making any modifications to the API.
If you want to give it a go for yourself, you can download the library from GitHub. At this point in time, the value getting/setting API is in the proof of concept stage. I'll add new types on request. If you don't want to wait for me you can get your hands dirty and contribute!
Comments