使用 Haskell 與 Android NDK 進行 Linux 原生開發

背景

Android 是 Google 公司基於 Linux 平臺開發的開源手機操做系統, 天然要對 C C++ 提供原生支持. 經過 NDK, Android應用程序能夠很是方便地實現 Java 與 C/C++代碼的相互溝通.java

隨着語言的發展, 近些年來出現了一些諸如 Rust, Haskell, Go等新的系統編程語言對 C/C++ 的系統編程語言地位發起了強烈的攻擊. (Kotlin-Native 這門技術也能實現 native 開發, 不過要依賴專門的垃圾回收器進行內存管理)linux

另外, 經過相應的交叉編譯鏈, 使它們在 Android 平臺上進行 NDK 開發成爲了新的可能.android

本文的主角是 Haskell, 一門純函數式編程語言.git

正文

本文會實現一個 JNI 的例子, 其中相關的 so 庫是使用 少許 C++ 代碼 + Haskell 來實現的.github

kotlin 層 (java 層)

這是 Activity 代碼 (裏面包含一個 JNI 接口) :編程

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, "已成功授予全部權限!", 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() {
    			// 搜索 "/sdcard/*.txt" 下的 text 文檔.
                 Log.w("demo", "${namesMatchingJNI("/sdcard/*.txt").joinToString()}}")
    }
    
    // 使用通配符進行模糊匹配搜索 sdcard 的相關文件
    external fun namesMatchingJNI(path: String): Array<String>
}
複製代碼

native 層 (C++ 和 Haskell)

  • 先來看相關 C++ 代碼:
#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);
    return strs;
}

}
複製代碼

上面這代碼只是少許的 C++ 代碼, 主要的功能是稍微地封裝調用 Haskell 實現的 namesMatching 函數.ruby

由於 JNI 不能直接調用 Haskell 代碼實現的函數, 藉助 FFI 實現間接調用 (跟 Rust 同樣):app

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

複製代碼
  • 接着使用 Haskell 實現的 namesMatching 函數:
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
            -- 在只有文件名的狀況下, 只在當前目錄查找.
              of
            ("", baseName) -> do
                curDir <- getCurrentDirectory
                listMatches curDir baseName
            -- 在包含目錄的狀況下
            (dirName, baseName)
                -- 因爲目錄自己可能也是一個符合 glob 模式的字符牀, 如(/foo*bar/far?oo/abc.txt)
             -> do
                dirs <-
                    if isPattern dirName
                        then namesMatching (dropTrailingPathSeparator dirName)
                        else return [dirName]
                -- 通過上面操做, 拿到全部符合規則的目錄
                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 ()
_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 ()
複製代碼

咱們藉助交叉編譯鏈, 把這段 Haskell 代碼編譯成靜態庫, 名爲 libHSandroid-hs-mobile-common-0.1.0.0-inplace-ghc8.6.5.a編程語言

其中 foreign export ccall "namesMatching" _namesMatchingC :: CString -> IO (Ptr CStringArrayLen)FFI 接口, 暴露給 C++ 代碼調用.ide

  • 接着, 在 Android app 主項目的 cmake 配置文件中 link 上面這個靜態庫 libHSandroid-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
        
        ...
        )
複製代碼
  • 最後, 編譯運行 Android app 主項目:

運行的結果(經過打印 log 來呈現):

// logcat

2019-08-26 18:14:43.662 12344-12344/com.xxx.yyy.helloworld W/demo: /sdcard/jl.txt, /sdcard/ceshitest.txt, /sdcard/treeCallBack.txt}

複製代碼

總結

Android 的 NDK 開發並非只有 C/C++, 還有別的一方天地. 特別是使用 C/C++ 寫業務邏輯的場景, 開發效率特別低.

相關文章
相關標籤/搜索