Возможно, одним из самых больших недостатков Java является его неспособность легко вызывать собственный код C/C++ и взаимодействовать с ним. Project Panama — это инициатива WIP, направленная на устранение этого серьезного недостатка за счет упрощения извлечения собственных привязок C/C++ и создания чужого API памяти для облегчения взаимодействия с собственным кодом C/C++.
Этот пост в блоге представляет собой личный отчет о том, как я попробовал его и дал некоторые отзывы. Небольшое предупреждение: я никогда раньше не писал код на C/C++, поэтому не обязательно предполагать, что то, что я делаю, на 100% правильно. Я тоже программист-любитель, а не профессионал. Я также впервые работаю с нативными привязками. Принимайте все с недоверием. Тем не менее, API-интерфейсы довольно просты и просты в использовании, как только вы выясните первоначальные проблемы в первый раз.
Зачем мне нужен проект Панама?
Во-первых, я хотел бы рассказать о моем личном примере использования Project Panama, так как он объясняет, почему я использую его по назначению. Я сделал свою собственную утилиту для разгона графических процессоров Nvidia под Linux под названием Goliath Envious FX и соответствующий API уровня абстракции Goliath Envious. Goliath Envious (в фокусе здесь) предоставляет хороший и чистый способ получения важных данных GPU. Базовое использование Goliath Envious выглядит примерно так:
// pre-init NvSMI attributes. NvSMI.init(); // GPU object representing the primary GPU(GPU-0) NvGPU primaryGPU = NvGPU.getPrimaryNvGPU(); System.out.println("GPU ID is " + primaryGPU.getIDNvReadable().getValue()); System.out.println("GPU Name is " + primaryGPU.getNameNvReadable().getValue()); System.out.println("GPU Family is " + primaryGPU.getFamilyNvReadable().getValue()); // NvSMI = nvidia-smi. NvSMI smi = NvSMI.getPrimaryNvGPUInstance(); System.out.println("GPU Utilization is " + smi.getCoreUtilization().getValue()); System.out.println("GPU Temp is " + smi.getCoreTemp().getValue()); System.out.println("GPU Default PowerLimit is " + smi.getDefaultPowerLimit().getValue());
Он очень прост и удобен в использовании. Проблема (помимо отсутствия документации, переплетения кода с JavaFX, дыр в API и других проблем) заключается в том, что он использует командную строку так же, как если бы вы открыли терминал и самостоятельно использовали nvidia-smi (и nvidia-settings), что очень плохо.
(Технически nvidia-smi не совсем ужасен, но nvidia-settings абсолютно ужасен. Настройки Nvidia вызывают серьезные проблемы с использованием ЦП и очень скрыты. Учитывая, что это приложение, связанное с играми, и оно будет открыто во время игры, это огромная проблема.) .
Решение? Используйте Project Panama, чтобы использовать родной язык программирования C/C++ NVML и NvXCtrl API, (надеюсь) обходя штрафы и ограничения производительности командной строки.
Примечание. Я использую здесь только NVML, так как в настоящее время не могу заставить jextract выдавать привязки для NvXCtrl из-за ошибок int64_t.
Использование JExtract для получения привязок
Первое, что нужно сделать, это извлечь привязки. Это делается с помощью jextract, другие примеры использования которого вы можете найти здесь. Первое, что вы заметите, это то, как легко все аргументы просто смешиваются и становятся супом переключателей командной строки. Что еще хуже, описания этих переключателей плохо документированы и не содержат примеров:
jextract Non-option arguments: [String] -- header files Option Description ------ ----------- -?, -h, --help print help -C <String> pass through argument for clang -I <String> specify include files path -L, --library-path <String> specify library path -d <String> specify where to place generated class files --dry-run parse header files but do not generate output jar --exclude-headers <String> exclude the headers matching the given pattern --exclude-symbols <String> exclude the symbols matching the given pattern --include-headers <String> include the headers matching the given pattern. If both --include-headers and --exclude- headers are specified, --include-headers are considered first. --include-symbols <String> include the symbols matching the given pattern. If both --include-symbols and --exclude- symbols are specified, --include-symbols are considered first. -l <String> specify a library --log <String> specify log level in java.util.logging.Level name --missing-symbols <String> action on missing native symbols. -- missing_symbols=error|exclude|ignore|warn --no-locations do not generate native location information in the .class files -o <String> specify output jar file or jmod file --package-map <String> specify package mapping as dir=pkg --record-library-path tells whether to record library search path in the .class files --src-dump-dir <String> specify output source dump directory --static-forwarder <Boolean> generate static forwarder class (default is true) -t, --target-package <String> target package for specified header files
Это просто сборка EA, поэтому, конечно, она может и, вероятно, улучшится в будущем, и они действительно предоставляют примеры использования (выше), но IMO, если вы хотите, чтобы люди использовали Project Panama и оставляли отзывы о нем, вам нужно предоставить некоторые лучшие документация. Прямо сейчас вам практически нужно пройтись по списку примеров использования (опять же, выше) и предположить, что они на самом деле делают, основываясь на этом примере.
Совет для профессионалов: если вы получаете предупреждение о том, что некоторые имена библиотек не могут быть разрешены, вы ошиблись.
Во многих приведенных примерах они используют:
/usr/lib/x86_64-linux-gnu
Чего нет в Arch Linux (то, что я использую). Предположительно, это символическая ссылка на версию GCC по умолчанию, которая существует только в Ubuntu. В Arch Linux это:
/usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0
… это текущий путь GCC.
Точно так же параметр -L либо вводит в заблуждение, либо исключение загрузки библиотеки Java вызывает ошибку, поскольку указанные здесь каталоги не добавляются в java.library.path в соответствии с сообщением об исключении.
Также не поддерживается динамическое указание и загрузка библиотек? Почему?
И еще одно, вы не используете фактическое имя файла данной библиотеки… по крайней мере, не с NVML. Я не знаю, виновата ли в этом Java или что здесь, но это было ужасно запутанным. Я потратил гораздо больше времени, чем хотел бы признать, пытаясь понять, почему Java не может найти библиотеку только для того, чтобы бегло просмотреть и найти пример OpenGL:
jextract -L /usr/lib/x86_64-linux-gnu -l glut -l GLU -l GL --record-library-path -t opengl -o opengl.jar /usr/include/GL/glut.h
…подождите, ГЛ? В каталоге /usr/lib нет библиотеки GL, только libGL.
Какие?
Да, вы должны удалить часть «lib» впереди. Дело в том, что некоторые библиотеки в каталоге /usr/lib не имеют «lib», прикрепленного спереди, как flatpak. Что происходит, когда вы их используете? Не знаю. Может быть, мир взорвется или что-то в этом роде.
Честно говоря, здесь действительно рекомендуется файл конфигурации, который можно использовать для последовательного извлечения привязок. Возможно, используйте функцию компиляции единого исходного кода Java 11 для автоматизации извлечения привязок.
(вставьте сюда какую-нибудь шутку на Javascript)
После всего этого, как вы извлекаете NVML? К счастью, это легко:
jextract -t org.nvidia -L /usr/lib -lnvidia-ml --record-library-path /opt/cuda/targets/x86_64-linux/include/nvml.h -o org.nvidia.nvml.jar
Примечание. Для создания привязок вам необходимо установить CUDA, но впоследствии его можно удалить, поскольку библиотека (nvidia-ml) должна поставляться с драйвером. Он нужен только для заголовочного файла.
API внешней памяти
Тяжелая часть позади. Теперь о хорошем, использовании привязок. Хорошей новостью является то, что NVML невероятно хорошо документирован до такой степени, что даже тому, кто не знает C/C++, было легко понять, как использовать API. Иностранный API Панамы также очень помогает здесь, так как он также довольно прост в использовании и понимании. Основные классы, на которые стоит обратить внимание:
- Scope — используется для выделения памяти внутри Java. Цель или полезность методов fork/merge не совсем ясны.
- Указатель — действует как оболочка объекта между кодом Java и C. Указатели либо заполняются данными функцией, которой вы их передаете, либо имеют набор данных из Java и передаются функциям API. Его использование чем-то похоже на опциональное.
- LayoutType — действует как примитивные модели данных для памяти C/C++, такие как целые числа, логические значения, структуры и т. д.
- NativeTypes — предоставляет большое разнообразие примитивных типов данных C/C++, таких как int, short, char и т. д.
Данные привязки API разделены на две части: nvml_h (перечисления и структуры) и nvml_api (функции и константы). Я не копирую/вставляю сюда документацию по API, вы должны прочитать ее сами. Я не хочу, чтобы меня судили за нарушение авторских прав.
Во-первых, нам нужно загрузить и инициализировать библиотеку NVML:
package goliath.nvtest; import java.foreign.Libraries; import java.foreign.Scope; import java.foreign.memory.LayoutType; import java.foreign.memory.Pointer; import java.lang.invoke.MethodHandles; import org.nvidia.nvml_h; import java.foreign.NativeTypes; import org.nvidia.nvml_lib; public class GoliathNvTest { public static void main(String[] args) { // Load the NVML library. Note: actualy name is libnvidia-ml.so. Libraries.loadLibrary(MethodHandles.lookup(), "nvidia-ml"); try(Scope s = nvml_lib.scope().fork()) { // initialize Nvidia Management Library nvml_lib.nvmlInit_v2(); } } }
С этого момента все фрагменты кода выполняются внутри оператора try.
Получение количества GPU
Прежде чем мы должны даже спросить NVML о чем-либо, связанном с нашим подключенным графическим процессором Nvidia, мы должны сначала спросить, сколько графических процессоров подключено. Для этого нам просто нужно сделать:
// Pointer reference to the returned GPU count Pointer<Integer> count = s.allocate(NativeTypes.INT); // call and send the count pointer to nvmlDeviceGetCount function. count is "filled" by the function. nvml_lib.nvmlDeviceGetCount_v2(count); // print GPU count System.out.println("Number of GPUs connected: " + count.get());
Теперь, когда у нас есть подсчет количества подключенных графических процессоров, мы можем перебирать эти графические процессоры Nvidia в цикле for позже, если захотим.
Получение ссылок на GPU
Совсем не сложно. Следующий шаг, фактически получение ссылок на GPU, немного сложнее:
// Pointer reference to the GPU struct Pointer<Pointer<nvml_h.nvmlDevice_st>> gpuPtr = s.allocate(LayoutType.ofStruct(nvml_h.nvmlDevice_st.class).pointer()); // call and send the gpuPtr to nvmlDeviceGetHandleByIndex function. nvml_lib.nvmlDeviceGetHandleByIndex_v2(count.get()-1, gpuPtr);
По сути, у нас есть тип структуры внутри типа структуры, поэтому необходимы два указателя, один из которых обертывает другой.
Получение имени графического процессора — что в имени?
Очевидно, что следующее, что нужно сделать, это напечатать имя графического процессора. К сожалению, я не смог получить эту работу:
// Name reference to GPU-0's name Pointer<Byte> namePtr = s.allocateCString(""); // fill namePtr nvml_lib.nvmlDeviceGetName(gpuPtr.get(), namePtr, nvml_lib.NVML_DEVICE_NAME_BUFFER_SIZE);
Проблема в том, что указатель представляет собой байт, а не массив символов, и в настоящее время я понятия не имею, как его преобразовать. Вероятно, есть способ более низкого уровня преобразовать его, но я его не вижу. Было бы неплохо, если бы для этого были предоставлены удобные методы, поскольку это вряд ли будет нечастым явлением.
Получение ограничения мощности графического процессора по умолчанию
Двигаясь дальше, давайте попробуем получить предел мощности по умолчанию для нашего графического процессора:
// pointer reference to default power limit Pointer<Integer> powerDefaultPtr = s.allocate(NativeTypes.INT); // fill powerDefaultPtr nvml_lib.nvmlDeviceGetPowerManagementDefaultLimit(gpuPtr.get(), powerDefaultPtr); // print default power limit System.out.println("Default power Limit is " + powerDefaultPtr.get()/1000 + "W");
Примечание. NVML сообщает о милливаттах, поэтому здесь используется преобразование его в ватты путем деления на 1000.
На моей GTX 1080 предел мощности по умолчанию составляет 215 Вт, и, как и ожидалось, это печатает 215 Вт. Все работает отлично.
Получение пределов минимальной/максимальной мощности графического процессора
Теперь давайте попробуем получить пределы минимальной/максимальной мощности:
// pointer references to min/max power limits Pointer<Integer> powerMinPtr = s.allocate(NativeTypes.INT); Pointer<Integer> powerMaxPtr = s.allocate(NativeTypes.INT); // fill min/max power limit pointers nvml_lib.nvmlDeviceGetPowerManagementLimitConstraints(gpuPtr.get(), powerMinPtr, powerMaxPtr); // print power limits System.out.println("Min Power Limit is " + powerMinPtr.get()/1000 + "W"); System.out.println("Max Power Limit is " + powerMaxPtr.get()/1000 + "W");
Опять же, печатает 108 Вт и 280 Вт, как и ожидалось для моего графического процессора.
Получение использования графического процессора
И, наконец, давайте получим использование графического процессора и памяти. Они хранятся в структуре и поэтому немного отличаются от приведенных выше:
// utilization struct nvml_h.nvmlUtilization_st utilStructPtr = s.allocate(LayoutType.ofStruct(nvml_h.nvmlUtilization_st.class)).get(); // fill GPU and Memory utilization struct nvml_lib.nvmlDeviceGetUtilizationRates(gpuPtr.get(), utilStructPtr.ptr()); // print utilization of gpu/memory System.out.println("GPU Utilization: " + utilStructPtr.gpu$get() + "%"); System.out.println("Memory Bandwidth Utilization: " + utilStructPtr.memory$get() + "%");
Который правильно печатает использование графического процессора и памяти, как и ожидалось.
Заключительные мысли и список пожеланий
Project Panama в целом безумно прост в использовании. Даже если он не работает так же хорошо, как нативное использование C to C, на мой взгляд, его все же стоит использовать (я не проверял производительность, и мне все равно). В моем случае буквально все лучше, чем командная строка, и Panama чрезвычайно проста в использовании, если вы правильно настроите jextract.
Я завершу все маркированным списком мыслей, возясь с NVML и другим программным обеспечением:
- Интерфейс командной строки jextract должен быть каким-то образом менее мутным. Может быть, здесь могут помочь переменные среды?
- Переключатель ведения журнала jextract кажется сломанным. Попытка установить «предупреждать» просто приводит к исключению.
- Интерфейс командной строки jextract нуждается в более качественной документации, чем сейчас, независимо от того, является ли это сборкой раннего доступа. Если вы потерпите неудачу при первом же препятствии, вы не сможете предоставить много отзывов.
- Должно быть больше удобных методов API для обработки общих последовательностей/массивов символов, возвращаемых типами C/C++. Опять же, я понятия не имею, что делать с этим значением Byte.
- Опять же, я не могу заставить jextract работать с NvXCtrl (фактически NvXCtrl_libs.h). Сначала он выдает ошибку о неизвестном типе bool, затем, если вы укажете заголовки clang, он жалуется на то, что int64_t неизвестен, несмотря на то, что он определен в stdint.h.
- Добавлена поддержка динамической загрузки системных библиотек. ПОЖАЛУЙСТА.
- Приведите примеры того, как извлекать гораздо более крупные/сложные проекты, такие как GTK. Я попытался это сделать, и он просто начал жаловаться на невозможность найти GDK.
- Прекратите использовать var в документации по коду и примерах. Никто не должен обращаться к IDE, чтобы выяснить, что такое тип переменной. Двусмысленности нет места в документации по коду. Это особенно бесит, потому что, несмотря на то, что большие головы в Oracle советуют другим использовать лучшие имена переменных, вы сами отстой в именовании переменных. Хотите пример со страницы примеров Панамы?
// call "readline" API
var p = readline(pstr);
Или как насчет Документации Project Loom:
try (var scope = FiberScope.open()) {
var fiber1 = scope.schedule(() -> "one");
var fiber2 = scope.schedule(() -> "two");
});
«scope» может быть как Scope, так и FiberScope. Я уверен, что есть много примеров JDK. Это действительно раздражает. Останови это.