Native Client

Case Study: Porting MAME to Native Client

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).

Porting strategy

A typical approach for porting a small project to Native Client consists of the following steps:

  1. Change the build system to use the toolchain provided by the Native Client SDK.
  2. Change immediately visible build configuration options to something Unix-ish.
  3. Add glue code for PPAPI (the bridge between the source code and the browser, also known as "Pepper").
  4. Get the code to compile.
  5. Get the code to link.
  6. Get the code to run.
  7. 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_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.

Porting issues

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:

File Modifications
makefile
  • Set target platform to Unix.
  • Enable SDL.
  • Hook up the Native Client SDK toolchain.
  • Skip use of the makedep tool.
  • Provide special handling for tools that are built and run as part of the build process.
src/osd/sdl/sdl.mak
  • Disable use of X11 and OpenGL.
  • Hard-code Native Client include and library paths for SDL.
src/lib/lib.mak
  • Emulate co-routines using pthreads.
  • Disable building of code that reads directories.
src/emu/emu.mak
  • Disable building of code that reads directories.
src/emu/cpu/cpu.mak
  • Provide special handling for tools that are built and run as part of the build process.
src/build/build.mak
  • Disable building of the makedep tool.
src/mame/mame.mak
  • Disable build of CPUs and platform code that relies on the Universal Dynamic Recompiler.

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 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:

  1. Do not change the build rule and wait for make to fail.
  2. 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.
  3. 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., 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.)

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/cothread with 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 src/emu/eminline.h

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 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_64

Use 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.

Problems related to newlib

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:

  • 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 infinity to spu_infinity.
  • Another part of sound emulation #defined its 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 #define. Adding #undef _ADD before the enum fixed the problem.

Conclusion

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

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.