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, or post_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.

Assignee Loading
Time tracking Loading