Skip to content

Add the VUnit Python bridge as a VUnit package - #1

Merged
LarsAsplund merged 43 commits into
VUnit:mainfrom
ru551n:main
Sep 24, 2026
Merged

LarsAsplund merged 43 commits into
VUnit:mainfrom
ru551n:main

Conversation

@ru551n

@ru551n ru551n commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

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, and integer_array_t as 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.

vu = VUnit.from_argv()
vu.add_vhdl_builtins()
vu.add_package("vunit-python-bridge", allow_setup=True)
library python_bridge;
context python_bridge.python_context;

How it works

The setup function of the package, which the run script allows with allow_setup=True:

  • builds or selects the foreign language interface for the selected simulator: a small C library embedding CPython, called through VHPIDIRECT (NVC, GHDL) or the FLI (Questa/ModelSim), or a VHPI application built with ccomp (Riviera-PRO/Active-HDL);
  • generates the VHDL binding the library and adds it to the python_bridge library;
  • registers the simulator hooks of Let packages run a setup step and register simulator hooks vunit#1221 that load the library: --load for NVC, linker and library path options for GHDL, -noautoldlibpath for Questa on Linux, and the Python DLL directory on PATH on 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

  • CI on the fork runs the Python unit tests and the VHDL feature tests on Linux (NVC 1.22.1 and master, GHDL mcode, llvm, llvm-jit and gcc), macOS (NVC, GHDL llvm) and Windows (NVC and GHDL with the DLLs, NVC with a gcc build), and installs the wheel on all three systems.
  • Locally: the unit tests on Python 3.10 and 3.14, and the feature tests from the installed wheel on NVC, GHDL and Questa.
  • Not tested: Riviera-PRO and Active-HDL, and Questa on Windows.

Notes

🤖 Generated with Claude Code

ru551n and others added 30 commits September 23, 2026 18:04
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>
ru551n and others added 10 commits September 23, 2026 18:11
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>
ru551n and others added 2 commits September 23, 2026 19:18
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>
Comment thread README.md Outdated
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`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this section.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: the releasing section is removed from the README in 908dca5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@LarsAsplund
LarsAsplund merged commit 8e23a34 into VUnit:main Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants