Friday 2 September 2016

Part 3: Going Native

Foreword

In the previous post we implemented two instructions within our emulator and ran a very simple 6502 machine language program.

As mentioned previously one of the goals of this blog series is to develop a emulator that run at an acceptable performance on a mobile Android device. One fact that comes into play with this goal, is that emulators are CPU intensive applications by nature and there is not really functionality that hardware acceleration can take care for us.

With CPU intensive applications it is recommended to use the Android NDK for developing a native application, or least for the performance critical parts.

For this reason, we will be exploring the Android NDK in this post.

We will start by exploring what the Android NDK is and how it is used in conjunction with the Android SDK.

Finally, we will rewrite our emulator from my previous blog using the Android NDK.

The NDK

As mentioned previously, the Android NDK is used to produced a binary that can run directly on the hardware cpu of the mobile device.

The end result of an application you develop using the Android SDK alone is also a binary, called byte code. This byte code needs to execute on an imaginary CPU called a virtual machine.

This imaginary CPU simulation adds some extra overhead to your application resulting in slower execution. This difference in performance between native vs VM shrinks with every new version of Android and apparently with Android 6.0 there is very little performance difference.

However, my mobile device don't support Android 6.0 yet, so I will give the VM-only challenge a miss for now :-)

One thing to take note of is that even if go completely native with your Android application, you will still require the Virtual machine in many instances. This is because many of the Android components, like MenuItem, is only available via the VM.

The Android OS, like the rest of the Java World, provides two way communication between native application code and Java via a mechanism called JNI (e.g. Java Native Interface).

More on JNI in coming sections...

Installing the Android NDK

Before we start, we need to install the Android NDK.

First step is to download the NDK. So, go to the site https://developer.android.com/ndk/downloads/index.html

Download the zip file for Linux.

Next, unzip the zip the file to any folder you have access to.

Next, we need to setup some environment variables. Preferably, the following environment variables you need to add to a script, so it automatically gets set each time you log into Linux:

export ANDROID_SDK=PathToAndroidSDK
export ANDROID_NDK=PathToAndroidNDK
export PATH=$PATH:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools:$ANDROID_NDK

That's it! The NDK is now installed and we can some development.

Making our Emulator Native

Let us do some native development.

Before we start I just would like to mention that the Android Studio Integration with the NDK is still very experimental. For instance, to compile the native code, you need to manually kick of the build process via a terminal session. The process will become clear in a moment.

Open up the Android project you finished off in the previous blog.

Alternatively, you can just download the GitHub release ch2, also mentioned in the previous blog. Then, just unzip the release and open up in Android Studio.

Then, right click on tha app node, and select New/Folder/JNI Folder. You will see a new folder created called jni. This will be the folder where all the native files will go into.

Next, right click on the jni folder and select C/C++ source file. In the box that pops up specify memory as the name and .c as the type. The source for memory.c should be as follows:

#include <string.h>
#include <jni.h>

jchar mainMem[] = {0xa9, 0x20, 0x85,0x8,0,0,0,0,0,0,0};

jchar memory_read(int address) {
  return mainMem[address];
}

void memory_write(int address, jchar value) {
  mainMem[address] = value;
}

Let us discuss this piece of code. The first interesting piece is the include jni.h. This provides access some essential declarations for our native code to interface with JNI to the virtual machine. A typical jni declaration used within this snippet of code is jchar. jchar ensure that the correct native type will be chosen, that corresponds to a char type on the java site.

We move onto the declaration of the variable mainMem. It looks almost exactly the same as the declaration we did in my previous blog where we did the Java implementation of the Memory class.

The memory_read and memory_write functions are fairly self explanatory.

Next, you need to create another .c source file within the jni folder called cpu.c. This source file will make use make use of the functions memory_read and memory_write of the file memory.c. In order to do this, we need to define a prototype for these methods within a header file. We will call this file memory.h and also create it within the jni folder. The contents for this header file should look as follows:

#ifndef MY_APPLICATION_MEMORY_H
#define MY_APPLICATION_MEMORY_H

#endif //MY_APPLICATION_MEMORY_H

unsigned char memory_read(int address);
void memory_write(int address, unsigned char value);


Finally, we get to cpu.c. This file should look as follows:

#include <memory.h>
#include <jni.h>

    jchar acc = 0;
    jchar xReg = 0;
    jchar yReg = 0;
    int pc = 0;
    int zeroFlag = 0;
    int negativeFlag = 0;



    void step() {
        int opCode = memory_read(pc);
        int address;
        pc++;
        switch (opCode) {
            case 0xa9:
                acc = memory_read(pc);
                zeroFlag = (acc == 0) ? 1 : 0;
                negativeFlag = ((acc & 0x80) != 0) ? 1 : 0;
                pc++;
            break;
            case 0x85:
                address = memory_read(pc);
                memory_write(address, acc);
                pc++;
            break;
        }
    }


As you can see, we include the header file memory.h, which gives us access to the memory methods.

We are about done with our native changes. What remains to be done is to create a build file that will tell the NDK build system what do with out source file.

Create a normal file, also within the jni folder called Android.mk. This file's contents should be as follow:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := native_emu
LOCAL_SRC_FILES := memory.c cpu.c
LOCAL_C_INCLUDES := $(LOCAL_PATH)/./
include $(BUILD_SHARED_LIBRARY)

This file file tells the NDK build system the following:
  • Which files are the source files
  • The include files are present in the same folder
  • Everything should be build into a module called native_emu.

Modifying the rest of the Application

From the previous section, it is obvious that we will be dropping the Java classes Cpu.java and Memory.java.

However, we will be retaining the class MainActivity.java. This will still be our application's front end showing us what is going on within our emulator.

Our Java front end will require two points of interaction with our native emulator:
  • Calling step on the emulator to execute an instruction
  • A way to get a memory dump from the native emulator
Let us start to tackle the step method.

First, before we can call any native method from Java, we first need to load the native module in which the required method resides. So, modify the MainActivity class as follows:

package com.johan.emulator;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

...
    static {
        System.loadLibrary("native_emu");
    }

}


The module name should be the same as the defined within Android.mk as LOCAL_MODULE.

Next, we should create a native method stub within the MainActivity class for the step method:

package com.johan.emulator;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

...
    public native void step();
...
}


The next step would be to add an entry point to the native_emu module for above mentioned native method stub.

To define the entry point, modify cpu.c as follows:

#include <memory.h>
#include <jni.h>

    jchar acc = 0;
    jchar xReg = 0;
    jchar yReg = 0;
    int pc = 0;
    int zeroFlag = 0;
    int negativeFlag = 0;



    void step() {
        int opCode = memory_read(pc);
        int address;
        pc++;
        switch (opCode) {
            case 0xa9:
                acc = memory_read(pc);
                zeroFlag = (acc == 0) ? 1 : 0;
                negativeFlag = ((acc & 0x80) != 0) ? 1 : 0;
                pc++;
            break;
            case 0x85:
                address = memory_read(pc);
                memory_write(address, acc);
                pc++;
            break;
        }
    }

void
Java_com_johan_emulator_MainActivity_step(JNIEnv* pEnv, jobject pObj)
{
  step();
}

So, basically we define the entry point method and within it we invoke the actual step method.

The signature of the entry point is very interesting. First of all, for these kind of methods you will always have at least the two shown parameters. You will always have these two parameters even if the method is invoked from the side without any parameters.

You will also see that entry point method has a very long name. How this method is composed is very important for JNI entry point methods.

Firstly, the method needs to start with the word Java. The rest of the method name is a fully package name qualified name of the Java native stub method. Note, however, that we use underscores and not dots.

Next, we should modify the onStepClick method of the MainActivity class to call the native step method and not the step method of the old Java Cpu class:

    public void onStepClick(View v) {
        step();
        refreshControls();
    }


Now, we should do the same exercise for the functionality showing the memory dump.

So, strictly speaking, we should be implementing a method returning the contents of the memory as a string within memory.c. String manipulation within C is a bit of a nightmare, however. So, let us try and make our life a bit easier. Within memory.c we just return the contents of the memory straight as an array, and do the string manipulation within Java.

We create the following method within memory.c:

jcharArray
Java_com_johan_emulator_MainActivity_dump(JNIEnv* pEnv, jobject pObj)
{
  jcharArray result;
  result = (*pEnv)->NewCharArray(pEnv,sizeof(mainMem)/sizeof(mainMem[0]));
  (*pEnv)->SetCharArrayRegion(pEnv,result,0,sizeof(mainMem)/sizeof(mainMem[0]),mainMem);
  return result;
}


This is a small Java reflection exercise :-) Arrays in java is actually an object, so we first need to create a Java CharArray object and then copy the contents of our native memory array to it. Finally we need to return this object.

As with our step method, we need to add a native stub method within our MainActivity class:

package com.johan.emulator;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
...
    public native char[] dump();
...
}


Also within MainActivity we need to create a method for converting the contents of the memory array to a String representation. We do a bit of copy and pasting from our old Java Memory class:

package com.johan.emulator;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

...
    public String getMemDumpAsString(char[] memContents) {
        String result = "";
        byte[] temp = new byte[48];
        for (int i=0; i < memContents.length; i++) {
            temp[i] = (byte) memContents[i];
        }

        for (int i = 0; i < temp.length; i++) {
            if ((i % 16) == 0) {
                String numberStr = Integer.toHexString(i);
                numberStr = "0000" + numberStr;
                result = result + "\n" + numberStr.substring(numberStr.length() - 4);
            }
            String number = "0" + Integer.toHexString(temp[i] & 0xff);
            number = number.substring(number.length() - 2);
            result = result + " " + number;
        }
        return result;
    }
    private void refreshControls() {
        TextView view = (TextView) findViewById(R.id.memoryDump);
        view.setText(getMemDumpAsString(dump()));
    }

...
}


We are finished coding. Now comes the fun part: Building everything...

Building and Running

Time for us to try and build everything.

Open up a terminal window and change to the jni directory.

At this point we assume that the ANDROID_SDK, ANDROID_NDK and PATH environment variables are setup as mentioned in a previous section.

Now, execute the command ndk-build. If all goes well, you will ndk compiling libraries for different platforms, like arm, x86 and so on.

After the build process has finished, you will see a new folder created called libs on the same level as the jni folder. Within the libs folder each platform will have a folder of its own.

The next thing we should do, is to make Android Studio aware of these native libraries. If we can get Android Studio to copy these libraries over to the target device during deployment, our app will just run.

The easiest way to make Android Studio see your native libraries is just to rename the libs folder to jniLibs. After the rename process you can just deploy normally. Android Studio will automatically detect the libraries and copy it over as part of the deployment process.

If all went well with the deployment, our App will behave exactly the same as in my previous blog. Not exactly exciting, but at least it is sanity check that we are on the right track with the native stuff :-)

In Summary

In the post we explored the Android NDK. We then installed it and then converted our emulator to a native version.

In the next post we will be implementing all the variants of the LOAD and STORE 6502 instructions within our emulator.

As usual, the source code I have provided as a downloadable zip file on GitHub: https://github.com/ovalcode/androidemuC64/releases/tag/ch3

No comments:

Post a Comment