In my previous post I was writing about cross-compiling Node.js for Android into a shared .so library. In this post I would like to show how you can embed this shared library into your Android app. You can find a complete example app on github. The shared library - libnode.so - will be loaded inside a static block during the start of the application. The JavaScript files that contain the actual code that should be executed by Node.js will be stored inside assets and copied to cache during application start-up. The normal way to start executing JavaScript from Node.js is to provide the .js file as command line argument to node executable. We can do the same thing by preparing a "fake" argv parameter array and pass it to node::Start() function.

Configuring the Build System in Android Studio(build.gradle & CMakeLists.txt)

When creating a new project in Android Studio, make sure you tick "Include C++ support" checkbox. This should create a CMakeLists.txt file inside of your app module's directory and change the build script inside of build.gradle. Your build.gradle should contain the proper path to CMakeLists.txt and some additional NDK-related configuration. In my build.gradle I also like to set some additional build flags for the used toolchain, platform, STL, and I also pick the abi(I've built libnode.so only for arm, so I changed the abi to armeabi-v7a). In your CMakeLists.txt you need to add the path to Node.js include headers, ensure that the external libnode.so shared library is copied to the appropriate folder that is used by the build system, and ensure that you can call functions of the shared lib from your native part of the project. You should add the following lines to your CMakeLists.txt

# Replace $ENV{VENDOR}/nodejs/.." with the path to
# your Node.js includes
include_directories(SYSTEM $ENV{VENDOR}/nodejs/include )
include_directories(SYSTEM $ENV{VENDOR}/nodejs/include/v8_include )

add_library( nodejs-lib SHARED IMPORTED)

# Replace "$ENV{VENDOR}/nodejs/lib/${ANDROID_ABI}/libnode.so" with
# the path to your libnode.so shared lib
set_target_properties( nodejs-lib
                      PROPERTIES IMPORTED_LOCATION
                      $ENV{VENDOR}/nodejs/lib/${ANDROID_ABI}/libnode.so )

# It still seems to be necessary to manually copy the external shared lib
# to the proper Android Studio project directory
file(COPY $ENV{VENDOR}/nodejs/lib/${ANDROID_ABI}/libnode.so
    DESTINATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})

# Link other required libraries with you native code part
find_library( log-lib log )
target_link_libraries( native-lib ${log-lib} nodejs-lib )

Loading Native Shared Libraries Into Memory

If you've set up your build scripts correctly, the build system should package at least two native libraries into the final APK file - your compiled native code, and libnode.so. To load the libraries into memory, your java class should contain a static block with System.loadLibrary() calls

    static {
        System.loadLibrary("node");
        System.loadLibrary("native-lib");
    }

Enabling Logging From console.log

To be able to see the output of console.log in Android logs, it is necessary to redirect the standard output streams into logcat. The basic idea is to redirect the output stream into a pipe, and then use android logging api to print the data read on the other end of pipe. Here you can find more interesting details about redirecting std streams to Android's log. I used the following code to make console.log work

    // Redirect streams to pipe()
    setvbuf(stdout, 0, _IOLBF, 0);
    setvbuf(stderr, 0, _IONBF, 0);
    static int pfd[2];
    pipe(pfd);
    dup2(pfd[1], 1);
    dup2(pfd[1], 2);

    // Start the logging thread
    auto logger = std::thread([](int* pipefd){
        char buf[128];
        std::size_t nBytes = 0;
        while((nBytes = read(pfd[0], buf, sizeof buf - 1)) > 0) {
            if(buf[nBytes - 1] == '\n') --nBytes;
            buf[nBytes] = 0;
            LOGD("%s", buf);
        }
    }, pfd);

    logger.detach();

This part should be inserted somewhere into your native C++ sources before you call node::Start() function.

Preparing argv Array With Arguments for Node and Calling node::Start()

The function that starts Node.js - node::Start() - takes the standard argv parameter of a main function in c/c++. Normally you pass these arguments from shell. The argument with index 1 is the filename of the js script that should be executed by node. In this case I don't start node from shell, but I can emulate this from code. You can either create a 2-dimensional array on the stack, like this - char* argv[] = {"node", "file.js", NULL}. However, I wanted to create argv dynamically from strings passed from Java to C. I just created an std::vector<char> and copied the string arguments passed from Java. The char pointers point to the beginning of each char array argument. It is not enough that only the char pointers are consecutive in memory. Also the content of the actual parameters should be consecutive. That means, if first parameter points to the string "node" and second parameter to "main.js"(char argv[] = {"node", "main.js", NULL}), advancing the pointer by 5 should point to the beginning of "main.js"(parameters are separated by '\0' terminator).

Java_eu_sisik_nodeexample_NodeService_startNode(JNIEnv *env, jobject instance, jobjectArray args)
{
    // Redirect logging here
    // ...

    int count = env->GetArrayLength(args);

    // Create a char buffer from all passed args that is continuous in memory
    std::vector<char> buffer;
    for (int i = 0; i < count; i++)
    {
        jstring str = (jstring)env->GetObjectArrayElement(args, i);
        const char* sptr = env->GetStringUTFChars(str, 0);

        // Append chars to the end of buffer
        do {
            buffer.push_back(*sptr);
        }
        while(*sptr++ != '\0');
    }

    std::vector<char*> argv;
    argv.push_back(&buffer[0]); // Push first argument ("executable" name)
    for (int i = 0; i < buffer.size() - 1; i++)
        if (buffer[i] == '\0') argv.push_back(&buffer[i+1]); // addresses of other arguments

    argv.push_back(NULL); // argv should be terminated by a null ptr

    // Start node with my fake argv
    node::Start(argv.size() - 1, argv.data());
}

Calling Native Code From Java

Once the native part is set up, in Java we can declare a native function that starts Node. We can pass the arguments as String array. The function should be called from a separate thread because the call to node::Start() will block. The java declaration of the native method from the previous code snippet(Java_eu_sisik_nodeexample_NodeService_startNode(...)) should look something like this

    package eu.sisik.nodeexample;
    ...
    class NodeService extends Service {
        ...
        private static native startNode(String... args)
    }

Ideally, the Java code that starts Node should be called from an Android Service that lives in a separate process. Node's process lifecycle management doesn't seem to fit completely with Android's way of managing app processes. When for example there is an error in JavaScript code, Node.js might try to exit from the whole process the app is running directly, ignoring Android's app lifecycle. I haven't had time yet to check the sources and find a way how to somehow manage Node's event loop manually from a regular thread.

The code snippets here only outline the main steps that I used to embed Node inside my Android app. If this is not enough to get you started, please also check the accompanying github example app.

Next Post Previous Post

Add a comment