相当于 UTF-16 的 MemorySegment.getUtf8String

问题描述 投票:0回答:2

我正在使用 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()
或基于字符的迭代方法更好的替代方法吗?

java utf-16 project-panama
2个回答
3
投票

字符集解码器提供了一种在 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));

3
投票

由于 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()
步骤,并有效地处理内存段,而无需复制步骤。

© www.soinside.com 2019 - 2024. All rights reserved.