How pysv works

pysv relies heavily on pybind11 to interface with Python interpreter. It is done via embedding the Python interpreter inside the shared library. You can find more details on pybind11’s documentation.

Function code generation

Every Python function is directly embedded into corresponding C function as shown below:

extern "C" {
__attribute__((visibility("default"))) int32_t foo() {
    auto globals = py::dict();
    auto locals = py::dict();

    py::exec(R"(
    def foo():
        return 42

    __result = foo()
    )", globals, locals);
    return locals["__result"].cast<int32_t>();
}
}

The entire function is declared inside extern C block and marked with proper visibility to ensure that normal C code can link against it, which is a requirement for SystemVerilog DPI calls.

There will be an overhead of redundant parsing of the same functions if it is called multiple times. We will improve it in the future release.

Imports

pysv looks through the call stack globals() and stores the import information for each @sv(). Then it dumps the imported objects’ name into the generated C++ code, which will be load up then first time the object is imported:

import numpy as np

@sv()
def foo():
  pass
auto globals = py::dict();
import_module("numpy", "np", globals);

Foreign definitions

pysv inspects the imports and see if there is any foreign modules have been imported and not in the system module. If any foreign object is detected, pysv will dump the current sys.path into the generated C++ code and have the Python interpreter load it when initialize the pysv runtime, as shown below:

auto SYS_PATH = {"/tmp/example",
                 "/usr/lib/python38.zip",
                 "/usr/lib/python3.8",
                 "/usr/lib/python3.8/lib-dynload",
                 "/tmp//env/lib/python3.8/site-packages"};

// add to sys.path later during initialization
for (auto const &path: SYS_PATH) {
    sys.attr("path").attr("append")(py::str(path));
}

Note

The dumped sys.path content is in absolute path, which implies that the simulator needs to have access to these directories for the interpreter to load foreign modules. It is recommended to compile pysv and simulate the test bench using the same filesystem.

SystemVerilog bindings

The SystemVerilog standard provides a connivent way, called DPI, to import C functions into SystemVerilog. pysv uses DPI to import generated functions into SystemVerilog and then wrap them using SystemVerilog semantics if needed.

Here is an example to see generated SystemVerilog class definition

class Foo:
def __init__(self):
    pass

@sv()
def bar(self):
    return 42
package pysv;
import "DPI-C" function chandle Foo_pysv_init();
import "DPI-C" function int Foo_bar(input chandle self);
import "DPI-C" function void Foo_destroy(input chandle self);
import "DPI-C" function void pysv_finalize();
class PySVObject;
chandle pysv_ptr;
endclass
class Foo extends PySVObject;
function new();
    pysv_ptr = Foo_pysv_init();
endfunction
function int bar();
    return Foo_bar(pysv_ptr);
endfunction
function void destroy();
    Foo_destroy(pysv_ptr);
endfunction
endclass
endpackage

DPI functions are in the form of

import "DPI-C" function chandle Foo_pysv_init();

Python types are converted into SystemVerilog types based on auto detection or user-provided DataType.

Notice that the class methods is flattened into normal function where the first argument is the C pointer. Each generated class will hold a pointer to its corresponding Python class object.