Add the VUnit Python bridge as a VUnit package - #1
Merged
Merged
Conversation
VHDL testbenches execute Python code and call Python functions through python_pkg (exec, eval, call with arg and kwarg, sessions, files, NumPy arrays for integer_array_t), enabled with add_python() after add_vhdl_builtins() and used through the python_context context. The implementation for NVC, GHDL and Questa/ModelSim is the Python bridge in vunit/python_bridge: a C library embedding CPython, called through VHPIDIRECT or the FLI, compiled on first use on Linux and macOS and cached under the output path, and shipped as prebuilt MSVC DLLs per CPython version on Windows. Riviera-PRO/Active-HDL use a VHPI application built the same way. Python errors are reported through the python_logger logger. NumPy becomes a dependency of VUnit, since integer_array_t values are exchanged as NumPy arrays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A testbench exercising python_pkg: executing code, evaluating expressions, calling functions with positional and keyword arguments, sessions, files, NumPy arrays, result types, error reporting, and a verification component whose behaviour is a Python function. Tests needing Python packages that VUnit does not depend on, and tests demonstrating error reporting, carry attributes so that they can be left out. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
python_bridge.yml tests the feature with CPython from python.org on Linux (NVC and every GHDL backend, with canaries on NVC built from master and the GHDL nightly builds), macOS and Windows, checks the package content, and builds the Windows DLLs with MSVC through the reusable python_bridge_dlls.yml, which the release job of push.yml also uses so that releases ship the DLLs while the repository holds no binaries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: VUnit provides join(a, b) from the path package, already in vunit_context, to concatenate path segments. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: The rest of VUnit's typed subprograms use _std_ulogic(_vector), so eval_std_logic and friends should be eval_std_ulogic. The result and parameter types become std_ulogic/std_ulogic_vector as well. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: exec_file should not resolve a relative file name against the run script directory. python_pkg resolves it explicitly against tb_path, the directory of the testbench file, like the file names of the other VUnit subprograms, and an absolute name is used as given. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: The generated arg and kwarg overloads belong in the arg and kwarg block of the package rather than in sections of their own further down. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: The p_arg_value overloads whose conversion cannot fail have no use for the name of the operation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: ieee.std_logic_1164.is_x already has the semantics of the local p_has_metavalue. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: to_call_str and call should take the arg_t arguments that arg and kwarg build, not strings the caller has converted itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: An arg or kwarg whose value cannot be converted returned null_arg, which made the call go ahead without the argument once python_logger was mocked. It now returns an argument whose Python source text raises the message that was reported, through the new __vunit__.error of the bridge runtime. to_call_str builds the call string in VHDL rather than through Python, which keeps the quotes of such a message intact. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: & should not be limited to keyword arguments. A group of positional arguments becomes *(1, 2,), a group of both kinds *(1,), **dict(a=1), and appending a positional argument to a group that already has keyword arguments is an error, like in Python. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: The comment above & and the user guide should explain what the identity is good for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: A session should be a VUnit object with an identity rather than a string. new_session(name) creates it from an identity under vunit_lib:python, name(session) gives the name back, and the errors of an operation are reported on the logger of its session. Mocking a logger does not capture what its children log, so the default session keeps reporting on python_logger itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
python_pkg and python_context are no longer part of VUnit itself: they are
installed as vunit-python-bridge and added to a project with
add_package("vunit-python-bridge"). vunit_pkg.toml names the python_bridge
library and the simulator independent sources, and the setup function does
what Builtins._add_python() did: pick the foreign language interface of the
selected simulator, build and cache the native library or the VHPI
application, generate the bridge package, add the resulting VHDL and register
the simulator hooks NVC, GHDL and Questa need to find the library.
The VHDL now compiles into python_bridge rather than vunit_lib, so it refers
to the VUnit packages through vunit_lib, the identity of the interface becomes
python_bridge:python, and a testbench gets the API with
library python_bridge;
context python_bridge.python_context;
The hooks take the simulator interface rather than the project, since a
package registers them for the project it is added to, and the package looks
up the foreign language interfaces of a simulator by name when the simulator
interface does not report them itself.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The user guide becomes the documentation of the package, built by a minimal Sphinx configuration of its own, and the README says what the package is, how to install it, how to use it, which simulators it serves and what it needs. The workflows install the package with a VUnit from the branch adding package support and run the Python unit tests, the feature tests and the example against it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VUnit now tells which foreign language interfaces a simulator supports, gives a GHDL interface a public backend and lets a package extend the vsim process it starts, so the package no longer has to work around what was missing. Ask the simulator interface for its foreign language interfaces instead of keeping a table of simulator names, and let the GHDL elaboration hook read the backend of the interface it is called with instead of determining it again. Give Questa -noautoldlibpath as a process flag rather than a run flag: the option is set up when the vsim process starts, so it is only honoured on the command line of that process, not on the vsim command of the do-file VUnit generates. Only a vsim listing the option among its own is given it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pushing a tag vX.Y.Z builds the distribution, checks what it contains, runs the tests against the wheel it built rather than the checkout, publishes it to PyPI and creates the GitHub release with the message of the tag as its body. The tag has to name the version in pyproject.toml, which tools/release.py checks before anything is built: a version published to PyPI cannot be taken back. Publishing is done by a PyPI trusted publisher, so there is no API token in the repository. Running the workflow manually does everything but publish and leaves the distribution as an artifact of the run, which is how a release is rehearsed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The example is an example of what VUnit can do with the package, not a part of the package, and it lives with the other VUnit examples. The package keeps its own tests and the README keeps the snippet showing how a run script adds it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VUnit no longer reports the foreign language interfaces of a simulator with supported_foreign_language_interfaces(), so the package decides for itself again: a table mapping the name of the selected simulator to the interface implementing python_ffi_pkg there, VHPIDIRECT for NVC and GHDL with a flavour each, the FLI for Questa/ModelSim and a VHPI application for Riviera-PRO/Active-HDL. A simulator that is not in the table is not supported and the error says which ones are. Name the simulators of bridge.py BRIDGE_SIMULATORS, since they are the subset the bridge itself serves and now sit next to the full list of the package. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The package context now carries the prefix the executables of the selected simulator were found in and, for GHDL, the backend of the installation there, and a simulator interface has a public prefix. Take them from there instead of asking the simulator class, which meant repeating find_prefix() and determine_backend() and knowing that the class has them at all. The setup functions of the bridge and of the VHPI application therefore take the simulator by name, prefix and backend rather than the class, and say so when the installation they build against was not found. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An operation now reports on the logger of the identity of the session it was performed in, get_logger(get_id(session)), the default session included. The default session used to be a special case reporting on python_logger itself, which made it the only session whose failures were caught without mocking a logger of its own. python_logger is now the logger of the parent identity of the sessions created from a name, so log levels and log handler settings made on it still apply to all of them, and it is used for what has no session: an argument value that cannot be converted and the transfer of an integer_array_t. A session also follows the conventions of the other VUnit objects, as actors do: the private p_id is now the public get_id, and new_session(id) takes an identity that already exists, which puts a session anywhere in the identity tree rather than only under python_bridge:python. The full name of the identity is the key of the Python namespace, so two sessions with the same name but different identities are namespaces of their own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VUnit now refuses to run a package's setup function unless the project opts in with allow_setup=True. The package's setup builds its native library and registers simulator options, so every add_package() call needs allow_setup=True. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ported from VUnit PR #1220 (4a3e31b8). * The package body of python_bridge_pkg is generated from the declarations of the template instead of being written by hand. * NumPy is imported unconditionally by the runtime since it is a dependency of the package, which also removes the staged value class and the runtime setup call that only flushed. * The result converters are looked up by the VHDL type name. * One compile helper serves the POSIX, Windows FLI and VHPI builds, and the Python.h check of the Windows FLI build reuses the POSIX one. The VHPI application is rebuilt without the separate staleness helper. * The foreign attribute strings are format strings rather than closures, the bridge is a named tuple and the unused parameters of bridge_sources and windows_dll_name are gone. * The C configuration parser uses a key table and the error text helpers use snprintf. * The token tests are one loop. The distribution content check of the workflow is kept as it is: the package lists its data files one pattern at a time, so every kind of file is still checked in the sdist and the wheel. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ported from VUnit PR #1220 (c73f4f66), the bridge side only. VUnit now takes the run script from the __main__ module and gives the package context a run_script_path of None when Python was not started with a script file, with python -c or in an interactive session. In that case the bridge puts the current directory first on sys.path as python -c does, and import_run_script fails with a message saying that VUnit was not started from a run script file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ported from VUnit PR #1220 (50a9d668). A failure of the setup function of the package to build the VHPI application or the Python bridge was logged and followed by sys.exit(1), ending the process of whatever called add_package(). The error is now raised with the same message, like the other errors of the setup function, and the user guide says so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ported from VUnit PR #1220 (6c700786). The Riviera-PRO/Active-HDL (VHPI) application is now in src/vunit_python_bridge/native/vhpi, so all C code of the package is under one directory. The package data, the distribution content checks of the workflows and the user guide follow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… line Ported from VUnit PR #1220 (ac6350bf). The first line of each file described a dictionary package. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ported from VUnit PR #1220 (2efe5af9). Questa on Windows often bundles no MinGW gcc, and not every Aldec installation may either. The Windows builds now use CC, else the gcc bundled with the simulator (Questa: gcc-*-mingw64*, Riviera-PRO: mingw), else gcc on PATH. NVC and GHDL also build the library with that gcc when the package has no prebuilt DLL, instead of failing. A gcc build imports the Python DLL directly, so its directory is added to PATH for the simulator processes. The Riviera-PRO/Active-HDL build could not find Python.h: it looked for include and libs next to sys.executable, which in a virtual environment has neither, and passed paths wrapped in quotes that subprocess escaped and ccomp received literally. Both builds now take the headers and the import library from the base Python installation, and ccomp gets one plain argument per path. The user guide and the README say so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ported from VUnit PR #1220 (25693bba). The bridge library and the Python DLL it embeds are loaded into the simulator process, and only 64-bit Python is supported. A 32-bit or ARM64 simulator failed late with an obscure load error. The setup function of the package now reads the machine type from the PE header of the simulator executable (nvc.exe, ghdl.exe, or vsim.exe for Questa/ModelSim and Riviera-PRO/Active-HDL) and rejects anything but x64, as it already did for Python. An executable that cannot be read is not checked. The package knows the simulator by name and prefix rather than by its VUnit class, so the check takes those, and the NVC and GHDL executable names follow the NVC and GHDL environment variables like VUnit does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ported from VUnit PR #1220 (128da0a3). A Windows job without the prebuilt DLLs builds the bridge with the MinGW gcc of the runner image and runs the feature tests with NVC. It exercises the build Questa/ModelSim always uses on Windows, for which there is no runner. Every Windows job also checks the PE machine type of the simulator (x64) and of a 32-bit executable of Windows. The embedded Python example step of the VUnit workflow has no counterpart here since the example stays in VUnit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The VHDL sources move from src/vunit_python_bridge/vhdl/src to src/vunit_python_bridge/hdl/src, the layout recommended for VUnit package repositories, and the generator of python_pkg.vhd and its template from vhdl/tools to hdl/tools next to them. vunit_pkg.toml, the package data, the setup code, the tests and the distribution content checks of the workflows follow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The package support of VUnit/vunit#1221 is merged into the master branch of VUnit/vunit, and the package works with it unchanged. CI and the release workflow install VUnit from there instead of from the branch the pull request came from, and the README and pyproject.toml say so, until the support is part of a VUnit release. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The package is released as a single pure Python wheel, tagged py3-none-any, with the five prebuilt Windows DLLs of the MSVC build job as package data, so the same wheel installs on every platform: Windows uses the DLL of its Python, Linux and macOS compile the bridge on first use. The release workflow already built it after the DLL job, with the DLLs downloaded into src/vunit_python_bridge/bin, together with the sdist, which it keeps. The distribution checks of CI and of the release workflow now look at the wheel on its own: exactly one wheel, tagged py3-none-any, holding every C and HDL source, the manifest and the Python modules, exactly the five bin/*.dll and no other binary. The sdist is checked for the same sources and DLLs. The README says what the release publishes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The built wheel is installed like a user would and tested from outside the checkout on Linux and macOS, where the bridge is compiled on first use, and on Windows with every supported Python, so each of the five prebuilt DLLs is run. The Windows jobs get nothing but Python and the simulator and check that the bridge was the DLL of the wheel and that nothing was compiled. The jobs are a reusable workflow, called by CI after the packaging job, which now uploads the distribution, and by the release workflow in place of its single GHDL smoke test, so a release is only published after the wheel passed on every system. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The homepage, the links to the C sources in the user guide and the PyPI trusted publisher name the repository the package is published from. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The one py3-none-any wheel, carrying the Windows DLLs, installs on every platform, so no sdist is built or published. The content checks cover the wheel only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tomllib is part of the standard library from Python 3.11. VUnit depends on tomli before that, and the unit tests failed to import on Python 3.10. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Apple's nm has no -D or --defined-only: list the external symbols of the dylib with -gU and drop the underscore Mach-O puts before C names. A static Python is not a framework build either, so the test faking one also clears PYTHONFRAMEWORK, which otherwise counts as a shared library on macOS. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The example was left to VUnit when the interface became a package, but VUnit does not ship it: it depends on this package, so it lives here. It is the example of VUnit/vunit#1220 using the package: the run script adds vunit-python-bridge instead of calling add_python(), and the VHDL takes the context from the python_bridge library. The README and the user guide point to it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On Linux, macOS and Windows, leaving out the tests that need optional Python packages and the ones failing by design. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ru551n
commented
Sep 23, 2026
Comment on lines
+98
to
+119
| ## Releasing | ||
|
|
||
| A release is made by tagging the commit that sets the version: | ||
|
|
||
| 1. Bump `version` in `pyproject.toml`. | ||
| 2. Commit it. | ||
| 3. Tag the commit `vX.Y.Z`, with the release notes as the tag message: | ||
| `git tag -a v0.2.0`. | ||
| 4. Push the tag: `git push origin v0.2.0`. | ||
|
|
||
| The [release workflow](.github/workflows/release.yml) then checks that the tag names the version in | ||
| `pyproject.toml`, builds the Windows DLLs, and one wheel, runs the tests against the | ||
| installed wheel with GHDL, publishes to PyPI and creates the GitHub release with the tag message as | ||
| its body. The wheel is pure Python (`py3-none-any`) and carries the five Windows DLLs, so the same | ||
| wheel installs on every platform: Windows uses the DLL of its Python, Linux and macOS compile the | ||
| bridge on first use. Running the workflow manually (`workflow_dispatch`) does everything but | ||
| publish and leaves the distribution as an artifact of the run, which is how a release is rehearsed. | ||
|
|
||
| Publishing uses a [PyPI trusted publisher](https://docs.pypi.org/trusted-publishers/), so there is | ||
| no API token in the repository. It has to be set up once on PyPI, for project | ||
| `vunit-python-bridge`: owner `VUnit`, repository `vunit-python-bridge`, workflow `release.yml`, | ||
| environment `pypi`. |
Contributor
Author
There was a problem hiding this comment.
Remove this section.
Contributor
Author
There was a problem hiding this comment.
Done: the releasing section is removed from the README in 908dca5.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
It adds vunit-python-bridge: a VUnit package that makes Python callable from VHDL. A testbench can execute Python code and call Python functions, for example a NumPy reference model, without leaving the simulation. Values cross with their VHDL types:
integer,real,string,boolean,std_ulogic,unsigned/signed, the vector types, andinteger_array_tas a NumPy array.It is the VHDL to Python interface of VUnit/vunit#1220, moved out of VUnit core into its own package on top of the package support of VUnit/vunit#1221. VUnit core keeps no part of it.
How it works
The setup function of the package, which the run script allows with
allow_setup=True:ccomp(Riviera-PRO/Active-HDL);python_bridgelibrary;--loadfor NVC, linker and library path options for GHDL,-noautoldlibpathfor Questa on Linux, and the Python DLL directory onPATHon Windows.The HDL sources are in
src/vunit_python_bridge/hdl/src.Distribution
One wheel,
py3-none-any, for every platform. It carries prebuilt MSVC DLLs of the bridge for CPython 3.10 to 3.14 on Windows. Linux and macOS compile the bridge on first use, and so does Windows with gcc when it has no DLL for the running Python, and always for Questa, whose FLI library links against the simulator. The release workflow builds the DLLs, the wheel, tests the wheel on Linux, macOS and Windows, and publishes it to PyPI from a tag through a trusted publisher.Testing
Notes
vunit-python-bridge: ownerVUnit, repositoryvunit-python-bridge, workflowrelease.yml, environmentpypi.🤖 Generated with Claude Code