xref: /aosp_15_r20/external/pigweed/seed/0108.rst (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1.. role:: python(code)
2   :language: python
3   :class: highlight
4
5.. _seed-0108:
6
7========================
80108: Emulators Frontend
9========================
10.. seed::
11   :number: 108
12   :name: Emulators Frontend
13   :status: Accepted
14   :proposal_date: 2023-06-24
15   :cl: 158190
16   :authors: Octavian Purdila
17   :facilitator: Armando Montanez
18
19-------
20Summary
21-------
22This SEED proposes a new Pigweed module that allows users to define emulator
23targets, start, control and interact with multiple running emulator instances,
24either through a command line interface or programmatically through Python APIs.
25
26-----------
27Definitions
28-----------
29An **emulator** is a program that allows users to run unmodified images compiled
30for :ref:`target <docs-targets>` on the host machine. The **host** is the machine that
31runs the Pigweed environment.
32
33An emulated **machine** or **board** is an emulator abstraction that typically
34has a correspondence in the real world - a product, an evaluation board, a
35hardware platform.
36
37An emulated machine can be extended / tweaked through runtime configuration
38options: add sensors on an i2c bus, connect a disk drive to a disk controller,
39etc.
40
41An emulator may use an **object model**, a hierarchical arrangement of emulator
42**objects** which are emulated devices (e.g. SPI controller) or internal
43emulator structures.
44
45An object can be accessed through an **object path** and can have
46**properties**. Device properties controls how the device is emulated
47(e.g. enables or disables certain features, defines memory sizes, etc.).
48
49A **channel** is a communication abstraction between the emulator and host
50processes. Examples of channels that an emulator can expose to the host:
51
52* An emulated UART could be exposed on the host as a `PTY
53  <https://en.wikipedia.org/wiki/Pseudoterminal>`_ or a socket.
54
55* A flash device could be exposed on the host as file.
56
57* A network device could be exposed on the host as a tun/tap interface.
58
59* A remote gdb interface could be exposed to the host as socket.
60
61A **monitor** is a control channel that allows the user to interactively or
62programmatically control the emulator: pause execution, inspect the emulator
63internal state, connect new devices, etc.
64
65----------
66Motivation
67----------
68While it is already possible to use emulators directly, there is a significant
69learning curve for using a specific emulator. Even for the same emulator each
70emulated machine (board) has its own peculiarities and it often requires tweaks
71to customize it to a specific project's needs through command line options or
72scripts (either native emulator scripts, if supported, or through helper shell
73scripts).
74
75Once started, the user is responsible for managing the emulator life-cycle,
76potentially for multiple instances. They also have to interact with it through
77various channels (monitor, debugger, serial ports) that requires some level of
78host resource management. Especially in the case of using multiple emulator
79instances manually managing host resources are burdensome.
80
81A frequent example is the default debugger ``localhost:1234`` port that can
82conflict with multiple emulator instances or with other debuggers running on the
83host. Another example: serials exposed over PTY have the pts number in
84``/dev/pts/`` allocated dynamically and it requires the user to retrieve it
85somehow.
86
87This gets even more complex when using different operating systems where some
88type of host resources are not available (e.g. no PTYs on Windows) or with
89limited functionality (e.g. UNIX sockets are supported on Windows > Win10 but
90only for stream sockets and there is no Python support available yet).
91
92Using emulators in CI is also difficult, in part because host resource
93management is getting more complicated due scaling (e.g. more chances of TCP
94port conflicts) and restrictions in the execution environment. But also because
95of a lack of high level APIs to control emulators and access their channels.
96
97--------
98Proposal
99--------
100Add a new Pigweed module that:
101
102* Allows users to define emulation :ref:`targets <docs-targets>` that
103  encapsulate the emulated machine configuration, the tools configuration and
104  the host channels configuration.
105
106* Provides a command line interface that manages multiple emulator instances and
107  provides interactive access to the emulator's host channels.
108
109* Provides a Python API to control emulator instances and access the emulator's
110  host channels.
111
112* Supports multiple emulators, QEMU and renode as a starting point.
113
114* Expose channels for gdb, monitor and user selected devices through
115  configurable host resources, sockets and PTYs as a starting point.
116
117The following sections will add more details about the configuration, the
118command line interface, the API for controlling and accessing emulators and the
119API for adding support for more emulators.
120
121
122Configuration
123=============
124The emulators configuration is part of the Pigweed root configuration file
125(``pigweed.json``) and reside in the ``pw:pw_emu`` namespace.
126
127Projects can define emulation targets in the Pigweed root configuration file and
128can also import predefined targets from other files. The pw_emu module provides
129a set of targets as examples and to promote reusability.
130
131For example, the following top level ``pigweed.json`` configuration includes a
132target fragment from the ``pw_emu/qemu-lm3s6965evb.json`` file:
133
134.. code-block::
135
136   {
137     "pw": {
138       "pw_emu": {
139         "target_files": [
140           "pw_emu/qemu-lm3s6965evb.json"
141         ]
142       }
143     }
144   }
145
146
147``pw_emu/qemu-lm3s6965evb.json`` defines the ``qemu-lm3s6965evb`` target
148that uses qemu as the emulator and lm3s6965evb as the machine, with the
149``serial0`` chardev exposed as ``serial0``:
150
151.. code-block::
152
153   {
154     "targets": {
155       "qemu-lm3s6965evb": {
156         "gdb": "arm-none-eabi-gdb",
157         "qemu": {
158           "executable": "qemu-system-arm",
159           "machine": "lm3s6965evb",
160           "channels": {
161             "chardevs": {
162               "serial0": {
163                 "id": "serial0"
164               }
165             }
166           }
167         }
168       }
169     }
170   }
171
172This target emulates a stm32f405 SoC and is compatible with the
173:ref:`target-lm3s6965evb-qemu` Pigweed build target.
174
175The configuration defines a ``serial0`` channel to be the QEMU **chardev** with
176the ``serial0`` id. The default type of the channel is used, which is TCP and
177which is supported by all platforms. The user can change the type by adding a
178``type`` key set to the desired type (e.g. ``pty``).
179
180The following configuration fragment defines a target that uses renode:
181
182.. code-block::
183
184   {
185     "targets": {
186       "renode-stm32f4_discovery": {
187         "gdb": "arm-none-eabi-gdb",
188         "renode": {
189           "executable": "renode",
190           "machine": "platforms/boards/stm32f4_discovery-kit.repl",
191           "channels": {
192             "terminals": {
193               "serial0": {
194                 "device-path": "sysbus.uart0",
195                 "type": "pty"
196               }
197             }
198           }
199         }
200       }
201     }
202   }
203
204Note that ``machine`` is used to identify which renode script to use to load the
205plaform description from and ``terminals`` to define which UART devices to
206expose to the host. Also note the ``serial0`` channel is set to be exposed as a
207PTY on the host.
208
209The following channel types are currently supported:
210
211* ``pty``: supported on Mac and Linux; renode only supports PTYs for
212  ``terminals`` channels.
213
214* ``tcp``: supported on all platforms and for all channels; it is also the
215  default type if no channel type is configured.
216
217The channel configuration can be set at multiple levels: emulator, target, or
218specific channel. The channel configuration takes precedence, then the target
219channel configuration then the emulator channel configuration.
220
221The following expressions are replaced in the configuration strings:
222
223* ``$pw_emu_wdir{relative-path}``: replaces statement with an absolute path by
224  concatenating the emulator's working directory with the given relative path.
225
226* ``$pw_emu_channel_port{channel-name}``: replaces the statement with the port
227  number for the given channel name; the channel type should be ``tcp``.
228
229* ``$pw_emu_channel_host{channel-name}``: replaces the statement with the host
230  for the given channel name; the channel type should be ``tcp``.
231
232* ``$pw_emu_channel_path{channel-name}``: replaces the statement with the path
233  for the given channel name; the channel type should be ``pty``.
234
235Besides running QEMU and renode as the main emulator, the target configuration
236allows users to start other programs before or after starting the main emulator
237process. This allows extending the emulated target with simulation or emulation
238outside of the main emulator. For example, for BLE emulation the main emulator
239could emulate just the serial port while the HCI emulation done is in an
240external program (e.g. `bumble <https://google.github.io/bumble>`_, `netsim
241<https://android.googlesource.com/platform/tools/netsim>`_).
242
243
244Command line interface
245======================
246The command line interfaces enables users to control emulator instances and
247access their channels interactively.
248
249.. code-block:: text
250
251   usage: pw emu [-h] [-i STRING] [-w WDIR] {command} ...
252
253   Pigweed Emulators Frontend
254
255    start               Launch the emulator and start executing, unless pause
256                        is set.
257    restart             Restart the emulator and start executing, unless pause
258                        is set.
259    run                 Start the emulator and connect the terminal to a
260                        channel. Stop the emulator when exiting the terminal
261    stop                Stop the emulator
262    load                Load an executable image via gdb. If pause is not set
263                        start executing it.
264    reset               Perform a software reset.
265    gdb                 Start a gdb interactive session
266    prop-ls             List emulator object properties.
267    prop-get            Show the emulator's object properties.
268    prop-set            Show emulator's object properties.
269    gdb-cmds            Run gdb commands in batch mode.
270    term                Connect with an interactive terminal to an emulator
271                        channel
272
273   optional arguments:
274    -h, --help            show this help message and exit
275    -i STRING, --instance STRING
276                          instance to use (default: default)
277    -w WDIR, --wdir WDIR  path to working directory (default: None)
278
279   commands usage:
280       usage: pw emu start [-h] [--file FILE] [--runner {None,qemu,renode}]
281                     [--args ARGS] [--pause] [--debug] [--foreground]
282                           {qemu-lm3s6965evb,qemu-stm32vldiscovery,qemu-netduinoplus2}
283        usage: pw emu restart [-h] [--file FILE] [--runner {None,qemu,renode}]
284                      [--args ARGS] [--pause] [--debug] [--foreground]
285                      {qemu-lm3s6965evb,qemu-stm32vldiscovery,qemu-netduinoplus2}
286        usage: pw emu stop [-h]
287        usage: pw emu run [-h] [--args ARGS] [--channel CHANNEL]
288                      {qemu-lm3s6965evb,qemu-stm32vldiscovery,qemu-netduinoplus2} FILE
289        usage: pw emu load [-h] [--pause] FILE
290        usage: pw emu reset [-h]
291        usage: pw emu gdb [-h] [--executable FILE]
292        usage: pw emu prop-ls [-h] path
293        usage: pw emu prop-get [-h] path property
294        usage: pw emu prop-set [-h] path property value
295        usage: pw emu gdb-cmds [-h] [--pause] [--executable FILE] gdb-command [gdb-command ...]
296        usage: pw emu term [-h] channel
297
298For example, the ``run`` command is useful for quickly running ELF binaries on an
299emulated target and seeing / interacting with a serial channel. It starts an
300emulator, loads an images, connects to a channel and starts executing.
301
302.. code-block::
303
304   $ pw emu run qemu-netduinoplus2 out/stm32f429i_disc1_debug/obj/pw_snapshot/test/cpp_compile_test.elf
305
306   --- Miniterm on serial0 ---
307   --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
308   INF  [==========] Running all tests.
309   INF  [ RUN      ] Status.CompileTest
310   INF  [       OK ] Status.CompileTest
311   INF  [==========] Done running all tests.
312   INF  [  PASSED  ] 1 test(s).
313   --- exit ---
314
315Multiple emulator instances can be run and each emulator instance is identified
316by its working directory. The default working directory for ``pw emu`` is
317``$PW_PROJECT_ROOT/.pw_emu/<instance-id>`` where ``<instance-id>`` is a command
318line option that defaults to ``default``.
319
320For more complex usage patterns, the ``start`` command can be used which will
321launch an emulator instance in the background. Then, the user can debug the
322image with the ``gdb`` command, connect to a channel (e.g. serial port) with the
323``term`` command, reset the emulator with the ``reset`` command, inspect or
324change emulator properties with the ``prop-ls``, ``prop-get``, ``prop-set`` and
325finally stop the emulator instance with ``stop``.
326
327
328Python APIs
329===========
330The pw_emu module offers Python APIs to launch, control and interact with an
331emulator instance.
332
333The following is an example of using these APIs which implements a simplified
334version of the ``run`` pw_emu CLI command:
335
336.. code-block:: python
337
338   # start an emulator instance and load the image to execute
339   # pause the emulator after loading the image
340   emu = Emulator(args.wdir)
341   emu.start(args.target, args.file, pause=True)
342
343   # determine the channel type and create a pyserial compatible URL
344   chan_type = emu.get_channel_type(args.chan)
345   if chan_type == 'tcp':
346       host, port = emu.get_channel_addr(chan)
347       url = f'socket://{host}:{port}'
348    elif chan_type == 'pty':
349        url =  emu.get_channel_path(chan)
350    else:
351        raise Error(f'unknown channel type `{chan_type}`')
352
353   # open the serial port and create a miniterm instance
354   serial = serial_for_url(url)
355   serial.timeout = 1
356   miniterm = Miniterm(serial)
357   miniterm.raw = True
358   miniterm.set_tx_encoding('UTF-8')
359   miniterm.set_rx_encoding('UTF-8')
360
361   # now that we are connected to the channel we can unpause
362   # this approach will prevent and data loses
363   emu.cont()
364
365   miniterm.start()
366   try:
367       miniterm.join(True)
368   except KeyBoardInterrupt:
369       pass
370   miniterm.stop()
371   miniterm.join()
372   miniterm.close()
373
374For convenience, a ``TemporaryEmulator`` class is also provided.
375
376It manages emulator instances that run in temporary working directories. The
377emulator instance is stopped and the working directory is cleared when the with
378block completes.
379
380It also supports interoperability with the pw emu cli, i.e.  starting the
381emulator with the CLI and controlling / interacting with it from the API.
382
383Usage example:
384
385.. code-block:: python
386
387   # programmatically start and load an executable then access it
388   with TemporaryEmulator() as emu:
389       emu.start(target, file)
390       with emu.get_channel_stream(chan) as stream:
391           ...
392
393
394    # or start it form the command line then access it programmatically
395    with TemporaryEmulator() as emu:
396        build.bazel(
397            ctx,
398            "run",
399            exec_path,
400            "--run_under=pw emu start <target> --file "
401        )
402
403        with emu.get_channel_stream(chan) as stream:
404            ...
405
406
407Intended API shape
408------------------
409This is not an API reference, just an example of the probable shape of the final
410API.
411
412:python:`class Emulator` is used to launch, control and interact with an
413emulator instance:
414
415.. code-block:: python
416
417   def start(
418       self,
419       target: str,
420       file: os.PathLike | None = None,
421       pause: bool = False,
422       debug: bool = False,
423       foreground: bool = False,
424       args: str | None = None,
425   ) -> None:
426
427|nbsp|
428   Start the emulator for the given target.
429
430   If file is set that the emulator will load the file before starting.
431
432   If pause is True the emulator is paused until the debugger is connected.
433
434   If debug is True the emulator is run in foreground with debug output
435   enabled. This is useful for seeing errors, traces, etc.
436
437   If foreground is True the emulator is run in foreground otherwise it is
438   started in daemon mode. This is useful when there is another process
439   controlling the emulator's life cycle (e.g. cuttlefish)
440
441   args are passed directly to the emulator
442
443:python:`def running(self) -> bool:`
444   Check if the main emulator process is already running.
445
446:python:`def stop(self) -> None`
447   Stop the emulator
448
449:python:`def get_gdb_remote(self) -> str:`
450   Return a string that can be passed to the target remote gdb command.
451
452:python:`def get_gdb(self) -> str | None:`
453   Returns the gdb command for current target.
454
455.. code-block:: python
456
457   def run_gdb_cmds(
458       commands : list[str],
459       executable: Path | None = None,
460       pause: bool = False
461   ) -> subprocess.CompletedProcess:
462
463|nbsp|
464   Connect to the target and run the given commands silently
465   in batch mode.
466
467   The executable is optional but it may be required by some gdb
468   commands.
469
470   If pause is set do not continue execution after running the
471   given commands.
472
473:python:`def reset() -> None`
474   Performs a software reset
475
476:python:`def list_properties(self, path: str) -> List[dict]`
477   Returns the property list for an emulator object.
478
479   The object is identified by a full path. The path is target specific and
480   the format of the path is emulator specific.
481
482   QEMU path example: /machine/unattached/device[10]
483
484   renode path example: sysbus.uart
485
486:python:`def set_property(path: str, prop: str, value: str) -> None`
487   Sets the value of an emulator's object property.
488
489:python:`def get_property(self, path: str, prop: str) -> None`
490   Returns the value of an emulator's object property.
491
492:python:`def get_channel_type(self, name: str) -> str`
493   Returns the channel type.
494
495   Currently ``pty`` or ``tcp`` are the only supported types.
496
497:python:`def get_channel_path(self, name: str) -> str:`
498   Returns the channel path. Raises InvalidChannelType if this is not a PTY
499   channel.
500
501:python:`def get_channel_addr(self, name: str) -> tuple:`
502   Returns a pair of (host, port) for the channel. Raises InvalidChannelType
503   if this is not a tcp channel.
504
505.. code-block:: python
506
507   def get_channel_stream(
508       name: str,
509       timeout: float | None = None
510   ) -> io.RawIOBase:
511
512|nbsp|
513   Returns a file object for a given host exposed device.
514
515   If timeout is None than reads and writes are blocking. If timeout is zero the
516   stream is operating in non-blocking mode. Otherwise read and write will
517   timeout after the given value.
518
519:python:`def get_channels(self) -> List[str]:`
520   Returns the list of available channels.
521
522:python:`def cont(self) -> None:`
523   Resume the emulator's execution
524
525---------------------
526Problem investigation
527---------------------
528Pigweed is missing a tool for basic emulators control and as shown in the
529motivation section directly using emulators directly is difficult.
530
531While emulation is not a goal for every project, it is appealing for some due
532to the low cost and scalability. Offering a customizable emulators frontend in
533Pigweed will make this even better for downstream projects as the investment to
534get started with emulation will be lower - significantly lower for projects
535looking for basic usage.
536
537There are two main use-cases that this proposal is addressing:
538
539* Easier and robust interactive debugging and testing on emulators.
540
541* Basic APIs for controlling and accessing emulators to help with emulator
542  based testing (and trivial CI deployment - as long as the Pigweed bootstrap
543  process can run in CI).
544
545The proposal focuses on a set of fairly small number of commands and APIs in
546order to minimize complexity and gather feedback from users before adding more
547features.
548
549Since the state of emulated boards may different between emulators, to enable
550users access to more emulated targets, the goal of the module is to support
551multiple emulators from the start.
552
553Two emulators were selected for the initial implementation: QEMU and
554renode. Both run on all Pigweed currently supported hosts (Linux, Mac, Windows)
555and there is good overlap in terms of APIs to configure, start, control and
556access host exposed channels to start with the two for the initial
557implementation. These emulators also have good support for embedded targets
558(with QEMU more focused on MMU class machines and renode fully focused on
559microcontrollers) and are widely used in this space for emulation purposes.
560
561
562Prior art
563=========
564While there are several emulators frontends available, their main focus is on
565graphical interfaces (`aqemu <https://sourceforge.net/projects/aqemu/>`_,
566`GNOME Boxes <https://wiki.gnome.org/Apps/Boxes>`_,
567`QtEmu <https://gitlab.com/qtemu/gui>`_,
568`qt-virt-manager <https://f1ash.github.io/qt-virt-manager/>`_,
569`virt-manager <https://virt-manager.org/>`_) and virtualization (
570`virsh <https://www.libvirt.org/>`_,
571`guestfish <https://libguestfs.org/>`_).
572`qemu-init <https://github.com/mm1ke/qemu-init>`_ is a qemu CLI frontend but since
573it is written in bash it does not work on Windows nor is easy to retrofit it to
574add Python APIs for automation.
575
576.. inclusive-language: disable
577
578The QEMU project has a few `Python modules
579<https://github.com/qemu/qemu/tree/master/python/qemu>`_ that are used
580internally for testing and debugging QEMU. :python:`qemu.machine.QEMUMachine`
581implements a QEMU frontend that can start a QEMU process and can interact with
582it. However, it is clearly marked for internal use only, it is not published on
583pypi or with the QEMU binaries. It is also not as configurable for pw_emu's
584use-cases (e.g. does not support running the QEMU process in the background,
585does not multiple serial ports, does not support configuring how to expose the
586serial port, etc.). The :python:`qemu.qmp` module is `published on pypi
587<https://pypi.org/project/qemu.qmp/>`_ and can be potentially used by `pw_emu`
588to interact with the emulator over the QMP channel.
589
590.. inclusive-language: enable
591
592---------------
593Detailed design
594---------------
595The implementation supports QEMU and renode as emulators and works on
596Linux, Mac and Windows.
597
598Multiple instances are supported in order to enable developers who work on
599multiple downstream Pigweed projects to work unhindered and also to run
600multiple test instances in parallel on the same machine.
601
602Each instance is identified by a system absolute path that is also used to store
603state about the running instance such as pid files for running processes,
604current emulator and target, etc. This directory also contains information about
605how to access the emulator channels (e.g. socket ports, PTY paths)
606
607.. mermaid::
608
609   graph TD;
610       TemporaryEmulator & pw_emu_cli[pw emu cli] <--> Emulator
611       Emulator <--> Launcher & Connector
612       Launcher  <--> Handles
613       Connector <--- Handles
614       Launcher <--> Config
615       Handles --Save--> WD --Load--> Handles
616       WD[Working Directory]
617
618The implementation uses the following classes:
619
620* :py:class:`pw_emu.Emulator`: the user visible APIs
621
622* :py:class:`pw_emu.core.Launcher`: an abstract class that starts an emulator
623  instance for a given configuration and target
624
625* :py:class:`pw_emu.core.Connector`: an abstract class that is the interface
626  between a running emulator and the user visible APIs
627
628* :py:class:`pw_emu.core.Handles`: class that stores specific information about
629  a running emulator instance such as ports to reach emulator channels; it is
630  populated by :py:class:`pw_emu.core.Launcher` and saved in the working
631  directory and used by :py:class:`pw_emu.core.Connector` to access the emulator
632  channels, process pids, etc.
633
634* :py:class:`pw_emu.core.Config`: loads the pw_emu configuration and provides
635  helper methods to get and validate configuration options
636
637
638Documentation update
639====================
640The following documentation should probably be updated to use ``pw emu`` instead
641of direct QEMU invocation: :ref:`module-pw_rust`,
642:ref:`target-lm3s6965evb-qemu`. The referenced QEMU targets are defined in
643fragment configuration files in the pw_emu module and included in the top level
644pigweed.json file.
645
646------------
647Alternatives
648------------
649UNIX sockets were investigated as an alternative to TCP for the host exposed
650channels. UNIX sockets main advantages over TCP is that it does not require
651dynamic port allocation which simplifies the bootstrap of the emulator (no need
652to query the emulator to determine which ports were allocated). Unfortunately,
653while Windows supports UNIX sockets since Win10, Python still does not support
654them on win32 platforms. renode also does not support UNIX sockets.
655
656--------------
657Open questions
658--------------
659
660Renode dynamic ports
661====================
662While renode allows passing 0 for ports to allocate a dynamic port, it does not
663have APIs to retrieve the allocated port. Until support for such a feature is
664added upstream, the following technique can be used to allocate a port
665dynamically:
666
667.. code-block:: python
668
669   sock = socket.socket(socket.SOCK_INET, socket.SOCK_STREAM)
670   sock.bind(('', 0))
671   _, port = socket.getsockname()
672   sock.close()
673
674There is a race condition that allows another program to fetch the same port,
675but it should work in most light use cases until the issue is properly resolved
676upstream.
677
678qemu_gcc target
679===============
680It should still be possible to call QEMU directly as described in
681:ref:`target-lm3s6965evb-qemu` however, once ``pw_emu`` is implemented it is
682probably better to define a lm3s6965evb emulation target and update the
683documentation to use ``pw emu`` instead of the direct QEMU invocation.
684
685
686.. |nbsp| unicode:: 0xA0
687   :trim:
688