background

Android is an open source mobile phone operating system developed by Google based on Linux platform, naturally providing native support for C++. The NDK makes it very easy for Android applications to communicate with Java and C/C++ code.

With the development of languages, some new system programming languages such as Rust, Haskell and Go have emerged in recent years to launch a strong attack on the status of C/C++ system programming language (Kotlin-native technology can also realize Native development. You rely on a dedicated garbage collector for memory management.)

In addition, through the corresponding cross-compilation chain, it becomes a new possibility for them to develop NDK on Android platform.

The subject of this article is Haskell, a purely functional programming language.

The body of the

This article implements a JNI example where the associated so library is implemented using a small amount of C++ code + Haskell.

Kotlin Layer (Java layer)

Here’s the Activity code (which contains a JNI interface) :

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var rxPermissions: RxPermissions = RxPermissions(this)

        rxPermissions
                .requestEachCombined(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ permission ->
                    // will emit 2 Permission objects
                    if (permission.granted) {
                        // `permission.name` is granted !
                        // toast. makeText(this@MainActivity, "All permissions granted successfully!" , Toast.LENGTH_SHORT).show()
                        doSthAfterAllPermissionGranted()
                    } else if (permission.shouldShowRequestPermissionRationale) {
                        // Denied permission without ask never again
                    } else {
                        // Denied permission with ask never again
                        // Need to go to the settings}})}private fun doSthAfterAllPermissionGranted(a) {
    			// Search the text document under "/sdcard/*.txt".
                 Log.w("demo"."${namesMatchingJNI("/sdcard/*.txt").joinToString()}}")}// Use wildcards for fuzzy matching to search sdcard related files
    external fun namesMatchingJNI(path: String): Array<String>
}
Copy the code

Native layer (C++ and Haskell)

  • Let’s start with the relevant C++ code:
#include <jni.h>

#include <unistd.h>
#include <sstream>
#include <string>

#include "ghcversion.h"
#include "HsFFI.h"
#include "Rts.h"

#include "my_log.h"
#include "Lib_stub.h"
#include "FileSystem_stub.h"
#include "ForeignUtils_stub.h"
#include "android_hs_common.h"

extern "C" {

JNIEXPORT jobjectArray
JNICALL
Java_com_xxx_yyy_MainActivity_namesMatchingJNI( JNIEnv *env, jobject thiz, jstring path) {

    LOG_ASSERT(NULL! = env,"JNIEnv cannot be NULL.");
    LOG_ASSERT(NULL! = thiz,"jobject cannot be NULL.");
    LOG_ASSERT(NULL! = path,"jstring cannot be NULL.");

    const char *c_value = env->GetStringUTFChars(path, NULL);
    CStringArrayLen *cstrArrLen = static_cast<CStringArrayLen *>(namesMatching(
            const_cast<char *>(c_value)));

    char **result = cstrArrLen->cstringArray;
    jsize len = cstrArrLen->length;

    env->ReleaseStringUTFChars(path, c_value);
    jobjectArray strs = env->NewObjectArray(len, env->FindClass("java/lang/String"),
                                            env->NewStringUTF(""));
    for (int i = 0; i < len; i++) {
        jstring str = env->NewStringUTF(result[i]);
        env->SetObjectArrayElement(strs, i, str);
    }
    // freeCStringArray frees the newArray pointer created in haskell module
    freeNamesMatching(cstrArrLen);
    returnstrs; }}Copy the code

The above code is just a bit of C++ code that encapsulates slightly the namesMatching function that calls Haskell.

Since JNI can’t call functions implemented by Haskell code directly, FFI makes indirect calls (just like Rust):


JVM -->  JNI  --> C++ -->  FFI --> Haskell

Copy the code
  • Then use theHaskellImplementation of thenamesMatchingFunction:
module Android.FileSystem
    ( matchesGlob
    , namesMatching
    ) where

import Android.ForeignUtils
import Android.Log
import Android.Regex.Glob (globToRegex.isPattern)

import Control.Exception (SomeException.handle)
import Control.Monad (forM)

import Foreign
import Foreign.C

import System.Directory (doesDirectoryExist.doesFileExist.getCurrentDirectory.getDirectoryContents)
import System.FilePath ((</>), dropTrailingPathSeparator, splitFileName)

import Text.Regex.Posix ((=~))

matchesGlob: :FilePath -> String -> Bool
matchesGlob name pat = name =~ globToRegex pat

_matchesGlobC name glob = do
    name <- peekCString name
    glob <- peekCString glob
    return $ matchesGlob name glob

doesNameExist: :FilePath -> IO Bool
doesNameExist name = do
    fileExists <- doesFileExist name
    if fileExists
        then return True
        else doesDirectoryExist name

listMatches: :FilePath -> String -> IO [String]
listMatches dirName pat = do
    dirName' <-
        if null dirName
            then getCurrentDirectory
            else return dirName
    handle (const (return []) :: (SomeException -> IO [String)) $do
        names <- getDirectoryContents dirName'
        let names' =
                if isHidden pat
                    then filter isHidden names
                    else filter (not . isHidden) names
        return (filter (`matchesGlob` pat) names')

isHidden('. ': _) =True
isHidden _ = False

listPlain: :FilePath -> String -> IO [String]
listPlain dirName baseName = do
    exists <-
        if null baseName
            then doesDirectoryExist dirName
            else doesNameExist (dirName </> baseName)
    return
        (if exists
             then [baseName]
             else [])

namesMatching: :FilePath -> IO [FilePath]
namesMatching pat
    | not $ isPattern pat = do
        exists <- doesNameExist pat
        return
            (if exists
                 then [pat]
                 else [])
    | otherwise = do
        case splitFileName pat
            -- Search only in the current directory when only the filename is present.
              of
            ("", baseName) -> do
                curDir <- getCurrentDirectory
                listMatches curDir baseName
            -- in the case of containing directories
            (dirName, baseName)
                -- Since the directory itself may also be a bed of characters that conform to the Glob pattern, such as (/foo*bar/far? oo/abc.txt)
             -> do
                dirs <-
                    if isPattern dirName
                        then namesMatching (dropTrailingPathSeparator dirName)
                        else return [dirName]
                After the above operation, get all the directories that match the rule
                let listDir =
                        if isPattern baseName
                            then listMatches
                            else listPlain
                pathNames <-
                    forM dirs $ \dir -> do
                        baseNames <- listDir dir baseName
                        return (map (dir </>) baseNames)
                return (concat pathNames)

_namesMatchingC: :CString -> IO (Ptr CStringArrayLen)
_namesMatchingC filePath = do
    filePath' <- peekCString filePath
    pathNames <- namesMatching filePath'
    pathNames' <- forM pathNames newCString :: IO [CString]
    newCStringArrayLen pathNames'

_freeNamesMatching: :Ptr CStringArrayLen -> IO(a)_freeNamesMatching ptr = do
    cstrArrLen <- peekCStringArrayLen ptr
    let cstrArrPtr = getCStringArray cstrArrLen
    freeCStringArray cstrArrPtr
    free ptr
    return ()

foreign export ccall "matchesGlob" _matchesGlobC :: CString -> CString -> IO Bool

foreign export ccall "namesMatching" _namesMatchingC :: CString -> IO (Ptr CStringArrayLen)

foreign export ccall "freeNamesMatching" _freeNamesMatching :: Ptr CStringArrayLen -> IO(a)Copy the code

We compiled this Haskell code into a static library named libhSandroid-hs-mobile-common-0.1.0.0-inplace-ghc8.6.5.a with the help of a cross-compilation chain

Foreign export ccall “namesMatching” _namesMatchingC :: CString -> IO (Ptr CStringArrayLen) is the FFI interface exposed to C++ code calls.

  • Next, link the static library above in the Cmake profile of the Android App main projectLibHSandroid - hs - mobile - common - 0.1.0.0 - inplace - ghc8.6.5. A

add_library(lib_hs STATIC IMPORTED)
set_target_properties(lib_hs
        PROPERTIES
        IMPORTED_LOCATION $ENV{HOME}/dev_kit/src_code/android-hs-mobile-common/dist-newstyle/build/${ALIAS_1_ANDROID_ABI}/ghc-8.6.5/android-hs-mobile-common-0.1.0.0/build/libHSandroid-hs-mobile-common-0.1.0.0-inplace-ghc8.6.5.a)

target_link_libraries( # Specifies the target library.
        native-lib

        # Links the target library to the log library
        # included in the NDK.. lib_hs ... )Copy the code
  • Finally, compile and run the main project of Android APP:

The result of the run (rendered by printing the log):

/ / logcat 18:14:43. 2019-08-26, 662, 12344-12344 / com. XXX, yyy, helloworld W/demo: /sdcard/jl.txt, /sdcard/ceshitest.txt, /sdcard/treeCallBack.txt}Copy the code

conclusion

Android NDK development is not just C/C++, there are other sides. In particular, the development efficiency is particularly low in scenarios where business logic is written in C/C++.