Главная » Хабрахабр » [Javawatch Live] История одного pull request. `os.version` в SubstrateVM

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

public class Main
}

Вначале, как выглядит выхлоп на настоящей 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.

Это очень здорово и совсем не напоминает те мучения, которые приходится испытывать в основном OpenJDK. Я не буду писать полный путь до файлика, потому что прямо в SVM встроена возможность генерить корректные проекты для IntelliJ IDEA и Eclipse. Итак: Пусть классы за нас открывает 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. Ура!

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

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

/** 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 и нагло скопипастить. Немного археологии и мародёрства никогда не помешает храброму исследователю!

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

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

private static native Properties initProperties(Properties props);

Noooooooooooooo.

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

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

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

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

extern char **environ; char **getEnviron() { return environ;
}

Теперь погрузимся немного глубже и откроем полные исходники 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.

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

Но вот в 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++ компилятора и так далее. Я пошёл в чатик и спросил Криса Ситона, какой тулчейн является самым правильным для сборки.

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

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

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

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

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

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

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

Выводы

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

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

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

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

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

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

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

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

Давайте посмотрим, что это за зверь такой, 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, чтобы память не утекла.

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

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

Выводы

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

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

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

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

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

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

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

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Нечеткая логика против ПИД. Скрещиваем ежа и ужа. Авиадвигатель и алгоритмы управления АЭС

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

Путь интроверта в карьере и бизнесе

Это откровенная статья дизайнера-интроверта, где я делюсь своей историей карьерного пути с выводами и цифрами моих доходов. Все как вы любите. Здесь будет только личный опыт, возможно, он кому-то будет полезен, а может и нет. Меня зовут Сергей Захаров. Начну ...