17370845950

如何在 JNA 中正确传递并接收 byte 类型的原生函数输出

本文详解如何使用 jna 的 `pointerbyreference` 正确调用接受 `byte**` 参数的 c 原生函数(如 voicevox core 的 `voicevox_wav_data`),安全获取动态分配的字节数组并避免段错误。

在 Java/Scala 中通过 JNA 调用 C 原生库时,遇到形如 int func(byte** output) 的函数签名是一个典型难点:它要求传入一个“指向指针的指针”,即让原生函数能修改你传入的指针变量本身(使其指向新分配的内存),而非仅写入已有数组。这与 byte*(对应 JNA 的 byte[] 或 Pointer)有本质区别——后者只能读写内存内容,无法改变指针地址。

直接尝试 Array[Byte] 会失败,因为 JNA 将其映射为 byte*,而非 byte**;Array[Array[Byte]] 不被 JNA 支持;而 Memory 虽可传入(因其是 Pointer 子类),但因未正确表达“指针的引用语义”,常导致返回无效地址或 SEGV 错误。

✅ 正确解法是使用 com.sun.jna.PointerByReference:

import com.sun.jna.{Library, Pointer, PointerByReference}
import com.sun.jna.Native

trait NativeLibrary extends Library {
  // 正确声明:接收 PointerByReference,对应 C 的 byte**
  def voicevox_wav_data(
    core: Pointer, 
    audio_query: Pointer, 
    speaker_id: Int, 
    output: PointerByReference
  ): Int

  def voicevox_wav_free(ptr: Pointer): Int
}

val lib = Native.load("voicevox_core", classOf[NativeLibrary])

调用时流程如下:

  1. 创建引用容器:val pbr = new PointerByReference()
    它在 JVM 堆上分配一个“可被原生代码写入的指针槽位”。

  2. 调用函数:val result = lib.voicevox_wav_data(core, query, speakerId, pbr)
    原生函数成功后,pbr.getValue() 将返回新分配的 byte*(即实际数据起始地址)。

  3. 提取字节数组:需预先知道数据长度(通常该函数另有配套接口返回 size,或文档明确说明,如 VOICEVOX 中 voicevox_wav_data_size):

    val dataPtr = pbr.getValue()
    val dataSize = lib.voicevox_wav_data_size(core, query, speakerId) // 示例:需按实际 API 获取
    val wavBytes = dataPtr.getByteArray(0, dataSize)
  4. 及时释放内存:该内存由原生库 malloc 分配,必须由对应 free 函数释放(如 voicevox_wav_free(dataPtr)),不可用 Java GC 管理,否则导致内存泄漏或重复释放崩溃。

⚠️ 关键注意事项:

  • PointerByReference 是唯一能准确建模 C 中 T**(双指针)语义的 JNA 类型;
  • 切勿对 pbr.getValue() 返回的 Pointer 调用 getByteArray 时使用任意长度——越界读取将引发 JVM 崩溃;
  • 若原生函数不提供数据长度,需查阅文档或源码确认约定(例如是否以 \0 结尾、是否含长度头等);
  • 在 Scala 中建议配合 Try 或 using 模式确保 voicevox_wav_free 被调用,避免资源泄漏。

掌握 PointerByReference 的使用,是安全桥接 C 动态内存管理与 JVM 内存模型的核心能力。