by Robert Muth
This article describes our experience porting Multiple Arcade Machine Emulator (MAME),
an emulator for a large number
of classic arcade games, to Native Client. We discuss a number of topics in detail, such as our overall porting strategy,
how we dealt with newlib incompatibilities, and how we handled binaries that are built and run as part of the build process.
We do not discuss topics such as how to load resources in Native Client or how to port SDL games to Native Client
(the articles listed in the Links section cover those topics).
Our port is based on MAME version 0.143 (zip file).
Note: This document describes how we ported MAME using tools on the Linux platform. The resulting code runs in the Google Chrome browser on all currently supported Native Client platforms (Windows, Mac, and Linux).
A typical approach for porting a small project to Native Client consists of the following steps:
For large projects such as MAME, this approach may require a lot of debugging in Step 6 ("get the code to run"). The problem is that when you make a change that does not work, it's hard to know whether you've actually broken the original code, or whether you're just not quite done yet with making the right adaptations for Native Client.
In this situation it's helpful to use a dual-build approach, where you build the same code base with both the local system toolchain and the Native Client toolchain. When you encounter a problem, try to find a fix and then build and test the code using the system toolchain first. If the fix is successful, apply the fix to the code you are compiling with the Native Client toolchain and proceed with the port.
The dual-build approach can facilitate the porting effort considerably, especially on systems like Linux that are very similar to the Native Client platform. An instructive example is the rewrite of the co-routine library mentioned below. We rewrote the library in a way that it could still be used with a regular MAME build on our local system; this allowed us to be confident about this particular change well before we completed the port.
Before diving into the port, we spent some time identifying and eliminating a few problem areas. To begin with, after reading background information about MAME, we decided not to support a number of components in the initial port:
SDL_ttf library to reduce porting time.We found a number of additional problems by grep'ing through the code base:
| Grep term | Problem |
|---|---|
signal |
Native Client does not support signal handling. |
mprotect |
Native Client does not support arbitray change of page table protection. |
opendir, readdir |
Native Client does not support directory traversal. |
pthread_setaffinity_np, etc. |
Native Client does not support advanced pthread usage. |
asm |
Hand-coded assembly needs special attention. |
fopen, open, etc. |
Loading of resources needs to be handled by the appropriate PPAPI interfaces. |
unlink, etc. |
Local file system manipulation needs to be handled by the appropriate PPAPI interfaces. |
We applied the dual-build approach specified above and fixed all of these problems while still building MAME with the
local system toolchain. We relied heavily on the system toolchain to help us locate problems in the code.
For example, in order to make our port quicker we wanted to avoid porting the SDL_ttf library,
which adds support for rendering certain fonts in MAME's built-in menu system. To address this issue,
we removed the SDL_ttf library from the link command and used the linker diagnostics to determine where the library was used.
Eyeballing the code indicated that there was a fallback, so we could safely comment out
all the code using SDL_ttf. We could also test that everything still worked by running MAME as a normal Linux executable.
We discuss a few of the problems listed above (e.g., hand-coded assembly) in more detail in the next section. Other problems (e.g., how to load resources in Native Client) are discussed in the articles listed in the Links section at the end of this document.
MAME's build system is based on recursive makefiles, and supports the most common platforms (e.g., Windows, OS X, and Linux). We had to make the following modifications to MAME's build system:
| File | Modifications |
|---|---|
makefile |
|
src/osd/sdl/sdl.mak |
|
src/lib/lib.mak |
|
src/emu/emu.mak |
|
src/emu/cpu/cpu.mak |
|
src/build/build.mak |
|
src/mame/mame.mak |
|
We discuss a few of these modifications in detail below.
MAME builds a number of binary tools that are subsequently run as part of the build process. For example, the build rule below
uses the $(FILE2STR) tool to convert (potentially) binary files
into C code that is linked into the final executable:
$(OBJ)/%.lh: $(SRC)/%.lay $(FILE2STR_TARGET) @echo Converting $<... $(FILE2STR) $< $@ layout_$(basename $(notdir $<))
The problem with this build rule is that
when we hook up the Native Client toolchain,
the $(FILE2STR) tool is built as a Native Client module that cannot
be run from the command line. Fortunately, the Native Client SDK includes a simple emulator that allows us to run some
Native Client modules from the command line. The build rule can be modified to use this emulator, like so:
NATIVE_CLIENT_EMU=$(NACL_SDK_ROOT)/toolchain/linux_x86/bin/sel_ldr_x86_32 -a $(OBJ)/%.lh: $(SRC)/%.lay $(FILE2STR_TARGET) @echo Converting $<... $(NATIVE_CLIENT_EMU) $(FILE2STR) $< $@ layout_$(basename $(notdir $<))
If compiling on a 64-bit machine, use sel_ldr_x86_64 instead.
When you want to compile using the standard system toolchain instead of the Native Client toolchain, simply set
NATIVE_CLIENT_EMU to the empty string.
The sel_ldr emulator approach may not work on non-Linux systems. In this case, you can use an admittedly
kludgy workaround:
$(FILE2STR)) or the target (e.g., $(OBJ)/%.lh)
with a prebuilt version obtained from a build with the system toolchain.For ports that use configure, an alternative approach for dealing with tools that are built and run as part of the
build process is to use two sets of variables. Set up one set of variables (e.g., CC, CXX, and LD)
to point to the Native Client toolchain, and the second set of variables (e.g., HOSTCC, HOSTCXX,
and HOSTLD) to point to the system toolchain. Then simply replace $CC with $HOSTCC in the rules
that build intermediate binaries. (This approach was not an option for MAME as it does not use configure.)
Another tool that is built and run as part of the MAME build process is makedep. The makedep tool scans
the entire source tree for C files and compiles a list of header file dependencies that is then made available to make.
After we started building MAME with the Native Client toolchain and became confident that we did not have to change much more code,
we simply eliminated the build and use of this tool altogether. (We could have used the emulator workaround mentioned above to deal with
the makedep tool, but chose not to.)
MAME contains several sources of machine instructions that are not directly produced by the Native Client toolchain, and that therefore do not comply with the Native Client security constraints. These include:
src/lib/cothread with hand-coded assemblysrc/emu/eminline.hWe re-implemented the co-routine library using pthreads, basically associating a semaphore with each co-routine. The semaphore gets signaled when the co-routine is activated, while the co-routine which is de-activated waits on its own semaphore. We verified that the performance using this approach was acceptable by testing the approach using the standard system toolchain first.
We completely disabled the UDR, including the games/drivers and system emulators
that relied on them. This reduced the number of supported games by approximately 25%.
Disabling the drivers required some manual work since there was no easy way to determine
the CPU used by a given driver. We ended up introducing our own list of compatible drivers in src/mame/nacl.lst
We dealt with the atomic ops by changing the code to standardize on compare_exchange32(), which was then
implemented using gcc's __sync_val_compare_and_swap intrinsic.
The remaining hand-coded assembly code was replaced with C implementations that MAME provided but did not use by default.
Note that we detected the sources of machine instructions mentioned above ahead of time, i.e. before actually compiling the code. It is entirely possible that sequences of inlined assembly code will go unnoticed and cause validation errors when the Native Client executable is launched. In this case the tools below will come in handy to track down the origin of the inlined assembly:
$(NACL_SDK_ROOT)/toolchain/linux_x86/bin/ncval_x86_32$(NACL_SDK_ROOT)/toolchain/linux_x86/bin/ncval_x86_64Use ncval_x86_32 if you're developing on a 32-bit machine, and ncval_x86_64 if you're developing on a
64-bit machine.
The Native Client toolchain currently ships with newlib, which is a
lightweight libc implementation that differs slightly from the more commonly used glibc.
The newlib-related problems we encountered during the port were all minor and could be fixed in a few minutes.
We list them here for completeness:
getc, which clashed with the newlib macro for getc.
It is not clear why this did not cause problems with glibc. We fixed this problem by simply
inserting "#undef getc" just before the field.infinity, which clashed with a symbol
defined by the math library. We fixed this problem by simply renaming infinity to spu_infinity.#defined its own version of log2(), which was already defined
in the math library. Guarding it with #if !defined(log2) solved the problem._ADD, which clashed with some unknown header file #define.
Adding #undef _ADD before the enum fixed the problem.The port of MAME was relatively challenging; combined with figuring out how to port SDL-based games and load resources in Native Client, the overall effort took us about 4 days to complete.
The core port resulted in a diff of about 1200 lines, changing about 20 files. That does not include:
src/mame/mame.lst (12600 lines) – the list of drivers supported by the portsrc/osd/sdl/nacl_file.c (400 lines) – code to deal with file access to resourcessrc/osd/sdl/nacl_glue.c (300 lines) – PPAPI/SDL glue code