Technical notes‎ > ‎

[Tool] Android NDK Native Program Launcher

Introduction

When developing Android application with native code, we typically need to debug it using pure native mode (compile the code using NDK, generate executable file, and execute it through adb shell). Sometimes, we even don’t want to spent time to develop an Android App, since we just simply want to test some native functions.

Take myself as an example, I deal with OpenCL and GPU everyday. Therefore, more than 80% of the program I wrote will be performance and benchmarking code. Since they directly operate the system driver or the hardware, running them in pure native mode via ADB becomes quite natural. I enjoy the efficiency and the speed the native programs bring me.

However, there are chances that the pure native code may not be able to run on the Android devices. The followings are some cases I met:

  1. The device is not ROOTed. So, you cannot push the native program to /data/ folder due to the lack of permission. You cannot give execute permission to the native executable by doing chmod +x file, therefore, it is not possible to run it on the device.
  2. You device has been ROOTed. You can run the code through ADB. However, when running the code with ADB, you have to connect your device to computer using a USB cable. What if you what to measure the system power consumption by some external power meter?? Since the USB cable provides extra power input to the device, connecting to the USB cable makes the power measurement impossible.
  3. For some devices, you don’t even have the right ADB drivers.
  4. Some unknown cases …

In all these cases, we need certain tools to help us run native program on un-rooted, standalone, no-USB-connected devices. After searching online, I didn’t find a satisfied one. Therefore, the Android Native Program Launcher is created.

Major Features

  • Do not require root permission.
  • You can run native code without ADB and USB cable connected to a computer.
  • Support loading native program from sdcard.
  • Support command line arguments.
  • Allow the user to set up LD_LIBRARY_PATH.
  • Support subfolders. Input files and configurations loaded by the native program can be put in subfolders, in the native program, use relative path to access them.
  • Support output files. Output files can be retrieved from the working directory.

Getting Started

Download address

NativeLauncher screenshot

What is provided?

  • An APK installer.
  • Some example NDK projects, along with build script.

Instructions

  1. Copy your native program (compiled by ndk-build) in a folder on your sdcard, e.g. /sdcard/test/.
  2. Copy all dependent shared libraries (.so) to that folder.
  3. Copy input files to the same folder.
  4. Press the button “Load native program”.
  5. Set command line arguments if there is any.
  6. Press button “Run” to execute the program.
  7. Check results under your folder, such as /sdcard/test/.

Some Details

  1. Q: Where is the working directory?
    A: The working directory is the directory the executable located.

  2. Q: How to set LD_LIBRARY_PATH? What is the library loading order?
    A: In the textedit field, type your custom library path. Please notice that, your working directory will be added to LD_LIBRARY_PATH automatically. For example, if you type “mylib“ in the field, the LD_LIBRARY_PATH will be set as LD_LIBRARY_PATH=mypath:working_directory:$LD_LIBRARY_PATH.

  3. Q: How to use subfolder?
    A: Subfolder can be accessed via relative path. For example, if we have a folder named “image“ in working directory containing an input image “lena.png“, in your code, you can access it via image/lena.png. Your output files will also be under the working directory.

Examples

Two examples are provided to demonstrate the functionality of the native program launcher. You can find the pre-compiled binaries in bin directory.

If you want, you can also compile the code using NDK. I provided a build_examples.sh script to simplify this process, please install NDK and make sure ndk-build in the system path, then you can use build_examples.sh to build the examples as follows:

$ cd examples
$ ./build_examples.sh

After compiling the code, let’s push the binaries into the sdcard

$ adb shell "mkdir -p /sdcard/examples"
$ adb push bin /sdcard/example

Then, you can use the NativeLauncher app to load native program and execute it.


Let’s next take a look at the details of two examples.

Example 1

test1.cpp

#include <iostream>
#include <fstream>

int main(int argc, char ** argv)
{
    if(argc > 1)
    {
        std::cout << "argc: " << argc << std::endl;
        for(int i = 0; i < argc; i ++)
        {
            std::cout << "argv[" << i << "]: " << argv[i] << std::endl;
        }
        std::cout << std::endl;
    }

    std::cout << "cout: Hello world!" << std::endl;
    std::cerr << "cerr: Hello world!" << std::endl;


    std::cout << "\nTest:\n" << std::endl;
    for(int i = 0; i < 20; i ++)
    {
        for(int j = 0; j < i; j ++)
            std::cout << "*";

        std::cout << std::endl;
    }

    std::ofstream ofs("results.txt");
    if(ofs.is_open()){
        ofs << "This is just a test." << std::endl;

        ofs.close();

        std::cout << "results.txt is generated." << std::endl;
    }

    return 0;
}

As you can see, this demo basically prints out command line arguments, some test messages, a simple figure using *, and finally save a results.txt file into sdcard.

We compile test1.cpp using the following makefile:
Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test1
LOCAL_LDLIBS   = -llog
LOCAL_CFLAGS    += -fPIE
LOCAL_LDFLAGS     += -fPIE -pie 
LOCAL_SRC_FILES := test1.cpp
include $(BUILD_EXECUTABLE)

Example 2

This example shows the support for shared library and LD_LIBRARY_PATH. We define a add() function and compile it into a shared library. Then in the main() function, we call add() function from the shared library to perform the computation.

addfunc.cpp

int add(int a, int b)
{
    return a + b;
}

test2.cpp

#include <iostream>

extern int add(int, int);
int main(int argc, char ** argv)
{
    std::cout << "Test shared library." << std::endl;
    int a = 1;
    int b = 2;
    std::cout << "a=" << a << std::endl
        <<"b=" << b << std::endl
        << "add(a, b)=" << add(a, b) << std::endl;

    return 0;
}

Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := addfunc
LOCAL_SRC_FILES := addfunc.cpp
include $(BUILD_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := test2
LOCAL_SRC_FILES := test2.cpp
LOCAL_CFLAGS    := -fPIE
LOCAL_LDFLAGS     := -fPIE -pie 
LOCAL_LDLIBS    := -L$(LOCAL_PATH)/../libs/$(TARGET_ARCH_ABI)/ -laddfunc 
include $(BUILD_EXECUTABLE)

Since the libaddfunc.so and test2 are in the same folder, when you load test2 executable file, the shared library libaddfunc.so is loaded automatically. So, when you run the program, you will see correct output:

Test shared library.
a=1
b=2
add(a,b)=3

However, if we move libaddfunc.so to a sub-directory called lib:

$ adb shell
$ cd sdcard/example/example2
$ mkdir lib
$ mv libaddfunc.so lib/

Then, if we execute the native program again, since this time the shared library is not in the search path, we will see the following message:

CANNOT LINK EXECUTABLE: could not load library "libaddfunc.so" needed by ".test2"; caused by library "libaddfunc.so" not found.

To fix this, we can type lib in the LD_LIBRARY_PATH text field. That being said, the current library search path becomes:

LD_LIBRARY_PATH=lib:working_directory:$LD_LIBRARY_PATH

Here, working directory is the folder you native program is located.

By doing this, we run the native program by pressing “Run” button again, and we should be able to see the right output message. This means that the shared library has been found and loaded.