Skip to content

FFI 设计模式

源:Kotlin/Native C Interop

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

模式关键:

  • 明确的 takerelease 配对
  • 使用句柄(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 设计模式的核心是明确性:明确数据所有权、明确错误边界、明确资源生命周期。遵循这些模式可以构建健壮、高性能的跨语言接口。