如何通过JNA将Java字符串数组传递给cgo编译的Go函数并排查段错误
问题成因
你遇到的段错误是由多个问题共同导致的,按影响优先级排序如下:
- 数组写入逻辑错误:
JavaStringArrayToGoStringSlice方法的循环中,你固定读取goStrings[0]的结构体数据写入所有数组位置,导致除第一个元素外的所有Go string结构体都是错误值,Go侧访问时直接指向非法地址。 - JNA结构体内存未同步:
_GoString_.ByValue是JNA的Structure对象,你设置完p、n字段后没有调用write()方法将Java侧的字段值同步到native内存,直接读取getPointer()拿到的是未初始化的垃圾数据,错误地址0xb01dfacedebac1e就是JVM回收内存后填充的毒指针,明确说明访问的内存已经失效。 - native内存无强引用持有:你在转换过程中创建的
DirectByteBuffer、Memory对象没有被任何强引用持有,方法返回后会被JVM GC直接回收,Go侧访问时对应的内存已经被释放。 - 字段类型不匹配(潜在问题):
_GoString_结构的长度字段你用了NativeSize,而Go string的长度字段类型是GoInt(64位系统下为int64),虽然64位环境下两者宽度一致,但32位环境下会直接出错。
可行解决方案
你可以按照以下步骤修正代码,完全规避问题:
1. 修正_GoString_结构定义
将长度字段类型替换为long,和Go侧类型完全对齐:
public class _GoString_ extends Structure { /** C type : const char* */ public Pointer p; // 替换NativeSize为long,对齐GoInt类型 public long n; public _GoString_() { super(); } protected List<String> getFieldOrder() { return Arrays.asList("p", "n"); } public _GoString_(Pointer p, long n) { super(); this.p = p; this.n = n; } public static class ByReference extends _GoString_ implements Structure.ByReference {}; public static class ByValue extends _GoString_ implements Structure.ByValue {}; }
2. 增加native内存持有逻辑,避免GC回收
使用ThreadLocal临时持有本次调用涉及的所有native内存对象,调用完Go函数后手动清理:
import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import com.sun.jna.Memory; import com.sun.jna.Native; import com.sun.jna.Pointer; // 临时持有native内存引用,避免GC提前回收 private static final ThreadLocal<List<Object>> NATIVE_MEM_HOLDER = ThreadLocal.withInitial(ArrayList::new);
3. 修正字符串转换逻辑
移除Unsafe相关的不安全操作,用JNA自带的Memory存储字符串字节,同步结构体到native内存:
public static _GoString_.ByValue JavaStringToGo(String jstr) { byte[] bytes = jstr.getBytes(StandardCharsets.UTF_8); // 申请native内存存储字符串字节 Memory strMem = new Memory(bytes.length); strMem.write(0, bytes, 0, bytes.length); NATIVE_MEM_HOLDER.get().add(strMem); _GoString_.ByValue goStr = new _GoString_.ByValue(); goStr.p = strMem; goStr.n = bytes.length; // 同步Java字段到native内存 goStr.write(); NATIVE_MEM_HOLDER.get().add(goStr); return goStr; }
4. 修正字符串数组转换逻辑
修复循环索引错误,正确写入每个Go string结构体:
public static GoSlice.ByValue JavaStringArrayToGoStringSlice(String[] strings) { int strStructSize = Native.getNativeSize(_GoString_.class); _GoString_.ByValue[] goStrings = new _GoString_.ByValue[strings.length]; for (int i = 0; i < strings.length; i++) { goStrings[i] = JavaStringToGo(strings[i]); } // 申请内存存储Go string结构体数组 Memory arrMem = new Memory((long) strings.length * strStructSize); NATIVE_MEM_HOLDER.get().add(arrMem); for (int i = 0; i < strings.length; i++) { // 修正索引,读取当前i对应的结构体数据 Pointer structPtr = goStrings[i].getPointer(); byte[] structBytes = structPtr.getByteArray(0, strStructSize); arrMem.write((long) i * strStructSize, structBytes, 0, structBytes.length); } GoSlice.ByValue slice = new GoSlice.ByValue(); slice.data = arrMem; slice.len = strings.length; slice.cap = strings.length; slice.write(); NATIVE_MEM_HOLDER.get().add(slice); return slice; } // 调用完Go函数后必须调用此方法清理引用,避免内存泄漏 public static void clearNativeMem() { NATIVE_MEM_HOLDER.get().clear(); }
5. 正确调用Go函数的示例
// 1. 转换参数 GoSlice.ByValue keys = JavaStringArrayToGoStringSlice(new String[]{"key1", "key2"}); GoSlice.ByValue values = JavaStringArrayToGoStringSlice(new String[]{"val1", "val2"}); _GoString_.ByValue expr = JavaStringToGo("your_expression"); // 2. 调用Go函数(请替换为你自己的JNA接口实例) Pointer resultPtr = YourGoLib.INSTANCE.PredicateEval(keys, values, expr); // 3. 读取返回结果 String result = resultPtr.getString(0, StandardCharsets.UTF_8.name()); // 4. 释放内存:Go返回的*C.char需要手动释放(你Go侧应该是用C.CString创建的返回值) Native.free(resultPtr); // 清理Java侧持有的native内存引用,允许GC回收 clearNativeMem();
内容的提问来源于stack exchange,提问作者Tian
相关产品推荐
相关产品推荐

