我正在使用 JDK 19 中的外部函数和内存 API ([JEP 424][1]) 将基于 JNA 的库移植到“纯”Java。
我的库处理的一个常见用例是从本机内存中读取(以 null 结尾的)字符串。对于大多数 *nix 应用程序,这些是“C 字符串”,并且 MemorySegment.getUtf8String() 方法足以完成任务。
但是,本机 Windows 字符串以 UTF-16 (LE) 存储。作为
TCHAR
数组或“宽字符串”引用,它们的处理方式与“C 字符串”类似,只是每个字符串消耗 2 个字节。
JNA 为此提供了一个
Native.getWideString()
方法,该方法调用本机代码来有效地迭代适当的字符集。
我没有看到与针对这些基于 Windows 的应用程序优化的
getUtf8String()
(和相应的 set...()
)等效的 UTF-16。
我可以通过几种方法解决这个问题:
new String(bytes, StandardCharsets.UTF_16LE)
并且:
trim()
split()
位于空分隔符上并提取第一个元素byte[]
的非常大的总大小),我可以逐个字符地迭代寻找空值.虽然我当然不会期望 JDK 为每个字符集提供本机实现,但我认为 Windows 代表了足够大的使用份额来支持其主要本机编码以及 UTF-8 便利方法。有没有一种我还没有发现的方法可以做到这一点?或者还有比我描述的
new String()
或基于字符的迭代方法更好的替代方法吗?
字符集解码器提供了一种在 Windows 上使用外部内存 API 处理以 null 结尾的
MemorySegment
宽/UTF16_LE 到 String
的方法。这可能与您的解决方法建议没有任何不同/改进,因为它涉及扫描结果字符缓冲区中的空位置。
public static String toJavaString(MemorySegment wide) {
return toJavaString(wide, StandardCharsets.UTF_16LE);
}
public static String toJavaString(MemorySegment segment, Charset charset) {
// JDK Panama only handles UTF-8, it does strlen() scan for 0 in the segment
// which is valid as all code points of 2 and 3 bytes lead with high bit "1".
if (StandardCharsets.UTF_8 == charset)
return segment.getUtf8String(0);
// if (StandardCharsets.UTF_16LE == charset) {
// return Holger answer
// }
// This conversion is convoluted: MemorySegment->ByteBuffer->CharBuffer->String
CharBuffer cb = charset.decode(segment.asByteBuffer());
// cb.array() isn't valid unless cb.hasArray() is true so use cb.get() to
// find a null terminator character, ignoring it and the remaining characters
final int max = cb.limit();
int len = 0;
while(len < max && cb.get(len) != '\0')
len++;
return cb.limit(len).toString();
}
走另一条路
String
-> 空终止 Windows 范围 MemorySegment
:
public static MemorySegment toCString(SegmentAllocator allocator, String s, Charset charset) {
// "==" is OK here as StandardCharsets.UTF_8 == Charset.forName("UTF8")
if (StandardCharsets.UTF_8 == charset)
return allocator.allocateUtf8String(s);
// else if (StandardCharsets.UTF_16LE == charset) {
// return Holger answer
// }
// For MB charsets it is safer to append terminator '\0' and let JDK append
// appropriate byte[] null termination (typically 1,2,4 bytes) to the segment
return allocator.allocateArray(JAVA_BYTE, (s+"\0").getBytes(charset));
}
/** Convert Java String to Windows Wide String format */
public static MemorySegment toWideString(String s, SegmentAllocator allocator) {
return toCString(allocator, s, StandardCharsets.UTF_16LE);
}
和你一样,我也想知道是否有比上述更好的方法。
JDK22更新
JDK22支持
StandardCharsets.XXX
的转换,因此从Java String到MemorySegment的转换很简单:
var seg = arena.allocateFrom(str, charset);
其他字符集的后备使用附加
\0
: 的方法
var seg = arena.allocateFrom(JAVA_BYTE, (s+"\0").getBytes(charset));
由于 Java 的
char
is 是 UTF-16 单位,因此外部 API 中不需要特殊的“宽字符串”支持,因为转换(在某些情况下可能只是复制操作)已经存在:
public static String fromWideString(MemorySegment wide) {
var cb = wide.asByteBuffer().order(ByteOrder.nativeOrder()).asCharBuffer();
int limit = 0; // check for zero termination
for(int end = cb.limit(); limit < end && cb.get(limit) != 0; limit++) {}
return cb.limit(limit).toString();
}
public static MemorySegment toWideString(String s, SegmentAllocator allocator) {
MemorySegment ms = allocator.allocateArray(ValueLayout.JAVA_CHAR, s.length() + 1);
ms.asByteBuffer().order(ByteOrder.nativeOrder()).asCharBuffer().put(s).put('\0');
return ms;
}
这并不是专门使用 UTF-16LE,而是当前平台的本机顺序,这通常是具有本机宽字符串的平台上的预期内容。当然,当在 Windows x86 或 x64 上运行时,这将导致 UTF-16LE 编码。
请注意,
CharBuffer
实现了CharSequence
,这意味着对于很多用例,您可以在读取宽字符串时省略最后的toString()
步骤,并有效地处理内存段,而无需复制步骤。