CIRCT 23.0.0git
Loading...
Searching...
No Matches
simulator.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5import os
6import re
7import signal
8import socket
9import subprocess
10import time
11from pathlib import Path
12from typing import Dict, List, Optional, Callable, IO
13import threading
14
15_thisdir = Path(__file__).parent
16CosimCollateralDir = _thisdir
17
18
19def is_port_open(port) -> bool:
20 """Check if a TCP port is open locally."""
21 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
22 result = sock.connect_ex(('127.0.0.1', port))
23 sock.close()
24 return True if result == 0 else False
25
26
28
29 def __init__(self, top: str) -> None:
30 # User source files.
31 self.user: List[Path] = []
32 # DPI shared objects.
33 self.dpi_so: List[str] = ["EsiCosimDpiServer"]
34 # DPI SV files.
35 self.dpi_sv: List[Path] = [
36 CosimCollateralDir / "Cosim_DpiPkg.sv",
37 CosimCollateralDir / "Cosim_Endpoint.sv",
38 CosimCollateralDir / "Cosim_CycleCount.sv",
39 CosimCollateralDir / "Cosim_Manifest.sv",
40 ]
41 # Name of the top module.
42 self.top = top
43
44 def add_file(self, file: Path):
45 """Add a single RTL file to the source list."""
46 if file.is_file():
47 self.user.append(file)
48 else:
49 raise FileNotFoundError(f"File {file} does not exist")
50
51 def add_dir(self, dir: Path):
52 """Add all the RTL files in a directory to the source list."""
53 for file in sorted(dir.iterdir()):
54 if file.is_file() and (file.suffix == ".sv" or file.suffix == ".v"):
55 self.user.append(file)
56 elif file.is_dir():
57 self.add_dir(file)
58
59 def dpi_so_paths(self) -> List[Path]:
60 """Return a list of all the DPI shared object files."""
61
62 def find_so(name: str) -> Path:
63 for path in Simulator.get_env().get("LD_LIBRARY_PATH", "").split(":"):
64 if os.name == "nt":
65 so = Path(path) / f"{name}.dll"
66 else:
67 so = Path(path) / f"lib{name}.so"
68 if so.exists():
69 return so
70 raise FileNotFoundError(f"Could not find {name}.so in LD_LIBRARY_PATH")
71
72 return [find_so(name) for name in self.dpi_so]
73
74 @property
75 def rtl_sources(self) -> List[Path]:
76 """Return a list of all the RTL source files."""
77 return self.dpi_sv + self.user
78
79
81
82 def __init__(self,
83 proc: subprocess.Popen,
84 port: int,
85 threads: Optional[List[threading.Thread]] = None,
86 gui: bool = False):
87 self.proc = proc
88 self.port = port
89 self.threads: List[threading.Thread] = threads or []
90 self.gui = gui
91
92 def force_stop(self):
93 """Make sure to stop the simulation no matter what."""
94 if self.proc:
95 os.killpg(os.getpgid(self.proc.pid), signal.SIGINT)
96 # Allow the simulation time to flush its outputs.
97 try:
98 self.proc.wait(timeout=1.0)
99 except subprocess.TimeoutExpired:
100 # If the simulation doesn't exit of its own free will, kill it.
101 self.proc.kill()
102
103 # Join reader threads (they should exit once pipes are closed).
104 for t in self.threads:
105 t.join()
106
107
109
110 # Some RTL simulators don't use stderr for error messages. Everything goes to
111 # stdout. Boo! They should feel bad about this. Also, they can specify that
112 # broken behavior by overriding this.
113 UsesStderr = True
114
115 def __init__(self,
116 sources: SourceFiles,
117 run_dir: Path,
118 debug: bool,
119 save_waveform: bool = False,
120 run_stdout_callback: Optional[Callable[[str], None]] = None,
121 run_stderr_callback: Optional[Callable[[str], None]] = None,
122 compile_stdout_callback: Optional[Callable[[str], None]] = None,
123 compile_stderr_callback: Optional[Callable[[str], None]] = None,
124 make_default_logs: bool = True,
125 macro_definitions: Optional[Dict[str, str]] = None):
126 """Simulator base class.
127
128 Optional sinks can be provided for capturing output. If not provided,
129 the simulator will write to log files in `run_dir`.
130
131 Args:
132 sources: SourceFiles describing RTL/DPI inputs.
133 run_dir: Directory where build/run artifacts are placed.
134 debug: Enable cosim debug mode.
135 save_waveform: When True and debug=True, dump simulator waveforms to a
136 waveform file. The exact format depends on the backend (e.g. FST for
137 Verilator, VCD for Questa). Requires debug to be enabled.
138 run_stdout_callback: Line-based callback for runtime stdout.
139 run_stderr_callback: Line-based callback for runtime stderr.
140 compile_stdout_callback: Line-based callback for compile stdout.
141 compile_stderr_callback: Line-based callback for compile stderr.
142 make_default_logs: If True and corresponding callback is not supplied,
143 create log file and emit via internally-created callback.
144 macro_definitions: Optional dictionary of macro definitions to be defined
145 during compilation.
146 """
147 self.sources = sources
148 self.run_dir = run_dir
149 self.debug = debug
150 self.save_waveform = save_waveform
151 self.macro_definitions = macro_definitions
152
153 # Unified list of any log file handles we opened.
154 self._default_files: List[IO[str]] = []
155
156 def _ensure_default(cb: Optional[Callable[[str], None]], filename: str):
157 """Return (callback, file_handle_or_None) with optional file creation.
158
159 Behavior:
160 * If a callback is provided, return it unchanged with no file.
161 * If no callback and make_default_logs is False, return (None, None).
162 * If no callback and make_default_logs is True, create a log file and
163 return a writer callback plus the opened file handle.
164 """
165 if cb is not None:
166 return cb, None
167 if not make_default_logs:
168 return None, None
169 p = self.run_dir / filename
170 p.parent.mkdir(parents=True, exist_ok=True)
171 logf = p.open("w+")
172 self._default_files.append(logf)
173
174 def _writer(line: str, _lf=logf):
175 _lf.write(line + "\n")
176 _lf.flush()
177
178 return _writer, logf
179
180 # Initialize all four (compile/run stdout/stderr) uniformly.
181 self._compile_stdout_cb, self._compile_stdout_log = _ensure_default(
182 compile_stdout_callback, 'compile_stdout.log')
183 self._compile_stderr_cb, self._compile_stderr_log = _ensure_default(
184 compile_stderr_callback, 'compile_stderr.log')
185 self._run_stdout_cb, self._run_stdout_log = _ensure_default(
186 run_stdout_callback, 'sim_stdout.log')
187 self._run_stderr_cb, self._run_stderr_log = _ensure_default(
188 run_stderr_callback, 'sim_stderr.log')
189
190 @staticmethod
191 def get_env() -> Dict[str, str]:
192 """Get the environment variables to locate shared objects."""
193
194 env = os.environ.copy()
195 env["LIBRARY_PATH"] = env.get("LIBRARY_PATH", "") + ":" + str(
196 _thisdir.parent / "lib")
197 env["LD_LIBRARY_PATH"] = env.get("LD_LIBRARY_PATH", "") + ":" + str(
198 _thisdir.parent / "lib")
199 return env
200
201 def compile_commands(self) -> List[List[str]]:
202 """Compile the sources. Returns the exit code of the simulation compiler."""
203 assert False, "Must be implemented by subclass"
204
205 def compile(self) -> int:
206 cmds = self.compile_commands()
207 self.run_dir.mkdir(parents=True, exist_ok=True)
208 for cmd in cmds:
210 cmd,
211 env=Simulator.get_env(),
212 cwd=None,
213 stdout_cb=self._compile_stdout_cb,
214 stderr_cb=self._compile_stderr_cb,
215 wait=True)
216 if isinstance(ret, int) and ret != 0:
217 print("====== Compilation failure")
218
219 # Always print both stdout and stderr so that linker errors (which go
220 # to stdout for cmake/ninja) are not silently hidden.
221 if self._compile_stdout_log is not None:
222 self._compile_stdout_log.seek(0)
223 stdout_content = self._compile_stdout_log.read()
224 if stdout_content:
225 print(stdout_content)
226 if self._compile_stderr_log is not None:
227 self._compile_stderr_log.seek(0)
228 stderr_content = self._compile_stderr_log.read()
229 if stderr_content:
230 print(stderr_content)
231
232 return ret
233 return 0
234
235 def run_command(self, gui: bool) -> List[str]:
236 """Return the command to run the simulation."""
237 assert False, "Must be implemented by subclass"
238
239 @property
240 def waveform_extension(self) -> str:
241 """File extension for waveform dumps.
242
243 Subclasses should override if their format differs. The Verilator C++
244 driver writes FST (via ``VerilatedFstC``); the generic SV driver uses
245 ``$dumpfile/$dumpvars`` which produces VCD.
246 """
247 return ".vcd"
248
249 def run_proc(self, gui: bool = False) -> SimProcess:
250 """Run the simulation process. Returns the Popen object and the port which
251 the simulation is listening on.
252
253 If user-provided stdout/stderr sinks were supplied in the constructor,
254 they are used. Otherwise, log files are created in `run_dir`.
255 """
256 self.run_dir.mkdir(parents=True, exist_ok=True)
257
258 env_gui = os.environ.get("COSIM_GUI", "0")
259 if env_gui != "0":
260 gui = True
261
262 # Erase the config file if it exists. We don't want to read
263 # an old config.
264 portFileName = self.run_dir / "cosim.cfg"
265 if os.path.exists(portFileName):
266 os.remove(portFileName)
267
268 # Run the simulation.
269 simEnv = Simulator.get_env()
270 if self.debug:
271 debug_file = (self.run_dir / "cosim_debug.log").resolve()
272 simEnv["COSIM_DEBUG_FILE"] = str(debug_file)
273 if "DEBUG_PERIOD" not in simEnv:
274 # Slow the simulation down to one tick per millisecond.
275 simEnv["DEBUG_PERIOD"] = "1"
276 if self.save_waveform:
277 waveform_file = (self.run_dir /
278 f"cosim_waveform{self.waveform_extension}").resolve()
279 simEnv["SAVE_WAVE"] = str(waveform_file)
280 rcmd = self.run_command(gui)
281 # Start process with asynchronous output capture.
282 proc, threads = self._start_process_with_callbacks(
283 rcmd,
284 env=simEnv,
285 cwd=self.run_dir,
286 stdout_cb=self._run_stdout_cb,
287 stderr_cb=self._run_stderr_cb,
288 wait=False)
289
290 # Get the port which the simulation RPC selected.
291 checkCount = 0
292 while (not os.path.exists(portFileName)) and \
293 proc.poll() is None:
294 time.sleep(0.1)
295 checkCount += 1
296 if checkCount > 500 and not gui:
297 raise Exception(f"Cosim never wrote cfg file: {portFileName}")
298 port = -1
299 while port < 0:
300 portFile = open(portFileName, "r")
301 for line in portFile.readlines():
302 m = re.match("port: (\\d+)", line)
303 if m is not None:
304 port = int(m.group(1))
305 portFile.close()
306
307 # Wait for the simulation to start accepting RPC connections.
308 checkCount = 0
309 while not is_port_open(port):
310 checkCount += 1
311 if checkCount > 200:
312 raise Exception(f"Cosim RPC port ({port}) never opened")
313 if proc.poll() is not None:
314 raise Exception("Simulation exited early")
315 time.sleep(0.05)
316 return SimProcess(proc=proc, port=port, threads=threads, gui=gui)
317
319 self, cmd: List[str], env: Optional[Dict[str, str]], cwd: Optional[Path],
320 stdout_cb: Optional[Callable[[str],
321 None]], stderr_cb: Optional[Callable[[str],
322 None]],
323 wait: bool) -> int | tuple[subprocess.Popen, List[threading.Thread]]:
324 """Start a subprocess and stream its stdout/stderr to callbacks.
325
326 If wait is True, blocks until process completes and returns its exit code.
327 If wait is False, returns the Popen object (threads keep streaming).
328 """
329 if os.name == "posix":
330 proc = subprocess.Popen(cmd,
331 stdout=subprocess.PIPE,
332 stderr=subprocess.PIPE,
333 env=env,
334 cwd=cwd,
335 text=True,
336 preexec_fn=os.setsid)
337 else: # windows
338 proc = subprocess.Popen(cmd,
339 stdout=subprocess.PIPE,
340 stderr=subprocess.PIPE,
341 env=env,
342 cwd=cwd,
343 text=True,
344 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
345
346 def _reader(pipe, cb):
347 if pipe is None:
348 return
349 for raw in pipe:
350 if raw.endswith('\n'):
351 raw = raw[:-1]
352 if cb:
353 try:
354 cb(raw)
355 except Exception as e:
356 print(f"Exception in simulator output callback: {e}")
357
358 threads: List[threading.Thread] = [
359 threading.Thread(target=_reader,
360 args=(proc.stdout, stdout_cb),
361 daemon=True),
362 threading.Thread(target=_reader,
363 args=(proc.stderr, stderr_cb),
364 daemon=True),
365 ]
366 for t in threads:
367 t.start()
368 if wait:
369 for t in threads:
370 t.join()
371 return proc.wait()
372 return proc, threads
373
374 def run(self,
375 inner_command: str,
376 gui: bool = False,
377 server_only: bool = False) -> int:
378 """Start the simulation then run the command specified. Kill the simulation
379 when the command exits."""
380
381 # 'simProc' is accessed in the finally block. Declare it here to avoid
382 # syntax errors in that block.
383 simProc = None
384 try:
385 simProc = self.run_proc(gui=gui)
386 if server_only:
387 # wait for user input to kill the server
388 input(
389 f"Running in server-only mode on port {simProc.port} - Press anything to kill the server..."
390 )
391 return 0
392 else:
393 # Run the inner command, passing the connection info via environment vars.
394 testEnv = os.environ.copy()
395 testEnv["ESI_COSIM_PORT"] = str(simProc.port)
396 testEnv["ESI_COSIM_HOST"] = "localhost"
397 ret = subprocess.run(inner_command, cwd=os.getcwd(),
398 env=testEnv).returncode
399 if simProc.gui:
400 print("GUI mode - waiting for simulator to exit...")
401 simProc.proc.wait()
402 return ret
403 finally:
404 if simProc and simProc.proc.poll() is None:
405 simProc.force_stop()
406
407
408def get_simulator(name: str,
409 sources: SourceFiles,
410 rundir: Path,
411 debug: bool,
412 save_waveform: bool = False) -> Simulator:
413 name = name.lower()
414 if name == "verilator":
415 from .verilator import Verilator
416 return Verilator(sources, rundir, debug, save_waveform=save_waveform)
417 elif name == "questa":
418 from .questa import Questa
419 return Questa(sources, rundir, debug, save_waveform=save_waveform)
420 else:
421 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)
Definition simulator.py:86
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)
Definition simulator.py:323
__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)
Definition simulator.py:125
int run(self, str inner_command, bool gui=False, bool server_only=False)
Definition simulator.py:377
str waveform_extension(self)
Definition simulator.py:240
List[List[str]] compile_commands(self)
Definition simulator.py:201
Dict[str, str] get_env()
Definition simulator.py:191
List[str] run_command(self, bool gui)
Definition simulator.py:235
SimProcess run_proc(self, bool gui=False)
Definition simulator.py:249
List[Path] rtl_sources(self)
Definition simulator.py:75
add_file(self, Path file)
Definition simulator.py:44
None __init__(self, str top)
Definition simulator.py:29
List[Path] dpi_so_paths(self)
Definition simulator.py:59
add_dir(self, Path dir)
Definition simulator.py:51
bool is_port_open(port)
Definition simulator.py:19