
本文详解如何使用 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])调用时流程如下:
创建引用容器:val pbr = new PointerByReference()
它在 JVM 堆上分配一个“可被原生代码写入的指针槽位”。调用函数:val result = lib.voicevox_wav_data(core, query, speakerId, pbr)
原生函数成功后,pbr.getValue() 将返回新分配的 byte*(即实际数据起始地址)。-
提取字节数组:需预先知道数据长度(通常该函数另有配套接口返回 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)
及时释放内存:该内存由原生库 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 内存模型的核心能力。










