Skip to content
Open
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ check(call_integer_vector("gain", arg(integer_vector'(1, 2))) = integer_vector'(
| Questa/ModelSim | FLI | Tested manually on Linux; the Windows build is untested |
| Riviera-PRO | VHPI | Tested manually on Linux and Windows |

A simulator provided by a VUnit package says how it loads the bridge
itself, by two attributes of its simulator interface class: see
[Simulator packages](https://vunit-python-bridge.readthedocs.io/en/latest/simulator_packages.html).

## Requirements

- VUnit 5.0.0.dev15 or later (`vunit_hdl` on PyPI), which pip installs with the package.
Expand Down
4 changes: 4 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ Questa/ModelSim FLI Tested manually on Linux; the Win
Riviera-PRO VHPI Tested manually on Linux and Windows
================================= ========== ========================================================

A simulator provided by a VUnit package says how it loads the bridge itself, see
:doc:`simulator_packages`.

Getting started
---------------

Expand Down Expand Up @@ -131,6 +134,7 @@ become available through ``python_context``:
:hidden:

how_it_works
simulator_packages

.. toctree::
:caption: About
Expand Down
5 changes: 3 additions & 2 deletions docs/requirements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ Requirements

* A VUnit with support for packages and simulator hooks, see the `README
<https://github.com/VUnit/vunit-python-bridge#readme>`__.
* NVC, GHDL, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`.
Any other simulator raises a ``RuntimeError``.
* NVC, GHDL, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`,
or a simulator from a VUnit package that describes how it loads the bridge, see
:ref:`python_bridge:simulator_packages`. Any other simulator raises a ``RuntimeError``.
* VHDL-2008 or later.
* CPython 3.10 or later with the standard (GIL) build. Free-threaded builds
are rejected with an error.
Expand Down
56 changes: 56 additions & 0 deletions docs/simulator_packages.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.. _python_bridge:simulator_packages:

Simulator packages
------------------

A simulator can be provided by a VUnit package, whose ``vunit_pkg.toml`` names
its simulator interface class. The bridge does not list such simulators.
It collects them from VUnit, which knows the simulator interface classes of the
packages added before the bridge. A class says how the bridge is served with
two attributes:

``foreign_language_interface``
How the simulator calls the bridge library: ``"VHPIDIRECT"``, ``"FLI"`` or
``"VHPI"``.

* ``"VHPIDIRECT"``: the foreign attributes are
``VHPIDIRECT <library file name> <entry point>``, with the file name
only, so the simulator has to load the library itself, from the flags or
the environment the hooks below give it.
* ``"FLI"``: the attributes of the FLI variant of the library, built against
the ``mti.h`` of the simulator installation, as for Questa/ModelSim.
* ``"VHPI"``: the attributes of the VHPI variant, built against the
``vhpi_user.h`` of the simulator installation, as for Riviera-PRO.

``native_library_hooks(library_file)``
A static or class method returning how the simulator loads the given
library: a ``dict`` of keyword arguments for
``context.register_simulator_hooks``, that is any of ``elab_flags``,
``run_flags``, ``process_flags`` and ``run_env``, each a function as
described under "Simulator Hooks" in the VUnit documentation of packages.
The bridge registers them for the simulator, with its own ``run_env`` on top
of the one returned, which adds ``VUNIT_PYTHON_BRIDGE_CONFIG`` to the
environment. The simulator interface must therefore apply the run
environment hooks to the simulation.

For example, for a simulator that loads a library given with ``--load`` when
it runs a test:

.. code-block:: python

class MySimulatorInterface(SimulatorInterface):
name = "mysim"
foreign_language_interface = "VHPIDIRECT"

@staticmethod
def native_library_hooks(library_file):
return {"run_flags": lambda simulator_interface: [f"--load={library_file}"]}

The simulators of these classes are added to the built-in ones the bridge
serves, and take precedence over a built-in simulator of the same ``name``.
Only the selected simulator is checked and has its hooks called. Selecting a
simulator that is not among them fails when the package is added, with an
error naming the simulators the bridge serves and the two attributes. So do
an unknown interface name, only one of the two attributes, and hooks that are
not a ``dict`` of the keys above. A class defining only one of the attributes
is ignored when another simulator is selected.
1 change: 1 addition & 0 deletions release_notes/news.d/41.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Simulators provided by VUnit packages can be used with the bridge, see "Simulator packages" in the documentation.
33 changes: 9 additions & 24 deletions src/vunit_python_bridge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

the VUnit Python bridge, a small C library (native/*.c) embedding CPython in the simulator
process, called through VHPIDIRECT (NVC, GHDL), the FLI (Questa/ModelSim) or VHPI
(Riviera-PRO).
(Riviera-PRO). A simulator provided by a VUnit package names its interface itself, see
bridge.bridge_simulators.

Modules:

Expand All @@ -40,20 +41,6 @@

VHDL_PATH = Path(__file__).parent.resolve() / "hdl" / "src"

# The foreign language interface implementing the package for a simulator. VUnit says which
# simulator was selected, the package says how it is served: NVC and GHDL both go through
# VHPIDIRECT but bind the library differently, Questa/ModelSim goes through the FLI and
# Riviera-PRO through VHPI.
FOREIGN_LANGUAGE_INTERFACES = {
"nvc": "VHPIDIRECT_NVC",
"ghdl": "VHPIDIRECT_GHDL",
"modelsim": "FLI",
"rivierapro": "VHPI",
}

# The simulators of the table, named as a user knows them
SUPPORTED_SIMULATORS = "NVC, GHDL, Questa/ModelSim or Riviera-PRO"


def setup(context):
"""
Expand All @@ -63,21 +50,18 @@ def setup(context):
foreign language interface of the selected simulator.
"""
# pylint: disable=import-outside-toplevel
from .bridge import setup as setup_bridge
from .bridge import bridge_simulators, select_simulator, setup as setup_bridge
from . import simulator_hooks

simulator_name = context.simulator_name
simulators = bridge_simulators(simulator_name)
if simulator_name is None:
raise RuntimeError(
f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS} but no simulator was found"
f"The vunit-python-bridge package requires {', '.join(simulators)} but no simulator was found"
)

interface = FOREIGN_LANGUAGE_INTERFACES.get(simulator_name)
if interface is None:
raise RuntimeError(
f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS}, "
f"it has no foreign language interface for {simulator_name}"
)
# Before anything is built, so a simulator the bridge cannot serve fails first
interface = select_simulator(simulators, simulator_name).interface

add_file_base_package(context)

Expand All @@ -87,8 +71,9 @@ def setup(context):
simulator_name,
context.simulator_prefix,
context.simulator_backend,
interface,
)

context.add_source_files(context.library.name, list(bridge.vhdl_files))

simulator_hooks.register(context, bridge)
simulator_hooks.register(context, bridge, simulators)
115 changes: 99 additions & 16 deletions src/vunit_python_bridge/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import re
import sys
from pathlib import Path
from typing import List, NamedTuple, Optional
from typing import Callable, Dict, List, NamedTuple, Optional

from vunit.sim_if.factory import SIMULATOR_FACTORY

from .native_library import (
LIBRARY_DIR_VARIABLE,
Expand All @@ -31,10 +33,20 @@
BRIDGE_PACKAGE_TEMPLATE = VHDL_SOURCE_PATH / "python_bridge_pkg.vhd.in"
CONFIG_FILE_NAME = "vunit_python_bridge.cfg"

# The simulators the bridge serves, reaching it through VHPIDIRECT, the FLI and VHPI.
FLI_SIMULATORS = ("modelsim",)
VHPI_SIMULATORS = ("rivierapro",)
BRIDGE_SIMULATORS = ("nvc", "ghdl") + FLI_SIMULATORS + VHPI_SIMULATORS
# The foreign language interface of each built-in VUnit simulator. VHPIDIRECT names the library
# by file name and the simulator hooks make the simulator load it, which GHDL does in its own way.
BUILTIN_SIMULATORS = {
"nvc": "VHPIDIRECT",
"ghdl": "VHPIDIRECT_GHDL",
"modelsim": "FLI",
"rivierapro": "VHPI",
}

# What the interface class of a simulator from a VUnit package defines instead, see
# docs/simulator_packages.rst
INTERFACE_ATTRIBUTE = "foreign_language_interface"
HOOKS_ATTRIBUTE = "native_library_hooks"
PACKAGE_INTERFACES = ("VHPIDIRECT", "FLI", "VHPI")

# GHDL backends that link the design ahead of time. For these the library
# token in the VHPIDIRECT attribute is passed to the linker instead of being
Expand All @@ -47,6 +59,16 @@
SUBPROGRAM_PATTERN = re.compile(r"^ (impure function|procedure) (\w+)(\([^)]*\))?( return \w+)?;", re.MULTILINE)


class BridgeSimulator(NamedTuple):
"""
How the bridge serves a simulator: its foreign language interface and, for a simulator from a
VUnit package, the native_library_hooks that make it load the library (None for a built-in one).
"""

interface: str
native_library_hooks: Optional[Callable]


class PythonBridge(NamedTuple):
"""
A prepared bridge: the native library and the generated VHDL.
Expand All @@ -63,6 +85,7 @@ def setup(
simulator_name: Optional[str] = None,
simulator_prefix: Optional[str] = None,
simulator_backend: Optional[str] = None,
interface: Optional[str] = None,
) -> PythonBridge:
"""
Prepare the Python bridge for a project. Called by the package setup function.
Expand All @@ -73,19 +96,18 @@ def setup(
:param simulator_prefix: The path its executables were found in.
:param simulator_backend: How the installation found there was built, which for GHDL
is the code generator deciding how the library is bound.
:param interface: The foreign language interface, see bridge_simulators. None takes it from
there by simulator name.
:returns: The bridge. Its vhdl_files are to be added to the package library.
"""
if simulator_name is not None and simulator_name not in BRIDGE_SIMULATORS:
raise PythonBridgeError(
f"The vunit-python-bridge package requires NVC, GHDL, Questa/ModelSim or Riviera-PRO, "
f"it is not supported for {simulator_name}"
)
if interface is None and simulator_name is not None:
interface = select_simulator(bridge_simulators(simulator_name), simulator_name).interface

check_python_build()
check_windows_64bit_simulator(simulator_name, simulator_prefix)

is_fli = simulator_name in FLI_SIMULATORS
is_vhpi = simulator_name in VHPI_SIMULATORS
is_fli = interface == "FLI"
is_vhpi = interface == "VHPI"
if (is_fli or is_vhpi) and simulator_prefix is None:
raise PythonBridgeError(
f"The {'FLI' if is_fli else 'VHPI'} variant of the bridge is built against the {simulator_name} "
Expand All @@ -105,13 +127,13 @@ def setup(
# and the library, by absolute path since Questa accepts it, for the FLI; the library by
# absolute path without its suffix, which Riviera-PRO adds, and the entry point registered
# by native/vhpi.c for VHPI; the VHPIDIRECT library token and the entry point itself for
# NVC and GHDL.
# VHPIDIRECT.
if is_fli:
foreign = f"fli_{{entry_point}} {library_file!s}"
elif is_vhpi:
foreign = f"VHPI {_vhpi_library_token(library_file)} {{entry_point}}"
else:
foreign = f"VHPIDIRECT {_vhpidirect_token(simulator_name, simulator_backend, library_file)} {{entry_point}}"
foreign = f"VHPIDIRECT {_vhpidirect_token(interface, simulator_backend, library_file)} {{entry_point}}"
bridge_package = root / "vhdl" / "python_bridge_pkg.vhd"
_write_if_changed(bridge_package, _render_bridge_package(foreign))

Expand All @@ -125,6 +147,67 @@ def setup(
)


def bridge_simulators(selected: Optional[str] = None) -> Dict[str, BridgeSimulator]:
"""
The simulators the bridge serves, by name: the built-in ones and every simulator VUnit knows,
from a simulator package, whose interface class defines foreign_language_interface and
native_library_hooks. The simulator packages are added to VUnit before the bridge. Only the
attributes of the selected simulator are checked, a class defining just one of them is
otherwise not a simulator the bridge serves.
"""
simulators = {name: BridgeSimulator(interface, None) for name, interface in BUILTIN_SIMULATORS.items()}
for simulator_class in SIMULATOR_FACTORY.supported_simulators():
interface = getattr(simulator_class, INTERFACE_ATTRIBUTE, None)
hooks = getattr(simulator_class, HOOKS_ATTRIBUTE, None)
if interface is None and hooks is None:
continue
if simulator_class.name == selected:
_check_attributes(simulator_class.name, interface, hooks)
elif interface is None or hooks is None:
continue
simulators[simulator_class.name] = BridgeSimulator(interface, hooks)
return simulators


def select_simulator(simulators: Dict[str, BridgeSimulator], name: str) -> BridgeSimulator:
"""
The simulator of the given name from bridge_simulators.
"""
if name not in simulators:
raise PythonBridgeError(
f"The vunit-python-bridge package requires {', '.join(simulators)}, or a simulator whose interface "
f"class says how it loads a native library, which {name} does not. Its simulator interface class must "
f"define {INTERFACE_ATTRIBUTE} ({', '.join(map(repr, PACKAGE_INTERFACES))}) and "
f"{HOOKS_ATTRIBUTE}(library_file), see 'Simulator packages' in the vunit-python-bridge documentation."
)
return simulators[name]


def _check_attributes(name: str, interface, hooks) -> None:
"""
Check the attributes a simulator interface class defines to be served by the bridge.
"""
for defined, missing, value in (
(INTERFACE_ATTRIBUTE, HOOKS_ATTRIBUTE, hooks),
(HOOKS_ATTRIBUTE, INTERFACE_ATTRIBUTE, interface),
):
if value is None:
raise PythonBridgeError(
f"The {name} simulator interface defines {defined} but not {missing}, "
"the vunit-python-bridge package needs both. Define it in the simulator interface class."
)
if interface not in PACKAGE_INTERFACES:
raise PythonBridgeError(
f"{INTERFACE_ATTRIBUTE} of the {name} simulator interface is {interface!r}, "
f"the vunit-python-bridge package knows {', '.join(map(repr, PACKAGE_INTERFACES))}"
)
if not callable(hooks):
raise PythonBridgeError(
f"{HOOKS_ATTRIBUTE} of the {name} simulator interface is not callable, it must be a static or "
"class method taking the library file"
)


def _prepare_library(simulator_prefix: Optional[Path], front_end: Optional[str]) -> Path:
"""
Prepare the library in the library directory, failing with a pointer to the variable
Expand All @@ -141,12 +224,12 @@ def _prepare_library(simulator_prefix: Optional[Path], front_end: Optional[str])
) from exc


def _vhpidirect_token(simulator_name, simulator_backend: Optional[str], library_file: Path) -> str:
def _vhpidirect_token(interface: Optional[str], simulator_backend: Optional[str], library_file: Path) -> str:
"""
The library token of the VHPIDIRECT attributes: the file name the simulator dlopen()s,
or the linker flag of the GHDL backends that link the design ahead of time.
"""
if simulator_name == "ghdl" and simulator_backend in GHDL_LINKING_BACKENDS:
if interface == "VHPIDIRECT_GHDL" and simulator_backend in GHDL_LINKING_BACKENDS:
return "-lvunit_python_bridge"
return library_file.name

Expand Down
12 changes: 10 additions & 2 deletions src/vunit_python_bridge/native_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ def _check_windows_python() -> None:
# Machine field values of the PE (Windows executable) header
_PE_MACHINE_X64 = 0x8664
_PE_MACHINE_NAMES = {_PE_MACHINE_X64: "64-bit x86 (x64)", 0x014C: "32-bit x86", 0xAA64: "ARM64"}
# The environment variables VUnit reads for the executable of NVC and GHDL
EXECUTABLE_VARIABLES = {"nvc": "NVC", "ghdl": "GHDL"}


def pe_machine(path: Path) -> Optional[int]:
Expand Down Expand Up @@ -192,8 +194,14 @@ def check_windows_64bit_simulator(simulator_name: str, simulator_prefix: Optiona
if simulator_prefix is None:
return
# NVC and GHDL name their executable, which VUnit lets the NVC and GHDL environment variables
# override, the Questa/ModelSim and Aldec interfaces use vsim
name = os.environ.get(simulator_name.upper(), simulator_name) if simulator_name in ("nvc", "ghdl") else "vsim"
# override, the Questa/ModelSim and Aldec interfaces use vsim. The executable of a simulator
# from a VUnit package is not known here.
if simulator_name in EXECUTABLE_VARIABLES:
name = os.environ.get(EXECUTABLE_VARIABLES[simulator_name], simulator_name)
elif simulator_name in ("modelsim", "rivierapro"):
name = "vsim"
else:
return
executable = Path(simulator_prefix) / (name if name.lower().endswith(".exe") else f"{name}.exe")
machine = pe_machine(executable)
if machine is not None and machine != _PE_MACHINE_X64:
Expand Down
Loading
Loading