Multi-Language Hierarchical Programming Interface¶
In the functional verification space, multi-environment and multi-language integration (eg SystemVerilog and C++) is performed via programming interfaces defined by the language standard (eg VPI, DPI, PSS function imports). All of these APIs leverage the C ABI for portability. The challenge is that UVM and PSS environments are hierarchical, and leverage object-oriented component hierarchies that group state (data) with operations. In contrast, existing programming interfaces are flat and don’t provide good support for capturing context. In addition, verification environments often need to qualify function capabilities – PSS solve vs target ; SystemVerilog blocking vs non-blocking. These distinctions often drive mapping decisions.
The Multi-Language Hierarchical Programming Interface (ml-hpi) defines a JSON schema for capturing hierarchical APIs, per-language bindings, and tools and language integrations.
ml-hpi is purely focused on methods, in contrast to other specifications, such as protobuf, that focus on data.
ml-hpi Schema¶
ml-hpi defines interface types, each with methods and sub-interface ‘fields’ that either point to an interface or an array of interfaces of the same type. For example:
ml-hpi:
interfaces:
- name: pkg.MyIf1
methods:
- name: foo
rtype: void
params:
- name: abc
type: int
- name: def
type: int
attr:
- solve: true
- target: false
- blocking: false
- name: pkg.MyIf2
# Could be methods here too
members:
- name: dma0
kind: field
type: pkg.MyIf1
- name: dmas
kind: array
type: pkg.MyIf1
The goal is that each language sees a hierarchical (object-oriented) interface that makes sense in the context of that language. For example, these interfaces would look like this in SystemVerilog:
package pkg;
interface class MyIf1;
pure virtual function void foo(int abc, int def);
endclass
interface class MyIf2;
pure virtual function MyIf1 dma0();
pure virtual function MyIf1 dmas_at(int idx);
pure virtual function int dmas_size();
endclass
endpackage
Note how methods are used to represent member fields and arrays to keep the API purely functional.
Likewise, a C++ representation is:
namespace pkg {
class MyIf1 {
virtual void foo(int abc, int def) = 0;
};
class MyIf2 {
virtual MyIf1 *dma0() = 0;
virtual MyIf1 *dmas_at(int idx) = 0;
virtual int dmas_size() = 0;
};
}
Interface Inheritance¶
An interface may extend a single base interface using the extends field. The
derived interface inherits all methods and members of the base.
ml-hpi:
interfaces:
- name: pkg.BaseIf
methods:
- name: init
rtype: void
- name: pkg.DerivedIf
extends: pkg.BaseIf
methods:
- name: configure
rtype: void
params:
- name: mode
type: uint32
This maps to single-inheritance abstract classes in each target language:
package pkg;
interface class BaseIf;
pure virtual function void init();
endclass
interface class DerivedIf extends BaseIf;
pure virtual function void configure(int unsigned mode);
endclass
endpackage
namespace pkg {
class BaseIf {
virtual void init() = 0;
};
class DerivedIf : public virtual BaseIf {
virtual void configure(uint32_t mode) = 0;
};
}
Multiple inheritance is not supported; each interface may have at most one extends entry.
The key idea is that any environment can export, or expose, an API of this form by providing an implementation of the proper language-specific interface classes. Because the classes are always pure virtual, these can be grafted onto existing class hierarchies. A cross-language connection can be made simply by implementing the shared interface in the client/requiring environment that calls that same interface implemented in the target/providing environment.
The following languages must be supported as both client and target environments:
C++
C
Python
SystemVerilog
PSS
Type System¶
ml-hpi defines a fixed set of built-in scalar types for method parameters and return values. All scalar types map to well-defined, aligned types in each target language. Interface types (defined in the interfaces section) are used only as sub-interface members, not as parameter or return types.
Built-in Scalar Types¶
ml-hpi type |
Description |
|---|---|
|
No value; valid as a return type only |
|
Boolean (true/false) |
|
8-bit signed / unsigned integer |
|
16-bit signed / unsigned integer |
|
32-bit signed / unsigned integer |
|
64-bit signed / unsigned integer |
|
Platform-width physical address; resolves to |
|
32-bit physical address; semantically distinct from |
|
64-bit physical address; semantically distinct from |
|
Opaque pointer-sized unsigned integer ( |
The addr32 / addr64 types are kept distinct from plain integer types so that language bindings and tools can apply address-specific handling (e.g., byte-enable generation, address space qualification, PSS addr_t mapping).
The uintptr type is intended for passing opaque handles across language boundaries where the receiving side treats the value as a pointer; the actual width is platform-dependent (32 or 64 bits).
Language Type Mappings¶
ml-hpi |
C |
C++ |
Python (plain) |
SystemVerilog |
PSS |
|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note:
addrresolves toaddr32oraddr64based on the platform address width configured at code-generation time.uintptrmaps tochandlein SystemVerilog and PSS — the standard opaque-handle type in both languages for holding C pointers — and touintptr_tin C and C++. See the Python language mapping forctypesandAnnotated[int, N]alternatives.
Schema File¶
The formal JSON Schema for ml-hpi YAML documents is defined in schema/ml-hpi.schema.json.
Interface Path¶
An interface path is a single non-negative integer that uniquely identifies any interface instance within a root interface hierarchy. Paths are a runtime concept — they are not part of the IDL schema, and they do not need to be computed until an implementation is created and registered. At that point every node in the hierarchy must be assigned a path so that a caller can navigate from root to any sub-interface using a single opaque integer handle.
Path Assignment Rules¶
Paths are assigned depth-first, left-to-right (in declaration order), using the following recursive size rules:
Node kind |
Size (slots) |
Path of node |
|---|---|---|
Leaf interface (no sub-interface members) |
1 |
Cumulative offset of preceding siblings |
|
|
Cumulative offset of preceding members |
|
1 + |
1 base-slot, then element |
A “leaf” interface is one whose members list is empty (i.e. it has only methods). Methods do not consume path slots — only sub-interface members do.
The root interface itself is not assigned a path; the caller holds it directly. Only its children are addressed via path.
Worked Example¶
Using the schema example from the Language Mappings section (BusIf with a regs field and a ports array, both of type RegIf):
RegIf → leaf interface, no sub-interface members → size = 1
BusIf layout:
regs field, type RegIf (size 1) → path 0
ports array, type RegIf (size 1)
ports base → path 1 (array base-slot)
ports[0] → path 2
ports[1] → path 3
ports[2] → path 4
ports[k] → path 1 + 1 + k = 2 + k
The original terse formulation “dmas[2] is 4 (1 for dma0 + 3 for dmas[2])” decomposes as:
1 for dma0:
regs/dma0is afieldof typeRegIf(size 1), so it consumes 1 slot (path 0) and the array base begins at offset 1.3 for dmas[2]: array base-slot (path 1) + index 2 × size(RegIf) = 1 + 2 = 3 slots into the array section → path 4.
Nested Composite Example¶
MemCtrlIf members:
dma0 field, type BusIf (size = 1 + 1 + 1 + ... = see below)
uart field, type BusIf
size(BusIf) = size(regs) + 1(array base) + size(ports[0..n-1])
= 1 + 1 + n×1 (for n array elements at runtime)
Because array size is dynamic, the total size of a composite interface containing arrays can only be determined at runtime, once the number of array elements is known. This is why path assignment happens when constructing the implementation object, not at code-generation time.
Implementation Use¶
When a language binding creates a concrete implementation of a root interface, it typically:
Allocates a flat array of interface pointers sized to the total path space.
Traverses the hierarchy depth-first, populating each slot with a pointer to the corresponding sub-interface object.
Stores the array alongside the root handle so that any path lookup is an O(1) index into the array.
The C binding, for example, can store this as:
typedef struct {
pkg_BusIf_t root; /* the root interface struct */
void *path_table[]; /* flat array, indexed by path */
} pkg_BusIf_impl_t;
In Python and C++ the same concept applies using a list or std::vector<void*> respectively. The implementation populates the table once during construction; callers then use a path integer to retrieve the right sub-interface without re-traversing the tree.
Language Mappings¶
All five languages are supported as both client (caller) and target (implementor) environments. The sections below specify how each ml-hpi construct maps to language-specific idioms. The worked examples all use the following interface definition as a reference:
ml-hpi:
interfaces:
- name: pkg.RegIf
methods:
- name: write32
rtype: void
params:
- name: addr
type: addr
- name: data
type: uint32
attr:
- target: true
- blocking: true
- name: read32
rtype: uint32
params:
- name: addr
type: addr
attr:
- target: true
- blocking: true
- name: pkg.BusIf
members:
- name: regs
kind: field
type: pkg.RegIf
- name: ports
kind: array
type: pkg.RegIf
- name: pkg.ExtRegIf
extends: pkg.RegIf
methods:
- name: reset
rtype: void
attr:
- target: true
- blocking: false
SystemVerilog¶
Interface Definition¶
ml-hpi interfaces map to interface class declarations inside a package named after the ml-hpi package prefix. Interface classes carry no state; all methods are pure virtual.
Method Mapping¶
Method attribute |
SV construct |
Notes |
|---|---|---|
|
|
Returns value directly |
|
|
Task with no output args |
|
|
Non-void return becomes |
Member Mapping¶
Member kind |
SV construct |
|---|---|
|
|
|
|
Inheritance¶
interface class DerivedIf extends BaseIf;
Example¶
package pkg;
interface class RegIf;
// blocking task, non-void return → output rval first
pure virtual task write32(input longint unsigned addr, input int unsigned data);
pure virtual task read32(output int unsigned rval, input longint unsigned addr);
endclass
interface class BusIf;
pure virtual function RegIf regs(); // field
pure virtual function RegIf ports_at(int idx); // array
pure virtual function int ports_size();
endclass
interface class ExtRegIf extends RegIf;
pure virtual function void reset(); // non-blocking
endclass
endpackage
As a C-callable target environment¶
When SystemVerilog is the target (implementor) and C/C++ is the client (caller), the DPI export/import mechanism bridges the two environments. Because DPI is flat, the hierarchical interface structure is encoded in two extra arguments prepended to every exported function: a root instance ID and an interface path.
Overview¶
C caller SystemVerilog target
--------- ----------------------
pkg_RegIf_write32(root, path, ...) → export dispatches via path table
→ forks task (blocking method)
pkg_RegIf_write32_complete(cb,...) ← import called on task completion
Registration is a pure SV concern. A generated Root class per interface type owns the instance registry. The user calls BusIfRoot::register(impl) from their testbench and receives a root_id, which they pass to C through whatever mechanism suits their flow (plusarg, VPI, a user-written DPI export, etc.).
Naming Convention¶
All generated DPI identifiers follow the pattern:
Purpose |
Name pattern |
DPI direction |
|---|---|---|
Method call (non-blocking) |
|
|
Method call (blocking) |
|
|
Blocking completion |
|
|
Package dots are replaced with underscores: pkg.sub → pkg_sub_....
Root registration has no DPI identifier — it is handled entirely within SV.
The Root Class¶
For each interface type that may serve as a root, a {IfName}Root class is generated. It carries a static registry mapping integer IDs to interface instances and provides a single static method:
class BusIfRoot;
// Static registry: root_id → implementation handle
static pkg::BusIf __registry[int];
static int __next_id = 0;
// Register an implementation; returns a root_id for use by C callers
static function int register(pkg::BusIf impl);
int id = __next_id++;
__registry[id] = impl;
return id;
endfunction
// Internal: look up a registered instance
static function pkg::BusIf get(int root_id);
return __registry[root_id];
endfunction
endclass
The user instantiates their concrete implementation class (which implements pkg::BusIf) and calls BusIfRoot::register() from an initial block:
initial begin
automatic my_bus_impl impl = new();
automatic int root_id = BusIfRoot::register(impl);
// Pass root_id to C — user chooses how (plusarg, VPI, user DPI export, etc.)
$display("bus root_id = %0d", root_id);
end
BusIfRoot has no inheritance relationship with the interface class itself; it is a standalone utility class that only holds the registry.
Function Arguments¶
Every DPI export takes root_id and path as its first two parameters, followed by method parameters in declaration order. For blocking methods a chandle cb completion token is appended as the last parameter.
// Non-blocking:
export "DPI-C" function {pkg}_{If}_{method};
function automatic {rtype} {pkg}_{If}_{method}(
int root_id,
int path,
{param0_type} {param0},
...
);
// Blocking:
export "DPI-C" function {pkg}_{If}_{method};
function automatic void {pkg}_{If}_{method}(
int root_id,
int path,
{param0_type} {param0},
...,
chandle cb // completion token passed back to C on finish
);
Blocking DPI exports are always declared as function (not task) so that they return immediately to C — the actual task is forked internally.
Blocking Completion Imports¶
For each blocking method, C provides an import that SV calls when the forked task finishes:
// void return:
void pkg_RegIf_write32_complete(void *cb);
// non-void return — result passed as final argument:
void pkg_RegIf_read32_complete(void *cb, uint32_t rval);
// Generated imports
import "DPI-C" function void pkg_RegIf_write32_complete(chandle cb);
import "DPI-C" function void pkg_RegIf_read32_complete(chandle cb, int unsigned rval);
Complete Generated Example¶
Using the pkg.RegIf / pkg.BusIf schema from the Language Mappings reference:
// ---------------------------------------------------------------
// Generated Root class for pkg::BusIf
// ---------------------------------------------------------------
class pkg_BusIfRoot;
static pkg::BusIf __registry[int];
static int __next_id = 0;
static function int register(pkg::BusIf impl);
int id = __next_id++;
__registry[id] = impl;
return id;
endfunction
static function pkg::BusIf get(int root_id);
return __registry[root_id];
endfunction
endclass
// ---------------------------------------------------------------
// Generated DPI package for pkg
// ---------------------------------------------------------------
package pkg_dpi;
// Imports — C provides these (one per blocking method)
import "DPI-C" function void pkg_RegIf_write32_complete(chandle cb);
import "DPI-C" function void pkg_RegIf_read32_complete(chandle cb,
int unsigned rval);
// Navigation helper
function automatic pkg::RegIf navigate(int root_id, int path);
automatic pkg::BusIf root = pkg_BusIfRoot::get(root_id);
if (path == 0) return root.regs();
path -= 1;
path -= 1;
return root.ports_at(path);
endfunction
// Non-blocking: ExtRegIf::reset
export "DPI-C" function pkg_ExtRegIf_reset;
function automatic void pkg_ExtRegIf_reset(int root_id, int path);
automatic pkg::ExtRegIf iface = pkg::ExtRegIf'(navigate(root_id, path));
iface.reset();
endfunction
// Blocking: RegIf::write32 (rtype void)
export "DPI-C" function pkg_RegIf_write32;
function automatic void pkg_RegIf_write32(
int root_id, int path,
longint unsigned addr,
int unsigned data,
chandle cb
);
automatic pkg::RegIf iface = navigate(root_id, path);
fork
begin
iface.write32(addr, data);
pkg_RegIf_write32_complete(cb);
end
join_none
endfunction
// Blocking: RegIf::read32 (rtype uint32)
export "DPI-C" function pkg_RegIf_read32;
function automatic void pkg_RegIf_read32(
int root_id, int path,
longint unsigned addr,
chandle cb
);
automatic pkg::RegIf iface = navigate(root_id, path);
fork
begin
automatic int unsigned rval;
iface.read32(rval, addr);
pkg_RegIf_read32_complete(cb, rval);
end
join_none
endfunction
endpackage
// ---------------------------------------------------------------
// User testbench: instantiate impl and register it
// ---------------------------------------------------------------
module tb;
initial begin
automatic my_bus_impl impl = new();
automatic int root_id = pkg_BusIfRoot::register(impl);
// hand root_id to C — e.g. via a user-written DPI export or plusarg
$display("pkg::BusIf root_id = %0d", root_id);
end
endmodule
C-Side Usage¶
C receives the root_id out-of-band (the mechanism is user-defined) and then calls the DPI exports directly:
#include <stdint.h>
/* Provided by the generated C header (declarations only — SV exports these) */
extern void pkg_RegIf_write32(int root_id, int path,
uint64_t addr, uint32_t data, void *cb);
extern void pkg_RegIf_read32 (int root_id, int path,
uint64_t addr, void *cb);
/* State for a pending async call */
typedef struct { int done; uint32_t result; } pending_t;
/* Completion imports — C provides, SV calls when tasks finish */
void pkg_RegIf_write32_complete(void *cb) {
((pending_t *)cb)->done = 1;
}
void pkg_RegIf_read32_complete(void *cb, uint32_t rval) {
pending_t *p = (pending_t *)cb;
p->result = rval;
p->done = 1;
}
/* Example: synchronous-style blocking read using spin-wait */
uint32_t bus_read32(int root_id, uint64_t addr) {
pending_t p = {0, 0};
pkg_RegIf_read32(root_id, /*path=regs*/ 0, addr, &p);
while (!p.done) { /* yield to scheduler */ }
return p.result;
}
Summary of DPI Constraints¶
Constraint |
Consequence |
|---|---|
DPI exports cannot be tasks at the C boundary |
Blocking methods always export as |
DPI has no OOP |
Hierarchy encoded via |
Registration is SV-only |
No C import for registration; |
|
C can pass any pointer as |
|
C receives control immediately; completion arrives asynchronously via import |
Thread safety |
SV is single-threaded within a time-step; concurrent blocking calls are serialized by the simulator scheduler |
C++¶
Interface Definition¶
ml-hpi interfaces map to C++ abstract classes (all methods = 0) inside a namespace corresponding to the package prefix. Headers should include <cstdint> and <functional>.
Method Mapping¶
Method attribute |
C++ form |
Notes |
|---|---|---|
|
|
Returns value directly |
|
|
Blocks internally; implementation may use pthread, coroutine, etc. |
|
|
Callback-based; return value delivered via |
Both sync and async blocking forms may be generated; the choice is a code-generation policy. The async form is omitted when rtype is void.
Member Mapping¶
Member kind |
C++ construct |
|---|---|
|
|
|
|
Inheritance¶
class DerivedIf : public virtual BaseIf { ... };
Virtual inheritance is used to support diamond-safe composition.
Example¶
#include <cstdint>
#include <functional>
namespace pkg {
class RegIf {
public:
virtual ~RegIf() = default;
// blocking – sync form
virtual void write32(uintptr_t addr, uint32_t data) = 0;
virtual uint32_t read32(uintptr_t addr) = 0;
// blocking – async form
virtual void write32(uintptr_t addr, uint32_t data,
std::function<void()> cb) = 0;
virtual void read32(uintptr_t addr,
std::function<void(uint32_t)> cb) = 0;
};
class BusIf {
public:
virtual ~BusIf() = default;
virtual RegIf *regs() = 0; // field
virtual RegIf *ports_at(int idx) = 0; // array
virtual int ports_size() = 0;
};
class ExtRegIf : public virtual RegIf {
public:
virtual void reset() = 0; // non-blocking
};
} // namespace pkg
C¶
Interface Definition¶
ml-hpi interfaces map to C structs containing function pointers. Each function pointer takes void *self as its first argument to carry the implementation context. Sub-interface field members are direct struct pointers; array members use _at / _size function pointers. Type names follow the pattern {pkg}_{Name}_t (package dots replaced by underscores).
Method Mapping¶
All methods become function pointers with void *self as the first parameter:
rtype (*method_name)(void *self, params...);
Blocking methods block internally; there is no separate async form in the C binding.
Member Mapping¶
Member kind |
C struct member |
|---|---|
|
|
|
|
Inheritance¶
The derived struct embeds the base struct as its first member, enabling safe upcasting via pointer cast:
typedef struct pkg_DerivedIf_s {
pkg_BaseIf_t base; /* MUST be first */
...
} pkg_DerivedIf_t;
Example¶
#include <stdint.h>
/* Forward declarations */
typedef struct pkg_RegIf_s pkg_RegIf_t;
typedef struct pkg_BusIf_s pkg_BusIf_t;
typedef struct pkg_ExtRegIf_s pkg_ExtRegIf_t;
typedef struct pkg_RegIf_s {
void (*write32)(void *self, uintptr_t addr, uint32_t data);
uint32_t (*read32) (void *self, uintptr_t addr);
} pkg_RegIf_t;
typedef struct pkg_BusIf_s {
pkg_RegIf_t *regs; /* field */
pkg_RegIf_t *(*ports_at) (void *self, int idx); /* array */
int (*ports_size)(void *self);
} pkg_BusIf_t;
typedef struct pkg_ExtRegIf_s {
pkg_RegIf_t base; /* MUST be first */
void (*reset)(void *self);
} pkg_ExtRegIf_t;
Python¶
Interface Definition¶
ml-hpi interfaces map to typing.Protocol classes. Protocols use structural subtyping — any class implementing the required methods satisfies the interface without explicit subclassing.
Three type representation styles are supported for parameters and return values:
Style |
When to use |
|---|---|
Plain ( |
Simplest; no width or signedness information |
|
When the implementation interacts with ctypes-based FFI |
|
Lightweight width hint usable by code-generation tools |
For the Annotated style, the metadata integer N is the bit width. Signedness is not encoded in the annotation itself — use named type aliases (e.g. Int32, UInt32) to preserve that distinction. Both resolve to the same Annotated[int, N] at runtime.
Type Mapping¶
ml-hpi |
ctypes |
Annotated |
Plain |
|---|---|---|---|
|
— |
— |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note:
addrresolves at code-generation time based on platform address width.uintptrusesctypes.c_void_pin both ctypes and Annotated styles, as it is an opaque pointer — not a general-purpose integer.
Method Mapping¶
Method attribute |
Python form |
|---|---|
|
|
|
|
Member Mapping¶
Member kind |
Python construct |
|---|---|
|
|
|
|
Inheritance¶
class DerivedIf(BaseIf, typing.Protocol):
...
Example¶
All three styles generate equivalent Protocols. The Annotated aliases can be shared from a generated ml_hpi_types module.
Plain style:
from __future__ import annotations
import typing
class RegIf(typing.Protocol):
async def write32(self, addr: int, data: int) -> None: ...
async def read32(self, addr: int) -> int: ...
class BusIf(typing.Protocol):
def regs(self) -> RegIf: ...
def ports_at(self, idx: int) -> RegIf: ...
def ports_size(self) -> int: ...
class ExtRegIf(RegIf, typing.Protocol):
def reset(self) -> None: ...
Annotated style:
from __future__ import annotations
import typing
from typing import Annotated
# Named aliases — same Annotated[int, N] repr, distinct names for signed/unsigned
Addr = Annotated[int, 64] # platform-width; 32 on 32-bit targets
UInt32 = Annotated[int, 32]
class RegIf(typing.Protocol):
async def write32(self, addr: Addr, data: UInt32) -> None: ...
async def read32(self, addr: Addr) -> UInt32: ...
class BusIf(typing.Protocol):
def regs(self) -> RegIf: ...
def ports_at(self, idx: int) -> RegIf: ...
def ports_size(self) -> int: ...
class ExtRegIf(RegIf, typing.Protocol):
def reset(self) -> None: ...
ctypes style:
from __future__ import annotations
import ctypes
import typing
class RegIf(typing.Protocol):
async def write32(self, addr: ctypes.c_uint64, data: ctypes.c_uint32) -> None: ...
async def read32(self, addr: ctypes.c_uint64) -> ctypes.c_uint32: ...
class BusIf(typing.Protocol):
def regs(self) -> RegIf: ...
def ports_at(self, idx: int) -> RegIf: ...
def ports_size(self) -> int: ...
class ExtRegIf(RegIf, typing.Protocol):
def reset(self) -> None: ...
PSS¶
Interface Definition¶
ml-hpi interfaces map to PSS component types inside a package. Sub-interface members are declared as component sub-instances or arrays. Methods are declared as function prototypes; the solve and target attributes determine the execution context in which they may be called.
Method Mapping¶
Attributes |
PSS construct |
Notes |
|---|---|---|
|
|
Callable from action solve/pre-solve context |
|
|
Callable from exec/target context only |
|
|
Callable from both contexts |
Member Mapping¶
Member kind |
PSS construct |
|---|---|
|
Component sub-instance: |
|
Component array: |
Inheritance¶
component pkg::DerivedIf : pkg::BaseIf {
...
};
Type Notes¶
addr→addr_t(PSS native platform-width address type)addr32/addr64→addr_tannotated with explicit width via tool configurationuintptr→chandle(PSS 2.0 opaque handle type)
Example¶
package pkg;
component RegIf {
// target:true → exec/target context
target function void write32(addr_t addr, bit<32> data);
target function bit<32> read32(addr_t addr);
};
component BusIf {
RegIf regs; // field
array<RegIf,*> ports; // array
};
component ExtRegIf : RegIf {
// non-blocking, target context
target function void reset();
};
endpackage
Comparative Analysis: Existing Tools and Approaches¶
Overview¶
The core problem ml-hpi addresses — a language-neutral, schema-driven IDL for hierarchical, method-focused cross-language APIs spanning C, C++, Python, SystemVerilog, and PSS — is not fully solved by any single existing tool. The landscape is covered by several partial solutions, each with significant gaps.
SystemVerilog DPI (Direct Programming Interface)¶
DPI is the industry-standard mechanism for calling C/C++ from SystemVerilog and vice versa. It is flat (no hierarchy), requires manual binding code, and only bridges SV ↔ C/C++. Python and PSS have no native DPI support. There is no schema or IDL; every interface must be hand-coded. DPI forms the low-level transport layer that higher-level tools build upon.
Gap vs ml-hpi: Flat API only, no hierarchy, no PSS, no Python, no schema, no method attributes.
VPI (Verilog Procedural Interface)¶
VPI provides C-level callback and introspection access to the simulation hierarchy at runtime. It is event-driven and focused on signal-level and hierarchy access rather than method-call APIs. Tools like cocotb use VPI to drive simulation from Python, but at the signal level rather than the interface/method level.
Gap vs ml-hpi: Signal-level, not method-API level. No PSS. No schema.
pysv¶
pysv allows Python classes and functions to be compiled (via pybind11) into a shared library and exposed to SystemVerilog as DPI-callable objects. It supports object-oriented Python ↔ SV bindings and preserves class hierarchies. The Python class is the de-facto “schema.”
Gap vs ml-hpi: Python-only source language (Python is the IDL). No C target. No PSS support. No method attributes (solve/target, blocking/non-blocking). Unidirectional design pattern (Python implements, SV calls).
PyHDL-IF¶
PyHDL-IF (also from fvutils) is the closest existing tool. It uses Python decorators (@exp, @imp) to define APIs that are exported to or imported from SystemVerilog, with automatic SV wrapper generation. It supports bidirectional calling (Python → SV and SV → Python) and generates hierarchical interface classes. It works at the API/method level rather than the signal level.
Gap vs ml-hpi: Python is the IDL (not a language-neutral schema). C is not a supported target language. PSS is not a first-class target. Method attributes specific to PSS (solve/target) and SV (blocking/non-blocking) are not represented. The schema lives in Python source code, making it inaccessible to non-Python tooling.
SWIG (Simplified Wrapper and Interface Generator)¶
SWIG generates bindings from C/C++ headers to Python, Java, Tcl, and other scripting languages. It handles class hierarchies and inheritance. The SWIG .i interface file is a schema of sorts, but is C/C++-centric.
Gap vs ml-hpi: No SystemVerilog or PSS support. C/C++ is always the canonical source; other languages are consumers. No method attributes. No EDA-specific concepts.
Apache Thrift / Protocol Buffers / Cap’n Proto¶
These IDL-based tools define cross-language data schemas and RPC interfaces. They generate bindings for many languages (C++, Python, Java, etc.) from a neutral schema. However, they are designed for network/process-boundary communication, focus on serializable data types, and have no concept of EDA languages (SystemVerilog, PSS) or verification-specific method attributes.
Gap vs ml-hpi: No SystemVerilog or PSS support. Focused on data serialization rather than in-process method dispatch. IPC/RPC overhead is inappropriate for tight simulation integration. No method attributes for verification semantics.
Comparative Summary¶
Feature |
DPI |
pysv |
PyHDL-IF |
SWIG |
Thrift/Protobuf |
ml-hpi |
|---|---|---|---|---|---|---|
Language-neutral schema/IDL |
✗ |
✗ |
✗ |
Partial (C-centric) |
✓ |
✓ |
Hierarchical (OOP) interfaces |
✗ |
✓ |
✓ |
✓ |
✗ |
✓ |
C++ client & target |
✓ |
Partial |
Partial |
✓ |
✓ |
✓ |
C client & target |
✓ |
✗ |
✗ |
✓ |
✓ |
✓ |
Python client & target |
✗ |
Partial |
✓ |
✓ |
✓ |
✓ |
SystemVerilog client & target |
✓ |
✓ |
✓ |
✗ |
✗ |
✓ |
PSS client & target |
✗ |
✗ |
✗ |
✗ |
✗ |
✓ |
PSS solve/target attributes |
✗ |
✗ |
✗ |
✗ |
✗ |
✓ |
SV blocking/non-blocking attrs |
✗ |
✗ |
✗ |
✗ |
✗ |
✓ |
Method-focused (not data) |
✓ |
✓ |
✓ |
✓ |
✗ |
✓ |
Conclusion¶
No existing tool satisfies all of ml-hpi’s requirements. PyHDL-IF is the closest in spirit — bidirectional, method-focused, hierarchical, SV+Python — but uses Python source code as its implicit IDL rather than a portable schema, and lacks C and PSS support. DPI and pysv address subsets of the language matrix. General-purpose IDL tools (SWIG, Thrift, Protobuf) cover more languages but have no EDA language support and no verification-specific method semantics.
ml-hpi’s unique contribution is a language-neutral YAML/JSON schema that captures hierarchical method interfaces with verification-specific attributes, from which correct per-language bindings can be generated for all five environments (C, C++, Python, SystemVerilog, PSS) as both client and target.