Возможно, одним из самых больших недостатков 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. Это действительно раздражает. Останови это.