I think Yosys is one most important open sources projects ever created, at least for hardware development. It enabled so much research and innovation in FPGA and ASIC area. It did what gcc did for software development.

Building and installation Link to heading

These are the steps to build yosys with pyosys(python bindings). It’s not enabled by default. So you have to pass ENABLE_PYOSYS. Also, I am using virtualenv as I don’t want to install anything with root.

source .venv/bin/activate
make config-gcc
make ENABLE_PYOSYS=1 -j4
make install PREFIX=`pwd`/local ENABLE_PYOSYS=1

export PATH=`pwd`/local/bin:$PATH

Hello world Link to heading

pyosys provides API to run yosys passes and provide design information using ys.Design() object.

from pyosys import libyosys as ys

design = ys.Design()

rtl_sources = ["Files here"]
ys.run_pass(f"tee -q -o yosys.log read_verilog -mem2reg -noopt  {include_string}  {" ".join(rtl_sources)}", design)
ys.run_pass("tee -q -a yosys.log hierarchy", design)

Then you can use all the cool stuff like design iterations and passes. Starting with list of modules compiled and instantiated cell(basically other modules)

        for module in self.design.selected_whole_modules_warn():

            print(f"module.name.str()")    
            for cell in module.selected_cells():
                print(f"\tcell: {cell.type.str()}")

Deepdive Link to heading

Now that we have hello world is out the way, Let’s dig deeper into the python binding. On core code side, There are several places where specific interfaces are added to be called from python modules like

#ifdef WITH_PYTHON
	static std::map<unsigned int, RTLIL::Design*> *get_all_designs(void);
#endif

and definition

#ifdef WITH_PYTHON
static std::map<unsigned int, RTLIL::Design*> all_designs;
std::map<unsigned int, RTLIL::Design*> *RTLIL::Design::get_all_designs(void)
{
	return &all_designs;
}
#endif

There are other API that are available in C++ (and python) like selected_whole_modules_warn

struct RTLIL::Design
{
	unsigned int hashidx_;
	unsigned int hash() const { return hashidx_; }
    ....
    ....
	std::vector<RTLIL::Module*> selected_modules() const;
	std::vector<RTLIL::Module*> selected_whole_modules() const;
	std::vector<RTLIL::Module*> selected_whole_modules_warn(bool include_wb = false) const;

For module init, It’s more complicated. Starting In kernel/yosys.cc, Py_Initialize is called after appending INIT_MODULE

	#ifdef WITH_PYTHON
		PyImport_AppendInittab((char*)"libyosys", INIT_MODULE);
		Py_Initialize();
		PyRun_SimpleString("import sys");
		signal(SIGINT, SIG_DFL);
	#endif

and INIT_MODULE is macro in same file

#ifdef WITH_PYTHON
#if PY_MAJOR_VERSION >= 3
#   define INIT_MODULE PyInit_libyosys
    extern "C" PyObject* INIT_MODULE();
#else
#   define INIT_MODULE initlibyosys
	extern "C" void INIT_MODULE();
#endif

So, where are the wrappers? the complicated PY stuff.

For that there is script external/yosys/misc/py_wrap_generator.py which dumps all wrapper code using BOOST PYTHON . This is snippet of the script showing the module init macro used.

	BOOST_PYTHON_MODULE(libyosys)
	{
		using namespace boost::python;

		class_<Initializer>("Initializer");
		scope().attr("_hidden") = new Initializer();

		def("log_to_stream", &log_to_stream);
""")

Side note about BOOST python modules, From the docs:

Introduction
This header provides the basic facilities needed to create a Boost.Python extension module.

Macros
BOOST_PYTHON_MODULE(name) is used to declare Python module initialization functions. The name argument must exactly match the name of the module to be initialized, and must conform to Python's identifier naming rules. Where you would normally write

extern "C" void initname()
{
   ...
}
Boost.Python modules should be initialized with

BOOST_PYTHON_MODULE(name)
{
   ...
}

So, who calls py_wrap_generator.py?

During build process, Makefile calls that script to generate the wrappers to be compiled into libyosys.so

PY_GEN_SCRIPT= py_wrap_generator

...
...

ifeq ($(ENABLE_PYOSYS),1)
$(PY_WRAPPER_FILE).cc: misc/$(PY_GEN_SCRIPT).py $(PY_WRAP_INCLUDES)
	$(Q) mkdir -p $(dir $@)
	$(P) python$(PYTHON_VERSION) -c "from misc import $(PY_GEN_SCRIPT); $(PY_GEN_SCRIPT).gen_wrappers(\"$(PY_WRAPPER_FILE).cc\")"
endif

The last bit is installing libyosys.so to be called from python as normal python module. The full path should be

.venv/lib/python3.10/dist-packages/pyosys/libyosys.so