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:
- Change the build system to use the toolchain provided by the Native Client SDK.
- Change immediately visible build configuration options to something Unix-ish.
- Add glue code for PPAPI (the bridge between the source code and the browser, also known as "Pepper").
- Get the code to compile.
- Get the code to link.
- Get the code to run.
- Modify the code as needed and iterate.
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.
Preliminary step: identifying and eliminating problem areas
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:
- We dropped all CPU emulation based on the Universal Dynamic Recompiler (UDR) because it would require significant to work to make JIT'ed code sequences compliant with Native Client.
- We dropped use of OpenGL to simplify the port (PPAPI only supports OpenGLES).
- We dropped use of the
SDL_ttflibrary to reduce porting time.
We found a number of additional problems by grep'ing through the code base:
||Native Client does not support signal handling.|
||Native Client does not support arbitray change of page table protection.|
||Native Client does not support directory traversal.|
||Native Client does not support advanced pthread usage.|
||Hand-coded assembly needs special attention.|
||Loading of resources needs to be handled by the appropriate PPAPI interfaces.|
||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
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 build configuration
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:
We discuss a few of these modifications in detail below.
Tools that are built and run as part of the build
MAME builds a number of binary tools that are subsequently run as part of the build process. For example, the build rule below
$(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,
$(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
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.
sel_ldr emulator approach may not work on non-Linux systems. In this case, you can use an admittedly
- Do not change the build rule and wait for make to fail.
- Manually replace either the tool (e.g.,
$(FILE2STR)) or the target (e.g.,
$(OBJ)/%.lh) with a prebuilt version obtained from a build with the system toolchain.
- Re-run make.
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.,
to point to the Native Client toolchain, and the second set of variables (e.g.,
HOSTLD) to point to the system toolchain. Then simply replace
$HOSTCC in the rules
that build intermediate binaries. (This approach was not an option for MAME as it does not use
Another tool that is built and run as part of the MAME build process is
makedep tool scans
the entire source tree for C files and compiles a list of header file dependencies that is then made available to
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
makedep tool, but chose not to.)
Hand-coded machine instructions
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:
- a co-routine package in
src/lib/cothreadwith hand-coded assembly
- a JIT compiler called Universal Dynamic Recompiler (UDR), which is used to emulate recent CPU families (MIPS, SH2, POWERPC, RSP)
- various atomic ops and other hand-coded assembly in headers included via
We 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
We dealt with the atomic ops by changing the code to standardize on
compare_exchange32(), which was then
implemented using gcc's
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:
ncval_x86_32 if you're developing on a 32-bit machine, and
ncval_x86_64 if you're developing on a
Problems related to newlib
The Native Client toolchain currently ships with newlib, which is a
libc implementation that differs slightly from the more commonly used
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:
- One of the MAME classes had a member named
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.
- The sound emulation defined a static variable named
infinity, which clashed with a symbol defined by the math library. We fixed this problem by simply renaming
- Another part of sound emulation
#definedits own version of
log2(), which was already defined in the math library. Guarding it with
#if !defined(log2)solved the problem.
- In the CPU emulation, an enum contained the symbol
_ADD, which clashed with some unknown header file
#undef _ADDbefore 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 port
src/osd/sdl/nacl_file.c(400 lines) – code to deal with file access to resources
src/osd/sdl/nacl_glue.c(300 lines) – PPAPI/SDL glue code
- mame_nacl.zip (diff for our port of MAME)
- nacl-mounts (layer that implements POSIX file IO interface; it supports pluggable filesystems, including Pepper-based implementations)
- How to Port SDL Games to Native Client (includes sample glue code for PPAPI)
- Loading Resources in Native Client (forthcoming article)
- Porting XaoS to Native Client