The Humble Beginnings of a SystemVerilog Reflection API

Reflection is a mechanism that allows "inspection of classes, interfaces, fields and methods at runtime without knowing the names of the interfaces, fields, methods at compile time. It also allows instantiation of new objects and invocation of methods". In e, using reflection together with define as compute macros allows us to do some really cool stuff.

A major complaint about SystemVerilog is that it lacks reflection capabilities. These could be useful for writing super generic code,but the main use I can see, though, is in testing. For example, in Java, the JUnit framework decides which methods to execute as unit tests based on annotations (@Test). Reflection could also allow a user to write some very generic checks, for example checking that after a method call the rand_mode() attribute of all variables of an object is set to 0. Mocking frameworks also use a lot of reflection to do their thing (I hear), though I don't really know the exact details.

Verilog and SystemVerilog provide the Verilog Programming Interface (VPI), which the IEEE 1800-2012 standard describes as a part of the "procedural interface that allows foreign language functions to access the internal data structures of a SystemVerilog simulation". It is defined as a C programming language interface to be used for writing C applications. Unfortunately, these functions aren't directly available in a SystemVerilog package. I've no idea why, since this is pretty low hanging fruit. (This isn't 100% true, since it is possible to use the VPI to "enhance" the simulator by defining new system tasks and functions, but the process is very cumbersome.)

I've used the VPI for some past projects. It's written in an object oriented style, but since it's plain old C code it doesn't have the same feel as a proper OOP language. This makes it not quite so comfortable to use. It is pretty powerful, though, and it allows a developer to mine a lot of information out of the compiled SystemVerilog code. On the other side of the HVL barricade, e's reflection API is excellent. It's every bit as powerful and very comfortable to use, making it a worthy reference.

Our goal in this post is to define a reflection API for SystemVerilog. Anything we develop should have a nice object oriented interface, with classes to model each language construct (i.e. variables, functions, tasks, classes, etc.). The most reasonable (and only) course of action is to leverage the existing VPI, by building an adaption layer.

The first thing we need to do is to find a way to access the VPI routines which are available in C from code written in SystemVerilog. Fortunately, this is possible via the Direct Programming Interface (DPI), which allows us to call code written in other programming languages. The interface to C code (called DPI-C) is thoroughly defined in the standard. We can use it write a package that imports the VPI functions into SystemVerilog. Apparently Dave Rich already beat me to the punch here (by a couple of years judging from the code comments), with his DVCon 2016 paper, Introspection into SystemVerilog without Turning It Inside Out, where he presents this exact idea. I had already created my own repository on GitHub, vpi, before reading his paper, so we'll be using that.

The VPI is fully documented in Annexes K, L and M of the IEEE 1800-2012 standard. These sections contain the header files that simulators must provide for VPI applications to include. In this post we're mostly interested in the parts related to classes and variables. For the next sections, I'll assume that readers are already familiar with the VPI. If you're new to the topic, you should give Sections 36, 37 (especially) and 38 of the LRM a quick read before you continue.

The first step is to mirror the type definitions on the C side into SystemVerilog. The DPI-C allows defines a rich set of equivalent type mappings from one language to the other, so this step is pretty straightforward:

typedef int PLI_INT32;
typedef longint PLI_INT64;
typedef chandle vpiHandle;

The basic thing we can do with a vpiHandle is to get certain properties it has. The properties simulation objects can have are defined in the VPI headers and also need to be mirrored into SystemVerilog:

parameter vpiUndefined = -1;
parameter vpiType = 1;
parameter vpiName = 2;

parameter vpiRandType = 610;
parameter vpiNotRand = 1;
parameter vpiRand = 2;
parameter vpiRandC = 3;

I've used parameters inside the package for them, but they could just as well have been constants. I'm not really sure what would have been better here (though now on second glance I'm leaning toward constants), so if you have some input here, I'd love to hear it.

Simulation objects are typically "connected" to each other via references. To traverse one-to-one relationships (e.g. a class has a class definition), we need to define the corresponding object types:

parameter vpiModule = 32;
parameter vpiPackage = 600;
parameter vpiClassDefn = 652;

It can also be the case that one simulation objects contains references to multiple other simulation objects of the same type (e.g. a class definition contains multiple variables). For traversing one-to-many relationships we need to define the appropriate values to pass to vpi_iterate(...):

parameter vpiVariables = 100;

Once we've set up our types and our other constants, we can import the VPI functions using the DPI-C:

import "DPI-C" context
function PLI_INT32 vpi_get(PLI_INT32 prop, vpiHandle obj);

import "DPI-C" context
function PLI_INT64 vpi_get64(PLI_INT32 prop, vpiHandle obj);

import "DPI-C" context
function string vpi_get_str(PLI_INT32 prop, vpiHandle obj);

import "DPI-C" context
function vpiHandle vpi_iterate(PLI_INT32 type_, vpiHandle ref_);

import "DPI-C" context
function vpiHandle vpi_handle(PLI_INT32 type_, vpiHandle ref_);

import "DPI-C" context
function vpiHandle vpi_scan(vpiHandle itr);

I've tried to use the same names for the arguments as in the LRM, but some of them are SystemVerilog keywords. In these cases I added an underscore as a suffix.

This didn't work directly when trying to compile the code. The SystemVerilog compiler complained that it couldn't find the vpi_*(...) functions anywhere. I guess this has something to do with the fact that when compiling it doesn't link the VPI binaries. What I tried to do is to define a dummy C file that just includes the VPI header:

#include "vpi_user.h"

I hope that this way I could force the linking to happen. This didn't really help. I think this is because, since we don't use any of the functions declared in the header, the C compiler assumes that we don't need them at all. After some searching online, I tried to find out if it's possible to force the compiler/linker to link unused symbols. This is what my search pointed me to:

PLI_BYTE8* (*vpi_get_str__)(PLI_INT32, vpiHandle) = &vpi_get_str;

I'm not a C expert, so don't quote me on this, but I think the code above should declare a function pointer with the respective return and argument types called vpi_get_str__(...) that points to the original vpi_get(...) function. Then, in our import code, instead of importing vpi_get_str(...), we would import vpi_get_str__(...). This also didn't work.

The only thing that did work was to define vpi_get_str__(...) as a wrapper function that calls the real vpi_get_str(...):

PLI_BYTE8* vpi_get_str__(PLI_INT32 prop, vpiHandle obj) {
return vpi_get_str(prop, obj);
}

After doing this for the other functions as well, I ended up with the following DPI-C imports:

import "DPI-C" context vpi_get__ =
function PLI_INT32 vpi_get(PLI_INT32 prop, vpiHandle obj);

import "DPI-C" context vpi_get64__ =
function PLI_INT64 vpi_get64(PLI_INT32 prop, vpiHandle obj);

import "DPI-C" context vpi_get_str__ =
function string vpi_get_str(PLI_INT32 prop, vpiHandle obj);

import "DPI-C" context vpi_iterate__ =
function vpiHandle vpi_iterate(PLI_INT32 type_, vpiHandle ref_);

import "DPI-C" context function vpiHandle vpi_handle(PLI_INT32 type_,
vpiHandle ref_);

import "DPI-C" context vpi_scan__ =
function vpiHandle vpi_scan(vpiHandle itr);

Instead of importing the original vpi_*(...) functions I imported the wrappers, which worked perfectly. I guess this is because the C compiler was happy when it saw the VPI functions getting used. While writing the post, though, I noticed that I didn't do this for vpi_handle(...), which I imported directly. This whole process of writing wrappers and importing them is pretty tedious, so if anyone has any idea what's going on here and how the imports could be streamlined, I'd very much appreciate it.

Implementation aspects aside, it's time to see the package in action:

module test;
import vpi::*;

initial begin
automatic chandle mod_it = vpi_iterate(vpiModule, null);

automatic chandle root = vpi_scan(mod_it);
$display("root = %d", root);
$display(" type = %s", vpi_get_str(vpiType, root));
$display(" name = %s", vpi_get_str(vpiName, root));

if (vpi_scan(mod_it) == null)
$display("no more top modules");
end
endmodule

The code above is going to figure out that the root module is called test and tells us that there are no more root modules. This isn't particularly impressive, but the important thing to note is that it's written exclusively in SystemVerilog.

Now that we've got the power of the VPI at our fingertips, it's time to start thinking about how to implement our reflection API. All reflection operations in e are handled by the rf_manager struct. This is as good a name as any other for the top level entity that I can think of at the moment. We want our rf_manager to be able to give us an object that contains all of the information about a certain class. This object will be of type rf_class. As a first step, we want to get the corresponding rf_class object based on the class name:

class rf_manager;
extern static function rf_class get_class_by_name(string name);
endclass

The get_class_by_name(...) method needs to look into our compiled library and check if it can find the definition of a class with the supplied name. This is done by traversing the VPI object model:

function rf_class rf_manager::get_class_by_name(string name);
vpiHandle package_it = vpi_iterate(vpiPackage, null);

while (1) begin
vpiHandle package_ = vpi_scan(package_it);
vpiHandle classdefn_it;

if (package_ == null)
break;
classdefn_it = vpi_iterate(vpiClassDefn, package_);
if (classdefn_it == null)
continue;

while (1) begin
vpiHandle classdefn = vpi_scan(classdefn_it);
if (classdefn == null)
break;
if (vpi_get_str(vpiName, classdefn) == name) begin
rf_class c = new(classdefn);
return c;
end
end
end
endfunction

The code above has some limitations. It only works for classes defined in packages and if the class name is used in multiple packages, it's going to stop at the first one it finds. This is something that's going to get fixed later, but now we're at the proof-of-concept phase.

I've made the get_class_by_name(...) static inside rf_manager, but a better idea might have been to make rf_manager a singleton and leave the function non-static. This way I could later implement state inside the class, which would be useful, for example, to cache calls to reflection methods (since I'm assuming VPI calls don't come cheap). I'm curious what you're thoughts are here.

What could we want to know about a class? For starters, we'd like to know its name and what variables are declared inside it:

class rf_class;
extern function string get_name();
extern function array_of_rf_variable get_variables();
extern function rf_variable get_variable_by_name(string name);

extern function new(vpiHandle classDefn);
endclass

We can get all of this information by traversing the VPI object model of the class definition, which is defined in Section 37.29 of the LRM:

function string rf_class::get_name();
return vpi_get_str(vpiName, classDefn);
endfunction


function array_of_rf_variable rf_class::get_variables();
rf_variable vars[$];
vpiHandle variables_it = vpi_iterate(vpiVariables, classDefn);
while (1) begin
rf_variable v;
vpiHandle variable = vpi_scan(variables_it);
if (variable == null)
break;
v = new(variable);
vars.push_back(v);
end
return vars;
endfunction


function rf_variable rf_class::get_variable_by_name(string name);
vpiHandle variables_it = vpi_iterate(vpiVariables, classDefn);
while (1) begin
vpiHandle variable = vpi_scan(variables_it);
if (variable == null)
break;
if (vpi_get_str(vpiName, variable) == name) begin
rf_variable v = new(variable);
return v;
end
end
endfunction

Once we have an object of type rf_variable, we can get its name or we can ask it whether it is a random or a state variable and if it is random, whether it is rand or randc:

class rf_variable;
extern function string get_name();
extern function bit is_rand();
extern function rand_type_e get_rand_type();
extern function new(vpiHandle variable);
endclass

And just as before, this information can be found by traversing the VPI object model, in this case the one defined in Section 37.17:

function string rf_variable::get_name();
return vpi_get_str(vpiName, variable);
endfunction

function bit rf_variable::is_rand();
return get_rand_type() != NOT_RAND;
endfunction

function rand_type_e rf_variable::get_rand_type();
PLI_INT32 rand_type = vpi_get(vpiRandType, variable);
case (rand_type)
vpiNotRand : return NOT_RAND;
vpiRand : return RAND;
vpiRandC : return RANDC;
default : $fatal(0, "Internal error");
endcase
endfunction

As you can see, once we know what information we want to extract, getting it is just a matter of consulting the VPI object models and performing the required traversal operations. Since this is a pretty laborious task, having a cleaner API based on object orientation can certainly help make things more usable.

Let's look at a simple example of the reflection API in action. Let's take a simple class definition:

package some_package;

class some_class;
int some_variable;
rand int some_rand_variable;
endclass

endpackage

Using the reflection API, we're able to print the random type of some_rand_variable:

module test;
import reflection::*;
import some_package::*;

initial begin
automatic rf_class rf_some_class =
rf_manager::get_class_by_name("some_class");
automatic rf_variable rf_some_rand_variable =
rf_some_class.get_variable_by_name("some_rand_variable");

rf_some_rand_variable.print();
end
endmodule

We've been calling our new package a reflection API, but what we've implemented so far is just introspection of the code structure (i.e. compile time aspects). Aside from implementing all relevant queries, to truly say that we've implemented introspection, we have to be able to get the values of variables from different objects (i.e. run time aspects). Once we have this, we'll be able to officially claim that we've implemented reflection when we're also able to set variable values. These steps will be coming soon, so stay tuned.

Experience shows that VPI support, particularly for SystemVerilog constructs, varies from vendor to vendor and strongly depends on how new the simulator version being used is. To check if the package can be used with a particular tool, I've written an "acceptance" test suite. It's based on SVUnit and it tries to check if all functionality is available. It's pretty light at the moment and it supports only one simulator, but it's a step in the right direction.

You can find the reflection package on GitHub. I'll be adding functionality to it when it's needed. As I've already said a couple of times throughout the post, I'm open to any ideas or contributions that you may have, so don't hesitate to use the comment section below or to get in touch with me via the contact page.

Comments