[Javawatch Live] История одного pull request. `os.version` в SubstrateVM

в 9:15, , рубрики: graal, graalvm, habracast, java, javawatch, podcast, substratevm, SVM, Блог компании JUG.ru Group, Компиляторы, Программирование, системное программирование

Прошел год с тех пор, как удалась предыдущая выходка: опубликовать вместо поста ролик на YouTube. «Стыдный разговор о синглтонах» набрал 7к просмотров на YouTube и вдвое больше на самом Хабре в текстовой версии. Для статьи, написанной в совершенно упоротом состоянии и рассказывающей о древнейшем баяне — это что-то вроде успеха.

Сегодня я всю ночь монтировал новый выпуск. На этот раз тема куда более свежая: история коммита в экспериментальную технологию — SubstrateVM. А вот градус упоротости поднялся на новый уровень.

Очень жду ваших комментариев! Напоминаю, что если вы хотите действительно что-то улучшить в этом посте, то лучше всего зафайлить ишшую на Github. Хотел бы сказать «ставьте лайки и подписывайтесь на новый канал», но ведь все его выпуски и так будут у вас в хабе Java?

Технически: в видео есть одна склейка ближе к концу. Просто я писал несжатое видео, и мой m2 ssd размером всего в пятьсот гигабайт быстро переполнился. А ни один другой жесткий диск не смог выдержать такого напора данных. Поэтому пришлось отключиться на полчаса и изголившись найти дополнительные пятьдесят гигов на запись последних нескольких минут. Это было достигнуто удалением файлов собранного GoogleChrome. Мнение о записывающем софте отписал в ФБ прямо в момент записи, там очень много боли.

Ещё из технически интересного: YouTube почему-то заблокировало мне live streaming. При этом на аккаунте нет ни единого страйка и клейма. Будем надеяться, что это просто косяк, и через 90 дней всё вернут назад.

В этой статье будут цитаты из кода, принадлежащего компании Oracle. Использовать у себя этот код нельзя (разве что вы прочитаете оригинальную лицензию, и она это позволяет на условиях, например, GPL). Это не шутка. Олсо, я предупреждал.

Присказка (а сказка будет впереди)

Многие уже наслушались историй, что «новая Java будет написана на Java» и недоумевают, как же такое может быть. Есть программный документ Project Metropolis и соответсвующее письмо Джона Роуза, но там всё довольно расплывчато.

Это звучит как какая-то жуткая, кровавая магия. В том же, что можно попробовать прямо сейчас, не просто нет магии, а всё тупо как обратная сторона лопаты, когда вам выбивают ею зубы. Конечно, есть некие нюансы, но об этом будет когда-нибудь очень потом.

Покажу это на примере одной поучительной истории, которая случилась летом. Как там в школах пишут сочинение «как я провел лето».

Для начала небольшая ремарка. Проект, который сейчас занимается Ahead-of-Time компиляцией в Oracle Labs — это GraalVM. Компонент, который, собственно, делает ништяки и превращает джавовый код в исполняемый файл (в экзешник) — это SubstrateVM или коротко SVM. Не путайте это с таким же сокращением, которым пользуются дата-сатанисты (support vector machine). Вот о SVM, как о ключевой части, мы и поговорим дальше.

Постановка задачи

Итак, «как я провел лето». Я сидел в отпуске, двачевал F5 на гитхабе Грааля и наткнулся на такую ишшую:

[Javawatch Live] История одного pull request. `os.version` в SubstrateVM - 1

Человеку хочется, чтобы os.version отдавала верное значение.

Ну чо, я же хотел починить баг? Пацан сказал — пацан сделал.

Идем проверять, не врет ли наш поциент.

public class Main {
    public static void main(String[] args) {
        System.out.println(System.getProperty("os.version"));
    }
}

Вначале, как выглядит выхлоп на настоящей Java: 4.15.0-32-generic. Да, это свежая Ubuntu LTS Bionic.

Теперь попробуем сделать то же на SVM:

$ ls
Main.java

$ javac -cp . Main.java
$ ls
Main.class  Main.java

$ native-image Main
Build on Server(pid: 18438, port: 35415)
   classlist:     151.77 ms
       (cap):   1,662.32 ms
       setup:   1,880.78 ms
error: Basic header file missing (<zlib.h>). Make sure libc and zlib headers are available on your system.
Error: Processing image build request failed

Ну да. Это потому, что специально для «чистого» теста я сделал совершенно новую виртуальную машину.

$ sudo apt-get install zlib1g-dev libc6 libc6-dev

$ native-image Main
Build on Server(pid: 18438, port: 35415)
   classlist:     135.17 ms
       (cap):     877.34 ms
       setup:   1,253.49 ms
  (typeflow):   4,103.97 ms
   (objects):   1,441.97 ms
  (features):      41.74 ms
    analysis:   5,690.63 ms
    universe:     252.43 ms
     (parse):   1,024.49 ms
    (inline):     819.27 ms
   (compile):   4,243.15 ms
     compile:   6,356.02 ms
       image:     632.29 ms
       write:     236.99 ms
     [total]:  14,591.30 ms

Абсолютные цифры времени выполнения могут ужаснуть. Но, во-первых, так и задумано: здесь применяются очень адские оптимизации. А во-вторых, это хилая виртуалка, что вы хотите.

И наконец, момент истины:

$ ./main
null

Похоже, наш гость не врал, действительно не работает.

Первый подход: кража свойств у хоста

Дальше я глобальным поиском поискал по os.version и обнаружил, что все эти свойства лежат в классе SystemPropertiesSupport.

Я не буду писать полный путь до файлика, потому что прямо в SVM встроена возможность генерить корректные проекты для IntelliJ IDEA и Eclipse. Это очень здорово и совсем не напоминает те мучения, которые приходится испытывать в основном OpenJDK. Пусть классы за нас открывает IDE. Итак:

public abstract class SystemPropertiesSupport {
    private static final String[] HOSTED_PROPERTIES = {
                    "java.version",
                    ImageInfo.PROPERTY_IMAGE_KIND_KEY,
                    "line.separator", "path.separator", "file.separator",                    
                    "os.arch", "os.name",
                    "file.encoding", "sun.jnu.encoding",
    };
   //...
}

Дальше я, совершенно не включая голову, просто пошел и добавил в этот набор еще одну переменную:

"os.arch", "os.name", "os.version"

Пересобираю, запускаю, получаю заветную строчку 4.15.0-32-generic. Ура!

Но вот проблема: теперь на каждой машине, где запущен этот код, он всегда выдает 4.15.0-32-generic. Даже там, где uname -a отдает предыдущую версию ведра, на старой Убунте.

Становится понятно, что эти переменные записываются в исходный файл в момент компиляции.
И действительно, нужно внимательней читать комментарии:

/** System properties that are taken from the VM hosting the image generator. */
private static final String[] HOSTED_PROPERTIES

Нужно применять другие способы.

Выводы

  • Если вам хочется, чтобы в SVM появилась system property из «основной джавы», сделать это очень просто. Прописываем нужное свойство в правильном месте, всё.
  • Работать можно в IDE, которая поддерживает Java и Python одновременно. Например, в IntelliJ IDEA Ultimate с Python-плагином или то же самое в Eclipse.

Второй подход

Если порыться в файлике SystemPropertiesSupport, то обнаруживаем куда более разумную штуку:

/** System properties that are lazily computed at run time on first access. */
private final Map<String, Supplier<String>> lazyRuntimeValues;

Кроме всего прочего, использование этих пропертей ещё и не блокирует процесс сборки экзешника. Понятно, что если мы напихаем сильно много в HOSTED_PROPERTIES, то всё будет тормозить.

Регистрация ленивых пропертей происходит очевидным образом, по ссылке на метод, который возвращает:

lazyRuntimeValues.put("user.name", this::userNameValue);
lazyRuntimeValues.put("user.home", this::userHomeValue);
lazyRuntimeValues.put("user.dir", this::userDirValue);

Причем все эти ссылки на методы — интерфейсные, и та же this::userDirValue реализуется для каждой из поддерживаемых платформ. В данном случае это PosixSystemPropertiesSupport и WindowsSystemPropertiesSupport.

Если из любопытства сходить в реализацию для Windows, то увидим печальное:

@Override
protected String userDirValue() {
    return "C:\Users\somebody";
}

Как видим, Windows ещё не поддерживается :-) Впрочем, настоящая задача в том, что генерация экзешников для Windows ещё не доделана, поэтому поддержка этих методов на самом деле была бы совершенно лишними усилиями.

То есть нужно реализовать следующий метод:

lazyRuntimeValues.put("os.version", this::osVersionValue);

И потом поддержать его в двух-трех имеющихся интерфейсах.

Но что туда писать?

Выводы

  • Если хочется добавить новое свойство, вычисляющееся в рантайме, то это дело написания одного метода. Результат может зависеть от текущей операционной системы, механизм переключения уже работает и есть не просит.

Немного археологии

Первое, что приходит в голову — подсмотреть реализацию в OpenJDK и нагло скопипастить. Немного археологии и мародёрства никогда не помешает храброму исследователю!

Смело открываем в Идее любой джавовый проект, пишем там System.getProperty("os.version"), и по ctrl+click переходим к реализации метода getProperty(). Оказывается, всё это лежит тупо в Properties.

Казалось бы, достаточно скопипастить то место, где эти Properties заполняются, и, задорно смеясь, убежать в пустоту. К сожалению, натыкаемся на проблему:

private static native Properties initProperties(Properties props);

Noooooooooooooo.

[Javawatch Live] История одного pull request. `os.version` в SubstrateVM - 2

А ведь так всё хорошо начиналось.

А был ли мальчик?

Как мы знаем, использовать C++ — плохо. Используется ли C++ в SVM?

Еще как! Для этого даже есть специальный пакет: src/com.oracle.svm.native.

И в этом пакете, ужас-ужас, лежит файл getEnviron.c с чем-то таким:

extern char **environ;

char **getEnviron() {
  return environ;
}

Пришло время обмазаться C++

Теперь погрузимся немного глубже и откроем полные исходники OpenJDK.

Если у кого-то их ещё нет, то можно посмотреть в вебе либо скачать. Предупреждаю, качаются они вот отсюда, всё еще с помощью Mercurial, и всё еще это займет где-то полчаса.

Нужный нам файл лежит по адресу src/java.base/share/native/libjava/System.c.

Заметили, что это путь к файлу, а не просто название? Всё правильно, свою новую блестящую модную Идею, купленную за 200$ в год, можно в помойку засунуть. Можно попробовать CLion, но, во избежание необратимых душевных повреждений, лучше просто взять Visual Studio Code. Он уже что-то подсвечивает, но ещё не понимает увиденного (не перечёркивает всё подряд красным).

Краткий пересказ System.c:

java_props_t *sprops = GetJavaProperties(env);
PUTPROP(props, "os.version", sprops->os_version);

В свою очередь, берутся они в src/java.base/unix/native/libjava/java_props_md.c.
Под каждую платформу есть свой такой файлик, переключаются они через #define.

И вот тут начинается. Платформ много. На всякую некрофилию вроде AIX можно забить, потому что GraalVM официально это не поддерживает (насколько знаю, вначале планируются GNU-Linux, macOS и Windows). GNU/Linux и Windows поддерживают использование <sys/utsname.h>, в которой есть готовые методы для получения имени и версии операционной системы.

Но вот в macOS есть жуткий кусок говнокода.

  • В нем захардкожено название «Mac OS X» (хотя оно давно macOS);
  • Оно зависит от версии макоси. До 10.9 в SDK не было функции operatingSystemVersion, и нужно было руками вычитывать SystemVersion.plist;
  • Для этого вычитывания оно использует ObjC расширения как-то так:
// Fallback if running on pre-10.9 Mac OS
    if (osVersionCStr == NULL) {
        NSDictionary *version = [NSDictionary dictionaryWithContentsOfFile :
                                 @"/System/Library/CoreServices/SystemVersion.plist"];
        if (version != NULL) {
            NSString *nsVerStr = [version objectForKey : @"ProductVersion"];
            if (nsVerStr != NULL) {
                osVersionCStr = strdup([nsVerStr UTF8String]);
            }
        }
    }

Если изначально была идея переписать это вручную в хорошем стиле, то она быстро разбилась о реальность. А вдруг я где-то накосячу в дебре этой лапши из if'ов, у кого-то это сломается, и меня повесят на центральной площади? Ну нафиг. Надо копипастить.

Выводы

  • IDE не понадобится;
  • Любое общение с C++ — это больно, неприятно, не понимаемо с первого взгляда.

Копипаста — это норм?

Это важный вопрос, от которого зависело количество дальнейших мучений. Переписывать вручную уж очень не хотелось, но попасть в суд за нарушение лицензий ещё хуже. Поэтому я пошел на гитхаб и спросил Codrut Stancu об этом напрямую. Вот что он ответил:

«Переиспользование кода OpenJDK, например, копипаста — это нормальная штука с точки зрения лицензирования. Тем не менее, для этого нужно иметь очень вескую причину. Если фичу можно реализовать, переиспользуя код JDK без копирования, например, пропатчить его подстановкой — это будет куда лучше».

Это звучит как официальное разрешение на копипаст!

Нормально же общались...

Я начал переносить этот кусок кода, но уперся в свою лень. Чтобы проверить работу под macOS разных версий, нужно найти как минимум одну с некрофильской 10.8 Mountain Lion. У меня доступно два своих яблочных девайса и один у подруги, плюс можно развернуть в какой-нибудь триальной VMWare.

Но лень. И эта лень меня спасла.

Я пошёл в чатик и спросил Криса Ситона, какой тулчейн является самым правильным для сборки. Какая поддерживается версия операционки, C++ компилятора и так далее.

В ответ получил удивлённое молчание чата и ответ Криса, что он не понял сути вопроса.

Прошло дофига времени, прежде чем Крис смог понять, что я хочу сделать, и попросил не делать так никогда.

That's really missing the idea of SVM. SVM is pure Java, it's not supposed to have code put into it from OpenJDK. You can read it, convert it into Java, but nobody wants C++ code from OpenJDK. That's the last thing we want.

Пример с математическими библиотеками его не убедил. Как минимум, они написаны на Си, и включение C++ означало бы подключение совершенного нового языка в кодовую базу. Причём такого, который фуфуфу.

Что же нужно делать? Писать на System Java.

И если уж обращения к C/C++ Platform SDK никак не избежать, то это должен быть какой-то одиночный системный вызов, завернутый в C API. Данные вытягиваются в Java и дальше бизнес-логика пишется строго на Java, даже если в Platform SDK есть удобные готовые способы сделать это иначе на стороне C++.

Я вздохнул и начал изучать исходники с целью разобраться, как это можно сделать по-другому.

Выводы

  • Обсуждайте с людьми в чатике все неясные подробности. Они отвечают, если вопросы не совсем идиотские. Хотя на этом примере видно, что и идиотские вопросы Крис обсуждать готов, даже если это не экономит лично его время;
  • C++ не присутствует в проекте вообще. Нет никаких оснований считать, что кто-то даст его притащить под полой;
  • Вместо этого нужно писать на System Java, используя Си в самом крайнем случае (например, при вызове platform SDK).

Скрипач не нужен

А Скрипач не нужен, родной. Он только лишнее топливо жрёт.

Тут меня обуяла некоторая грусть, потому что вот глядите. Если в Windows у нас есть <sys/utsname.h>, и мы тупо надеемся на его ответ — это легко и просто.

Но если его нет, придётся делать что?

  • Звать встроенные команды cmd или утилиты Windows? Выдающие текст на русском языке, который надо парсить. Это самое дно, и оно может не совпадать с тем, что в этом месте ответит настоящий OpenJDK.
  • Брать из Реестра? Даже здесь есть нюансы, например, при переходе с Windows 7 на 10 поменялся метод хранения циферок в Реестре, и в Windows 10 версию нужно либо руками клеить из мажорной и минорной компоненты, либо просто отвечать, что это Windows 10 одной цифрой. Какой из этих способов правильней (не заставит задницы пользователей раскаляться) — неясно.

К счастью, мои душевные терзания были прерваны пулреквестом Paul Woegerer, который всё это починил.

Интересно, что вначале всё починилось в мастере (os.version перестал отдавать null в тесте), и только потом я заметил пулреквест. Проблема в том, что этот коммит не отмечен на Гитхабе как пулреквест — это простой коммит с надписью PullRequest: graal/1885 в комментарии. Дело в том, что чуваки в Oracle Labs не используют Github, он им нужен только для взаимодействия с внешними коммитерами. Всем нам, кому не посчастливилось работать в Oracle Labs, необходимо подписаться на оповещения о новых коммитах в репозиторий и читать их все.

Зато теперь можно расслабиться и посмотреть, как эту фичу реализовывать правильно.

Давайте посмотрим, что это за зверь такой, System Java.

Как я уже говорил ранее, всё просто, как обратная сторона лопаты, когда тебе пытаются выбить зубы. И так же болезненно. Взглянем на цитату из пула:

@Override
    protected String osVersionValue() {
        if (osVersionValue != null) {
            return osVersionValue;
        }

        /* On OSX Java returns the ProductVersion instead of kernel release info. */
        CoreFoundation.CFDictionaryRef dict = CoreFoundation._CFCopyServerVersionDictionary();
        if (dict.isNull()) {
            dict = CoreFoundation._CFCopySystemVersionDictionary();
        }
        if (dict.isNull()) {
            return osVersionValue = "Unknown";
        }
        CoreFoundation.CFStringRef dictKeyRef = DarwinCoreFoundationUtils.toCFStringRef("MacOSXProductVersion");
        CoreFoundation.CFStringRef dictValue = CoreFoundation.CFDictionaryGetValue(dict, dictKeyRef);
        CoreFoundation.CFRelease(dictKeyRef);
        if (dictValue.isNull()) {
            dictKeyRef = DarwinCoreFoundationUtils.toCFStringRef("ProductVersion");
            dictValue = CoreFoundation.CFDictionaryGetValue(dict, dictKeyRef);
            CoreFoundation.CFRelease(dictKeyRef);
        }
        if (dictValue.isNull()) {
            return osVersionValue = "Unknown";
        }
        osVersionValue = DarwinCoreFoundationUtils.fromCFStringRef(dictValue);
        CoreFoundation.CFRelease(dictValue);
        return osVersionValue;
    }

Иначе говоря, мы пишем на Java слово в слово то, что написали бы на Си.

Гляньте, как записан DarwinExecutableName:

 @Override
    public Object apply(Object[] args) {
        /* Find out how long the executable path is. */
        final CIntPointer sizePointer = StackValue.get(CIntPointer.class);
        sizePointer.write(0);
        if (DarwinDyld._NSGetExecutablePath(WordFactory.nullPointer(), sizePointer) != -1) {
            VMError.shouldNotReachHere("DarwinExecutableName.getExecutableName: Executable path length is 0?");
        }
        /* Allocate a correctly-sized buffer and ask again. */
        final byte[] byteBuffer = new byte[sizePointer.read()];
        try (PinnedObject pinnedBuffer = PinnedObject.create(byteBuffer)) {
            final CCharPointer bufferPointer = pinnedBuffer.addressOfArrayElement(0);
            if (DarwinDyld._NSGetExecutablePath(bufferPointer, sizePointer) == -1) {
                /* Failure to find executable path. */
                return null;
            }
            final String executableString = CTypeConversion.toJavaString(bufferPointer);
            final String result = realpath(executableString);
            return result;
        }
    }

Все вот эти CIntPointer, CCharPointer, PinnedObject, каково.

На мой вкус, это неудобно и некрасиво. Нужно вручную работать с указателями, которые выглядят как Java-классы. Нужно вовремя звать соответствующий release, чтобы память не утекла.

Но если вам кажется, что это неоправданные меры, то можете снова взглянуть на реализацию GC в .NET и ужаснуться, к чему приводит C++, если вовремя не остановиться. Напоминаю, это один огромный CPP-файл размером более мегабайта. Есть некие описания его работы, но они явно недостаточны для понимания внешним контрибьютором. Код выше, пусть и мерзко выглядящий, вполне себе понимаем и анализируем средствами статического анализа для Java.

Что касается сути коммита, у меня к нему вопросы. И как минимум там не реализована поддержка Windows. Когда для Windows появится кодген, попробую взять эту задачу на себя.

Выводы

  • Нужно писать на System Java. Нахваливать, называть сладким хлебушком. Вариантов всё равно нет;
  • Подпишитесь на нотификации от репозитория на GitHub и читайте коммиты, иначе важные PR пролетят стороной;
  • По возможности спрашивайте о любой большой фиче тех, кто отвечает за эту область. Есть куча вещей, которые реализованы, но о них пока не известно широкой общественности. Есть шанс изобрести велосипед, причем значительно более плохой, чем сделанный ребятами из Oracle Labs;
  • Когда беретесь за фичу, обязательно сообщите ответственному на гитхабе. Если он не отвечает — напишите письмо, адреса всех участников команды легко гуглятся.

Эпилог

На этом кончается эта битва, но не война вообще.

Боец, чутко жди новых статей на Хабре и вписывайся в наши ряды!

Хочу напомнить, что на следующую конференцию Joker приедет Олег Шелаев — единственный официальный русскоговорящий евангелист GraalVM из Oracle. Название доклада («Компилируем Java ahead-of-time с GraalVM») как бы намекает, что без SubstrateVM не обойдётся.

Кстати, Олегу недавно выдано табельное оружие — аккаунт на Хабре, shelajev-oleg. Постов там пока еще нет, но по этому юзернейму можно кастовать.

Пообщаться Олегом и Олегом можно в нашем чатике-междусобойчике в Telegram: @graalvm_ru. В отличие от ишшуёв на Гитхабе, там можно общаться в произвольной форме, и никто не забанит (но это не точно).

Также напоминаю, что мы каждую неделю вместе с подкастом «Разбор Полетов» делаем выпуск «Java-дайджеста». Например, вот таким был последний дайджест. Время от времени там проскакивают новости и про GraalVM (по факту, я не превращаю весь выпуск в выпуск новостей GraalVM только из-за уважения к аудитории :-)

Спасибо, что прочитали это — и до новых встреч!

Автор: Олег Чирухин

Источник

* - обязательные к заполнению поля