This post complements an earlier post about test discovery. This should close the gap between simulation start and actual cocotb test call.

cocotb hello-world in 2 minutes Link to heading

I will write down the basic commands to run the adder example that ships with cocotb. I am assuming latest iverilog is installed already.

virtualenv .venv
source .venv/bin/activate

cd cocotb
pip install .
cd examples/adder/tests
make

You will probably see something like this

     0.00ns INFO     Seeding Python random module with 1642336775
     0.00ns WARNING  Pytest not found, assertion rewriting will not occur
     0.00ns INFO     Found test test_adder.adder_basic_test
     0.00ns INFO     Found test test_adder.adder_randomised_test
     0.00ns INFO     running adder_basic_test (1/2)
VCD info: dumpfile dump.vcd opened for output.
     2.00ns INFO     adder_basic_test passed
     2.00ns INFO     running adder_randomised_test (2/2)
    22.00ns INFO     adder_randomised_test passed
    22.00ns INFO     ******************************************************************************************
                     ** TEST                              STATUS  SIM TIME (ns)  REAL TIME (s)  RATIO (ns/s) **
                     ******************************************************************************************
                     ** test_adder.adder_basic_test        PASS           2.00           0.00        869.36  **
                     ** test_adder.adder_randomised_test   PASS          20.00           0.00       5147.27  **
                     ******************************************************************************************
                     ** TESTS=2 PASS=2 FAIL=0 SKIP=0                     22.00           0.04        615.90  **
                     ******************************************************************************************

You can try make -n and see iverlog build and run commands. These are the final iverilog commands to run the simulation(without Makefiles)

export PYTHONPATH=$PWD/../model:$PYTHONPATH
iverilog -o sim_build/sim.vvp -D COCOTB_SIM=1 -s adder -f sim_build/cmds.f -g2012   /examples/adder/tests/../hdl/adder.sv
MODULE=test_adder TESTCASE= TOPLEVEL=adder TOPLEVEL_LANG=verilog \
         vvp -M ../../../.venv/lib/python3.8/site-packages/cocotb/libs -m libcocotbvpi_icarus   sim_build/sim.vvp

The infrastructure Link to heading

The core of cocotb is compiled into the following shared objects

ls  .venv/lib/python3.8/site-packages/cocotb/libs/
libcocotb.so            libcocotbvhpi_ius.so       libcocotbvpi_ghdl.so     libcocotbvpi_modelsim.so   libembed.so   libpygpilog.so
libcocotbutils.so       libcocotbvhpi_modelsim.so  libcocotbvpi_icarus.vpl  libcocotbvpi_vcs.so        libgpilog.so
libcocotbvhpi_aldec.so  libcocotbvpi_aldec.so      libcocotbvpi_ius.so      libcocotbvpi_verilator.so  libgpi.so

iverilog load libcocotbvpi_icarus.vpl which loads the rest of cocotb libraries

$ldd  .venv/lib/python3.8/site-packages/cocotb/libs/libcocotbvpi_icarus.vpl
	libgpi.so => /.venv/lib/python3.8/site-packages/cocotb/libs/libgpi.so (0x00007f63699b4000)
	libgpilog.so => /.venv/lib/python3.8/site-packages/cocotb/libs/libgpilog.so (0x00007f63699af000)
	libcocotbutils.so => /.venv/lib/python3.8/site-packages/cocotb/libs/libcocotbutils.so (0x00007f63695a4000)
	libembed.so => /.venv/lib/python3.8/site-packages/cocotb/libs/libembed.so (0x00007f636959f000)

Compilation Link to heading

setuptools is used here to configure the build of external C/C++ files. In setup.py, the external modules are configured with get_ext

    ext_modules=get_ext(),

get_ext first configures the common libraries used by all simulators

def _get_common_lib_ext(include_dir, share_lib_dir):
    """
    Defines common libraries.

    All libraries go into the same directory to enable loading without modifying the library path (e.g. LD_LIBRARY_PATH).
    In Makefile `LIB_DIR` (s) is used to point to this directory.
    """

    #
    #  libcocotbutils
    #
    libcocotbutils_sources = [
        os.path.join(share_lib_dir, "utils", "cocotb_utils.cpp")
    ]
    if os.name == "nt":
        libcocotbutils_sources += ["libcocotbutils.rc"]
    libcocotbutils_libraries = ["gpilog"]
    if os.name != "nt":
        libcocotbutils_libraries.append("dl")  # dlopen, dlerror, dlsym
    libcocotbutils = Extension(
        os.path.join("cocotb", "libs", "libcocotbutils"),
        define_macros=[("COCOTBUTILS_EXPORTS", "")] + _extra_defines,
        include_dirs=[include_dir],
        libraries=libcocotbutils_libraries,
        sources=libcocotbutils_sources,
    )

Then configures simulator-specific libraries. For icarus, libcocotbvpi is configured here

    #
    #  Icarus Verilog
    #
    icarus_extra_lib = []
    logger.info("Compiling libraries for Icarus Verilog")
    if os.name == "nt":
        icarus_extra_lib = ["icarus"]

    icarus_vpi_ext = _get_vpi_lib_ext(
        include_dir=include_dir,
        share_lib_dir=share_lib_dir,
        sim_define="ICARUS",
        extra_lib=icarus_extra_lib,
    )
    ext.append(icarus_vpi_ext)

And core VPI files are configured here(common for all simualtors)

def _get_vpi_lib_ext(
    include_dir, share_lib_dir, sim_define, extra_lib=[], extra_lib_dir=[]
):
    lib_name = "libcocotbvpi_" + sim_define.lower()
    libcocotbvpi_sources = [
        os.path.join(share_lib_dir, "vpi", "VpiImpl.cpp"),
        os.path.join(share_lib_dir, "vpi", "VpiCbHdl.cpp"),
    ]

Bootstrap and Python interpreter init Link to heading

In VpiImpl.cpp, vlog_startup_routines called by simulator when shared objects are loaded

COCOTBVPI_EXPORT void (*vlog_startup_routines[])() = {
    register_embed, gpi_load_extra_libs, register_initial_callback,
    register_final_callback, nullptr};

These functions register implementations callbacks (VPI, VHPI, FLI). But the important one is gpi_load_exra_libs as it embeds python

    /* Finally embed Python */
    embed_init_python();

embed_init_python calls _embed_init_python(defined in gpi_embed.cpp) which start the interpreter

extern "C" COCOTB_EXPORT void _embed_init_python(void) {
    assert(!gtstate);  // this function should not be called twice

    to_python();
    set_program_name_in_venv();
    Py_Initialize(); /* Initialize the interpreter */
    PySys_SetArgvEx(1, argv, 0);

    /* Swap out and return current thread state and release the GIL */
    gtstate = PyEval_SaveThread();
    to_simulator();

Jump to python (sim_init) Link to heading

At this point, python interpreter is initialized. But we need to jump to python to start the test discovery and execution.

Starting with register_initial_callback on VPI bootstrap list above

static void register_initial_callback() {
    sim_init_cb = new VpiStartupCbHdl(vpi_table);
    sim_init_cb->arm_callback();
}

And arm_callback registers cb_rtn to call handle_vpi_callback

VpiCbHdl::VpiCbHdl(GpiImplInterface *impl) : GpiCbHdl(impl) {
    vpi_time.type = vpiSimTime;

    cb_data.cb_rtn = handle_vpi_callback;
}

int VpiCbHdl::arm_callback() {
    if (m_state == GPI_PRIMED) {
        fprintf(stderr, "Attempt to prime an already primed trigger for %s!\n",
                m_impl->reason_to_string(cb_data.reason));
    }

    // Only a problem if we have not been asked to deregister and register
    // in the same simulation callback
    if (m_obj_hdl != NULL && m_state != GPI_DELETE) {
        fprintf(stderr, "We seem to already be registered, deregistering %s!\n",
                m_impl->reason_to_string(cb_data.reason));
        cleanup_callback();
    }

    vpiHandle new_hdl = vpi_register_cb(&cb_data);

And handle_vpi_callback calls run_callback

    if (old_state == GPI_PRIMED) {
        cb_hdl->set_call_state(GPI_CALL);
        cb_hdl->run_callback();

And run_callback calls gpi_embed_init

int VpiStartupCbHdl::run_callback() {
    ...
    ...
    gpi_embed_init(info.argc, info.argv);

    return 0;
}

In GpiCommon.cpp, gpi_embed_init calls embed_sim_init

void gpi_embed_init(int argc, char const *const *argv) {
    if (embed_sim_init(argc, argv)) gpi_embed_end();
}

In embed.cpp,embed_sim_init calls _embed_sim_init

extern "C" int embed_sim_init(int argc, char const *const *argv) {
    if (init_failed) {
        // LCOV_EXCL_START
        return -1;
        // LCOV_EXCL_STOP
    } else {
        return _embed_sim_init(argc, argv);
    }
}

In gpi_embed.cpp, _embed_sim_init gets the entry module and function for python

extern "C" COCOTB_EXPORT int _embed_sim_init(int argc,
                                             char const *const *argv) {

    auto entry_utility_module = PyImport_ImportModule("pygpi.entry");

    auto entry_info_tuple =
        PyObject_CallMethod(entry_utility_module, "load_entry", NULL);
    if (!entry_info_tuple) {

Here is the lookup code for pygpi/entry.py, which looks for cocotb:_initialise_testbench

def load_entry() -> Tuple[ModuleType, Callable]:
    """Gather entry point information by parsing :envvar:`PYGPI_ENTRY_POINT`."""
    entry_point_str = os.environ.get("PYGPI_ENTRY_POINT", "cocotb:_initialise_testbench")
    try:
        if ":" not in entry_point_str:
            raise ValueError("Invalid PYGPI_ENTRY_POINT, missing entry function (no colon).")
        entry_module_str, entry_func_str = entry_point_str.split(":", 1)
        entry_module = importlib.import_module(entry_module_str)
        entry_func = reduce(getattr, entry_func_str.split('.'), entry_module)

Note that _embed_sim_init formats argv for _initialise_testbench_ and then calls it.

    auto cocotb_retval =
        PyObject_CallFunctionObjArgs(entry_point, argv_list, NULL);

_initialise_testbench_ is defined cocotb/__init__.py which calls RegressionManager.from_discovery

def _initialise_testbench_(argv_):
...
...
...
    # start Regression Manager
    global regression_manager
    regression_manager = RegressionManager.from_discovery(top)
    regression_manager.execute()

Fin.