FFI 设计模式
FFI (Foreign Function Interface) 设计模式是跨语言边界调用的核心。本文总结数据边界设计、错误处理、资源生命周期管理和性能优化的最佳模式,适用于 Kotlin/Native 与 C/C++/Java 的互操作。
FFI 核心原则
最小化边界跨越
kotlin
// ❌ 反模式:频繁跨边界
fun processData(data: List<Int>) {
data.forEach { value ->
// 每次迭代都跨 JNI 边界
nativeProcessSingle(value) // 慢!
}
}
// ✅ 模式:批量处理
fun processData(data: List<Int>) {
// 一次性传递所有数据
nativeProcessBatch(data.toIntArray()) // 快!
}原则: JNI/FFI 调用有固定开销(~100-200ns),批量处理可摊销成本。
数据所有权明确
kotlin
// 模式:明确数据所有权
interface DataOwnership {
// 调用者保留所有权
fun processView(data: ByteArray) // 只读,不释放
// 接收者获得所有权
fun processOwned(data: ByteArray) // 接管,负责释放
// 共享所有权
fun processShared(data: ByteArray) // 引用计数
}数据边界设计模式
模式一:值类型传递
适用于小型数据(< 64 字节)。
kotlin
// Kotlin 侧
data class Point3D(val x: Float, val y: Float, val z: Float)
@CName("processPoint")
external fun processPoint(x: Float, y: Float, z: Float): Float
// Native 侧
@CName("processPoint")
fun processPoint(x: Float, y: Float, z: Float): Float {
return sqrt(x * x + y * y + z * z)
}优点: 无内存分配,无所有权问题
缺点: 参数过多影响性能
模式二:指针传递(视图)
适用于大型数据只读访问。
kotlin
@CName("analyzeBuffer")
fun analyzeBuffer(
env: CPointer<JNIEnvVar>,
thiz: jobject,
buffer: jbyteArray
): jint {
val jniEnv = env.pointed.pointed!!
// GetByteArrayElements - 可能复制,也可能直接访问
val ptr = jniEnv.GetByteArrayElements!!(env, buffer, null)
if (ptr == null) return -1
val length = jniEnv.GetArrayLength!!(env, buffer)
try {
// 只读访问,不修改
var sum = 0
for (i in 0 until length) {
sum += ptr[i].toUByte().toInt()
}
return sum
} finally {
// JNI_ABORT = 不回写修改
jniEnv.ReleaseByteArrayElements!!(env, buffer, ptr, JNI_ABORT)
}
}模式关键:
- 使用
JNI_ABORT避免不必要的回写 - 异常安全:始终释放资源
模式三:所有权转移
适用于数据需要被 Native 长期持有。
kotlin
@CName("takeOwnership")
fun takeOwnership(
env: CPointer<JNIEnvVar>,
thiz: jobject,
data: jbyteArray
): jlong {
val jniEnv = env.pointed.pointed!!
val length = jniEnv.GetArrayLength!!(env, data)
// 分配 Native 内存
val nativeData = nativeHeap.allocArray<ByteVar>(length)
// 复制数据
val javaPtr = jniEnv.GetByteArrayElements!!(env, data, null)
if (javaPtr != null) {
memcpy(nativeData, javaPtr, length.toULong())
jniEnv.ReleaseByteArrayElements!!(env, data, javaPtr, JNI_ABORT)
}
// 返回指针(作为 long)
return nativeData.toLong()
}
@CName("releaseOwnership")
fun releaseOwnership(
env: CPointer<JNIEnvVar>,
thiz: jobject,
handle: jlong
) {
val nativeData = handle.toCPointer<ByteVar>()
nativeHeap.free(nativeData!!)
}模式关键:
- 明确的
take和release配对 - 使用句柄(long)避免直接暴露指针
模式四:零拷贝(Direct ByteBuffer)
适用于共享内存访问。
kotlin
// Kotlin 侧
val directBuffer = ByteBuffer.allocateDirect(1024)
@CName("processDirectBuffer")
external fun processDirectBuffer(buffer: ByteBuffer): Int
// Native 侧
@CName("processDirectBuffer")
fun processDirectBuffer(
env: CPointer<JNIEnvVar>,
thiz: jobject,
buffer: jobject
): jint {
val jniEnv = env.pointed.pointed!!
// 获取直接访问指针 - 零拷贝!
val ptr = jniEnv.GetDirectBufferAddress!!(env, buffer)
?.reinterpret<ByteVar>() ?: return -1
val capacity = jniEnv.GetDirectBufferCapacity!!(env, buffer).toInt()
// 直接读写
for (i in 0 until capacity) {
ptr[i] = (ptr[i].toUByte().toInt() xor 0xFF).toByte()
}
return capacity
}优点: 真正的零拷贝,性能最优
缺点: 需要预先分配,内存在 GC 外
错误处理模式
模式一:返回码模式
传统 C 风格,适用于性能关键路径。
kotlin
// 定义错误码
object ErrorCode {
const val SUCCESS = 0
const val INVALID_PARAM = -1
const val OUT_OF_MEMORY = -2
const val IO_ERROR = -3
}
@CName("processWithErrorCode")
fun processWithErrorCode(
env: CPointer<JNIEnvVar>,
thiz: jobject,
input: jbyteArray,
output: jbyteArray
): jint {
if (input == null || output == null) {
return ErrorCode.INVALID_PARAM
}
val jniEnv = env.pointed.pointed!!
val inputLen = jniEnv.GetArrayLength!!(env, input)
val outputLen = jniEnv.GetArrayLength!!(env, output)
if (outputLen < inputLen * 2) {
return ErrorCode.INVALID_PARAM
}
// 处理逻辑
val inputPtr = jniEnv.GetByteArrayElements!!(env, input, null)
?: return ErrorCode.OUT_OF_MEMORY
val outputPtr = jniEnv.GetByteArrayElements!!(env, output, null)
if (outputPtr == null) {
jniEnv.ReleaseByteArrayElements!!(env, input, inputPtr, JNI_ABORT)
return ErrorCode.OUT_OF_MEMORY
}
try {
// 实际处理
for (i in 0 until inputLen) {
outputPtr[i * 2] = inputPtr[i]
outputPtr[i * 2 + 1] = 0
}
return ErrorCode.SUCCESS
} finally {
jniEnv.ReleaseByteArrayElements!!(env, input, inputPtr, JNI_ABORT)
jniEnv.ReleaseByteArrayElements!!(env, output, outputPtr, 0) // 回写
}
}模式二:异常传播模式
适用于 JNI,错误需要在 Java 层处理。
kotlin
@CName("processWithException")
fun processWithException(
env: CPointer<JNIEnvVar>,
thiz: jobject,
data: jbyteArray
): jbyteArray? {
val jniEnv = env.pointed.pointed!!
if (data == null) {
// 抛出 Java 异常
val exceptionClass = jniEnv.FindClass!!(env, "java/lang/IllegalArgumentException")
jniEnv.ThrowNew!!(env, exceptionClass, "data cannot be null")
return null
}
val length = jniEnv.GetArrayLength!!(env, data)
if (length > 1024 * 1024) {
val exceptionClass = jniEnv.FindClass!!(env, "java/lang/IllegalArgumentException")
jniEnv.ThrowNew!!(env, exceptionClass, "data too large (max 1MB)")
return null
}
// 正常处理
val result = jniEnv.NewByteArray!!(env, length)
// ...
return result
}模式三:Result 类型模式
Kotlin 风格,类型安全。
kotlin
// Kotlin 侧
sealed class ProcessResult {
data class Success(val data: ByteArray) : ProcessResult()
data class Error(val code: Int, val message: String) : ProcessResult()
}
@CName("processWithResult")
external fun processWithResult(data: ByteArray): ProcessResult
// Native 侧 - 返回复杂对象
@CName("processWithResult")
fun processWithResult(
env: CPointer<JNIEnvVar>,
thiz: jobject,
data: jbyteArray
): jobject? {
val jniEnv = env.pointed.pointed!!
// 根据情况创建 Success 或 Error 对象
val resultClass = if (/* 成功条件 */ true) {
jniEnv.FindClass!!(env, "com/example/ProcessResult\$Success")
} else {
jniEnv.FindClass!!(env, "com/example/ProcessResult\$Error")
}
// 创建对象实例
// ...
return null // 简化示例
}资源生命周期模式
模式一:RAII (Resource Acquisition Is Initialization)
kotlin
// Native 资源类
class NativeResource(
private val env: CPointer<JNIEnvVar>,
private val data: jbyteArray
) {
private val ptr: CPointer<ByteVar>?
private val length: Int
init {
val jniEnv = env.pointed.pointed!!
length = jniEnv.GetArrayLength!!(env, data)
ptr = jniEnv.GetByteArrayElements!!(env, data, null)
}
fun use(block: (CPointer<ByteVar>, Int) -> Unit) {
ptr?.let { block(it, length) }
}
fun close() {
ptr?.let {
val jniEnv = env.pointed.pointed!!
jniEnv.ReleaseByteArrayElements!!(env, data, it, JNI_ABORT)
}
}
}
// 使用
@CName("processWithRAII")
fun processWithRAII(
env: CPointer<JNIEnvVar>,
thiz: jobject,
data: jbyteArray
): jint {
return memScoped {
val resource = NativeResource(env, data)
defer { resource.close() } // 自动清理
var sum = 0
resource.use { ptr, length ->
for (i in 0 until length) {
sum += ptr[i].toUByte().toInt()
}
}
sum
}
}模式二:引用计数
kotlin
class RefCountedBuffer {
private var data: CPointer<ByteVar>? = null
private var refCount = 0
private val mutex = Lock()
fun retain(): RefCountedBuffer {
mutex.withLock {
refCount++
}
return this
}
fun release() {
mutex.withLock {
refCount--
if (refCount == 0) {
data?.let { nativeHeap.free(it) }
data = null
}
}
}
}模式三:弱引用清理
kotlin
// Java 侧
class NativeHandle(private val nativePtr: Long) {
private val cleaner = Cleaner.create(this) {
// 当 Java 对象被 GC 时自动调用
nativeRelease(nativePtr)
}
companion object {
@JvmStatic
external fun nativeRelease(ptr: Long)
}
}性能优化模式
模式一:对象池
kotlin
class JNIObjectPool<T>(
private val factory: () -> T,
private val reset: (T) -> Unit,
private val maxSize: Int = 10
) {
private val pool = mutableListOf<T>()
fun acquire(): T {
return if (pool.isNotEmpty()) {
pool.removeAt(pool.size - 1)
} else {
factory()
}
}
fun release(obj: T) {
if (pool.size < maxSize) {
reset(obj)
pool.add(obj)
}
}
}
// 使用
val byteArrayPool = JNIObjectPool(
factory = { ByteArray(1024) },
reset = { it.fill(0) }
)模式二:缓存 JNI ID
kotlin
object JNICache {
private val methodCache = mutableMapOf<String, jmethodID>()
private val fieldCache = mutableMapOf<String, jfieldID>()
fun getMethodID(
env: CPointer<JNIEnvVar>,
clazz: jclass,
name: String,
signature: String
): jmethodID? {
val key = "$name:$signature"
return methodCache.getOrPut(key) {
val jniEnv = env.pointed.pointed!!!!
jniEnv.GetMethodID!!(env, clazz, name, signature)!!
}
}
}模式三:线程局部存储
kotlin
// 避免跨线程传递 JNIEnv
@ThreadLocal
var threadLocalEnv: CPointer<JNIEnvVar>? = null
@CName("initThread")
fun initThread(env: CPointer<JNIEnvVar>) {
threadLocalEnv = env
}
fun getCurrentEnv(): CPointer<JNIEnvVar> {
return threadLocalEnv ?: error("未初始化 JNIEnv")
}线程安全模式
模式一:AttachCurrentThread
kotlin
fun callJavaFromNativeThread(
jvm: CPointer<JavaVMVar>,
callback: jobject
) {
val jvmFunctions = jvm.pointed.pointed!!
memScoped {
val envPtr = alloc<CPointerVar<JNIEnvVar>>()
// attach 当前线程
val result = jvmFunctions.AttachCurrentThread!!(
jvm,
envPtr.ptr.reinterpret(),
null
)
if (result == JNI_OK) {
val env = envPtr.value!!
try {
// 调用 Java 方法
callJavaMethod(env, callback)
} finally {
// 必须 detach
jvmFunctions.DetachCurrentThread!!(jvm)
}
}
}
}模式二:全局引用
kotlin
class GlobalRefManager(private val env: CPointer<JNIEnvVar>) {
private val globalRefs = mutableSetOf<jobject>()
fun makeGlobal(localRef: jobject): jobject {
val jniEnv = env.pointed.pointed!!
val globalRef = jniEnv.NewGlobalRef!!(env, localRef)!!
globalRefs.add(globalRef)
return globalRef
}
fun deleteGlobal(globalRef: jobject) {
val jniEnv = env.pointed.pointed!!
if (globalRefs.remove(globalRef)) {
jniEnv.DeleteGlobalRef!!(env, globalRef)
}
}
fun cleanup() {
val jniEnv = env.pointed.pointed!!
globalRefs.forEach { jniEnv.DeleteGlobalRef!!(env, it) }
globalRefs.clear()
}
}最佳实践总结
设计检查清单
- ✅ 是否最小化了边界跨越次数?
- ✅ 数据所有权是否明确?
- ✅ 错误处理是否完整?
- ✅ 资源是否正确释放?
- ✅ 是否考虑了线程安全?
- ✅ 性能热点是否优化?
常见陷阱
内存泄漏
未释放 Get*ArrayElements 获取的指针会导致内存泄漏。
JNIEnv 跨线程
JNIEnv 指针不能跨线程使用,必须通过 AttachCurrentThread 获取。
局部引用溢出
在长时间循环中创建局部引用会导致局部引用表溢出,需要手动删除。
kotlin
// ❌ 错误
for (i in 0 until 10000) {
val str = jniEnv.NewStringUTF!!(env, "text") // 泄漏!
}
// ✅ 正确
for (i in 0 until 10000) {
val str = jniEnv.NewStringUTF!!(env, "text")
// 使用 str
jniEnv.DeleteLocalRef!!(env, str) // 手动删除
}FFI 设计模式的核心是明确性:明确数据所有权、明确错误边界、明确资源生命周期。遵循这些模式可以构建健壮、高性能的跨语言接口。