[Kotlin/Native] 封裝 JNI 常用函數(shù)

先來看一個最基本的 K/N 作用于 JNI 的函數(shù)讶泰,它將是一切的開端:

@CName("Java_com_rarnu_common_HelloJni_hello")
fun jniHello(env: CPointer<JNIEnvVar>, thiz: jobject): jstring = memScoped {
    return env.pointed.pointed!!.NewStringUTF!!.invoke(env, "Hello NDK".cstr.ptr)!!
}

你是不是會覺得寫這樣的代碼很麻煩,一點都不 Kotlin拂到,甚至還有一些反感痪署?

如果不爽就對了,如果爽的話也就沒有這篇了兄旬,為了把代碼寫舒服了狼犯,真的也是要付出不少代價的,至少在 K/N 的場景下领铐,沒有一些舒服的封裝真的會讓人生不如死的悯森。

那么開始正文,首先想要封裝的東西是文件操作的 API绪撵,由于之前基本上都在用 JVM 下的 Kotlin瓢姻,遇到文件操作基本上就直接 File 了,可惜 K/N 下沒有這東西音诈,K/N 能用的東西基本上就是 Kotlin 標準庫以及 cinterop雹舀,雖說很強大唆迁,但是真的用起來卻是實實在在的麻煩治专,比如讀取一個文本文件:

actual fun readText() = memScoped {
    val st = alloc<stat>()
    stat(innerFilePath, st.ptr)
    val size = st.st_size
    val buf = allocArray<ByteVar>(size)
    val f = fopen(innerFilePath, "rb")
    fread(buf, 1UL, size.toULong(), f)
    fclose(f)
    buf.toKString()
}

誒沼撕,這么一寫不就是 C 么?Kotlin 自己沒有 API喇聊?是的恍风,目前就是沒有,所以才如此強調(diào) cinterop誓篱,所幸的是 cinterop 轉(zhuǎn)換的函數(shù)與 Kotlin 相容性很好朋贬,而且也忠于原始的 C 庫。

這里有一個關(guān)鍵的關(guān)型窜骄,即 stat锦募,它的定義是這樣的:

@kotlinx.cinterop.internal.CStruct 
public final class stat public constructor(rawPtr: kotlinx.cinterop.NativePtr ) : kotlinx.cinterop.CStructVar

或許一開始接觸的人都會很郁悶,構(gòu)造函數(shù)里那個 NativePtr 參數(shù)是啥啊研,要怎么傳參構(gòu)造呢御滩?在這里我必須告訴各位的是,以后看到這種構(gòu)造方式的類型党远,直接在 memScoped 里面 alloc 就好削解,可以直接得到想要的對象,至于利用構(gòu)造函數(shù)來構(gòu)造沟娱,放棄這個想法吧氛驮。相同的情況也出現(xiàn)在構(gòu)造 jni 調(diào)用 java 的傳參問題上,后面會講到济似。

在 K/N 里面矫废,JetBrains 還算照顧我們,提供了一些不錯的轉(zhuǎn)換函數(shù)砰蠢,比如說以下這些蓖扑,可以讓開發(fā)變得更簡單:

toKString()    //  將一個CPointer<ByteVar> 轉(zhuǎn)換成 Kotlin 字符串
readBytes()    //  將一個 CPointer<ByteVar> 轉(zhuǎn)換成 Kotlin 的 ByteArray
cstr           // 將一個 Kotlin 字符串轉(zhuǎn)換成 CValues<ByteVar>
toCValues()    // 將一個 Kotlin ByteArray 轉(zhuǎn)換成 CValues<ByteVar>

所以我們可以在此基礎(chǔ)之上,輕松的寫出以下函數(shù):

actual fun readContent() = memScoped {
    val st = alloc<stat>()
    stat(innerFilePath, st.ptr)
    val size = st.st_size
    val buf = allocArray<ByteVar>(size)
    val f = fopen(innerFilePath, "rb")
    fread(buf, 1UL, size.toULong(), f)
    fclose(f)
    buf.readBytes(size.toInt())
}
actual fun writeContent(content: ByteArray) = memScoped {
    val f = fopen(innerFilePath, "wb")
    val buf = content.toCValues()
    val ret = fwrite(buf, buf.size.toULong(), 1, f)
    fclose(f)
    ret == 0UL
}
actual fun list(): List<String> = memScoped {
    val list = mutableListOf<String>()
    val d = opendir(innerFilePath)
    while (true) {
        val entry = readdir(d)
        if (entry == NULL) break
        val dname = entry!!.pointed.d_name.toKString()
        if (dname == "." || dname == ".." || dname.trim() == "") continue
        list.add(dname)
    }
    return list
}

同樣的思路台舱,可以把常用的文件操作都包裝起來律杠,就像 JVM 下的 File 一樣,在這里(點擊查看)我放了一份封裝后的文件竞惋,可以直接取用柜去。


下面要來封裝一下 JNI 相關(guān)的函數(shù)了,像開頭那種寫法太不友好了拆宛,有必要造橋嗓奢。橋的造法各有千秋,這里我不打算對各種方法作任何的評論浑厚,只談自己的封裝方法股耽。

首先我一定會把 env: CPointer<JNIEnvVar> 這個對象封裝掉,在 JNI 方法中钳幅,時時刻刻要用它豺谈,于是先寫個簡單架子,把 env 和一些常用函數(shù)寫進去:

data class JniClass(val jclass: jclass)
data class JniObject(val jobject: jobject)
data class JniMethod(val jmethod: jmethodID)

fun asJniClass(jclass: jclass?) = if (jclass != null) JniClass(jclass) else null
fun asJniObject(jobject: jobject?) = if (jobject != null) JniObject(jobject) else null
fun asJniMethod(jmethodID: jmethodID?) = if (jmethodID != null) JniMethod(jmethodID) else null

class JniBridge(val env: CPointer<JNIEnvVar>) {
    private val innerEnv = env.pointed.pointed!!
    private val fNewStringUTF = innerEnv.NewStringUTF!!
    private val fGetStringUTFChars = innerEnv.GetStringUTFChars!!
    private val fReleaseStringUTFChars = innerEnv.ReleaseStringUTFChars!!
    ... ...
}

基本的架子就這么簡單贡这,后面慢慢加內(nèi)容茬末,有了這些東西后,可以寫兩個基本函數(shù)盖矫,用于完成 Stringjstring 的相互轉(zhuǎn)換:

class JniBridge(val env: CPointer<JNIEnvVar>) {
    ... ...
    private fun toJString(string: String) = memScoped {
        val result = asJniObject(fNewStringUTF(env, string.cstr.ptr))
        check()
        result
    }
    private fun toKString(string: jstring) = memScoped {
        val isCopy = alloc<jbooleanVar>()
        val chars = fGetStringUTFChars(env, string, isCopy.ptr)
        var ret: String? = null
        if (chars != null) {
            ret = chars.toKString()
            fReleaseStringUTFChars(env, string, chars)
        }
        ret
    }
}

這次再次遇到 alloc<jbooleanVar> 這種寫法丽惭,同樣的,它也是一個接受 NativePtr 作為構(gòu)造參數(shù)的類型辈双,可以直接 alloc责掏。

寫完后要怎么用呢?方法如下:

val jniStr = JniBridge(env).toJString("hello")

此時就能得到一個 jstring 類型的對象了湃望。但是對于我來說换衬,我覺得它依然不方便痰驱,我希望可以在字符串對象上直接轉(zhuǎn)換,那么再擴展下:

class JniBridge(val env: CPointer<JNIEnvVar>) {
    ... ...
    fun String.asJString() = toJString(this)!!.jobject
    fun jstring.asKString() = toKString(this)
}

這下可好瞳浦,是不是更加不知道怎么用了担映?因為 Stringjstring 的上下文都沒有嘛,現(xiàn)在就要開始變魔術(shù)了叫潦,我們在 JniBridge 里再加一個方法:

class JniBridge(val env: CPointer<JNIEnvVar>) {
    ... ...
    val fPushLocalFrame = innerEnv.PushLocalFrame!!
    val fPopLocalFrame = innerEnv.PopLocalFrame!!
    ... ...
    inline fun <T> withLocalFrame(block: JniBridge.() -> T): T {
        if (fPushLocalFrame(env, 0) < 0) throw Error("Cannot push new local frame")
        try { return block() } finally { fPopLocalFrame(env, null) }
    }
}

有了這個函數(shù)后蝇完,我們可以在全局加一個函數(shù),來實現(xiàn)對完整上下文的包裝:

inline fun <T> jniWith(env: CPointer<JNIEnvVar>, block: JniBridge.() -> T) = 
    JniBridge(env).withLocalFrame(block)

下面就是使用了矗蕊,我們現(xiàn)在已經(jīng)完成了變魔術(shù)所需要的條件短蜕,把本文開頭的那個函數(shù)改一下:

@CName("Java_com_rarnu_common_HelloJni_hello")
fun jniHello(env: CPointer<JNIEnvVar>, thiz: jobject): jstring = jniWith(env) {
    "Hello NDK".asJString()
}

這么一來,就看不到 JNI 函數(shù)在背后的動作了傻咖,API 非常簡潔朋魔,對開發(fā)者友好。我們可以把所有的操作都寫在 jniWith 里面卿操,它具備 JniBridge 的完整上下文铺厨。


下面再來看一下如何從 JNI 調(diào)用 Java 方法,有了上面的封裝經(jīng)驗后硬纤,要搞個好玩的東西出來就簡單了:

class JniBridge(val env: CPointer<JNIEnvVar>) {
    ... ...
    private val fFindClass = innerEnv.FindClass!!
    private val fGetMethodID = innerEnv.GetMethodID!!
    private val fCallObjectMethodA = innerEnv.CallObjectMethodA!!
    private val fGetObjectClass = innerEnv.GetObjectClass!!
    ... ...

    fun findClass(name: String) = memScoped { 
        asJniClass(fFindClass(env, name.cstr.ptr)) 
    }
    fun getObjectClass(obj: jobject) = memScoped { 
        asJniClass(fGetObjectClass(env, obj)) 
    }
    fun getMethodID(clazz: JniClass?, name: String, signature: String) = memScoped {
        asJniMethod(fGetMethodID(env, clazz?.jclass, name.cstr.ptr, signature.cstr.ptr)) 
    }
    fun callObjectMethod(receiver: JniObject?, method: JniMethod, vararg arguments: Any?) = memScoped {
        asJniObject(fCallObjectMethodA(env, receiver?.jobject, method.jmethod, null))
    }
}

細心的話你會發(fā)現(xiàn)這里留了個尾巴解滓,調(diào)用函數(shù)如何傳參呢?雖然寫了 arguments 參數(shù)筝家,但是實際傳參是 null洼裤,很顯然這里需要把參數(shù)補齊。

這個參數(shù)是一個 CPointer<jvalue> 類型的對象溪王,因此我們就必須把參數(shù)構(gòu)造成這樣的腮鞍,才可以正常傳遞,在此又要多寫一個函數(shù):

class JniBridge(val env: CPointer<JNIEnvVar>) {
    ... ...
    private fun toJValues(arguments: Array<out Any?>, scope: MemScope): CPointer<jvalue>? {
        val result = scope.allocArray<jvalue>(arguments.size)
        arguments.mapIndexed { index, it -> when (it) {
            null -> result[index].l = null
            is JniObject -> result[index].l = it.jobject
            is String -> result[index].l = toJString(it)?.jobject
            is Int -> result[index].i = it
            is Long -> result[index].j = it
            is Byte -> result[index].b = it
            is Short -> result[index].s = it
            is Double -> result[index].d = it
            is Float -> result[index].f = it
            is Char -> result[index].c = it.toInt().toUShort()
            is Boolean -> result[index].z = (if (it) JNI_TRUE else JNI_FALSE).toUByte()
            else -> throw Error("Unsupported conversion for ${it::class.simpleName}")
        }}
        return result
    }
}

看起來很復(fù)雜莹菱,但是實質(zhì)上是在根據(jù)不同的參數(shù)類型移国,對 jvalue 進行填充,這里再一次的用到了 alloc道伟,來對一組 jvalue 進行初始化迹缀,這是必須掌握的一種寫法,要好好記住哦:)

這里還有一個 scope: MemScope 參數(shù)蜜徽,這是個什么東西呢祝懂?其實它來源于 memScoped 方法,會直接構(gòu)造出一個 MemScope 類型拘鞋,toJValues 必須用這種傳入 scope 的方法來實現(xiàn)砚蓬,是因為 memScoped 會在函數(shù)結(jié)束時,回收分配的內(nèi)存盆色,而我們構(gòu)造出來的 CPointer<jvalue> 卻必須被返回灰蛙,并且被使用后才可以銷毀祟剔,因此對它的內(nèi)存管理必須依賴上一個 scope。

好了摩梧,有了這個方法后物延,改一下上面的代碼:

fun callObjectMethod(receiver: JniObject?, method: JniMethod, vararg arguments: Any?) = memScoped {
    asJniObject(fCallObjectMethodA(env, receiver?.jobject, method.jmethod, toJValues(arguments, this@memScoped)))
}

不過我依然覺得這樣不簡潔,我希望有更簡單的寫法障本,加一個擴展:

class JniBridge(val env: CPointer<JNIEnvVar>) {
    ... ...
    fun Array<*>.asJValues(scope: MemScope) = toJValues(this, scope)
}

這樣就又可以做一個很細小的改動了:

fun callObjectMethod(receiver: JniObject?, method: JniMethod, arguments: Array<Any>?) = memScoped {
    asJniObject(fCallObjectMethodA(env, receiver?.jobject, method.jmethod, arguments?.asJValues(this@memScoped)))
}

最終我們想要的效果是這樣的:

@CName("Java_com_rarnu_common_HelloJni_callJvm")
fun jniCallJvm(env: CPointer<JNIEnvVar>, thiz: jobject): jstring = jniWith(env) {
    val jcls = getObjectClass(thiz)
    val jmthd = getMethodID(jcls, "callFromNative", "(ILjava/lang/String;)Ljava/lang/String;")
    callObjectMethod(thiz, jmthd!!, 1, "NDK")!!.jobject
}

以上對于 JNI 的封裝,我同樣提供了一個完整文件供取用响鹃,點擊查閱

另外驾霜,我也發(fā)布了一個 K/N for NDK 的封裝庫,如果你打算使用 Kotlin 來開發(fā) NDK 應(yīng)用买置,可以直接在 gradle 內(nèi)使用它:

... ... 
maven { url 'http://119.3.22.119:8081/repository/maven-releases' }
... ...
implementation 'com.rarnu:kn-common-ndk:0.0.1'            // for common
... ...
implementation 'com.rarnu:kn-common-ndk-android64:0.0.1'  // for android64
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末粪糙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子忿项,更是在濱河造成了極大的恐慌蓉冈,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轩触,死亡現(xiàn)場離奇詭異寞酿,居然都是意外死亡,警方通過查閱死者的電腦和手機脱柱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門伐弹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人榨为,你說我怎么就攤上這事惨好。” “怎么了随闺?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵日川,是天一觀的道長。 經(jīng)常有香客問我矩乐,道長龄句,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任散罕,我火速辦了婚禮撒璧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘笨使。我一直安慰自己卿樱,他們只是感情好,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布硫椰。 她就那樣靜靜地躺著繁调,像睡著了一般萨蚕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蹄胰,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天岳遥,我揣著相機與錄音,去河邊找鬼裕寨。 笑死浩蓉,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的宾袜。 我是一名探鬼主播捻艳,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼庆猫!你這毒婦竟也來了认轨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤月培,失蹤者是張志新(化名)和其女友劉穎嘁字,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杉畜,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡纪蜒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了此叠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片霍掺。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖拌蜘,靈堂內(nèi)的尸體忽然破棺而出杆烁,到底是詐尸還是另有隱情,我是刑警寧澤简卧,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布兔魂,位于F島的核電站,受9級特大地震影響举娩,放射性物質(zhì)發(fā)生泄漏析校。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一铜涉、第九天 我趴在偏房一處隱蔽的房頂上張望智玻。 院中可真熱鬧,春花似錦芙代、人聲如沸吊奢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽页滚。三九已至召边,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間裹驰,已是汗流浹背隧熙。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留幻林,地道東北人贞盯。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像沪饺,于是被迫代替她去往敵國和親躏敢。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容