12from pathlib
import Path
13from typing
import Dict, List, Optional, Callable, IO, Union
16_thisdir = Path(__file__).parent
17CosimCollateralDir = _thisdir
18_SUPPORTED_SIMULATORS = (
"verilator",
"questa")
22 """Check if a TCP port is open locally."""
23 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
24 result = sock.connect_ex((
'127.0.0.1', port))
26 return True if result == 0
else False
30 """Return the simulator backends known to the ESI cosim runtime."""
31 return list(_SUPPORTED_SIMULATORS)
34def is_simulator_available(name: str) -> bool:
35 """Return True if the requested simulator backend is usable.
37 This checks the executable and environment conventions used by the Python
38 cosim backends, so pytest callers can skip simulator-backed tests before
39 generating or compiling hardware collateral.
42 if name ==
"verilator":
43 from .verilator
import Verilator
44 if Verilator._find_verilator_bin()
is None:
46 return Verilator._find_verilator_root()
is not None
48 return shutil.which(
"vsim")
is not None
49 raise ValueError(f
"Unknown simulator: {name}")
53 """Return the known simulator backends available in this environment."""
55 name
for name
in _SUPPORTED_SIMULATORS
if is_simulator_available(name)
63 self.user: List[Path] = []
65 self.dpi_so: List[str] = [
"EsiCosimDpiServer"]
67 self.dpi_sv: List[Path] = [
68 CosimCollateralDir /
"Cosim_DpiPkg.sv",
69 CosimCollateralDir /
"Cosim_Endpoint.sv",
70 CosimCollateralDir /
"Cosim_CycleCount.sv",
71 CosimCollateralDir /
"Cosim_Manifest.sv",
77 """Add a single RTL file to the source list."""
81 raise FileNotFoundError(f
"File {file} does not exist")
84 """Add all the RTL files in a directory to the source list."""
85 for file
in sorted(dir.iterdir()):
86 if file.is_file()
and (file.suffix ==
".sv" or file.suffix ==
".v"):
92 """Return a list of all the DPI shared object files (the loadable
93 artifact: ``.so`` on POSIX, ``.dll`` on Windows)."""
94 return [self.
_find_dpi(name, link=
False)
for name
in self.dpi_so]
97 """Return a list of files to pass to the linker for the DPI libraries.
98 On POSIX this is the same as ``dpi_so_paths()``; on Windows it is the
99 import library (``.lib``) sitting next to the DLL."""
100 return [self.
_find_dpi(name, link=
True)
for name
in self.dpi_so]
103 is_windows = os.name ==
"nt"
105 def check_path(p: Path) -> Optional[Path]:
107 suffix =
".lib" if link
else ".dll"
108 cand = p / f
"{name}{suffix}"
110 cand = p / f
"lib{name}.so"
111 return cand
if cand.exists()
else None
113 env = Simulator.get_env()
115 search_env = env.get(
"PATH",
"")
118 search_env = env.get(
"LD_LIBRARY_PATH",
"")
120 for path
in search_env.split(env_sep):
123 p = check_path(Path(path))
132 _thisdir.parent /
"lib",
133 _thisdir.parent.parent /
"lib",
135 p = check_path(candidate)
139 suffix = (
".lib" if link
else ".dll")
if is_windows
else ".so"
140 raise FileNotFoundError(f
"Could not find {name}{suffix}")
144 """Return a list of all the RTL source files."""
145 return self.dpi_sv + self.user
151 proc: subprocess.Popen,
153 threads: Optional[List[threading.Thread]] =
None,
157 self.threads: List[threading.Thread] = threads
or []
161 """Make sure to stop the simulation no matter what."""
167 self.
proc.send_signal(signal.CTRL_BREAK_EVENT)
168 except (OSError, ValueError):
171 os.killpg(os.getpgid(self.
proc.pid), signal.SIGINT)
174 self.
proc.wait(timeout=1.0)
175 except subprocess.TimeoutExpired:
180 for t
in self.threads:
186 CompileCommand = List[str]
187 CompileFunction = Callable[[], Optional[int]]
188 CompileStep = Union[CompileCommand, CompileFunction]
196 sources: SourceFiles,
199 save_waveform: bool =
False,
200 run_stdout_callback: Optional[Callable[[str],
None]] =
None,
201 run_stderr_callback: Optional[Callable[[str],
None]] =
None,
202 compile_stdout_callback: Optional[Callable[[str],
None]] =
None,
203 compile_stderr_callback: Optional[Callable[[str],
None]] =
None,
204 make_default_logs: bool =
True,
205 macro_definitions: Optional[Dict[str, str]] =
None):
206 """Simulator base class.
208 Optional sinks can be provided for capturing output. If not provided,
209 the simulator will write to log files in `run_dir`.
212 sources: SourceFiles describing RTL/DPI inputs.
213 run_dir: Directory where build/run artifacts are placed.
214 debug: Enable cosim debug mode.
215 save_waveform: When True and debug=True, dump simulator waveforms to a
216 waveform file. The exact format depends on the backend (e.g. FST for
217 Verilator, VCD for Questa). Requires debug to be enabled.
218 run_stdout_callback: Line-based callback for runtime stdout.
219 run_stderr_callback: Line-based callback for runtime stderr.
220 compile_stdout_callback: Line-based callback for compile stdout.
221 compile_stderr_callback: Line-based callback for compile stderr.
222 make_default_logs: If True and corresponding callback is not supplied,
223 create log file and emit via internally-created callback.
224 macro_definitions: Optional dictionary of macro definitions to be defined
234 self._default_files: List[IO[str]] = []
236 def _ensure_default(cb: Optional[Callable[[str],
None]], filename: str):
237 """Return (callback, file_handle_or_None) with optional file creation.
240 * If a callback is provided, return it unchanged with no file.
241 * If no callback and make_default_logs is False, return (None, None).
242 * If no callback and make_default_logs is True, create a log file and
243 return a writer callback plus the opened file handle.
247 if not make_default_logs:
250 p.parent.mkdir(parents=
True, exist_ok=
True)
252 self._default_files.
append(logf)
254 def _writer(line: str, _lf=logf):
255 _lf.write(line +
"\n")
262 compile_stdout_callback,
'compile_stdout.log')
264 compile_stderr_callback,
'compile_stderr.log')
266 run_stdout_callback,
'sim_stdout.log')
268 run_stderr_callback,
'sim_stderr.log')
272 """Get the environment variables to locate shared objects."""
274 env = os.environ.copy()
278 str(_thisdir.parent),
279 str(_thisdir.parent /
"lib"),
280 str(_thisdir.parent.parent /
"lib"),
287 env[
"PATH"] = sep.join(lib_dirs) + sep + env.get(
"PATH",
"")
289 env[
"LIBRARY_PATH"] = env.get(
"LIBRARY_PATH",
290 "") +
":" +
":".join(lib_dirs)
291 env[
"LD_LIBRARY_PATH"] = env.get(
"LD_LIBRARY_PATH",
292 "") +
":" +
":".join(lib_dirs)
296 """Return the compile steps for the simulator.
298 Each step may be either a shell command (`List[str]`) or a Python callback
299 (`Callable[[], Optional[int]]`). Python callbacks should return `0` or
300 `None` on success and a non-zero integer on failure.
302 assert False,
"Must be implemented by subclass"
306 env=Simulator.get_env(),
311 if isinstance(ret, int)
and ret != 0:
312 print(
"====== Compilation failure")
320 print(stdout_content)
325 print(stderr_content)
327 return ret
if isinstance(ret, int)
else 1
334 if not isinstance(ret, int):
335 raise TypeError(
"compile step callback must return int or None")
341 self.
run_dir.mkdir(parents=
True, exist_ok=
True)
349 """Return the command to run the simulation."""
350 assert False,
"Must be implemented by subclass"
354 """File extension for waveform dumps.
356 Subclasses should override if their format differs. The Verilator C++
357 driver writes FST (via ``VerilatedFstC``); the generic SV driver uses
358 ``$dumpfile/$dumpvars`` which produces VCD.
362 def run_proc(self, gui: bool =
False) -> SimProcess:
363 """Run the simulation process. Returns the Popen object and the port which
364 the simulation is listening on.
366 If user-provided stdout/stderr sinks were supplied in the constructor,
367 they are used. Otherwise, log files are created in `run_dir`.
369 self.
run_dir.mkdir(parents=
True, exist_ok=
True)
371 env_gui = os.environ.get(
"COSIM_GUI",
"0")
377 portFileName = self.
run_dir /
"cosim.cfg"
378 if os.path.exists(portFileName):
379 os.remove(portFileName)
382 simEnv = Simulator.get_env()
385 simEnv[
"COSIM_DEBUG_FILE"] = str(debug_file)
386 if "DEBUG_PERIOD" not in simEnv:
388 simEnv[
"DEBUG_PERIOD"] =
"1"
390 waveform_file = (self.
run_dir /
391 f
"cosim_waveform{self.waveform_extension}").
resolve()
392 simEnv[
"SAVE_WAVE"] = str(waveform_file)
405 while (
not os.path.exists(portFileName))
and \
409 if checkCount > 500
and not gui:
410 raise Exception(f
"Cosim never wrote cfg file: {portFileName}")
413 portFile = open(portFileName,
"r")
414 for line
in portFile.readlines():
415 m = re.match(
"port: (\\d+)", line)
417 port = int(m.group(1))
423 return SimProcess(proc=proc, port=port, threads=threads, gui=gui)
426 self, cmd: List[str], env: Optional[Dict[str, str]], cwd: Optional[Path],
427 stdout_cb: Optional[Callable[[str],
428 None]], stderr_cb: Optional[Callable[[str],
430 wait: bool) -> int | tuple[subprocess.Popen, List[threading.Thread]]:
431 """Start a subprocess and stream its stdout/stderr to callbacks.
433 If wait is True, blocks until process completes and returns its exit code.
434 If wait is False, returns the Popen object (threads keep streaming).
436 if os.name ==
"posix":
437 proc = subprocess.Popen(cmd,
438 stdout=subprocess.PIPE,
439 stderr=subprocess.PIPE,
443 preexec_fn=os.setsid)
445 proc = subprocess.Popen(cmd,
446 stdout=subprocess.PIPE,
447 stderr=subprocess.PIPE,
451 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
453 def _reader(pipe, cb):
457 if raw.endswith(
'\n'):
462 except Exception
as e:
463 print(f
"Exception in simulator output callback: {e}")
465 threads: List[threading.Thread] = [
466 threading.Thread(target=_reader,
467 args=(proc.stdout, stdout_cb),
469 threading.Thread(target=_reader,
470 args=(proc.stderr, stderr_cb),
484 server_only: bool =
False) -> int:
485 """Start the simulation then run the command specified. Kill the simulation
486 when the command exits."""
496 f
"Running in server-only mode on port {simProc.port} - Press anything to kill the server..."
501 testEnv = os.environ.copy()
502 testEnv[
"ESI_COSIM_PORT"] = str(simProc.port)
503 testEnv[
"ESI_COSIM_HOST"] =
"localhost"
504 ret = subprocess.run(inner_command, cwd=os.getcwd(),
505 env=testEnv).returncode
507 print(
"GUI mode - waiting for simulator to exit...")
511 if simProc
and simProc.proc.poll()
is None:
515def get_simulator(name: str,
516 sources: SourceFiles,
519 save_waveform: bool =
False) -> Simulator:
521 if name ==
"verilator":
522 from .verilator
import Verilator
523 return Verilator(sources, rundir, debug, save_waveform=save_waveform)
524 elif name ==
"questa":
525 from .questa
import Questa
526 return Questa(sources, rundir, debug, save_waveform=save_waveform)
528 raise ValueError(f
"Unknown simulator: {name}")
static void print(TypedAttr val, llvm::raw_ostream &os)
static mlir::Operation * resolve(Context &context, mlir::SymbolRefAttr sym)
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
__init__(self, subprocess.Popen proc, int port, Optional[List[threading.Thread]] threads=None, bool gui=False)
int|tuple[subprocess.Popen, List[threading.Thread]] _start_process_with_callbacks(self, List[str] cmd, Optional[Dict[str, str]] env, Optional[Path] cwd, Optional[Callable[[str], None]] stdout_cb, Optional[Callable[[str], None]] stderr_cb, bool wait)
__init__(self, SourceFiles sources, Path run_dir, bool debug, bool save_waveform=False, Optional[Callable[[str], None]] run_stdout_callback=None, Optional[Callable[[str], None]] run_stderr_callback=None, Optional[Callable[[str], None]] compile_stdout_callback=None, Optional[Callable[[str], None]] compile_stderr_callback=None, bool make_default_logs=True, Optional[Dict[str, str]] macro_definitions=None)
int run(self, str inner_command, bool gui=False, bool server_only=False)
List[CompileStep] compile_commands(self)
int _run_compile_command(self, CompileCommand cmd)
str waveform_extension(self)
int _run_compile_step(self, CompileStep step)
List[str] run_command(self, bool gui)
SimProcess run_proc(self, bool gui=False)
List[Path] rtl_sources(self)
List[Path] dpi_link_paths(self)
add_file(self, Path file)
Path _find_dpi(self, str name, bool link)
None __init__(self, str top)
List[Path] dpi_so_paths(self)
List[str] supported_simulators()
List[str] available_simulators()