This post is about the current implementation of cocotb bootstrap from VPI to python. I wrote an earlier post about it but things change all the time and there has been a good refactor (in 2.0, I think?).
Setup Link to heading
When cocotb Makefiles are used, they build COCOTB_BOOTSTRAP which has shared object and entry point separated with ,
GPI_ENTRY_ := $(python -m cocotb_tools.config --gpi-entry-point) # libgpi.so,gpi_initialize
PYGPI_ENTRY_ := $(python -m cocotb_tools.config --pygpi-entry-point) # cocotb.simulator*.so,pygpi_initialize
COCOTB_BOOTSTRAP := $(GPI_ENTRY_):$(LIBPYTHON_LOC):$(PYGPI_ENTRY_)
VPI bootstrap Link to heading
First point of entry is VPI callback registered with vlog_startup_routines, where it registers call to cocotb_bootstrap_entry in src/cocotb/share/lib/gpi/bootstrapper.cpp
// VPI: Verilog simulators call vlog_startup_routines[].
COCOTB_EXPORT void (*vlog_startup_routines[])() = {cocotb_bootstrap_entry,
nullptr};
load_bootstrap iterates over COCOTB_BOOTSTRAP and calls the entry point in each library in the following format LIB:FUNC,LIB:FUNC. This effectively calls gpi_initialize and pygpi_initialize
static int load_bootstrap() {
const char *bootstrap_env = getenv("COCOTB_BOOTSTRAP");
if (!bootstrap_env || !bootstrap_env[0]) {
fprintf(stderr, "[%s:%d]: COCOTB_BOOTSTRAP is not set or is empty\n",
__FILE__, __LINE__);
return -1;
}
std::string entries = bootstrap_env;
std::string::size_type start = 0;
while (start <= entries.size()) {
const auto next_separator = entries.find(entry_separator, start);
const auto end = next_separator == std::string::npos ? entries.size()
: next_separator;
const std::string entry =
trim_whitespace(entries.substr(start, end - start));
if (!entry.empty()) {
const auto function_separator = entry.rfind(entry_point_separator);
const bool has_entry_point =
function_separator != std::string::npos;
const std::string library =
has_entry_point
? trim_whitespace(entry.substr(0, function_separator))
: entry;
const std::string function =
has_entry_point
? trim_whitespace(entry.substr(function_separator + 1))
: "";
if (library.empty() || (has_entry_point && function.empty())) {
fprintf(stderr,
"[%s:%d]: Invalid entry '%s' in COCOTB_BOOTSTRAP\n",
__FILE__, __LINE__, entry.c_str());
return -1;
}
void *library_handle = utils_dyn_open(library.c_str());
if (!library_handle) {
fprintf(stderr, "[%s:%d]: Unable to load library '%s'\n",
__FILE__, __LINE__, library.c_str());
return -1;
}
if (has_entry_point) {
void *function_handle =
utils_dyn_sym(library_handle, function.c_str());
if (!function_handle) {
fprintf(
stderr,
"[%s:%d]: Unable to find entry point '%s' in library "
"'%s'\n",
__FILE__, __LINE__, function.c_str(), library.c_str());
return -1;
}
auto entry_func =
reinterpret_cast<bootstrap_entry_func>(function_handle);
const int error = entry_func();
if (error) {
fprintf(
stderr,
"[%s:%d]: Entry point '%s' in library '%s' failed\n",
__FILE__, __LINE__, function.c_str(), library.c_str());
return -1;
}
gpi_initialize Link to heading
gpi_initialize calls the core GPI depending on simulator, mainly choosing VPI/FLI/VHPI so python can access signals and register value change callbacks. GPI is an interesting piece of engineering and worth several posts.
int gpi_initialize() {
gpi_init_logging_and_debug();
gpi_entry_point();
// Initialization requires at least one GPI implementation.
return gpi_has_registered_impl() ? 0 : 1;
}
void gpi_entry_point() {
LOG_TRACE("=> [ GPI Init ]");
/* Lets look at what other libs we were asked to load too */
char *lib_env = getenv("GPI_IMPL");
if (lib_env) {
std::string lib_list = lib_env;
std::string const delim = ",";
std::vector<std::string> to_load;
size_t e_pos = 0;
while (std::string::npos != (e_pos = lib_list.find(delim))) {
std::string lib = lib_list.substr(0, e_pos);
lib_list.erase(0, e_pos + delim.length());
to_load.push_back(lib);
}
if (lib_list.length()) {
to_load.push_back(lib_list);
}
gpi_load_libs(to_load);
}
gpi_print_registered_impl();
LOG_TRACE("[ GPI Init ] =>");
}
pygpi_initialize Link to heading
At this point, in src/cocotb/share/lib/pygpi/embed.cpp, pygpi_initialize sets up the python interpreter and registers the start of sim callback start_of_sim_time.
extern "C" PYGPI_EXPORT int pygpi_initialize(void) {
pygpi_init_debug();
pygpi_logging_initialize();
PYGPI_LOG_TRACE("GPI Init => [ PYGPI Init ]");
DEFER(PYGPI_LOG_TRACE("[ PYGPI Init ] => GPI Init"));
if (python_init_called) {
// LCOV_EXCL_START
PYGPI_LOG_ERROR("PyGPI library initialized again!");
return -1;
// LCOV_EXCL_STOP
}
python_init_called = 1;
// must set program name to Python executable before initialization, so
// initialization can determine path from executable
static wchar_t interpreter_path[PATH_MAX], sys_executable[PATH_MAX];
if (get_interpreter_path(interpreter_path, sizeof(interpreter_path))) {
// LCOV_EXCL_START
return -1;
// LCOV_EXCL_STOP
}
PYGPI_LOG_INFO("Using Python %s interpreter at %ls", PY_VERSION,
interpreter_path);
/* Use the new Python Initialization Configuration from Python 3.8. */
PyConfig config;
PyStatus status;
PyConfig_InitPythonConfig(&config);
DEFER(PyConfig_Clear(&config));
PyConfig_SetString(&config, &config.executable, interpreter_path);
status = PyConfig_SetArgv(&config, 1, argv);
if (PyStatus_Exception(status)) {
// LCOV_EXCL_START
PYGPI_LOG_ERROR("Failed to set ARGV during the Python initialization");
if (status.err_msg != NULL) {
PYGPI_LOG_ERROR("\terror: %s", status.err_msg);
}
if (status.func != NULL) {
PYGPI_LOG_ERROR("\tfunction: %s", status.func);
}
return -1;
// LCOV_EXCL_STOP
}
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
// LCOV_EXCL_START
PYGPI_LOG_ERROR("Failed to initialize Python");
if (status.err_msg != NULL) {
PYGPI_LOG_ERROR("\terror: %s", status.err_msg);
}
if (status.func != NULL) {
PYGPI_LOG_ERROR("\tfunction: %s", status.func);
}
return -1;
// LCOV_EXCL_STOP
}
/* Sanity check: make sure sys.executable was initialized to
* interpreter_path. */
PyObject *sys_executable_obj = PySys_GetObject("executable");
if (sys_executable_obj == NULL) {
// LCOV_EXCL_START
PYGPI_LOG_ERROR("Failed to load sys.executable");
// LCOV_EXCL_STOP
} else if (PyUnicode_AsWideChar(sys_executable_obj, sys_executable,
sizeof(sys_executable)) == -1) {
// LCOV_EXCL_START
PYGPI_LOG_ERROR("Failed to convert sys.executable to wide string");
// LCOV_EXCL_STOP
} else if (wcscmp(interpreter_path, sys_executable) != 0) {
// LCOV_EXCL_START
PYGPI_LOG_ERROR(
"Unexpected sys.executable value (expected '%ls', got '%ls')",
interpreter_path, sys_executable);
// LCOV_EXCL_STOP
}
gpi_register_start_of_sim_time_callback(start_of_sim_time, nullptr);
gpi_register_finalize_callback(finalize, nullptr);
/* Before returning we check if the user wants pause the simulator thread
such that they can attach */
const char *pause = getenv("COCOTB_ATTACH");
if (pause) {
unsigned long sleep_time = strtoul(pause, NULL, 10);
/* This should check for out-of-range parses which returns ULONG_MAX and
sets errno, as well as correct parses that would be sliced by the
narrowing cast */
if (errno == ERANGE || sleep_time >= UINT_MAX) {
// LCOV_EXCL_START
PYGPI_LOG_ERROR(
"COCOTB_ATTACH only needs to be set to ~30 seconds");
return -1;
// LCOV_EXCL_STOP
}
if ((errno != 0 && sleep_time == 0) || (sleep_time <= 0)) {
// LCOV_EXCL_START
PYGPI_LOG_ERROR(
"COCOTB_ATTACH must be set to an integer base 10 or omitted");
return -1;
// LCOV_EXCL_STOP
}
PYGPI_LOG_INFO(
"Waiting for %lu seconds - attach to PID %d with your debugger",
sleep_time, getpid());
sleep((unsigned int)sleep_time);
}
return 0;
}
Then start_of_sim_time calls python load_entry which calls cocotb test discovery and execution.
src/cocotb/share/lib/pygpi/embed.cpp
static int start_of_sim_time(void *) {
PYGPI_LOG_TRACE("GPI Start Sim => [ PYGPI Start ]");
DEFER(PYGPI_LOG_TRACE("[ PYGPI Start ] => GPI Start Sim"));
// Check that we are not already initialized
if (embed_init_called) {
// LCOV_EXCL_START
PYGPI_LOG_ERROR("PyGPI library initialized again!");
return -1;
// LCOV_EXCL_STOP
}
embed_init_called = 1;
// Ensure that the current thread is ready to call the Python C API
auto gstate = PyGILState_Ensure();
DEFER(PyGILState_Release(gstate));
c_to_python();
DEFER(python_to_c());
auto entry_utility_module = PyImport_ImportModule("pygpi.entry");
if (!entry_utility_module) {
// LCOV_EXCL_START
PyErr_Print();
return -1;
// LCOV_EXCL_STOP
}
DEFER(Py_DECREF(entry_utility_module));
auto cocotb_retval =
PyObject_CallMethod(entry_utility_module, "load_entry", nullptr);
Python load_entry Link to heading
In src/pygpi/entry.py, load_entry calls the following registered python functions.
def load_entry() -> None:
"""Gather entry point information by parsing :envvar:`PYGPI_USERS`."""
entry_points_str: list[str] = _env.get_list(
"PYGPI_USERS",
(
"cocotb_tools._coverage:start_cocotb_library_coverage",
"cocotb.logging:_configure",
"cocotb._init:init_package_from_simulation",
"cocotb.regression:_run_regression",
),
)
# Parse the entry point string of the form "module:func,module:func,...".
# Any failure prevents any entry points from being loaded.
entry_points: list[tuple[str, str]] = []
try:
for entry_point_str in entry_points_str:
entry_module_str, entry_func_str = entry_point_str.split(":")
# TODO maybe some basic validation of the module and function names.
# WITHOUT IMPORTING THEM.
entry_points.append((entry_module_str, entry_func_str))
except Exception as e:
raise RuntimeError(f"Failure to parse PYGPI_USERS {entry_point_str!r}") from e
# Run all entry points.
# Expect failure to stop the loading of any additional entry points.
for entry_module_str, entry_func_str in entry_points:
entry_module = importlib.import_module(entry_module_str)
entry_func: Callable[[], object] = operator.attrgetter(entry_func_str)(
entry_module
)
entry_func()
Regression - Discovery and running Link to heading
Finally, _run_regression calls _setup_regression_manager, which creates the RegressionManager and discovers tests, then calls _manager_inst.start_regression() to run them.
def _run_regression() -> None:
"""Setup and run a regression."""
# sys.path normally includes "" (the current directory), but does not appear to when Python is embedded.
# Add it back because users expect to be able to import files in their test directory.
sys.path.insert(0, "")
# From https://www.python.org/dev/peps/pep-0565/#recommended-filter-settings-for-test-runners
# If the user doesn't want to see these, they can always change the global
# warning settings in their test module.
if not sys.warnoptions:
warnings.simplefilter("default")
_setup_regression_manager()
if _env.get_bool("COCOTB_LIST_TESTS", False):
_manager_inst.list_tests()
else:
_manager_inst.start_regression()
shutdown.register(_manager_inst._on_sim_end)