diff --git a/README.md b/README.md index e7d6582..5cb2f82 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,8 @@ vunit-python-bridge embeds a Python interpreter in the simulator process so that can execute Python code and call Python functions — a NumPy reference model, a constraint solver, a plot of what the design just produced — without leaving the simulation. The VHDL API, `python_pkg`/`python_context`, is compiled into the `python_bridge` library and is implemented for -the selected simulator by a foreign language interface the package builds itself: a small C library -called through VHPIDIRECT (NVC, GHDL) or the FLI (Questa/ModelSim), or a VHPI application built with -the simulator's own compiler driver (Riviera-PRO/Active-HDL). Values cross the interface with their +the selected simulator by a small C library the package builds itself, called through VHPIDIRECT +(NVC, GHDL), the FLI (Questa/ModelSim) or VHPI (Riviera-PRO). Values cross the interface with their VHDL types: `integer`, `real`, `string`, `boolean`, `std_ulogic`, `unsigned`/`signed`, the vector types, and `integer_array_t` as a NumPy array. @@ -74,7 +73,7 @@ check(call_integer_vector("gain", arg(integer_vector'(1, 2))) = integer_vector'( | NVC | VHPIDIRECT | Tested on Linux, macOS and Windows | | GHDL (mcode, llvm-jit, llvm, gcc) | VHPIDIRECT | Tested on Linux, macOS and Windows | | Questa/ModelSim | FLI | Tested manually on Linux; the Windows build is untested | -| Riviera-PRO, Active-HDL | VHPI | Untested; a subset of the API, see the documentation | +| Riviera-PRO | VHPI | Tested manually on Linux and Windows | ## Requirements diff --git a/docs/how_it_works.rst b/docs/how_it_works.rst index 1aaa535..2572c67 100644 --- a/docs/how_it_works.rst +++ b/docs/how_it_works.rst @@ -1,16 +1,20 @@ .. _python_bridge:native: -How it works (NVC, GHDL and Questa) ------------------------------------- +How it works +------------ -For NVC, GHDL and Questa, the interpreter is embedded in the simulator process -by a small C library, the VUnit Python bridge (`src/vunit_python_bridge/native +The interpreter is embedded in the simulator process by a small C library, the +VUnit Python bridge (`src/vunit_python_bridge/native `__). NVC and GHDL call it through VHPIDIRECT; Questa/ModelSim calls it through the FLI, using the front end in `native/fli.c `__ -that converts the FLI parameters of one foreign subprogram per entry point. The generated VHDL -and everything above it is the same for all three. The interpreter is started +that converts the FLI parameters of one foreign subprogram per entry point. +Riviera-PRO, which has no VHPIDIRECT, calls it through VHPI, using +the front end in `native/vhpi.c +`__ +that registers a foreign subprogram per entry point. The generated VHDL and +everything above it is the same for all simulators. The interpreter is started on first use, is never restarted within a simulation, and uses no signal handlers of its own. @@ -20,7 +24,9 @@ Linux below. It is rebuilt automatically when the source, the Python version or the Python installation changes. For Questa the FLI front end is compiled in too, against the ``mti.h`` of the - simulator, and the simulator installation is part of the cache key. The + simulator, and for Riviera-PRO the VHPI front end, against the + ``vhpi_user.h`` of the simulator. The simulator installation is then part of + the cache key. The package ships no prebuilt Linux library. Windows @@ -29,15 +35,16 @@ Windows copied to the library directory, see below, and nothing is compiled. A development checkout of the package does not contain the DLLs; they can be built with ``tools/build_python_bridge.py`` from an MSVC developer prompt, and without - them the library is built with gcc like for Questa. For Questa the library - must be linked against the simulator's ``mtipli``, so it is always built on + them the library is built with gcc like for Questa. For Questa and + Riviera-PRO the library must be linked against the simulator's + ``mtipli`` or ``aldecpli``, so it is always built on first use with gcc, against the headers and the import library of the Python running VUnit. The compiler is ``CC`` if set, else the MinGW gcc bundled with the simulator (``gcc-*-mingw64*`` next to the Questa executables' directory), else ``gcc`` on ``PATH``. Many Questa installations on Windows bundle none. A gcc build imports the Python DLL directly, so the - package adds its directory to ``PATH`` for the simulator processes. These - builds are untested. + package adds its directory to ``PATH`` for the simulator processes. The + Questa build is untested. Library directory The libraries are built or copied to a directory of their own rather than to the @@ -57,7 +64,10 @@ Library directory simulators leave out. The directory can be shared by projects since every build has a directory named after what it was built from. The configuration of a project stays in ``/python_bridge`` and the simulator hooks - pass its path to the library in ``VUNIT_PYTHON_BRIDGE_CONFIG``. + pass its path to the library in ``VUNIT_PYTHON_BRIDGE_CONFIG``. The VUnit + interface of Riviera-PRO has no hook for the environment of a + simulation, so for them the setup sets the variable in the environment of + the VUnit process, which the simulator inherits. The bridge uses the full (version specific) CPython ABI rather than the Stable ABI since embedding the interpreter in the environment VUnit runs in @@ -90,6 +100,13 @@ starts, so it has to go on the command line of the process rather than on the ``vsim`` listing it in ``vsim -help all``. No simulator options need to be set by the user. +The VHPI attributes generated for Riviera-PRO name the library by +absolute path without its suffix, which the simulator adds. The attribute +separates its parts by spaces, so the setup fails, asking for +``VUNIT_PYTHON_BRIDGE_LIBRARY_DIR`` to be set, when the path of the library +directory has a space. The front end transfers strings as vectors of character +positions, since Riviera-PRO ends a string value at a line feed. + Tested configurations ~~~~~~~~~~~~~~~~~~~~~~ @@ -102,6 +119,7 @@ GHDL llvm-jit 6.0.0, 7.0.0-dev GHDL llvm 6.0.0, 7.0.0-dev GHDL gcc 6.0.0, 7.0.0-dev Questa (FLI) Altera Starter FPGA Edition 2025.3 +Riviera-PRO (VHPI) 2026.04 ========================== ======================================================== On Windows, CI tests NVC 1.22 and GHDL mcode (nightly) with Python 3.10, 3.12, @@ -110,5 +128,4 @@ Windows machines without a compiler, there with every supported Python, so each of its prebuilt DLLs is run. Questa is tested manually on Linux and is not covered by CI; the Windows build -of its bridge library is untested. Riviera-PRO/Active-HDL (VHPI) is not tested -at all. +of its bridge library is untested. diff --git a/docs/index.rst b/docs/index.rst index d58a554..142c151 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,7 +57,7 @@ Simulator Interface Status NVC VHPIDIRECT Tested on Linux, macOS and Windows GHDL (mcode, llvm-jit, llvm, gcc) VHPIDIRECT Tested on Linux, macOS and Windows Questa/ModelSim FLI Tested manually on Linux; the Windows build is untested -Riviera-PRO, Active-HDL VHPI Untested; a subset of the API, see :doc:`other_simulators` +Riviera-PRO VHPI Tested manually on Linux and Windows ================================= ========== ======================================================== Getting started @@ -127,7 +127,6 @@ become available through ``python_context``: :hidden: how_it_works - other_simulators .. toctree:: :caption: About diff --git a/docs/other_simulators.rst b/docs/other_simulators.rst deleted file mode 100644 index e8ac486..0000000 --- a/docs/other_simulators.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. _python_bridge:other_simulators: - -Other simulators ------------------ - -Riviera-PRO/Active-HDL (VHPI) implement ``python_ffi_pkg`` with a VHPI -application, built from the C sources in `src/vunit_python_bridge/native/vhpi -`__ -with their ``ccomp`` driver. On Windows, ``ccomp`` is given the directory of -the gcc bundled with the simulator (``/mingw``) or, when there is -none, of ``gcc`` on ``PATH``. The package builds the application under the -output path (``//libraries``) the first time it is -added and rebuilds it when the sources, the Python running VUnit or the -simulator installation change, so a run script needs nothing beyond -``add_package("vunit-python-bridge", allow_setup=True)``. - -This application differs from the Python bridge in a few ways: only the -default session exists, the operations implemented by the bridge -(``integer_array_t`` values, the ``boolean``, ``std_ulogic``, vector and -``integer_array_t`` results, ``exec_file``) report that they require NVC, GHDL -or Questa, a Python error stops the simulation with the message printed by the -application rather than through the logger of the session, and ``real`` values -outside the single precision float range are rejected. - -The ``embedded_python`` example of this repository, -`examples/embedded_python -`__, -is a complete example covering all three simulator families. - -Its ``tb_example.vhd`` has a test case for each part of the API: ``exec`` and -``eval`` with the types they convert, calls with positional, keyword and group -arguments, a 20 register status dump, wide -``arg_unsigned``/``arg_signed`` and ``std_ulogic`` argument values, a 2-D -``integer_array_t`` image transposed by NumPy, the result types of ``eval`` and -``call``, Python files executed with ``exec_file`` or imported with -``import_module_from_file``, two models loaded into a session each, and a -Python model failing with the logger of the default session mocked. Its last -test case drives ``python_model``, a verification component whose behaviour is -the Python function in ``python_model.py`` rather than VHDL. The component finds -that file by joining its name to ``base_path``, the directory of the run script, which -``run.py`` sets as the base directory, while the testbench gives the files it -loads itself relative to its own directory. See :ref:`python_bridge:file_paths`. diff --git a/docs/python_files.rst b/docs/python_files.rst index 5712d0c..ebf2709 100644 --- a/docs/python_files.rst +++ b/docs/python_files.rst @@ -4,8 +4,7 @@ Python files and modules exec_file --------- -``exec_file`` executes a Python file (where the Python bridge implements the -API: NVC, GHDL and Questa), with the equivalent of +``exec_file`` executes a Python file, with the equivalent of .. code-block:: python diff --git a/docs/requirements.rst b/docs/requirements.rst index cbd520a..a722a53 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -3,11 +3,8 @@ Requirements * A VUnit with support for packages and simulator hooks, see the `README `__. -* NVC, GHDL or Questa/ModelSim, through the VUnit Python bridge, see - :ref:`python_bridge:native`, or Riviera-PRO/Active-HDL (VHPI) through the VHPI application, which - the package builds in the same way, see - :ref:`python_bridge:other_simulators`. Any other simulator raises a - ``RuntimeError``. +* NVC, GHDL, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`. + 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. @@ -20,7 +17,7 @@ Requirements * Windows: a 64-bit CPython from python.org (or compatible, such as ``actions/setup-python`` and ``uv``). No compiler is needed for NVC and GHDL when the package includes the prebuilt DLLs. Otherwise, and always for - Questa and Riviera-PRO/Active-HDL, a MinGW-w64 ``gcc`` is needed: ``CC``, the + Questa and Riviera-PRO, a MinGW-w64 ``gcc`` is needed: ``CC``, the one bundled with the simulator if there is one, or ``gcc`` on ``PATH``. MSYS2/MinGW Pythons are not supported. The simulator must be a 64-bit (x64) build too, since the Python DLL is loaded into its process; the package diff --git a/docs/running_code.rst b/docs/running_code.rst index ed3b1de..987b85d 100644 --- a/docs/running_code.rst +++ b/docs/running_code.rst @@ -95,8 +95,7 @@ name selects the overload: info("pi = " & to_string(eval_real("pi"))); -On NVC, GHDL and Questa, where the API is implemented by the VUnit Python -bridge, ``eval`` has more result types: +``eval`` has more result types: * ``eval_boolean``, returning ``boolean``. Only ``bool``/``numpy.bool_`` is accepted. It also makes the result of ``eval`` usable as a condition: @@ -163,7 +162,7 @@ VHDL Python ``std_ulogic`` ``bool``, 1 and H give ``True``, 0 and L give ``False`` ``unsigned`` ``int``, passed as ``arg_unsigned``/``kwarg_unsigned`` ``signed`` ``int``, passed as ``arg_signed``/``kwarg_signed`` -``integer_array_t`` NumPy array, not on Riviera-PRO/Active-HDL +``integer_array_t`` NumPy array ========================== ======================================================== An ``unsigned`` or ``signed`` value of any width becomes an exact Python @@ -199,7 +198,7 @@ passed as a number or as the string of its characters: call("model.scale", arg_unsigned(unsigned(gain))); call("model.push", arg(to_string(slv))); -On NVC, GHDL and Questa, ``call`` returns the same types as ``eval``: +``call`` returns the same types as ``eval``: ``call_boolean``, ``call_std_ulogic``, ``call_std_ulogic_vector``, ``call_integer_array``, ``call_string``, ``call_real_vector`` and ``call_integer_vector_ptr``, plus the procedures ``call_std_ulogic_vector``, @@ -262,3 +261,25 @@ rather than by assigning to a variable in a loop: ... call("model.configure", registers(0)); + +The example +~~~~~~~~~~~ + +The ``embedded_python`` example of this repository, +`examples/embedded_python +`__, +is a complete example that runs on every supported simulator. + +Its ``tb_example.vhd`` has a test case for each part of the API: ``exec`` and +``eval`` with the types they convert, calls with positional, keyword and group +arguments, a 20 register status dump, wide +``arg_unsigned``/``arg_signed`` and ``std_ulogic`` argument values, a 2-D +``integer_array_t`` image transposed by NumPy, the result types of ``eval`` and +``call``, Python files executed with ``exec_file`` or imported with +``import_module_from_file``, two models loaded into a session each, and a +Python model failing with the logger of the default session mocked. Its last +test case drives ``python_model``, a verification component whose behaviour is +the Python function in ``python_model.py`` rather than VHDL. The component finds +that file by joining its name to ``base_path``, the directory of the run script, which +``run.py`` sets as the base directory, while the testbench gives the files it +loads itself relative to its own directory. See :ref:`python_bridge:file_paths`. diff --git a/docs/sessions.rst b/docs/sessions.rst index 8050975..4336066 100644 --- a/docs/sessions.rst +++ b/docs/sessions.rst @@ -65,9 +65,6 @@ included: mock(golden_logger, failure); -On Riviera-PRO/Active-HDL (VHPI), only the default session is supported: -passing any other session fails with a clear error. - .. _python_bridge:instance_sessions: Independent models of component instances @@ -134,8 +131,7 @@ session, keeping the mutable model state on ``self`` rather than in globals: y <= call("model.accumulate", arg(x), session => session); The module, and so the class, is shared by all sessions (see the caveats -below), while every session has a ``model`` object of its own. Like all -non-default sessions, this needs the Python bridge: NVC, GHDL or Questa. +below), while every session has a ``model`` object of its own. Caveats ~~~~~~~ diff --git a/docs/types.rst b/docs/types.rst index 165b2ee..d57cd40 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -12,14 +12,11 @@ Type mapping * ``real_vector`` ↔ ``list`` of ``float`` (``call_real_vector`` needs the bridge). * ``integer_vector_ptr_t`` ↔ ``list`` of ``int``. * ``std_ulogic``/``std_ulogic_vector`` ↔ ``str``, one character per element - out of ``U X 0 1 Z W L H -``, left to right (results only, Python bridge). -* ``signed``/``unsigned`` ↔ ``int`` (procedure results only, Python bridge). -* ``integer_array_t`` ↔ ``numpy.ndarray`` (Python bridge), see + out of ``U X 0 1 Z W L H -``, left to right (results only). +* ``signed``/``unsigned`` ↔ ``int`` (procedure results only). +* ``integer_array_t`` ↔ ``numpy.ndarray``, see :ref:`python_bridge:integer_array`. -The types marked as needing the Python bridge are available on NVC, GHDL and -Questa, but not on Riviera-PRO/Active-HDL. - Results are strict: a value that does not fit the VHDL type, or is of the wrong Python type, is an error. Values are never silently truncated or wrapped. @@ -29,12 +26,6 @@ wrapped. integer_array_t and NumPy -------------------------- -.. note:: - - ``integer_array_t`` values are transferred by the VUnit Python bridge and - are therefore available on NVC, GHDL and Questa, but not on - Riviera-PRO/Active-HDL. - An ``integer_array_t`` argument is transferred to Python and referred to by the expression as a NumPy array of dtype ``int32``, so it can be reused in several calls. It is indexed like VUnit, so that elements correspond diff --git a/docs/vhdl_api.py b/docs/vhdl_api.py index 1a175ee..440fe24 100644 --- a/docs/vhdl_api.py +++ b/docs/vhdl_api.py @@ -22,10 +22,8 @@ ( "python_ffi_pkg", "python_ffi_pkg_bridge.vhd", - "Sessions, ``exec`` and the basic ``eval`` result types. The package is implemented for " - "the selected simulator: by the Python bridge for NVC, GHDL and Questa and by a VHPI " - "application for Riviera-PRO/Active-HDL, which declares the same API, see " - ":doc:`../other_simulators`.", + "Sessions, ``exec`` and the basic ``eval`` result types, implemented by the Python bridge, " + "see :doc:`../how_it_works`.", ), ( "python_pkg", diff --git a/examples/embedded_python/run.py b/examples/embedded_python/run.py index 703db4d..6c89fff 100644 --- a/examples/embedded_python/run.py +++ b/examples/embedded_python/run.py @@ -117,8 +117,7 @@ def main(): vunit_python_bridge.set_relative_file_base(root) vu.add_package("vunit-python-bridge", allow_setup=True) vu.add_random() - # The setup function of the package builds the foreign language application of the simulator - # (the Python bridge, or the VHPI application) under the output path. + # The setup function of the package builds the Python bridge for the simulator. lib = vu.add_library("lib") lib.add_source_files(root / "*.vhd") diff --git a/pyproject.toml b/pyproject.toml index 1b1d581..d333272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,8 +51,6 @@ vunit_python_bridge = [ "hdl/tools/*.py", "native/*.c", "native/*.h", - "native/vhpi/*.c", - "native/vhpi/*.h", "bin/*.dll", ] diff --git a/release_notes/news.d/21.feature.md b/release_notes/news.d/21.feature.md new file mode 100644 index 0000000..bbf55bd --- /dev/null +++ b/release_notes/news.d/21.feature.md @@ -0,0 +1 @@ +Riviera-PRO is a supported simulator. diff --git a/src/vunit_python_bridge/__init__.py b/src/vunit_python_bridge/__init__.py index 9d2e23f..eaf8a39 100644 --- a/src/vunit_python_bridge/__init__.py +++ b/src/vunit_python_bridge/__init__.py @@ -12,18 +12,16 @@ ``python_pkg``/``python_context`` into the ``python_bridge`` library and sets up the foreign language interface implementing them for the selected simulator: -* NVC, GHDL (VHPIDIRECT) and Questa/ModelSim (FLI) are served by the VUnit Python bridge, a - small C library (native/*.c) embedding CPython in the simulator process. -* Riviera-PRO/Active-HDL are served by a VHPI application built with the simulator's own - compiler driver. +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). Modules: * bridge: setup of the bridge, its configuration and its generated VHDL. * native_library: compiles and caches the library on Linux and macOS, selects the prebuilt DLL on Windows. -* foreign_application: builds the VHPI application. -* simulator_hooks: makes NVC and GHDL find the library. +* simulator_hooks: makes the simulators find the library and its configuration. * runtime: runs inside the simulator's embedded interpreter. * file_base: the base directory of ``base_path``, ``set_relative_file_base``. @@ -45,30 +43,27 @@ # 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/Active-HDL through a VHPI application of their own. +# Riviera-PRO through VHPI. FOREIGN_LANGUAGE_INTERFACES = { "nvc": "VHPIDIRECT_NVC", "ghdl": "VHPIDIRECT_GHDL", "modelsim": "FLI", "rivierapro": "VHPI", - "activehdl": "VHPI", } # The simulators of the table, named as a user knows them -SUPPORTED_SIMULATORS = "NVC, GHDL, Questa/ModelSim, Riviera-PRO or Active-HDL" +SUPPORTED_SIMULATORS = "NVC, GHDL, Questa/ModelSim or Riviera-PRO" def setup(context): """ Complete the addition of the package. - ``vunit_pkg.toml`` lists the sources both variants of the package share, this adds the - foreign language interface of the selected simulator: the VHPI application for - Riviera-PRO/Active-HDL, the VUnit Python bridge for everyone else. + ``vunit_pkg.toml`` lists the sources of the package, this adds the bridge for the + foreign language interface of the selected simulator. """ # pylint: disable=import-outside-toplevel from .bridge import setup as setup_bridge - from .foreign_application import setup_vhpi_application from . import simulator_hooks simulator_name = context.simulator_name @@ -86,12 +81,6 @@ def setup(context): add_file_base_package(context) - if interface == "VHPI": - # Riviera-PRO/Active-HDL, the simulators served by the VHPI application - context.add_source_files(context.library.name, [VHDL_PATH / "python_pkg_vhpi.vhd"]) - setup_vhpi_application(context.output_path, simulator_name, context.simulator_prefix) - return - bridge = setup_bridge( context.output_path, context.run_script_path, diff --git a/src/vunit_python_bridge/bridge.py b/src/vunit_python_bridge/bridge.py index cf59d18..8f6d60a 100644 --- a/src/vunit_python_bridge/bridge.py +++ b/src/vunit_python_bridge/bridge.py @@ -31,10 +31,10 @@ 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 and through the FLI. -# The rest of the simulators of the package are served by the VHPI application. +# The simulators the bridge serves, reaching it through VHPIDIRECT, the FLI and VHPI. FLI_SIMULATORS = ("modelsim",) -BRIDGE_SIMULATORS = ("nvc", "ghdl") + FLI_SIMULATORS +VHPI_SIMULATORS = ("rivierapro",) +BRIDGE_SIMULATORS = ("nvc", "ghdl") + FLI_SIMULATORS + VHPI_SIMULATORS # 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 @@ -77,7 +77,7 @@ def setup( """ if simulator_name is not None and simulator_name not in BRIDGE_SIMULATORS: raise PythonBridgeError( - f"The vunit-python-bridge package requires NVC, GHDL or Questa/ModelSim, " + f"The vunit-python-bridge package requires NVC, GHDL, Questa/ModelSim or Riviera-PRO, " f"it is not supported for {simulator_name}" ) @@ -85,13 +85,16 @@ def setup( check_windows_64bit_simulator(simulator_name, simulator_prefix) is_fli = simulator_name in FLI_SIMULATORS - if is_fli and simulator_prefix is None: + is_vhpi = simulator_name in VHPI_SIMULATORS + if (is_fli or is_vhpi) and simulator_prefix is None: raise PythonBridgeError( - f"The FLI variant of the bridge is built against the {simulator_name} installation, but it was not found" + f"The {'FLI' if is_fli else 'VHPI'} variant of the bridge is built against the {simulator_name} " + "installation, but it was not found" ) root = Path(output_path) / "python_bridge" - library_file = _prepare_library(Path(simulator_prefix) if is_fli else None) + front_end = "fli" if is_fli else "vhpi" if is_vhpi else None + library_file = _prepare_library(Path(simulator_prefix) if front_end else None, front_end) run_script_dir = str(Path.cwd() if run_script_path is None else Path(run_script_path).resolve().parent) # The configuration belongs to the project, the library may be shared. The simulator # hooks tell the library where the configuration is. @@ -99,13 +102,16 @@ def setup( _write_if_changed(config_file, _config_text(run_script_dir)) # The foreign attribute string of an entry point: the name of its wrapper in native/fli.c - # and the library, by absolute path since Questa accepts it, for the FLI; the VHPIDIRECT - # library token and the entry point itself for NVC and GHDL. - foreign = ( - f"fli_{{entry_point}} {library_file!s}" - if is_fli - else f"VHPIDIRECT {_vhpidirect_token(simulator_name, simulator_backend, library_file)} {{entry_point}}" - ) + # 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. + 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}}" bridge_package = root / "vhdl" / "python_bridge_pkg.vhd" _write_if_changed(bridge_package, _render_bridge_package(foreign)) @@ -119,14 +125,14 @@ def setup( ) -def _prepare_library(fli_prefix: Optional[Path]) -> Path: +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 selecting another one when it cannot be written, e.g. a read-only home directory. """ directory = library_directory() try: - return prepare_library(directory, fli_prefix) + return prepare_library(directory, simulator_prefix, front_end) except OSError as exc: source = f"set by {LIBRARY_DIR_VARIABLE}" if os.environ.get(LIBRARY_DIR_VARIABLE) else "the default" raise PythonBridgeError( @@ -145,6 +151,20 @@ def _vhpidirect_token(simulator_name, simulator_backend: Optional[str], library_ return library_file.name +def _vhpi_library_token(library_file: Path) -> str: + """ + The library of the VHPI attributes: its path without the suffix the simulator adds. The + attribute separates its parts by spaces, so the path cannot have any. + """ + token = str(library_file.with_suffix("")) + if any(character.isspace() for character in token): + raise PythonBridgeError( + f"The Python bridge library for VHPI is in {library_file.parent!s}, whose path has a space, which the " + f"VHPI foreign attribute cannot name. Set {LIBRARY_DIR_VARIABLE} to a directory without spaces." + ) + return token + + def _render_bridge_package(foreign: str) -> str: """ The generated python_bridge_pkg.vhd: the declarations of the template with the foreign diff --git a/src/vunit_python_bridge/foreign_application.py b/src/vunit_python_bridge/foreign_application.py deleted file mode 100644 index 0d401b9..0000000 --- a/src/vunit_python_bridge/foreign_application.py +++ /dev/null @@ -1,90 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this file, -# You can obtain one at http://mozilla.org/MPL/2.0/. -# -# Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com - -""" -Building the foreign language application implementing ``python_pkg``/``python_context`` -for the simulators the VUnit Python bridge does not serve. - -``setup_vhpi_application`` is called by the setup function of the package for -Riviera-PRO/Active-HDL (VHPI). The application is built under the output path on first use and -rebuilt when its sources, the Python running VUnit or the simulator change. -""" - -from pathlib import Path -import os -import sys -import hashlib - -from .native_library import ( - add_python_dll_to_path, - check_windows_64bit_simulator, - compile_library, - python_dev_paths, - windows_gcc, -) - -SRC_PATH = Path(__file__).parent.resolve() / "native" / "vhpi" - - -def setup_vhpi_application(output_path, simulator_name, simulator_prefix): - """ - Build the VHPI application for Riviera-PRO/Active-HDL under the output path, unless the one - already there was built from the same sources for the same Python and simulator. - - :param simulator_prefix: The path the executables of the simulator were found in, which is - where its ccomp compiler driver is. - """ - if simulator_prefix is None: - raise RuntimeError(f"The VHPI application is built with the ccomp of {simulator_name}, but it was not found") - - check_windows_64bit_simulator(simulator_name, simulator_prefix) - target = Path(output_path) / simulator_name / "libraries" / "python.dll" - sources = [SRC_PATH / "python_pkg_vhpi.c", SRC_PATH / "python_pkg.c"] - simulator_prefix = Path(simulator_prefix).resolve() - fingerprint = _fingerprint(sources, simulator_prefix) - fingerprint_file = target.with_suffix(target.suffix + ".fingerprint") - if target.exists() and fingerprint_file.exists() and fingerprint_file.read_text(encoding="utf-8") == fingerprint: - return - - target.parent.mkdir(parents=True, exist_ok=True) - fingerprint_file.unlink(missing_ok=True) - _build_vhpi(target, sources, simulator_prefix) - fingerprint_file.write_text(fingerprint, encoding="utf-8") - - -def _fingerprint(sources, simulator_prefix): - """ - Everything the built application depends on: the C sources and headers, the Python that is - embedded and the simulator whose headers and libraries are used. - """ - digest = hashlib.sha256() - for path in sorted(sources) + sorted(SRC_PATH.glob("*.h")): - digest.update(path.name.encode("utf-8") + b"\0" + path.read_bytes() + b"\0") - for item in [sys.executable, sys.prefix, sys.version, sys.platform, str(simulator_prefix)]: - digest.update(item.encode("utf-8") + b"\0") - return digest.hexdigest() - - -def _build_vhpi(target, sources, simulator_prefix): - """ - Compile the VHPI application with the ccomp compiler driver of Riviera-PRO/Active-HDL. - """ - python_include, python_libs = python_dev_paths() - ccomp = simulator_prefix / ("ccomp.exe" if sys.platform == "win32" else "ccomp") - env = None - if sys.platform == "win32": - add_python_dll_to_path() - # Riviera-PRO bundles a MinGW gcc under /mingw, other installations may not. - # ponytail: untested (no Aldec license), assumes ccomp finds gcc on PATH when none is bundled - gcc = windows_gcc(simulator_prefix) - if gcc is not None: - env = dict(os.environ, PATH=os.pathsep.join([str(Path(gcc[0]).parent), os.environ.get("PATH", "")])) - # One argument per item: subprocess quotes them, quotes added here would reach ccomp literally - args = [str(ccomp), "-vhpi", "-dbg", "-verbose", "-o", str(target)] - args += ["-l", f"python{sys.version_info[0]}{sys.version_info[1]}", "-l", "python3", "-l", "_tkinter"] - args += ["-I", str(python_include), "-I", str(SRC_PATH), "-L", str(python_libs)] - args += [str(path) for path in sources] - compile_library(args, target, target, env) diff --git a/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd b/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd index 3b2e491..702a468 100644 --- a/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd +++ b/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd @@ -4,22 +4,15 @@ -- -- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com -- --- The foreign language interface of the Python package for NVC and GHDL, --- implemented with the VUnit Python bridge (src/vunit_python_bridge). It provides --- the same subprograms as the FLI and VHPI variants but over the private --- python_bridge_pkg, which the setup function of the package generates. --- --- Deviations from the other variants: --- * The subprograms are impure since they call impure foreign subprograms. --- * Errors are reported as failures on the logger of the session they were --- performed in rather than aborting the simulation directly, which makes --- them observable from VHDL. --- * Sessions other than the default one are supported. +-- The foreign language interface of the Python package for every simulator, +-- implemented with the VUnit Python bridge (src/vunit_python_bridge) over the +-- private python_bridge_pkg, which the setup function of the package generates. +-- The subprograms are impure since they call impure foreign subprograms. Errors +-- are reported as failures on the logger of the session they were performed in, +-- which makes them observable from VHDL. -- -- The p_ prefixed declarations are the private primitives the bridge --- operations of python_pkg are built on. They are not part of the API. The FLI and VHPI --- variants of this package declare the same primitives and implement them by --- reporting a failure. +-- operations of python_pkg are built on. They are not part of the API. library vunit_lib; use vunit_lib.id_pkg.all; diff --git a/src/vunit_python_bridge/hdl/src/python_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_pkg.vhd index 449e4b1..69929f0 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg.vhd @@ -82,8 +82,7 @@ package python_pkg is -- -- H and L are read as 1 and 0. Any other metavalue is an error. -- - -- An integer_array_t value is transferred to Python by the Python bridge, - -- which is only available for NVC, GHDL and Questa, and is referred to by + -- An integer_array_t value is transferred to Python and is referred to by -- the expression, which means that it can be used in several calls. function arg(value : real_vector) return arg_t; function kwarg(kw : string; value : real_vector) return arg_t; @@ -157,13 +156,10 @@ package python_pkg is impure function "&"(l, r : arg_t) return arg_t; ----------------------------------------------------------------------------- - -- Operations implemented by the Python bridge (NVC, GHDL and Questa) + -- Operations built on the primitives of python_ffi_pkg ----------------------------------------------------------------------------- - -- The operations below are implemented by the VUnit Python bridge and are - -- therefore only available for NVC, GHDL and Questa; the other simulators - -- report a failure when they are used. Like the other operations, every one - -- of them takes the session it is performed in as its last parameter, - -- defaulting to the default session. + -- Like the other operations, every one of them takes the session it is + -- performed in as its last parameter, defaulting to the default session. ----------------------------------------------------------------------------- -- Results of eval: boolean, std_ulogic, vectors and arrays @@ -869,7 +865,7 @@ package body python_pkg is end; ----------------------------------------------------------------------------- - -- Operations implemented by the Python bridge (NVC, GHDL and Questa) + -- Operations built on the primitives of python_ffi_pkg ----------------------------------------------------------------------------- -- Python represents std_ulogic values by these characters constant p_std_ulogic_characters : string(1 to 9) := "UX01ZWLH-"; diff --git a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd deleted file mode 100644 index 0512c4a..0000000 --- a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd +++ /dev/null @@ -1,283 +0,0 @@ --- This package provides a dictionary types and operations --- --- This Source Code Form is subject to the terms of the Mozilla Public --- License, v. 2.0. If a copy of the MPL was not distributed with this file, --- You can obtain one at http://mozilla.org/MPL/2.0/. --- --- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com - -use std.textio.all; - -library vunit_lib; -use vunit_lib.id_pkg.all; -use vunit_lib.integer_array_pkg.all; -use vunit_lib.integer_vector_ptr_pkg.all; -use vunit_lib.logger_pkg.all; - -package python_ffi_pkg is - -- The identity of the Python interface, the identity of python_logger and - -- the parent of the identities of the sessions created from a name - constant p_python_id : id_t := get_id("python_bridge:python"); - - -- A session is a Python namespace with an identity, created from a name - -- under the identity of the Python interface or from an identity of its - -- own. Only the default session, the __main__ namespace, is supported by - -- this foreign language interface. - type python_session_t is record - p_data : integer_vector_ptr_t; - end record; - - impure function new_session(name : string) return python_session_t; - impure function new_session(id : id_t) return python_session_t; - - -- The identity of a session, which is what two sessions are compared by - impure function get_id(session : python_session_t) return id_t; - - -- The name of the identity of a session, without hierarchy - impure function name(session : python_session_t) return string; - - -- The session the operations are performed in when no other one is given. - -- It is created the way new_session does, which cannot be called here since - -- its body is not elaborated until the package body is. - constant default_session : python_session_t := - (p_data => new_integer_vector_ptr(1, value => to_integer(get_id("default", parent => p_python_id)))); - - procedure python_setup; - -- TODO: Looks like Riviera-PRO requires the path to the shared library to be fixed at compile time - -- and that may become a bit limited. VHDL standard allow for expressions. - attribute foreign of python_setup : procedure is "VHPI libraries/python python_setup"; - procedure python_cleanup; - attribute foreign of python_cleanup : procedure is "VHPI libraries/python python_cleanup"; - - function p_eval_integer(expr : string) return integer; - attribute foreign of p_eval_integer : function is "VHPI libraries/python eval_integer"; - impure function eval_integer(expr : string; session : python_session_t := default_session) return integer; - alias eval is eval_integer[string, python_session_t return integer]; - - function p_eval_real(expr : string) return real; - attribute foreign of p_eval_real : function is "VHPI libraries/python eval_real"; - impure function eval_real(expr : string; session : python_session_t := default_session) return real; - alias eval is eval_real[string, python_session_t return real]; - - function p_eval_integer_vector(expr : string) return integer_vector; - attribute foreign of p_eval_integer_vector : function is "VHPI libraries/python eval_integer_vector"; - impure function eval_integer_vector( - expr : string; session : python_session_t := default_session - ) return integer_vector; - alias eval is eval_integer_vector[string, python_session_t return integer_vector]; - - function p_eval_real_vector(expr : string) return real_vector; - attribute foreign of p_eval_real_vector : function is "VHPI libraries/python eval_real_vector"; - impure function eval_real_vector( - expr : string; session : python_session_t := default_session - ) return real_vector; - alias eval is eval_real_vector[string, python_session_t return real_vector]; - - function p_eval_string(expr : string) return string; - attribute foreign of p_eval_string : function is "VHPI libraries/python eval_string"; - impure function eval_string(expr : string; session : python_session_t := default_session) return string; - alias eval is eval_string[string, python_session_t return string]; - - procedure p_exec(code : string); - attribute foreign of p_exec : procedure is "VHPI libraries/python exec"; - procedure exec(code : string; session : python_session_t := default_session); - - ----------------------------------------------------------------------------- - -- Private, the primitives the bridge operations of python_pkg are built on - ----------------------------------------------------------------------------- - -- Some operations of python_pkg are implemented by the VUnit Python - -- bridge, which is only available for NVC, GHDL and Questa. The primitives - -- below are declared so that python_pkg has one body for every simulator, - -- but they report a failure when they are used. - - -- Logger of the Python interface, the parent of the session loggers - constant python_logger : logger_t := get_logger(p_python_id); - - -- Result kinds, must match src/vunit_python_bridge/runtime.py - constant p_kind_integer : integer := 0; - constant p_kind_real : integer := 1; - constant p_kind_boolean : integer := 2; - constant p_kind_string : integer := 3; - constant p_kind_std_ulogic : integer := 4; - constant p_kind_std_ulogic_vector : integer := 5; - constant p_kind_signed : integer := 6; - constant p_kind_unsigned : integer := 7; - constant p_kind_integer_array : integer := 8; - constant p_kind_integer_vector : integer := 9; - constant p_kind_real_vector : integer := 10; - - -- Names of the operations, used in the error messages - impure function p_exec_file_operation( - file_name : string; session : python_session_t := default_session - ) return string; - impure function p_eval_operation(expr : string; session : python_session_t := default_session) return string; - - -- Execute the Python file with the given name - impure function p_exec_file( - file_name : string; session : python_session_t := default_session - ) return boolean; - - -- Evaluate expr and convert the value to the VHDL type given by kind. width - -- is the length of a std_ulogic_vector, signed or unsigned result, -1 when - -- it is not known. - impure function p_eval( - expr : string; - kind : integer; - width : integer; - operation : string; - session : python_session_t := default_session - ) return boolean; - - -- The result of the last evaluation - impure function p_result_integer return integer; - impure function p_result_string return string; - impure function p_result_integer_array return integer_array_t; - - -- Transfer an integer_array_t to Python and return the id it is staged - -- under, -1 on failure. - impure function p_stage_array(arr : integer_array_t; operation : string) return integer; -end package; - -package body python_ffi_pkg is - impure function new_session(name : string) return python_session_t is - begin - return new_session(get_id(name, parent => p_python_id)); - end; - - impure function new_session(id : id_t) return python_session_t is - begin - return (p_data => new_integer_vector_ptr(1, value => to_integer(id))); - end; - - impure function get_id(session : python_session_t) return id_t is - begin - return to_id(get(session.p_data, 0)); - end; - - impure function name(session : python_session_t) return string is - begin - return name(get_id(session)); - end; - - impure function p_is_default(session : python_session_t) return boolean is - begin - return get_id(session) = get_id(default_session); - end; - - procedure p_check_session(session : python_session_t) is - begin - if not p_is_default(session) then - report "Python sessions are only supported with NVC, GHDL and Questa" severity failure; - end if; - end; - - impure function eval_integer(expr : string; session : python_session_t := default_session) return integer is - begin - p_check_session(session); - return p_eval_integer(expr); - end; - - impure function eval_real(expr : string; session : python_session_t := default_session) return real is - begin - p_check_session(session); - return p_eval_real(expr); - end; - - impure function eval_integer_vector( - expr : string; session : python_session_t := default_session - ) return integer_vector is - begin - p_check_session(session); - return p_eval_integer_vector(expr); - end; - - impure function eval_real_vector( - expr : string; session : python_session_t := default_session - ) return real_vector is - begin - p_check_session(session); - return p_eval_real_vector(expr); - end; - - impure function eval_string(expr : string; session : python_session_t := default_session) return string is - begin - p_check_session(session); - return p_eval_string(expr); - end; - - procedure exec(code : string; session : python_session_t := default_session) is - begin - p_check_session(session); - p_exec(code); - end; - - ----------------------------------------------------------------------------- - -- Private, the primitives the bridge operations of python_pkg are built on - ----------------------------------------------------------------------------- - procedure p_unsupported(name : string) is - begin - failure(python_logger, name & " requires NVC, GHDL or Questa"); - end; - - impure function p_exec_file_operation( - file_name : string; session : python_session_t := default_session - ) return string is - begin - if p_is_default(session) then - return "exec_file(""" & file_name & """)"; - end if; - return "exec_file(""" & file_name & """, session => """ & name(session) & """)"; - end; - - impure function p_eval_operation(expr : string; session : python_session_t := default_session) return string is - begin - if p_is_default(session) then - return "eval(""" & expr & """)"; - end if; - return "eval(""" & expr & """, session => """ & name(session) & """)"; - end; - - impure function p_exec_file( - file_name : string; session : python_session_t := default_session - ) return boolean is - begin - p_unsupported("exec_file"); - return false; - end; - - impure function p_eval( - expr : string; - kind : integer; - width : integer; - operation : string; - session : python_session_t := default_session - ) return boolean is - begin - p_unsupported(operation); - return false; - end; - - impure function p_result_integer return integer is - begin - p_unsupported("p_result_integer"); - return integer'low; - end; - - impure function p_result_string return string is - begin - p_unsupported("p_result_string"); - return ""; - end; - - impure function p_result_integer_array return integer_array_t is - begin - p_unsupported("p_result_integer_array"); - return null_integer_array; - end; - - impure function p_stage_array(arr : integer_array_t; operation : string) return integer is - begin - p_unsupported(operation); - return -1; - end; -end package body; diff --git a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py index 8dc3252..79897d8 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -10,10 +10,8 @@ The template holds the parts of python_pkg that are written by hand. The generated parts are the argument value types of call, the result types of eval and call and the execution of Python files. They are mostly one overload per -type, which is why they are generated. The operations that are implemented by -the Python bridge (NVC, GHDL and Questa) are built on the p_ prefixed -primitives of python_ffi_pkg, which the VHPI variant of that package -implements by reporting a failure. +type, which is why they are generated. They are built on the p_ prefixed +primitives of python_ffi_pkg. """ from pathlib import Path @@ -41,9 +39,8 @@ # name of the operation to report the value it cannot convert # suffix: appended to the arg and kwarg names, empty for an overload # -# The integer_array_t value is transferred by the Python bridge, which makes it -# available for NVC, GHDL and Questa only. The other values are built from the -# Python source text of the value alone and work on any simulator. +# The integer_array_t value is transferred by the Python bridge. The other +# values are built from the Python source text of the value alone. # # The unsigned and signed values are passed to typed names, arg_unsigned and # arg_signed, on purpose: plain overloads would make a string literal diff --git a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in index 27372e6..b5074da 100644 --- a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in @@ -82,8 +82,7 @@ package python_pkg is -- -- H and L are read as 1 and 0. Any other metavalue is an error. -- - -- An integer_array_t value is transferred to Python by the Python bridge, - -- which is only available for NVC, GHDL and Questa, and is referred to by + -- An integer_array_t value is transferred to Python and is referred to by -- the expression, which means that it can be used in several calls. $arg_declarations @@ -146,13 +145,10 @@ $arg_declarations impure function "&"(l, r : arg_t) return arg_t; ----------------------------------------------------------------------------- - -- Operations implemented by the Python bridge (NVC, GHDL and Questa) + -- Operations built on the primitives of python_ffi_pkg ----------------------------------------------------------------------------- - -- The operations below are implemented by the VUnit Python bridge and are - -- therefore only available for NVC, GHDL and Questa; the other simulators - -- report a failure when they are used. Like the other operations, every one - -- of them takes the session it is performed in as its last parameter, - -- defaulting to the default session. + -- Like the other operations, every one of them takes the session it is + -- performed in as its last parameter, defaulting to the default session. ----------------------------------------------------------------------------- -- Results of eval: boolean, std_ulogic, vectors and arrays @@ -713,7 +709,7 @@ $arg_functions end; ----------------------------------------------------------------------------- - -- Operations implemented by the Python bridge (NVC, GHDL and Questa) + -- Operations built on the primitives of python_ffi_pkg ----------------------------------------------------------------------------- -- Python represents std_ulogic values by these characters constant p_std_ulogic_characters : string(1 to 9) := "UX01ZWLH-"; diff --git a/src/vunit_python_bridge/native/vhpi.c b/src/vunit_python_bridge/native/vhpi.c new file mode 100644 index 0000000..c02a61e --- /dev/null +++ b/src/vunit_python_bridge/native/vhpi.c @@ -0,0 +1,223 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com + * + * VHPI front end of the bridge, compiled into the library only for + * Riviera-PRO, which has no direct binding (VHPIDIRECT). Every + * entry point of bridge.h is registered as a foreign subprogram of the same + * name, bound by the "VHPI " attributes of the + * generated python_bridge_pkg.vhd. The callbacks read the parameters with + * vhpi_get_value, delegate, and write the out parameters and the return value + * with vhpi_put_value, so that the bridge itself, the generated VHDL and + * python_ffi_pkg_bridge.vhd are the same for all simulators. + * + * Strings are transferred as vectors of character positions: Riviera-PRO ends + * a vhpiStrVal at a line feed. + */ + +#include "bridge.h" + +#include + +/* The base name of the library file, which the attributes name the library by */ +#define LIBRARY_NAME "libvunit_python_bridge_vhpi" + +/* Length of the chunk subtypes of python_bridge_pkg */ +#define CHUNK_LENGTH 1024 + +/* ------------------------------------------------------------------------ */ +/* Parameters and results */ +/* ------------------------------------------------------------------------ */ + +static vhpiHandleT parameter(const vhpiCbDataT *cb_data, int32_t index) { + return vhpi_handle_by_index(vhpiParamDecls, cb_data->obj, index); +} + +static int32_t get_integer(const vhpiCbDataT *cb_data, int32_t index) { + vhpiValueT value = {0}; + vhpiHandleT handle = parameter(cb_data, index); + + value.format = vhpiIntVal; + vhpi_get_value(handle, &value); + vhpi_release_handle(handle); + return (int32_t)value.value.intg; +} + +static void get_vector(const vhpiCbDataT *cb_data, int32_t index, vhpiFormatT format, void *buffer, size_t size) { + vhpiValueT value = {0}; + vhpiHandleT handle = parameter(cb_data, index); + + value.format = format; + value.bufSize = size; + value.value.ptr = buffer; + vhpi_get_value(handle, &value); + vhpi_release_handle(handle); +} + +static void put_vector(const vhpiCbDataT *cb_data, int32_t index, vhpiFormatT format, void *buffer, size_t size) { + vhpiValueT value = {0}; + vhpiHandleT handle = parameter(cb_data, index); + + value.format = format; + value.bufSize = size; + value.numElems = CHUNK_LENGTH; + value.value.ptr = buffer; + vhpi_put_value(handle, &value, vhpiDeposit); + vhpi_release_handle(handle); +} + +static void return_integer(const vhpiCbDataT *cb_data, int32_t result) { + vhpiValueT value = {0}; + + value.format = vhpiIntVal; + value.value.intg = (vhpiIntT)result; + vhpi_put_value(cb_data->obj, &value, vhpiDeposit); +} + +static void return_real(const vhpiCbDataT *cb_data, double result) { + vhpiValueT value = {0}; + + value.format = vhpiRealVal; + value.value.real = result; + vhpi_put_value(cb_data->obj, &value, vhpiDeposit); +} + +static void get_string_chunk(const vhpiCbDataT *cb_data, int32_t index, char *chunk) { + vhpiEnumT positions[CHUNK_LENGTH]; + + get_vector(cb_data, index, vhpiEnumVecVal, positions, sizeof(positions)); + for (int32_t idx = 0; idx < CHUNK_LENGTH; idx++) { + chunk[idx] = (char)positions[idx]; + } +} + +static void put_string_chunk(const vhpiCbDataT *cb_data, int32_t index, const char *chunk) { + vhpiEnumT positions[CHUNK_LENGTH]; + + for (int32_t idx = 0; idx < CHUNK_LENGTH; idx++) { + positions[idx] = (unsigned char)chunk[idx]; + } + put_vector(cb_data, index, vhpiEnumVecVal, positions, sizeof(positions)); +} + +/* ------------------------------------------------------------------------ */ +/* Entry points */ +/* ------------------------------------------------------------------------ */ + +static void vhpi_vpy_setup(const vhpiCbDataT *cb_data) { return_integer(cb_data, vpy_setup()); } + +static void vhpi_vpy_cleanup(const vhpiCbDataT *cb_data) { return_integer(cb_data, vpy_cleanup()); } + +static void vhpi_vpy_buffer_clear(const vhpiCbDataT *cb_data) { return_integer(cb_data, vpy_buffer_clear()); } + +static void vhpi_vpy_buffer_append(const vhpiCbDataT *cb_data) { + char chunk[CHUNK_LENGTH]; + + get_string_chunk(cb_data, 0, chunk); + return_integer(cb_data, vpy_buffer_append(chunk, get_integer(cb_data, 1))); +} + +static void vhpi_vpy_begin(const vhpiCbDataT *cb_data) { return_integer(cb_data, vpy_begin()); } + +static void vhpi_vpy_execute(const vhpiCbDataT *cb_data) { + return_integer(cb_data, vpy_execute(get_integer(cb_data, 0))); +} + +static void vhpi_vpy_eval(const vhpiCbDataT *cb_data) { + return_integer(cb_data, vpy_eval(get_integer(cb_data, 0), get_integer(cb_data, 1))); +} + +static void vhpi_vpy_push_array(const vhpiCbDataT *cb_data) { + return_integer(cb_data, vpy_push_array(get_integer(cb_data, 0), get_integer(cb_data, 1), get_integer(cb_data, 2), + get_integer(cb_data, 3), get_integer(cb_data, 4), get_integer(cb_data, 5))); +} + +static void vhpi_vpy_array_write(const vhpiCbDataT *cb_data) { + int32_t chunk[CHUNK_LENGTH]; + + get_vector(cb_data, 0, vhpiIntVecVal, chunk, sizeof(chunk)); + return_integer(cb_data, vpy_array_write(chunk, get_integer(cb_data, 1))); +} + +static void vhpi_vpy_stage(const vhpiCbDataT *cb_data) { return_integer(cb_data, vpy_stage()); } + +static void vhpi_vpy_result_integer(const vhpiCbDataT *cb_data) { return_integer(cb_data, vpy_result_integer()); } + +static void vhpi_vpy_result_real(const vhpiCbDataT *cb_data) { return_real(cb_data, vpy_result_real()); } + +static void vhpi_vpy_result_meta(const vhpiCbDataT *cb_data) { + return_integer(cb_data, vpy_result_meta(get_integer(cb_data, 0))); +} + +static void vhpi_vpy_error_length(const vhpiCbDataT *cb_data) { return_integer(cb_data, vpy_error_length()); } + +/* The read procedures fill a whole chunk, the part beyond length is left as it was */ +static void vhpi_vpy_result_read_string(const vhpiCbDataT *cb_data) { + char chunk[CHUNK_LENGTH]; + + get_string_chunk(cb_data, 0, chunk); + vpy_result_read_string(chunk, get_integer(cb_data, 1), get_integer(cb_data, 2)); + put_string_chunk(cb_data, 0, chunk); +} + +static void vhpi_vpy_result_read_integers(const vhpiCbDataT *cb_data) { + int32_t chunk[CHUNK_LENGTH]; + + get_vector(cb_data, 0, vhpiIntVecVal, chunk, sizeof(chunk)); + vpy_result_read_integers(chunk, get_integer(cb_data, 1), get_integer(cb_data, 2)); + put_vector(cb_data, 0, vhpiIntVecVal, chunk, sizeof(chunk)); +} + +static void vhpi_vpy_result_read_reals(const vhpiCbDataT *cb_data) { + double chunk[CHUNK_LENGTH]; + + get_vector(cb_data, 0, vhpiRealVecVal, chunk, sizeof(chunk)); + vpy_result_read_reals(chunk, get_integer(cb_data, 1), get_integer(cb_data, 2)); + put_vector(cb_data, 0, vhpiRealVecVal, chunk, sizeof(chunk)); +} + +static void vhpi_vpy_error_read(const vhpiCbDataT *cb_data) { + char chunk[CHUNK_LENGTH]; + + get_string_chunk(cb_data, 0, chunk); + vpy_error_read(chunk, get_integer(cb_data, 1), get_integer(cb_data, 2)); + put_string_chunk(cb_data, 0, chunk); +} + +/* ------------------------------------------------------------------------ */ +/* Registration */ +/* ------------------------------------------------------------------------ */ + +static void register_entry_point(vhpiForeignT kind, const char *name, void (*execf)(const vhpiCbDataT *)) { + vhpiForeignDataT data = {kind, (char *)LIBRARY_NAME, (char *)name, NULL, execf}; + + if (vhpi_register_foreignf(&data) == NULL) { + vhpi_assert(vhpiError, "VUnit Python bridge: failed to register the foreign subprogram %s", name); + } +} + +static void register_entry_points(void) { + register_entry_point(vhpiFuncF, "vpy_setup", vhpi_vpy_setup); + register_entry_point(vhpiFuncF, "vpy_cleanup", vhpi_vpy_cleanup); + register_entry_point(vhpiFuncF, "vpy_buffer_clear", vhpi_vpy_buffer_clear); + register_entry_point(vhpiFuncF, "vpy_buffer_append", vhpi_vpy_buffer_append); + register_entry_point(vhpiFuncF, "vpy_begin", vhpi_vpy_begin); + register_entry_point(vhpiFuncF, "vpy_execute", vhpi_vpy_execute); + register_entry_point(vhpiFuncF, "vpy_eval", vhpi_vpy_eval); + register_entry_point(vhpiFuncF, "vpy_push_array", vhpi_vpy_push_array); + register_entry_point(vhpiFuncF, "vpy_array_write", vhpi_vpy_array_write); + register_entry_point(vhpiFuncF, "vpy_stage", vhpi_vpy_stage); + register_entry_point(vhpiFuncF, "vpy_result_integer", vhpi_vpy_result_integer); + register_entry_point(vhpiFuncF, "vpy_result_real", vhpi_vpy_result_real); + register_entry_point(vhpiFuncF, "vpy_result_meta", vhpi_vpy_result_meta); + register_entry_point(vhpiFuncF, "vpy_error_length", vhpi_vpy_error_length); + register_entry_point(vhpiProcF, "vpy_result_read_string", vhpi_vpy_result_read_string); + register_entry_point(vhpiProcF, "vpy_result_read_integers", vhpi_vpy_result_read_integers); + register_entry_point(vhpiProcF, "vpy_result_read_reals", vhpi_vpy_result_read_reals); + register_entry_point(vhpiProcF, "vpy_error_read", vhpi_vpy_error_read); +} + +VPY_EXPORT void (*vhpi_startup_routines[])(void) = {register_entry_points, NULL}; diff --git a/src/vunit_python_bridge/native/vhpi/python_pkg.c b/src/vunit_python_bridge/native/vhpi/python_pkg.c deleted file mode 100644 index 8fd2725..0000000 --- a/src/vunit_python_bridge/native/vhpi/python_pkg.c +++ /dev/null @@ -1,125 +0,0 @@ -// Riviera-PRO/Active-HDL (VHPI) implementation of python_pkg: Python error handling -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com - -#include "python_pkg.h" - -#include - -static py_error_handler_callback py_error_handler = NULL; -static ffi_error_handler_callback ffi_error_handler = NULL; - -void register_py_error_handler(py_error_handler_callback callback) { - py_error_handler = callback; -} - -void register_ffi_error_handler(ffi_error_handler_callback callback) { - ffi_error_handler = callback; -} - -char* get_string(PyObject* pyobj) { - PyObject* str = PyObject_Str(pyobj); - if (str == NULL) { - return NULL; - } - - PyObject* str_utf_8 = PyUnicode_AsEncodedString(str, "utf-8", NULL); - Py_DECREF(str); - if (str_utf_8 == NULL) { - return NULL; - } - - // A copy, since the bytes object is released here. The string is only used - // on the error path, so the copy is never freed. - char* result = strdup(PyBytes_AS_STRING(str_utf_8)); - Py_DECREF(str_utf_8); - - return result; -} - -void check_conversion_error(const char* expr) { - PyObject* exc = PyErr_Occurred(); - if (exc != NULL) { - Py_DECREF(exc); - py_error_handler("parsing evaluation result of", expr, NULL, true); - } -} - -void handle_type_check_error(PyObject* pyobj, const char* context, - const char* expr) { - PyTypeObject* type = pyobj->ob_type; - if (type == NULL) { - py_error_handler(context, expr, "Expression evaluates to an unknown type.", - true); - } - const char* type_name_str = type->tp_name; - Py_DECREF(type); - - if (type_name_str == NULL) { - py_error_handler(context, expr, "Expression evaluates to an unknown type.", - true); - } - - char error_message[100] = "Expression evaluates to "; - strncat(error_message, type_name_str, 75); - py_error_handler(context, expr, error_message, true); -} - -PyObject* eval(const char* expr) { - PyObject* pyobj = PyRun_String(expr, Py_eval_input, globals, locals); - if (pyobj == NULL) { - py_error_handler("evaluating", expr, NULL, true); - } - - return pyobj; -} - -int get_integer(PyObject* pyobj, const char* expr, bool dec_ref_count) { - // Check that the Python object has the correct type - if (!PyLong_Check(pyobj)) { - handle_type_check_error(pyobj, "evaluating to integer", expr); - } - - // Convert from Python-typed to C-typed value and check for any errors such as - // overflow/underflow - long value = PyLong_AsLong(pyobj); - if (dec_ref_count) { - Py_DECREF(pyobj); - } - check_conversion_error(expr); - - // TODO: Assume that the simulator is limited to 32-bits for now - if ((value > pow(2, 31) - 1) || (value < -pow(2, 31))) { - py_error_handler("parsing evaluation result of", expr, - "Result out of VHDL integer range.", true); - } - - return (int)value; -} - -double get_real(PyObject* pyobj, const char* expr, bool dec_ref_count) { - // Check that the Python object has the correct type - if (!PyFloat_Check(pyobj)) { - handle_type_check_error(pyobj, "evaluating to real", expr); - } - - // Convert from Python-typed to C-typed value and check for any errors such as - // overflow/underflow - double value = PyFloat_AsDouble(pyobj); - if (dec_ref_count) { - Py_DECREF(pyobj); - } - check_conversion_error(expr); - - // TODO: Assume that the simulator is limited to 32-bits for now - if ((value > 3.4028234664e38) || (value < -3.4028234664e38)) { - py_error_handler("parsing evaluation result of", expr, - "Result out of VHDL real range.", true); - } - - return value; -} diff --git a/src/vunit_python_bridge/native/vhpi/python_pkg.h b/src/vunit_python_bridge/native/vhpi/python_pkg.h deleted file mode 100644 index 298a4d4..0000000 --- a/src/vunit_python_bridge/native/vhpi/python_pkg.h +++ /dev/null @@ -1,31 +0,0 @@ -// Riviera-PRO/Active-HDL (VHPI) implementation of python_pkg: shared declarations -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com - -#include -#include - -#define PY_SSIZE_T_CLEAN -#include "Python.h" - -extern PyObject* globals; -extern PyObject* locals; - -typedef void (*py_error_handler_callback)(const char*, const char*, const char*, - bool); -void register_py_error_handler(py_error_handler_callback callback); - -typedef void (*ffi_error_handler_callback)(const char*, bool); -void register_ffi_error_handler(ffi_error_handler_callback callback); - -char* get_string(PyObject* pyobj); -void check_conversion_error(const char* expr); -void handle_type_check_error(PyObject* pyobj, const char* context, - const char* expr); -PyObject* eval(const char* expr); -int get_integer(PyObject* pyobj, const char* expr, bool dec_ref_count); -double get_real(PyObject* pyobj, const char* expr, bool dec_ref_count); diff --git a/src/vunit_python_bridge/native/vhpi/python_pkg_vhpi.c b/src/vunit_python_bridge/native/vhpi/python_pkg_vhpi.c deleted file mode 100644 index 7187081..0000000 --- a/src/vunit_python_bridge/native/vhpi/python_pkg_vhpi.c +++ /dev/null @@ -1,366 +0,0 @@ -// Riviera-PRO/Active-HDL (VHPI) implementation of python_pkg: the foreign subprograms -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com - -#include -#include -#include - -#include "python_pkg.h" - -PyObject* globals = NULL; -PyObject* locals = NULL; - -#define MAX_VHDL_PARAMETER_STRING_LENGTH 100000 - -PLI_VOID python_cleanup(const struct vhpiCbDataS* cb_p); - -static void py_error_handler(const char* context, const char* code_or_expr, - const char* reason, bool cleanup) { - const char* unknown_error = "Unknown error"; - - // Use provided error reason or try extracting the reason from the Python - // exception - if (reason == NULL) { - PyObject *ptype, *pvalue, *ptraceback; - PyErr_Fetch(&ptype, &pvalue, &ptraceback); - if (ptype != NULL) { - reason = get_string(pvalue); - } - // The reason has been extracted. The exception must not be left set: - // Py_FinalizeEx in python_cleanup would fail on it. - Py_XDECREF(ptype); - Py_XDECREF(pvalue); - Py_XDECREF(ptraceback); - } - - // Clean-up Python session first in case vhpi_assert stops the simulation - if (cleanup) { - python_cleanup(NULL); - } - - // Output error message - reason = reason == NULL ? unknown_error : reason; - if (code_or_expr == NULL) { - vhpi_assert(vhpiError, "ERROR %s:\n\n%s\n\n", context, reason); - } else { - vhpi_assert(vhpiError, "ERROR %s:\n\n%s\n\n%s\n\n", context, code_or_expr, - reason); - } - - // Stop the simulation if vhpi_assert didn't. - vhpi_control(vhpiStop); -} - -static void ffi_error_handler(const char* context, bool cleanup) { - vhpiErrorInfoT err; - - // Clean-up Python session first in case vhpi_assert stops the simulation - if (cleanup) { - python_cleanup(NULL); - } - - if (vhpi_check_error(&err)) { - vhpi_assert(err.severity, "ERROR %s: \n\n%s (%d): %s\n\n", context, - err.file, err.line, err.message); - - } else { - vhpi_assert(vhpiError, "ERROR %s\n\n", context); - } - - // Stop the simulation if vhpi_assert didn't. - vhpi_control(vhpiStop); -} - -PLI_VOID python_setup(const struct vhpiCbDataS* cb_p) { - Py_Initialize(); - if (!Py_IsInitialized()) { - ffi_error_handler("Failed to initialize Python", false); - } - - PyObject* main_module = PyImport_AddModule("__main__"); - if (main_module == NULL) { - ffi_error_handler("Failed to get the main module", true); - } - - globals = PyModule_GetDict(main_module); - if (globals == NULL) { - ffi_error_handler("Failed to get the global dictionary", true); - } - - // globals and locals are the same at the top-level - locals = globals; - - register_py_error_handler(py_error_handler); - register_ffi_error_handler(ffi_error_handler); -} - -PLI_VOID python_cleanup(const struct vhpiCbDataS* cb_p) { - if (!Py_IsInitialized()) { - // The interpreter was never started, there is nothing to release - return; - } - - if (locals != NULL) { - Py_DECREF(locals); - } - - if (Py_FinalizeEx()) { - vhpi_assert(vhpiWarning, "WARNING: Failed to finalize Python"); - } -} - -// python_setup is optional: every operation starts the interpreter if it is -// not already running. -static void ensure_initialized(void) { - if (!Py_IsInitialized()) { - python_setup(NULL); - } -} - -static const char* get_parameter(const struct vhpiCbDataS* cb_p) { - // Get parameter from VHDL function call - vhpiHandleT parameter_handle = - vhpi_handle_by_index(vhpiParamDecls, cb_p->obj, 0); - if (parameter_handle == NULL) { - ffi_error_handler("getting VHDL parameter handle", true); - } - - vhpiValueT parameter; - static char vhdl_parameter_string[MAX_VHDL_PARAMETER_STRING_LENGTH]; - - parameter.bufSize = MAX_VHDL_PARAMETER_STRING_LENGTH; - parameter.value.str = vhdl_parameter_string; - parameter.format = vhpiStrVal; - - if (vhpi_get_value(parameter_handle, ¶meter)) { - ffi_error_handler("getting VHDL parameter value", true); - } - - vhpi_release_handle(parameter_handle); - - return vhdl_parameter_string; -} - -PLI_VOID eval_integer(const struct vhpiCbDataS* cb_p) { - ensure_initialized(); - - // Get expression parameter from VHDL function call - const char* expr = get_parameter(cb_p); - - // Eval(uate) expression in Python - PyObject* eval_result = eval(expr); - - // Return result to VHDL - vhpiValueT vhdl_result; - vhdl_result.format = vhpiIntVal; - vhdl_result.bufSize = 0; - vhdl_result.value.intg = get_integer(eval_result, expr, true); - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - py_error_handler("returning result for evaluation of", expr, NULL, true); - } -} - -PLI_VOID eval_real(const struct vhpiCbDataS* cb_p) { - ensure_initialized(); - - // Get expression parameter from VHDL function call - const char* expr = get_parameter(cb_p); - - // Eval(uate) expression in Python - PyObject* eval_result = eval(expr); - - // Return result to VHDL - vhpiValueT vhdl_result; - vhdl_result.format = vhpiRealVal; - vhdl_result.bufSize = 0; - vhdl_result.value.real = get_real(eval_result, expr, true); - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - py_error_handler("returning result for evaluation of", expr, NULL, true); - } -} - -PLI_VOID eval_integer_vector(const struct vhpiCbDataS* cb_p) { - ensure_initialized(); - - // Get expression parameter from VHDL function call - const char* expr = get_parameter(cb_p); - - // Eval(uate) expression in Python - PyObject* pyobj = eval(expr); - - // Check that the eval results in a list. TODO: tuple and sets of integers - // should also work - if (!PyList_Check(pyobj)) { - handle_type_check_error(pyobj, "evaluating to integer_vector", expr); - } - - const int list_size = PyList_GET_SIZE(pyobj); - const int n_bytes = list_size * sizeof(int); - int* int_array = (int*)malloc(n_bytes); - - for (int idx = 0; idx < list_size; idx++) { - int_array[idx] = get_integer(PyList_GetItem(pyobj, idx), expr, false); - } - Py_DECREF(pyobj); - - // Return result to VHDL - vhpiValueT vhdl_result; - vhdl_result.format = vhpiIntVecVal; - vhdl_result.bufSize = n_bytes; - vhdl_result.numElems = list_size; - vhdl_result.value.intgs = (vhpiIntT*)int_array; - - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiSizeConstraint)) { - free(int_array); - py_error_handler( - "setting size constraints when returning result for evaluation of", - expr, NULL, true); - } - - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - free(int_array); - py_error_handler("returning result for evaluation of", expr, NULL, true); - } - - free(int_array); -} - -PLI_VOID eval_real_vector(const struct vhpiCbDataS* cb_p) { - ensure_initialized(); - - // Get expression parameter from VHDL function call - const char* expr = get_parameter(cb_p); - - // Eval(uate) expression in Python - PyObject* pyobj = eval(expr); - - // Check that the eval results in a list. TODO: tuple and sets of integers - // should also work - if (!PyList_Check(pyobj)) { - handle_type_check_error(pyobj, "evaluating to real_vector", expr); - } - - const int list_size = PyList_GET_SIZE(pyobj); - const int n_bytes = list_size * sizeof(double); - double* double_array = (double*)malloc(n_bytes); - - for (int idx = 0; idx < list_size; idx++) { - double_array[idx] = get_real(PyList_GetItem(pyobj, idx), expr, false); - } - Py_DECREF(pyobj); - - // Return result to VHDL - vhpiValueT vhdl_result; - vhdl_result.format = vhpiRealVecVal; - vhdl_result.bufSize = n_bytes; - vhdl_result.numElems = list_size; - vhdl_result.value.reals = double_array; - - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiSizeConstraint)) { - free(double_array); - py_error_handler( - "setting size constraints when returning result for evaluation of", - expr, NULL, true); - } - - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - free(double_array); - py_error_handler("returning result for evaluation of", expr, NULL, true); - } - - free(double_array); -} - -PLI_VOID eval_string(const struct vhpiCbDataS* cb_p) { - ensure_initialized(); - - // Get expression parameter from VHDL function call - const char* expr = get_parameter(cb_p); - - // Eval(uate) expression in Python - PyObject* pyobj = eval(expr); - - char* str = get_string(pyobj); - Py_DECREF(pyobj); - - // Return result to VHDL - vhpiValueT vhdl_result; - vhdl_result.format = vhpiStrVal; - vhdl_result.bufSize = strlen(str) + 1; // null termination included - vhdl_result.numElems = strlen(str); - vhdl_result.value.str = str; - - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiSizeConstraint)) { - py_error_handler( - "setting size constraints when returning result for evaluation of", - expr, NULL, true); - } - - if (vhpi_put_value(cb_p->obj, &vhdl_result, vhpiDeposit)) { - py_error_handler("returning result for evaluation of", expr, NULL, true); - } -} - -PLI_VOID exec(const struct vhpiCbDataS* cb_p) { - ensure_initialized(); - - // Get code parameter from VHDL procedure call - const char* code = get_parameter(cb_p); - - // Exec(ute) Python code - if (PyRun_String(code, Py_file_input, globals, locals) == NULL) { - py_error_handler("executing", code, NULL, true); - } -} - -PLI_VOID register_foreign_subprograms() { - char library_name[] = "python"; - - char python_setup_name[] = "python_setup"; - vhpiForeignDataT python_setup_data = {vhpiProcF, library_name, - python_setup_name, NULL, python_setup}; - // vhpi_assert doesn't seem to work at this point - assert(vhpi_register_foreignf(&python_setup_data) != NULL); - - char python_cleanup_name[] = "python_cleanup"; - vhpiForeignDataT python_cleanup_data = { - vhpiProcF, library_name, python_cleanup_name, NULL, python_cleanup}; - assert(vhpi_register_foreignf(&python_cleanup_data) != NULL); - - char eval_integer_name[] = "eval_integer"; - vhpiForeignDataT eval_integer_data = {vhpiProcF, library_name, - eval_integer_name, NULL, eval_integer}; - assert(vhpi_register_foreignf(&eval_integer_data) != NULL); - - char eval_real_name[] = "eval_real"; - vhpiForeignDataT eval_real_data = {vhpiProcF, library_name, eval_real_name, - NULL, eval_real}; - assert(vhpi_register_foreignf(&eval_real_data) != NULL); - - char eval_integer_vector_name[] = "eval_integer_vector"; - vhpiForeignDataT eval_integer_vector_data = {vhpiProcF, library_name, - eval_integer_vector_name, NULL, - eval_integer_vector}; - assert(vhpi_register_foreignf(&eval_integer_vector_data) != NULL); - - char eval_real_vector_name[] = "eval_real_vector"; - vhpiForeignDataT eval_real_vector_data = { - vhpiProcF, library_name, eval_real_vector_name, NULL, eval_real_vector}; - assert(vhpi_register_foreignf(&eval_real_vector_data) != NULL); - - char eval_string_name[] = "eval_string"; - vhpiForeignDataT eval_string_data = {vhpiProcF, library_name, - eval_string_name, NULL, eval_string}; - assert(vhpi_register_foreignf(&eval_string_data) != NULL); - - char exec_name[] = "exec"; - vhpiForeignDataT exec_data = {vhpiProcF, library_name, exec_name, NULL, exec}; - assert(vhpi_register_foreignf(&exec_data) != NULL); -} - -PLI_VOID (*vhpi_startup_routines[])() = {register_foreign_subprograms, NULL}; diff --git a/src/vunit_python_bridge/native_library.py b/src/vunit_python_bridge/native_library.py index 55f51b4..40511a0 100644 --- a/src/vunit_python_bridge/native_library.py +++ b/src/vunit_python_bridge/native_library.py @@ -31,6 +31,8 @@ BINARY_PATH = PACKAGE_PATH / "bin" # Front end only compiled into the FLI variant of the library FLI_SOURCE_NAME = "fli.c" +# The front ends of the simulators without direct binding: their source files +FRONT_END_SOURCE_NAMES = {"fli": FLI_SOURCE_NAME, "vhpi": "vhpi.c"} # Directory of the bridge libraries, overriding the default of library_directory() LIBRARY_DIR_VARIABLE = "VUNIT_PYTHON_BRIDGE_LIBRARY_DIR" @@ -66,23 +68,26 @@ def library_directory() -> Path: return Path.home() / ".vunit-python-bridge" -def prepare_library(root: Path, simulator_prefix: Optional[Path] = None) -> Path: +def prepare_library(root: Path, simulator_prefix: Optional[Path], front_end: Optional[str]) -> Path: """ Path of the bridge library for the running Python, built or selected under root. :param simulator_prefix: Executable directory of a simulator calling the bridge through - the FLI (Questa/ModelSim). The FLI variant of the library, which - also contains native/fli.c, is then built against its headers. - None selects the VHPIDIRECT variant used by NVC and GHDL. + a front end, the FLI (Questa/ModelSim) or VHPI (Riviera-PRO). + The variant of the library with that front end, native/fli.c or + native/vhpi.c, is then built against its headers. None selects the + VHPIDIRECT variant used by NVC and GHDL. + :param front_end: "fli" or "vhpi", the front end of the simulator at simulator_prefix, None + without one. """ if sys.platform == "win32": add_python_dll_to_path() if simulator_prefix is not None: - return _build_windows_library(root, simulator_prefix) + return _build_windows_library(root, simulator_prefix, front_end) return check_windows_load(_prepare_windows_library(root)) if simulator_prefix is not None: - return _prepare_posix_library(root, simulator_prefix) - return check_posix_load(_prepare_posix_library(root)) + return _prepare_posix_library(root, simulator_prefix, front_end) + return check_posix_load(_prepare_posix_library(root, None, None)) # Windows errors of a library that an execution policy (AppLocker, Windows Defender Application @@ -211,7 +216,7 @@ def _prepare_windows_library(root: Path) -> Path: name = windows_dll_name() source = BINARY_PATH / name if not source.is_file(): - return _build_windows_library(root) + return _build_windows_library(root, None, None) data = source.read_bytes() directory = root / f"cp{sys.version_info[0]}{sys.version_info[1]}-win_amd64-{hashlib.sha256(data).hexdigest()[:12]}" target = directory / "vunit_python_bridge.dll" @@ -297,13 +302,14 @@ def _compiler() -> List[str]: ) -def bridge_sources(fli: bool = False) -> List[Path]: +def bridge_sources(front_end: Optional[str]) -> List[Path]: """ - The C source files of the bridge library. fli adds the FLI front end. + The C source files of the bridge library, plus those of a front end, "fli" or "vhpi", if any. """ - sources = sorted(item for item in NATIVE_PATH.glob("*.c") if item.name != FLI_SOURCE_NAME) - if fli: - sources.append(NATIVE_PATH / FLI_SOURCE_NAME) + front_ends = set(FRONT_END_SOURCE_NAMES.values()) + sources = sorted(item for item in NATIVE_PATH.glob("*.c") if item.name not in front_ends) + if front_end is not None: + sources.append(NATIVE_PATH / FRONT_END_SOURCE_NAMES[front_end]) return sources @@ -320,6 +326,19 @@ def _fli_include_dir(simulator_prefix: Path) -> str: return str(include) +def _vhpi_include_dir(simulator_prefix: Path) -> str: + """ + Directory of vhpi_user.h, interfaces/include of the Riviera-PRO installation. + """ + include = Path(simulator_prefix).resolve().parent / "interfaces" / "include" + if not (include / "vhpi_user.h").is_file(): + raise PythonBridgeError( + f"VHDL Python support needs the VHPI header vhpi_user.h of the simulator to build the Python " + f"bridge library, but it was not found in {include!s}" + ) + return str(include) + + def _source_fingerprint() -> str: """ Hash of all bridge sources and headers. @@ -381,21 +400,23 @@ def check_exec_mount(directory: Path) -> None: ) -def _prepare_posix_library(root: Path, simulator_prefix: Optional[Path] = None) -> Path: +def _prepare_posix_library(root: Path, simulator_prefix: Optional[Path], front_end: Optional[str]) -> Path: """ Compile the bridge for the running Python, reusing a cached build when possible. - The FLI variant, selected with simulator_prefix, differs only in the added front end, - the added include directory and the file name, so both variants share this cache: the - simulator prefix is part of the key through its include directory. + The FLI and VHPI variants, selected with simulator_prefix and front_end, differ only in the + added front end, the added include directory and the file name, so all variants share this + cache: the simulator prefix is part of the key through its include directory. """ - fli = simulator_prefix is not None python_library = _python_library() include_dirs = _include_dirs() - if simulator_prefix is not None: - include_dirs = include_dirs + [_fli_include_dir(simulator_prefix)] + if simulator_prefix is None: + name, sources = "libvunit_python_bridge.so", bridge_sources(None) + else: + include_dir = _vhpi_include_dir if front_end == "vhpi" else _fli_include_dir + include_dirs = include_dirs + [include_dir(simulator_prefix)] + name, sources = f"libvunit_python_bridge_{front_end}.so", bridge_sources(front_end) - name = "libvunit_python_bridge_fli.so" if fli else "libvunit_python_bridge.so" directory = _posix_cache_directory(root, python_library, include_dirs) library_file = directory / name directory.mkdir(parents=True, exist_ok=True) @@ -409,7 +430,7 @@ def _prepare_posix_library(root: Path, simulator_prefix: Optional[Path] = None) compiler + ["-shared", "-fPIC", "-O2", "-fvisibility=hidden"] + [f"-I{path}" for path in include_dirs] - + [str(path) for path in bridge_sources(fli=fli)] + + [str(path) for path in sources] + ["-o", str(tmp)] + [str(python_library), f"-Wl,-rpath,{python_library.parent!s}"] + ([f"-Wl,-install_name,@rpath/{name}"] if sys.platform == "darwin" else [f"-Wl,-soname,{name}"]) @@ -418,13 +439,15 @@ def _prepare_posix_library(root: Path, simulator_prefix: Optional[Path] = None) return compile_library(cmd, tmp, library_file) -def compile_library(cmd: List[str], tmp: Path, library_file: Path, env: Optional[Dict[str, str]] = None) -> Path: +def compile_library( + cmd: List[str], tmp: Path, library_file: Path, env: Optional[Dict[str, str]] = None, cwd: Optional[Path] = None +) -> Path: """ Run a compiler command building tmp and move the result to library_file, failing with the compiler output. """ try: - proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, env=env) + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, env=env, cwd=cwd) except OSError as exc: raise PythonBridgeError(f"Failed to run the C compiler {cmd[0]!r}: {exc}") from exc if proc.returncode != 0: @@ -485,19 +508,17 @@ def add_python_dll_to_path() -> None: os.environ["PATH"] = os.pathsep.join([item for item in paths if item] + [directory]) -def _build_windows_library(root: Path, simulator_prefix: Optional[Path] = None) -> Path: +def _build_windows_library(root: Path, simulator_prefix: Optional[Path], front_end: Optional[str]) -> Path: """ Build the library on Windows with gcc (see windows_gcc) against the headers and the import library of the Python running VUnit, reusing a cached build like the POSIX one. - With simulator_prefix, the FLI variant for Questa/ModelSim: the prebuilt DLLs cannot be used - for it, since they are MSVC builds without the FLI front end, which has to be linked against - the simulator's own libmtipli. Without, the VHPIDIRECT variant for NVC and GHDL, built only - when there is no prebuilt DLL. - - Untested: this environment has no Windows installation. + With simulator_prefix, the variant with the front end of the simulator, the FLI for + Questa/ModelSim or VHPI for Riviera-PRO: the prebuilt DLLs cannot be used for it, + since they are MSVC builds without the front end, which has to be linked against the + simulator's own libmtipli or aldecpli. Without, the VHPIDIRECT variant for NVC and GHDL, + built only when there is no prebuilt DLL. """ - fli = simulator_prefix is not None compiler = windows_gcc(simulator_prefix) if compiler is None: bundled = "" @@ -509,12 +530,18 @@ def _build_windows_library(root: Path, simulator_prefix: Optional[Path] = None) "or set the CC environment variable." ) python_include, python_libs = python_dev_paths() - include_dirs = [python_include] + ([_fli_include_dir(simulator_prefix)] if simulator_prefix is not None else []) - - name = "vunit_python_bridge_fli.dll" if fli else "vunit_python_bridge.dll" + include_dirs = [python_include] + simulator_libraries = [] + if front_end is not None: + include_dir = _vhpi_include_dir if front_end == "vhpi" else _fli_include_dir + include_dirs.append(include_dir(simulator_prefix)) + simulator_library = "-laldecpli" if front_end == "vhpi" else "-lmtipli" + simulator_libraries = [f"-L{Path(simulator_prefix).resolve()!s}", simulator_library] + + name = f"vunit_python_bridge_{front_end}.dll" if front_end else "vunit_python_bridge.dll" key_items = [_source_fingerprint(), sys.version, sys.base_prefix, str(simulator_prefix), " ".join(compiler)] key = hashlib.sha256("\n".join(key_items).encode("utf-8")).hexdigest()[:16] - directory = root / f"cp{sys.version_info[0]}{sys.version_info[1]}-win_amd64-{'fli' if fli else 'gcc'}-{key}" + directory = root / f"cp{sys.version_info[0]}{sys.version_info[1]}-win_amd64-{front_end or 'gcc'}-{key}" library_file = directory / name if library_file.is_file(): return library_file @@ -524,11 +551,11 @@ def _build_windows_library(root: Path, simulator_prefix: Optional[Path] = None) cmd = ( compiler + ["-shared", "-m64", "-O2", "-D__USE_MINGW_ANSI_STDIO=1"] - + (["-freg-struct-return"] if fli else []) + + (["-freg-struct-return"] if front_end == "fli" else []) + [f"-I{path}" for path in include_dirs] - + [str(path) for path in bridge_sources(fli=fli)] + + [str(path) for path in bridge_sources(front_end)] + ["-o", str(tmp)] + [f"-L{python_libs!s}", f"-lpython{sys.version_info[0]}{sys.version_info[1]}"] - + ([f"-L{Path(simulator_prefix).resolve()!s}", "-lmtipli"] if simulator_prefix is not None else []) + + simulator_libraries ) return compile_library(cmd, tmp, library_file) diff --git a/src/vunit_python_bridge/simulator_hooks.py b/src/vunit_python_bridge/simulator_hooks.py index 3c696d6..e68af8e 100644 --- a/src/vunit_python_bridge/simulator_hooks.py +++ b/src/vunit_python_bridge/simulator_hooks.py @@ -45,6 +45,10 @@ def register(context, bridge: PythonBridge) -> None: context.register_simulator_hooks( "modelsim", process_flags=_modelsim_process_flags(), run_env=_config_run_env(bridge) ) + if context.simulator_name == "rivierapro": + # Its interface has no run environment hook, but starts vsim with the environment of this + # process, after the setup + os.environ[CONFIG_FILE_VARIABLE] = str(bridge.config_file) def _config_run_env(bridge: PythonBridge): diff --git a/tests/run.py b/tests/run.py index a6ffc9d..042f652 100644 --- a/tests/run.py +++ b/tests/run.py @@ -20,29 +20,6 @@ ROOT = Path(__file__).parent -# The VHPI application reports Python errors fatally rather than as failures -# on the logger of the session, so the negative tests of tb_python_pkg cannot -# mock that logger there and fail by design. The Python bridge (NVC, GHDL, -# Questa) makes them observable from VHDL and there the whole testbench passes. -EXPECTED_FAILURES_VHPI = [ - "lib.tb_python_pkg.Test eval of integer with overflow from Python to C", - "lib.tb_python_pkg.Test eval of integer with underflow from Python to C", - "lib.tb_python_pkg.Test eval of integer with overflow from C to VHDL", - "lib.tb_python_pkg.Test eval of integer with underflow from C to VHDL", - "lib.tb_python_pkg.Test exceptions in exec", - "lib.tb_python_pkg.Test exceptions in eval", - "lib.tb_python_pkg.Test eval with type error", - "lib.tb_python_pkg.Test raising exception", - # The VHPI application rejects real values outside the float range and has - # no representation for a value that is not finite - "lib.tb_python_pkg.Test eval of real with overflow from C to VHDL", - "lib.tb_python_pkg.Test eval of real with underflow from C to VHDL", -] - -# Simulators where python_pkg is implemented by the VUnit Python bridge -BRIDGE_SIMULATORS = ["nvc", "ghdl", "modelsim"] - - def remote_test(): """ Called from VHDL to verify that the run script can be imported into the @@ -51,26 +28,6 @@ def remote_test(): return 2 -def verify(results, expected_failures): - """ - Accept the expected failures, and nothing else. - """ - tests = results.get_report().tests - expected = [name for name in expected_failures if name in tests] - failed = sorted(name for name, test in tests.items() if test.status == "failed") - - unexpected = [name for name in failed if name not in expected] - if unexpected: - raise RuntimeError("Unexpected test failures:\n" + "\n".join(unexpected)) - - missing = [name for name in expected if name not in failed] - if missing: - raise RuntimeError("Tests expected to fail but did not:\n" + "\n".join(missing)) - - if expected: - print(f"Verified {len(expected)} expected failures: {', '.join(name.split('.')[-1] for name in expected)}") - - def run_file_base_tests(args): """ Run the tests of base_path with a base directory, which is fixed for a run and @@ -97,28 +54,20 @@ def run_file_base_tests(args): def main(): args = VUnitCLI().parse_args() - # The expected failures must not make the run script fail - args.exit_0 = True # The tests of base_path without a base directory need the variable unset os.environ.pop(FILE_BASE_VARIABLE, None) vu = VUnit.from_args(args) vu.add_vhdl_builtins() vu.add_package("vunit-python-bridge", allow_setup=True) - simulator_name = vu.get_simulator_name() - expected_failures = [] if simulator_name in BRIDGE_SIMULATORS else list(EXPECTED_FAILURES_VHPI) - lib = vu.add_library("lib") lib.add_source_file(ROOT / "tb_python_pkg.vhd") - if simulator_name in BRIDGE_SIMULATORS: - # The operations implemented by the Python bridge - lib.add_source_file(ROOT / "tb_python_pkg_bridge.vhd") + lib.add_source_file(ROOT / "tb_python_pkg_bridge.vhd") vu.set_compile_option("rivierapro.vcom_flags", ["-dbg"]) vu.set_sim_option("rivierapro.vsim_flags", ["-interceptcoutput"]) - def post_run(results): - verify(results, expected_failures) + def post_run(results): # pylint: disable=unused-argument run_file_base_tests(args) vu.main(post_run=post_run) diff --git a/tests/tb_python_pkg_bridge.vhd b/tests/tb_python_pkg_bridge.vhd index 2842016..713ea21 100644 --- a/tests/tb_python_pkg_bridge.vhd +++ b/tests/tb_python_pkg_bridge.vhd @@ -4,9 +4,8 @@ -- -- Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com -- --- Tests of the python_pkg operations that are implemented by the Python --- bridge and therefore only available for NVC and GHDL. The rest of the --- package is tested by tb_python_pkg. +-- Tests of the python_pkg operations beyond exec and the eval and call results +-- of tb_python_pkg: sessions, typed results, arguments and integer_array_t. library ieee; use ieee.std_logic_1164.all; @@ -825,7 +824,7 @@ begin --------------------------------------------------------------------- -- integer_array_t --------------------------------------------------------------------- - elsif run("Test that a 2D array is indexed like VUnit, get(a, x, y) is a[x, y]") then + elsif run("Test that a 2D array is indexed like VUnit") then arr := new_2d(width => 3, height => 2, bit_width => 16, is_signed => false); for y in 0 to 1 loop for x in 0 to 2 loop @@ -838,7 +837,7 @@ begin ); check_true(call_boolean("orientation_ok", arg(arr))); - elsif run("Test that a 3D array is indexed like VUnit, get(a, x, y, z) is a[x, y, z]") then + elsif run("Test that a 3D array is indexed like VUnit") then arr := new_3d(width => 3, height => 2, depth => 4, bit_width => 16, is_signed => false); for y in 0 to 1 loop for x in 0 to 2 loop diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 31527d4..f9df6ce 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -29,7 +29,7 @@ import tomli as tomllib # A dependency of VUnit before Python 3.11 import vunit_python_bridge -from vunit_python_bridge import bridge as bridge_setup, file_base, foreign_application, native_library, simulator_hooks +from vunit_python_bridge import bridge as bridge_setup, file_base, native_library, simulator_hooks PACKAGE_ROOT = Path(vunit_python_bridge.__file__).parent.resolve() MANIFEST = PACKAGE_ROOT / "vunit_pkg.toml" @@ -164,8 +164,7 @@ def test_sources_are_the_simulator_independent_vhdl(self): def test_the_simulator_dependent_vhdl_is_not_listed(self): includes = [item for source in self.package["sources"] for item in source["include"]] - for name in ("python_pkg_vhpi.vhd", "python_ffi_pkg_bridge.vhd"): - self.assertNotIn(f"hdl/src/{name}", includes) + self.assertNotIn("hdl/src/python_ffi_pkg_bridge.vhd", includes) class TestForeignLanguageInterfaces(unittest.TestCase): @@ -182,22 +181,17 @@ def test_every_supported_simulator_has_its_interface(self): "ghdl": "VHPIDIRECT_GHDL", "modelsim": "FLI", "rivierapro": "VHPI", - "activehdl": "VHPI", }, ) def test_the_simulators_the_bridge_serves_are_the_ones_it_is_set_up_for(self): - # Everything that is not the VHPI application is served by the bridge, which has - # to know the same simulators. - served_by_the_bridge = { - name - for name, interface in vunit_python_bridge.FOREIGN_LANGUAGE_INTERFACES.items() - if interface != "VHPI" - } - self.assertEqual(served_by_the_bridge, set(bridge_setup.BRIDGE_SIMULATORS)) + # Every simulator is served by the bridge, which has to know the same simulators. + self.assertEqual( + set(vunit_python_bridge.FOREIGN_LANGUAGE_INTERFACES), set(bridge_setup.BRIDGE_SIMULATORS) + ) def test_the_message_names_every_simulator(self): - for simulator in ("NVC", "GHDL", "Questa/ModelSim", "Riviera-PRO", "Active-HDL"): + for simulator in ("NVC", "GHDL", "Questa/ModelSim", "Riviera-PRO"): self.assertIn(simulator, vunit_python_bridge.SUPPORTED_SIMULATORS) @@ -240,34 +234,6 @@ def test_rejects_unsupported_simulator(self): # The message names the simulators so a user knows what is supported. self.assertIn(vunit_python_bridge.SUPPORTED_SIMULATORS, str(ctx.exception)) - def test_vhpi_adds_python_pkg_vhpi_and_builds_the_application(self): - for name in ("rivierapro", "activehdl"): - self._check_vhpi_application_built(name) - - def _check_vhpi_application_built(self, name): - simulator = self._simulator(name) - context = self._context(simulator) - with mock.patch("vunit_python_bridge.bridge.setup") as setup_mock, mock.patch( - "vunit_python_bridge.foreign_application.setup_vhpi_application" - ) as vhpi_mock: - vunit_python_bridge.setup(context) - setup_mock.assert_not_called() - vhpi_mock.assert_called_once_with(context.output_path, name, context.simulator_prefix) - - self.assertEqual(self._added_names(context), {"python_pkg_vhpi.vhd", "python_file_base_pkg.vhd"}) - self.assertEqual({library for library, _ in context.added_files}, {"python_bridge"}) - # The VHPI application needs no simulator hooks - self.assertEqual(context.hooks, {}) - - def test_application_build_failure_is_reported(self): - context = self._context(self._simulator("rivierapro")) - with mock.patch( - "vunit_python_bridge.foreign_application.setup_vhpi_application", - side_effect=RuntimeError("no compiler"), - ): - with self.assertRaisesRegex(RuntimeError, "no compiler"): - vunit_python_bridge.setup(context) - def _fake_bridge(self): return bridge_setup.PythonBridge( library_file=Path("/fake/cache/libvunit_python_bridge.so"), @@ -280,8 +246,7 @@ def _fake_bridge(self): def _check_bridge_files_added(self, simulator, backend=None): """ - The generated bridge files, and not the VHPI application package, are added - for a simulator served by the Python bridge. + The generated bridge files are added for a simulator. """ context = self._context(simulator, backend=backend) fake_bridge = self._fake_bridge() @@ -301,7 +266,6 @@ def _check_bridge_files_added(self, simulator, backend=None): for expected in fake_bridge.vhdl_files: self.assertIn(("python_bridge", expected), context.added_files) - self.assertNotIn("python_pkg_vhpi.vhd", self._added_names(context)) return context def test_vhpidirect_adds_the_bridge_files(self): @@ -312,13 +276,19 @@ def test_fli_adds_the_bridge_files(self): # Questa/ModelSim is served by the bridge too, through native/fli.c. self._check_bridge_files_added(self._simulator("modelsim")) - def test_fli_uses_the_bridge_not_the_vhpi_application(self): - context = self._context(self._simulator("modelsim")) - with mock.patch("vunit_python_bridge.bridge.setup", return_value=self._fake_bridge()), mock.patch( - "vunit_python_bridge.foreign_application.setup_vhpi_application" - ) as vhpi_mock: - vunit_python_bridge.setup(context) - vhpi_mock.assert_not_called() + def test_vhpi_adds_the_bridge_files(self): + # Riviera-PRO is served by the bridge too, through native/vhpi.c. + with mock.patch.dict(os.environ): + self._check_bridge_files_added(self._simulator("rivierapro")) + + def test_vhpi_simulators_are_told_the_configuration_by_the_environment(self): + # Their VUnit interfaces have no run environment hook, but start vsim with this one. + with mock.patch.dict(os.environ): + os.environ.pop(simulator_hooks.CONFIG_FILE_VARIABLE, None) + self._check_bridge_files_added(self._simulator("rivierapro")) + self.assertEqual( + os.environ[simulator_hooks.CONFIG_FILE_VARIABLE], str(self._fake_bridge().config_file) + ) def test_hooks_are_registered_for_every_simulator_the_bridge_serves(self): context = self._check_bridge_files_added(self._simulator("nvc")) @@ -383,7 +353,7 @@ def _reset(): def _setup(self, name="nvc"): """ - Set the package up for a simulator, with bridge.setup() and the VHPI application stubbed, + Set the package up for a simulator, with bridge.setup() stubbed, and return the context and the generated package. """ simulator = mock.Mock() @@ -396,7 +366,6 @@ def _setup(self, name="nvc"): ) with ( mock.patch("vunit_python_bridge.bridge.setup", return_value=fake_bridge), - mock.patch("vunit_python_bridge.foreign_application.setup_vhpi_application"), mock.patch("vunit_python_bridge.simulator_hooks.register"), ): vunit_python_bridge.setup(context) @@ -543,7 +512,7 @@ def test_variable_is_not_read_at_import(self): self.assertIsNone(file_base._relative_file_base) # pylint: disable=protected-access def test_bridge_and_vhpi_add_the_package(self): - for name in ("nvc", "ghdl", "modelsim", "rivierapro", "activehdl"): + for name in ("nvc", "ghdl", "modelsim", "rivierapro"): self._reset() context, package = self._setup(name) self.assertIn(("python_bridge", package), context.added_files, name) @@ -638,8 +607,34 @@ def test_no_simulator_prefix_for_vhpidirect(self): self.assertIsNone(prepare_mock.call_args.args[1]) def test_unsupported_simulator_raises(self): - with self.assertRaisesRegex(RuntimeError, "NVC, GHDL or Questa/ModelSim"): - self._setup("rivierapro") + with self.assertRaisesRegex(RuntimeError, "Questa/ModelSim or Riviera-PRO, it is not supported for xsim"): + self._setup("xsim") + + def test_vhpi_attributes_name_the_library_without_its_suffix_and_the_entry_point(self): + # Riviera-PRO adds the suffix, and matches the library to the name native/vhpi.c registers. + bridge = self._setup("rivierapro", library_file_name="libvunit_python_bridge_vhpi.so") + text = self._ffi_text(bridge) + library = bridge.library_file.with_suffix("") + self.assertIn(f'"VHPI {library!s} vpy_begin"', text) + self.assertEqual(set(re.findall(r'"VHPI \S+ (\w+)"', text)), set(EXPECTED_EXPORTS)) + source = (native_library.NATIVE_PATH / "vhpi.c").read_text(encoding="utf-8") + self.assertIn(f'#define LIBRARY_NAME "{library.name}"', source) + + def test_vhpi_front_end_registers_every_entry_point(self): + source = (native_library.NATIVE_PATH / "vhpi.c").read_text(encoding="utf-8") + registered = re.findall(r'register_entry_point\(vhpi\w+, "(\w+)", vhpi_(\w+)\);', source) + self.assertEqual({name for name, _ in registered}, set(EXPECTED_EXPORTS)) + self.assertEqual([name for name, wrapper in registered if name != wrapper], []) + + def test_vhpi_library_path_with_a_space_raises(self): + with self.assertRaisesRegex(RuntimeError, "has a space.*VUNIT_PYTHON_BRIDGE_LIBRARY_DIR"): + self._setup("rivierapro", library_file_name="my dir/libvunit_python_bridge_vhpi.so") + + def test_vhpi_library_is_the_vhpi_variant(self): + with mock.patch("vunit_python_bridge.bridge.prepare_library") as prepare_mock: + prepare_mock.return_value = self.tempdir / "cache" / "libvunit_python_bridge_vhpi.so" + bridge_setup.setup(self.tempdir / "out", self.run_script, "rivierapro", "/sim/bin") + self.assertEqual(prepare_mock.call_args.args[1:], (Path("/sim/bin"), "vhpi")) def test_all_vhpidirect_tokens_fit_ghdl_limit(self): for simulator_name, backend in ( @@ -855,36 +850,57 @@ def test_fli_variant_adds_the_front_end_and_the_simulator_headers(self): prefix = self._fake_simulator_prefix() calls = [] with mock.patch("subprocess.run", side_effect=self._compiler_stub(calls)): - library_file = native_library.prepare_library(self.tempdir / "out", prefix) + library_file = native_library.prepare_library(self.tempdir / "out", prefix, "fli") self.assertEqual(len(calls), 1) self.assertEqual(library_file.name, "libvunit_python_bridge_fli.so") self.assertIn(str(native_library.NATIVE_PATH / "fli.c"), calls[0]) self.assertIn(f"-I{prefix.parent / 'include'!s}", calls[0]) + def test_vhpi_variant_adds_the_front_end_and_the_simulator_headers(self): + prefix = self.tempdir / "riviera" / "bin" + prefix.mkdir(parents=True) + include = prefix.parent / "interfaces" / "include" + include.mkdir(parents=True) + (include / "vhpi_user.h").write_text("/* fake */\n", encoding="utf-8") + calls = [] + with mock.patch("subprocess.run", side_effect=self._compiler_stub(calls)): + library_file = native_library.prepare_library(self.tempdir / "out", prefix, "vhpi") + self.assertEqual(library_file.name, "libvunit_python_bridge_vhpi.so") + self.assertIn(str(native_library.NATIVE_PATH / "vhpi.c"), calls[0]) + self.assertNotIn(str(native_library.NATIVE_PATH / "fli.c"), calls[0]) + self.assertIn(f"-I{include.resolve()!s}", calls[0]) + + def test_missing_vhpi_user_h_raises_actionable_error(self): + prefix = self.tempdir / "riviera" / "bin" + prefix.mkdir(parents=True) + with self.assertRaisesRegex(RuntimeError, "vhpi_user.h"): + native_library.prepare_library(self.tempdir / "out", prefix, "vhpi") + def test_vhpidirect_variant_omits_the_front_end(self): calls = [] # The stub compiler writes no real library with mock.patch("subprocess.run", side_effect=self._compiler_stub(calls)), mock.patch("ctypes.CDLL"): - library_file = native_library.prepare_library(self.tempdir / "out") + library_file = native_library.prepare_library(self.tempdir / "out", None, None) self.assertEqual(library_file.name, "libvunit_python_bridge.so") self.assertNotIn(str(native_library.NATIVE_PATH / "fli.c"), calls[0]) + self.assertNotIn(str(native_library.NATIVE_PATH / "vhpi.c"), calls[0]) def test_fli_and_vhpidirect_variants_are_cached_separately(self): prefix = self._fake_simulator_prefix() calls = [] # The stub compiler writes no real library with mock.patch("subprocess.run", side_effect=self._compiler_stub(calls)), mock.patch("ctypes.CDLL"): - vhpidirect = native_library.prepare_library(self.tempdir / "out") - fli = native_library.prepare_library(self.tempdir / "out", prefix) + vhpidirect = native_library.prepare_library(self.tempdir / "out", None, None) + fli = native_library.prepare_library(self.tempdir / "out", prefix, "fli") self.assertEqual(len(calls), 2) self.assertNotEqual(fli.parent, vhpidirect.parent) def test_second_fli_setup_reuses_cache_without_compiling(self): prefix = self._fake_simulator_prefix() with mock.patch("subprocess.run", side_effect=self._compiler_stub([])): - first = native_library.prepare_library(self.tempdir / "out", prefix) + first = native_library.prepare_library(self.tempdir / "out", prefix, "fli") with mock.patch("subprocess.run") as run_mock: - second = native_library.prepare_library(self.tempdir / "out", prefix) + second = native_library.prepare_library(self.tempdir / "out", prefix, "fli") run_mock.assert_not_called() self.assertEqual(first, second) @@ -896,8 +912,8 @@ def test_another_simulator_installation_gets_its_own_cache_dir(self): (other / "bin").mkdir() calls = [] with mock.patch("subprocess.run", side_effect=self._compiler_stub(calls)): - first = native_library.prepare_library(self.tempdir / "out", first_prefix) - second = native_library.prepare_library(self.tempdir / "out", other / "bin") + first = native_library.prepare_library(self.tempdir / "out", first_prefix, "fli") + second = native_library.prepare_library(self.tempdir / "out", other / "bin", "fli") self.assertEqual(len(calls), 2) self.assertNotEqual(first.parent, second.parent) @@ -905,7 +921,7 @@ def test_missing_mti_h_raises_actionable_error(self): prefix = self.tempdir / "questa" / "bin" prefix.mkdir(parents=True) with self.assertRaisesRegex(RuntimeError, "mti.h"): - native_library.prepare_library(self.tempdir / "out", prefix) + native_library.prepare_library(self.tempdir / "out", prefix, "fli") def test_no_prebuilt_library_in_repository(self): # Linux libraries are built on first use and Windows DLLs are added to releases by CI @@ -1114,7 +1130,7 @@ def test_missing_dll_for_running_version_builds_with_gcc(self): self.assertEqual( native_library._prepare_windows_library(root), Path("built.dll") # pylint: disable=protected-access ) - build.assert_called_once_with(root) + build.assert_called_once_with(root, None, None) def test_non_win_amd64_platform_raises(self): with mock.patch("vunit_python_bridge.native_library.sysconfig.get_platform", return_value="mingw"): @@ -1156,7 +1172,7 @@ def test_build_without_gcc_raises_actionable_error(self): with mock.patch.object(native_library, "windows_gcc", return_value=None): with self.assertRaisesRegex(native_library.PythonBridgeError, "MinGW-w64.*PATH.*CC"): native_library._build_windows_library( - Path("root"), Path("questa/win64") + Path("root"), Path("questa/win64"), "fli" ) # pylint: disable=protected-access def _build_cmd(self, tempdir, simulator_prefix): @@ -1167,7 +1183,7 @@ def _build_cmd(self, tempdir, simulator_prefix): mock.patch.object(native_library, "compile_library", side_effect=lambda cmd, tmp, target: target) as build, ): target = native_library._build_windows_library( - tempdir, simulator_prefix + tempdir, simulator_prefix, "fli" if simulator_prefix else None ) # pylint: disable=protected-access return target, build.call_args[0][0] @@ -1409,67 +1425,5 @@ def test_modelsim_process_flags_empty_on_windows(self): run.assert_not_called() -class TestForeignApplicationBuild(unittest.TestCase): - """ - The VHPI application is built under the output path once and rebuilt when its inputs change. - """ - - def test_builds_once_and_rebuilds_when_the_fingerprint_changes(self): - with create_tempdir() as tempdir: - output_path = Path(tempdir) / "out" - prefix = str(Path(tempdir) / "rivierapro" / "bin") - target = output_path / "rivierapro" / "libraries" / "python.dll" - - def fake_build(target, sources, simulator_prefix): # pylint: disable=unused-argument - target.write_text("built", encoding="utf-8") - - with mock.patch.object(foreign_application, "_build_vhpi", side_effect=fake_build) as build: - foreign_application.setup_vhpi_application(output_path, "rivierapro", prefix) - foreign_application.setup_vhpi_application(output_path, "rivierapro", prefix) - self.assertEqual(build.call_count, 1) - self.assertTrue(target.exists()) - self.assertTrue(target.with_suffix(".dll.fingerprint").exists()) - - # Another simulator installation changes the fingerprint - other = str(Path(tempdir) / "other" / "bin") - with mock.patch.object(foreign_application, "_build_vhpi", side_effect=fake_build) as build: - foreign_application.setup_vhpi_application(output_path, "rivierapro", other) - self.assertEqual(build.call_count, 1) - - # A missing library is rebuilt even with a matching fingerprint - target.unlink() - with mock.patch.object(foreign_application, "_build_vhpi", side_effect=fake_build) as build: - foreign_application.setup_vhpi_application(output_path, "rivierapro", other) - self.assertEqual(build.call_count, 1) - - def test_ccomp_gets_one_unquoted_argument_per_path(self): - with ( - mock.patch("sys.platform", "linux"), - mock.patch.object(foreign_application, "python_dev_paths", return_value=("/py/include", Path("/py/lib"))), - mock.patch.object(foreign_application, "compile_library") as build, - ): - foreign_application._build_vhpi( # pylint: disable=protected-access - Path("/out dir/python.dll"), [Path("/src/a.c"), Path("/src/b.c")], Path("/riviera/bin") - ) - args = build.call_args[0][0] - self.assertEqual(args[args.index("-o") + 1], str(Path("/out dir/python.dll"))) - self.assertIn("/py/include", args) - self.assertEqual(args[-2:], [str(Path("/src/a.c")), str(Path("/src/b.c"))]) - self.assertFalse(any('"' in arg for arg in args)) - - def test_failed_build_leaves_no_fingerprint(self): - with create_tempdir() as tempdir: - output_path = Path(tempdir) / "out" - prefix = str(Path(tempdir) / "rivierapro" / "bin") - with mock.patch.object(foreign_application, "_build_vhpi", side_effect=RuntimeError("boom")): - with self.assertRaises(RuntimeError): - foreign_application.setup_vhpi_application(output_path, "rivierapro", prefix) - self.assertFalse(list((output_path / "rivierapro" / "libraries").glob("*.fingerprint"))) - - def test_a_missing_simulator_installation_is_reported(self): - with self.assertRaisesRegex(RuntimeError, "it was not found"): - foreign_application.setup_vhpi_application(Path("/out"), "rivierapro", None) - - if __name__ == "__main__": unittest.main() diff --git a/tools/build_python_bridge.py b/tools/build_python_bridge.py index 19cee4c..4ea8eed 100644 --- a/tools/build_python_bridge.py +++ b/tools/build_python_bridge.py @@ -77,7 +77,7 @@ def main(): "/Brepro", f"/I{include_dir}", f"/Fo{build_dir}\\", - *[str(path) for path in NATIVE_LIBRARY.bridge_sources()], + *[str(path) for path in NATIVE_LIBRARY.bridge_sources(None)], f"/Fe{output}", "/link", "/Brepro",