Есть ли эффективный способ получить строку Java из внешней функции, которая возвращает указатель char в стиле C?
Например, библиотека SQLite содержит функцию для возврата номера версии библиотеки:
SQLITE_API const char *sqlite3_libversion(void);
Используя API внешних функций и памяти Java, я могу вызвать эту функцию следующим образом:
final MemorySegment ms = this.symbolLookup.find("sqlite3_libversion")
.orElseThrow(() -> new RuntimeException("Could not find method 'sqlite3_libversion"));
final FunctionDescriptor fd = FunctionDescriptor.of(ValueLayout.ADDRESS);
final Linker linker = Linker.nativeLinker();
final MemorySegment result = (MemorySegment)linker.downcallHandle(ms, fd).invoke();
try (final Arena arena = Arena.ofConfined()){
final MemorySegment ptr = result.reinterpret(10, arena, null);
return ptr.getUtf8String(0);
}
Проблема в том, что я создал новый MemorySegment произвольного размера 10. Это нормально для этого примера, но как правильно получить строку из char *, если я понятия не имею о размере массива символов?
Вы можете переинтерпретировать меньший размер, если хотите быть безопаснее (например, в случае функции, которая может возвращать строку, которая не завершена должным образом). В этом случае выберите размер, который, вероятно, будет достаточно большим для любой разумной строки (где «разумный» определяется контекстом кода).
(«изменяет границы» было бы более точным, поскольку «возвращает новый экземпляр с новыми границами»)
@Slaw, есть ли у вас ссылка на то, где говорится, что переинтерпретация корректирует только границы на стороне Java?
Документы, похоже, не делают этого явным. Но общая документация MemorySegment и общий дизайн FFM API, по крайней мере, подразумевают это. Как отметил Йорн в другом комментарии, сегмент по сути является указателем на область памяти (см. этот раздел Javadoc). А выделение родной памяти из Java — это работа SegmentAllocator.
@Slaw Я бы согласился, что документы не являются явными, но после отражения названия метода (методов) retinterpret() и перечитывания начального абзаца MemorySegment документации действительно видно, что это оболочка, которая, как вы говорите, может изменять свои границы, но не изменяет родную память, которая его поддерживает. Тем не менее, было бы полезно добавить предложение в документы, чтобы прояснить этот момент. Я считаю, что все еще можно выйти «за конец» массива, если не быть осторожным, но это не хуже, если использовать только C.
«Я считаю, что все еще можно выйти «за конец» массива, если не соблюдать осторожность, но это не хуже, если использовать только C» — Да, нужно быть осторожным, используя reinterpret. Эти методы явно небезопасны; неправильное их использование может привести к сбоям или повреждению памяти, поэтому они будут «ограничены». Хотя с getString все будет в порядке, если строка заканчивается нулем, а размер сегмента не меньше длины строки.




У вас должна быть возможность переинтерпретировать MemorySegment до размера для преобразования UTF-8 с подходящим значением для byteSize. Некоторые API/библиотеки могут иметь документацию или определение файла заголовка, которое дает ожидаемый размер:
// JDK21:
return result.reinterpret(byteSize).getUtf8String(0);
// JDK22:
return result.reinterpret(byteSize).getString(0);
Вызов reinterpret не перераспределяет фрагмент памяти размером byteSize — он просто возвращает MemorySegment, который разрешает доступ к этому диапазону.
Пример JDK22, который использует большой размер:
private static final SymbolLookup SQLITE = SymbolLookup.libraryLookup("sqlite3", Arena.global());
private static final MemorySegment MS = SQLITE.find("sqlite3_libversion")
.orElseThrow(() -> new RuntimeException("Could not find method 'sqlite3_libversion"));
private static final Linker LINKER = Linker.nativeLinker();
private static final MethodHandle MH = LINKER.downcallHandle(MS, FunctionDescriptor.of(ValueLayout.ADDRESS));
public static void main(String... args) throws Throwable {
final MemorySegment result = (MemorySegment)MH.invoke();
String ver = result.reinterpret(Integer.MAX_VALUE).getString(0);
System.out.println("SQLITE version "+ver);
}
Обратите внимание, что использование jextract упрощает настройку привязок.
Нет ли риска при использовании Integer.MAX_VALUE при переинтерпретации сегмента памяти? Судя по всему, Javadoc подразумевает, что любое передаваемое значение соответствует размеру нового MemorySegment.
@D-Dᴙum A MemorySegment можно рассматривать как указатель на область памяти с прикрепленными к ней границами (размер, время жизни, поток). reinterpret создает новый указатель, указывающий на ту же область памяти, но с другими границами, прикрепленными к нему.
@Jorn Тот факт, что размер сегмента не связан напрямую с базовой областью памяти, вероятно, может быть более явным в документации. Если я ничего не упускаю, я понимаю, что кому-то это может быть не сразу понятно. Честно говоря, какой бы хорошей ни была документация (а я считаю ее довольно хорошей), некоторые вещи все же могут удивлять. Например, очевидно, что сегмент, возвращаемый reinterpret, всегда будет доступен для записи, даже если исходный сегмент был доступен только для чтения. Я не вижу никаких указаний на это в документах; Я даже не уверен, ожидаемое ли это поведение или ошибка.
@Slaw Мне кажется, это довольно очевидно, если принять во внимание метод asSlice. Это также возвращает новое представление базовой области с другим, меньшим размером. Возможно, мы могли бы добавить несколько слов, например, «подкреплено одной и той же областью памяти».
@Slaw Проблема с reinterpret, доступная только для чтения, является ошибкой. По крайней мере, в javadoc должно быть указано, изменяет ли он свойство только для чтения, но в этом случае reinterpret вообще не следует изменять свойство только для чтения.
@Jorn, Я думаю, это тоже довольно очевидно, но не обязательно сразу очевидно. Я хотел ответить на этот комментарий и надеялся на какое-то конкретное предложение или абзац, на который я мог бы указать, вместо того, чтобы полагаться на «Ну, если вы прочитаете документацию полностью, вы увидите, как MemorySegment устроен».
@Слоу, я согласен. Информация должна быть доступна «локально» при просмотре reinterpret (и asSlice). Я отправил: bugs.openjdk.org/browse/JDK-8333886 для уточнения документации и bugs.openjdk.org/browse/JDK-8333884 для найденной вами ошибки.
Метод MemorySegment::getString(long) (Java 22) предназначен для чтения строки, завершающейся нулем. Таким образом, вы можете использовать
reinterpret(Integer.MAX_VALUE).getString(0)для чтения любой строки произвольной длины с нулевым символом в конце, которая может поместиться в JavaString. Реинтерпретация фактически не выделяет больше памяти, а просто меняет границы (на стороне Java).