Proposal: Foreign Subprogram Binding for VHDL (Simulation / Testbench)
Motivation / Background
I found the following VUnit pull request very interesting, as it shows a practical way to invoke foreign-language code from VHDL testbenches while keeping the VHDL side readable and explicit:
- VUnit PR: https://github.com/VUnit/vunit/pull/986
- Example testbench: https://github.com/VUnit/vunit/blob/98a57f2cc8c27a26771883e9398f4c7c9dd6d990/examples/vhdl/embedded_python/tb_example.vhd
This raised the question whether a language-level, officially supported mechanism for calling foreign-language code from VHDL could be defined in a language-agnostic way.
The idea would be to standardise a VHDL-side construct that looks similar to a normal function or procedure declaration, while allowing tools to bind it to an implementation written in a foreign language. Internally, such a feature could be implemented using existing mechanisms such as VHPI, or via other tool-specific backends. If it's feasible to expand VHPI with a higher-level, typed interface for this, that would also be a nice path.
Scope
- Simulation / testbench only (non-synthesizable).
- Define a typed foreign subprogram declaration that resembles normal VHDL functions/procedures.
- Attach an open binding specification describing how the foreign implementation is resolved.
- Keep the mechanism language-agnostic and tool-neutral.
Proposed construct (illustrative)
Foreign function binding (resolving an implementation from a file)
package dsp_coeff_pkg is
type real_vector is array (natural range <>) of real;
function butterworth_lp(
fs_hz: real;
fc_hz: real;
order: natural
) return real_vector
with foreign =>
(
lang => "python",
entrypoint => "scripts/dsp_coeff.py:butterworth_lp",
phase => pre_sim,
-- Optional environment hint
env =>
(
kind => "venv",
path => "build/.venv",
req => "requirements.txt"
),
cache => by_hash
);
end package;
Foreign function binding (inline foreign code block)
This variant embeds a small foreign-language code block directly in VHDL metadata. The VHDL signature remains the authoritative interface.
package inline_demo_pkg is
type real_vector is array (natural range <>) of real;
function mk_gain_vec(
gain: real;
n: natural
) return real_vector
with foreign =>
(
lang => "python",
phase => pre_sim,
-- Inline code (illustrative)
code =>
"from typing import List\n"
& "def mk_gain_vec(gain: float, n: int) -> List[float]:\n"
& " return [gain for _ in range(n)]\n",
entrypoint => "mk_gain_vec"
);
end package;
Foreign procedure binding
package gen_pkg is
type int_vector is array (natural range <>) of integer;
procedure gen_prbs(
seed: in natural;
len: in natural;
outv: out int_vector
)
with foreign =>
(
lang => "python",
entrypoint => "scripts/prbs.py:gen_prbs",
phase => on_demand
);
end package;
Notes
-
with foreign => (...)is illustrative; an attribute or pragma-based form would also be possible. - The VHDL subprogram signature remains the authoritative interface.
Execution model (sketch)
- Foreign subprograms may execute in defined phases such as
pre_elab,pre_sim,on_demand, orpost_sim. - Execution is non-synthesizable and explicitly opt-in.
- Tools may cache results based on inputs and binding metadata.
Minimal type mapping (initial idea)
- Scalars:
integer,real,boolean,time string-
std_logic,std_logic_vector - One-dimensional arrays of scalar numerics
Example: simplistic Python implementation (file-based)
scripts/dsp_coeff.py
from typing import List
def butterworth_lp(fs_hz: float, fc_hz: float, order: int) -> List[float]:
# Minimal demonstration implementation
scale = 1.0 / max(1.0, fs_hz)
return [1.0 * scale, 2.0 * scale, 1.0 * scale]
Example: calling an existing script via relative path
VHDL binding
function calc_crc32(data: std_logic_vector) return std_logic_vector
with foreign =>
(
lang => "python",
entrypoint => "./scripts/crc32.py:calc_crc32",
phase => on_demand
);
Python script
scripts/crc32.py
import zlib
def calc_crc32(data_bits: str) -> str:
n = len(data_bits)
val = int(data_bits, 2) if n else 0
b = val.to_bytes((n + 7) // 8, byteorder="big", signed=False)
crc = zlib.crc32(b) & 0xFFFFFFFF
return format(crc, "032b")
Implementation considerations
- The standard would define only the VHDL-visible behaviour.
- Tools could implement this using:
- VHPI extensions or helper layers
- Embedded runtimes
- External processes or RPC services
- Containerized execution
- If expanding VHPI to support a higher-level, typed foreign-subprogram interface is feasible, that could be a natural implementation path.