[Kotlin/Native] 封装 JNI 常用函数

先来看一个最基本的 K/N 作用于 JNI 的函数,它将是一切的开端:

@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?是的,目前就是没有,所以才如此强调 cinterop,所幸的是 cinterop 转换的函数与 Kotlin 相容性很好,而且也忠于原始的 C 库。

这里有一个关键的关型,即 stat,它的定义是这样的:

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

或许一开始接触的人都会很郁闷,构造函数里那个 NativePtr 参数是啥,要怎么传参构造呢?在这里我必须告诉各位的是,以后看到这种构造方式的类型,直接在 memScoped 里面 alloc 就好,可以直接得到想要的对象,至于利用构造函数来构造,放弃这个想法吧。相同的情况也出现在构造 jni 调用 java 的传参问题上,后面会讲到。

在 K/N 里面,JetBrains 还算照顾我们,提供了一些不错的转换函数,比如说以下这些,可以让开发变得更简单:

toKString()    //  将一个CPointer<ByteVar> 转换成 Kotlin 字符串
readBytes()    //  将一个 CPointer<ByteVar> 转换成 Kotlin 的 ByteArray
cstr           // 将一个 Kotlin 字符串转换成 CValues<ByteVar>
toCValues()    // 将一个 Kotlin ByteArray 转换成 CValues<ByteVar>

所以我们可以在此基础之上,轻松的写出以下函数:

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 相关的函数了,像开头那种写法太不友好了,有必要造桥。桥的造法各有千秋,这里我不打算对各种方法作任何的评论,只谈自己的封装方法。

首先我一定会把 env: CPointer<JNIEnvVar> 这个对象封装掉,在 JNI 方法中,时时刻刻要用它,于是先写个简单架子,把 env 和一些常用函数写进去:

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!!
    ... ...
}

基本的架子就这么简单,后面慢慢加内容,有了这些东西后,可以写两个基本函数,用于完成 Stringjstring 的相互转换:

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 作为构造参数的类型,可以直接 alloc

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

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

此时就能得到一个 jstring 类型的对象了。但是对于我来说,我觉得它依然不方便,我希望可以在字符串对象上直接转换,那么再扩展下:

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

这下可好,是不是更加不知道怎么用了?因为 Stringjstring 的上下文都没有嘛,现在就要开始变魔术了,我们在 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) }
    }
}

有了这个函数后,我们可以在全局加一个函数,来实现对完整上下文的包装:

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

下面就是使用了,我们现在已经完成了变魔术所需要的条件,把本文开头的那个函数改一下:

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

这么一来,就看不到 JNI 函数在背后的动作了,API 非常简洁,对开发者友好。我们可以把所有的操作都写在 jniWith 里面,它具备 JniBridge 的完整上下文。


下面再来看一下如何从 JNI 调用 Java 方法,有了上面的封装经验后,要搞个好玩的东西出来就简单了:

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))
    }
}

细心的话你会发现这里留了个尾巴,调用函数如何传参呢?虽然写了 arguments 参数,但是实际传参是 null,很显然这里需要把参数补齐。

这个参数是一个 CPointer<jvalue> 类型的对象,因此我们就必须把参数构造成这样的,才可以正常传递,在此又要多写一个函数:

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
    }
}

看起来很复杂,但是实质上是在根据不同的参数类型,对 jvalue 进行填充,这里再一次的用到了 alloc,来对一组 jvalue 进行初始化,这是必须掌握的一种写法,要好好记住哦:)

这里还有一个 scope: MemScope 参数,这是个什么东西呢?其实它来源于 memScoped 方法,会直接构造出一个 MemScope 类型,toJValues 必须用这种传入 scope 的方法来实现,是因为 memScoped 会在函数结束时,回收分配的内存,而我们构造出来的 CPointer<jvalue> 却必须被返回,并且被使用后才可以销毁,因此对它的内存管理必须依赖上一个 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 的封装,我同样提供了一个完整文件供取用,点击查阅

另外,我也发布了一个 K/N for NDK 的封装库,如果你打算使用 Kotlin 来开发 NDK 应用,可以直接在 gradle 内使用它:

... ... 
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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容