[Javawatch Live] L'histoire d'une demande de tirage. `os.version` dans SubstrateVM

Un an s'est écoulé depuis la réussite de l'astuce précédente: publier une vidéo sur YouTube au lieu d'un article. «Discours honteux sur singleton» a gagné 7 000 vues sur YouTube et deux fois plus sur Habré dans la version texte. Pour un article écrit dans un état complètement têtu et parlant de l'ancien accordéon à boutons - c'est un peu un succès.

Aujourd'hui, j'ai installé une nouvelle version toute la nuit. Cette fois, le sujet est beaucoup plus récent: l'histoire du commit à la technologie expérimentale - SubstrateVM. Mais le degré de ténacité a atteint un nouveau niveau.



Au plaisir de vos commentaires! Je vous rappelle que si vous voulez vraiment améliorer quelque chose dans ce post, il est préférable de déposer celui-ci sur Github . Je voudrais dire "aimez et abonnez-vous à la nouvelle chaîne , mais toutes ses versions seront-elles de toute façon dans votre hub Java?"

Techniquement: la vidéo a un collage plus proche de la fin. Je viens d'écrire une vidéo non compressée et mon ssd m2 de seulement cinq cents gigaoctets a rapidement débordé. Et aucun autre disque dur ne pourrait résister à une telle pression de données. Par conséquent, j'ai dû me déconnecter pendant une demi-heure et je me suis mis en quatre pour trouver cinquante concerts supplémentaires pour enregistrer les dernières minutes. Cela a été réalisé en supprimant les fichiers compilés par GoogleChrome . J'ai écrit sur le logiciel d'enregistrement dans le FB juste au moment de l'enregistrement , il y a beaucoup de douleur.

Un autre des aspects techniquement intéressants: YouTube pour une raison quelconque m'a bloqué la diffusion en direct. Dans le même temps, il n'y a pas une seule grève et stigmatisation sur le compte. Espérons que ce n'est qu'un jambage, et après 90 jours, tout sera retourné.

Cet article citera un code appartenant à Oracle. Vous ne pouvez pas utiliser ce code pour vous-même (à moins que vous n'ayez lu la licence d'origine et qu'il ne l'autorise dans les conditions, par exemple, de la GPL). Ce n'est pas une blague. Olso, j'ai prévenu.

Prikazka (et un conte de fées sera à venir)


Beaucoup ont déjà entendu des histoires selon lesquelles «le nouveau Java sera écrit en Java» et se demandent comment cela pourrait être. Il y a un document de politique de Project Metropolis et une lettre correspondante de John Rose , mais tout y est assez vague.

Cela ressemble à une sorte de magie effrayante et sanglante. Dans la même chose que vous pouvez essayer en ce moment, non seulement il n'y a pas de magie, mais tout est stupide comme le dos d'une pelle lorsque vous enlevez vos dents avec. Bien sûr, il y a quelques nuances, mais ce sera un jour très tard.

Je vais le montrer sur l'exemple d'une histoire instructive qui s'est produite pendant l'été. Comment ils écrivent l'essai «Comment j'ai passé l'été» dans les écoles.

Pour commencer une petite remarque. Le projet en cours de compilation Ahead-of-Time dans Oracle Labs est GraalVM. Le composant qui, en fait, fait nishtyaki et transforme le code Java en un fichier exécutable (en un exécutable) est SubstrateVM ou SVM pour faire court. Ne confondez pas cela avec la même abréviation utilisée par les satanistes de données (support vector machine). Il s'agit de SVM, en tant que partie clé, nous parlerons plus loin.

Énoncé du problème


Donc, "comment j'ai passé l'été." Je me suis assis en vacances, j'ai passé deux F5 sur le github du Graal et je suis tombé sur celui-ci :



Une personne veut que os.version donne la bonne valeur.

Eh bien cho, je voulais corriger le bug? Le garçon a dit - le garçon l'a fait.

Nous allons vérifier si notre client ment.

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

Tout d'abord, à quoi ressemble l'échappement en vrai Java: 4.15.0-32-generic . Oui, c'est une nouvelle version d'Ubuntu LTS Bionic.

Essayez maintenant de faire de même sur 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 

Et bien oui. En effet, j'ai créé une toute nouvelle machine virtuelle spécialement pour le test «propre».

 $ 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 

Les chiffres absolus d'exécution peuvent être horribles. Mais, d’abord, c’est ce qu’on voulait faire: des optimisations très infernales sont appliquées ici. Et deuxièmement, c'est une machine virtuelle fragile que vous voulez.

Et enfin, le moment de vérité:

 $ ./main null 

Il semble que notre invité n'a pas menti, ne fonctionne vraiment pas.

Première approche: voler les propriétés de l'hôte


Ensuite, j'ai recherché os.version dans le os.version et os.version constaté que toutes ces propriétés se SystemPropertiesSupport dans la classe SystemPropertiesSupport .

Je n'écrirai pas le chemin complet du fichier, car la possibilité de générer les projets corrects pour IntelliJ IDEA et Eclipse est intégrée directement dans SVM. C'est très cool et ne rappelle pas du tout le tourment que la plupart du temps OpenJDK doit vivre. Laissez l'IDE ouvrir des classes pour nous. Donc:

 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", }; //... } 

Ensuite, sans inclure la tête du tout, je suis simplement allé ajouter une autre variable à cet ensemble:

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

Je reconstruis, je cours, j'obtiens la ligne convoitée 4.15.0-32-generic . Hourra!

Mais voici le problème: maintenant sur chaque machine où ce code est exécuté, il émet toujours 4.15.0-32-generic . Même là où uname -a rend la version précédente du bucket, sur l'ancien Ubunt.

Il devient clair que ces variables sont écrites dans le fichier source au moment de la compilation.
Et vraiment, vous devez lire attentivement les commentaires:

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

D'autres méthodes doivent être appliquées.

Conclusions


  • Si vous voulez qu'une propriété système de «Java principal» apparaisse dans SVM, c'est très simple. Nous écrivons la propriété souhaitée au bon endroit, c'est tout.
  • Vous pouvez travailler dans l'IDE, qui prend en charge Java et Python en même temps. Par exemple, dans IntelliJ IDEA Ultimate avec un plugin Python ou le même dans Eclipse.

Deuxième approche


Si vous fouillez dans le SystemPropertiesSupport SystemPropertiesSupport, nous trouvons une chose beaucoup plus raisonnable:

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

Entre autres choses, l'utilisation de ces propriétés ne bloque toujours pas le processus de construction d'un exécutable. Il est clair que si nous HOSTED_PROPERTIES beaucoup dans HOSTED_PROPERTIES , alors tout ralentira.

L'enregistrement des propriétés paresseuses se produit de manière évidente, par référence à une méthode qui renvoie:

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

De plus, toutes ces références de méthode sont des interfaces, et la même this::userDirValue est implémentée pour chacune des plateformes prises en charge. Dans ce cas, ce sont PosixSystemPropertiesSupport et WindowsSystemPropertiesSupport .

Si nous sortons par curiosité d'une implémentation pour Windows, nous verrons la triste chose:

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

Comme vous pouvez le voir, Windows n'est pas encore pris en charge :-) Cependant, le vrai problème est que la génération d'exécutables pour Windows n'est pas encore terminée, donc la prise en charge de ces méthodes serait en fait des efforts complètement inutiles.

Autrement dit, vous devez implémenter la méthode suivante:

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

Et puis le prendre en charge dans deux ou trois interfaces disponibles.

Mais quoi y écrire?

Conclusions


  • Si vous souhaitez ajouter une nouvelle propriété calculée en runtime, il s'agit d'écrire une méthode. Le résultat peut dépendre du système d'exploitation actuel, le mécanisme de commutation fonctionne déjà et ne le demande pas.

Un peu d'archéologie


La première chose qui me vient à l'esprit est de jeter un œil à une implémentation dans OpenJDK et de copier-coller effrontément. Un peu d'archéologie et de pillages n'empêcheront jamais un courageux explorateur!

N'hésitez pas à ouvrir n'importe quel projet Java dans l'Idea, à y écrire System.getProperty("os.version") et, en System.getProperty("os.version") + clic, accédez à l'implémentation de la méthode getProperty() . Il s'avère que tout cela se trouve bêtement dans les Properties .

Il semblerait, juste copier-coller l'endroit où ces Properties sont remplies, et rire avec ferveur, s'enfuir dans le vide. Malheureusement, nous rencontrons un problème:

 private static native Properties initProperties(Properties props); 

Noooooooooooooo.



Mais tout a si bien commencé.

Y avait-il un garçon?


Comme nous le savons, l'utilisation de C ++ est mauvaise. Le C ++ est-il utilisé dans SVM?

Comme ça! Il existe même un package spécial pour cela: src/com.oracle.svm.native .

Et dans ce paquet, horror-horror, se trouve le fichier getEnviron.c avec quelque chose comme ceci:

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

Il est temps de jouer avec C ++


Maintenant, plongez un peu plus et ouvrez toutes les sources OpenJDK.

Si quelqu'un ne les a pas déjà, vous pouvez les consulter sur le Web ou les télécharger. Je vous préviens, ils se balancent d'ici , toujours avec l'aide de Mercurial, et cela prendra encore environ une demi-heure.

Le fichier dont nous avons besoin se trouve dans src/java.base/share/native/libjava/System.c .

Avez-vous remarqué qu'il s'agit du chemin d'accès au fichier, et pas seulement du nom? C'est vrai, vous pouvez pousser votre nouvelle idée à la mode brillante, achetée 200 $ par an. Vous pouvez essayer CLion , mais afin d'éviter des dommages mentaux irréversibles, il est préférable de simplement prendre Visual Studio Code . Il met déjà en évidence quelque chose, mais ne comprend toujours pas ce qu'il a vu (il ne raye pas tout en rouge).

Relecture courte de System.c :

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

À leur tour, ils sont pris dans src/java.base/unix/native/libjava/java_props_md.c .
Chaque plate-forme a son propre fichier, ils passent par #define .

Et ici ça commence. Il existe de nombreuses plateformes. Toute nécrophilie comme AIX peut être notée, car GraalVM ne la prend pas officiellement en charge (pour autant que je sache, GNU-Linux, macOS et Windows sont prévus au début). GNU / Linux et Windows prennent en charge l'utilisation de <sys/utsname.h> , qui a des méthodes prédéfinies pour obtenir le nom et la version du système d'exploitation.

Mais macOS a un terrible morceau de govnokod .

  • Le nom «Mac OS X» y est enduit dur (bien qu'il ait longtemps été macOS);
  • Cela dépend de la version de makoshi. Avant la version 10.9, le SDK n'avait pas de fonction operatingSystemVersion et vous deviez lire SystemVersion.plist main;
  • Pour cette soustraction, il utilise l'extension ObjC quelque chose comme ceci:

 // 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]); } } } 

Si au départ il y avait une idée de réécrire manuellement dans un bon style, alors ça s'est rapidement écrasé dans la réalité. Et si je déconne quelque part dans la jungle de ces nouilles de ifs, quelqu'un le cassera et me suspendra sur la place centrale? Eh bien nafig. Il est nécessaire de copier-coller.

Conclusions


  • L'IDE n'est pas nécessaire;
  • Toute communication avec C ++ est douloureuse, désagréable, incomprise à première vue.

Le copier-coller est la norme?


C'est une question importante dont dépend la quantité de tourments supplémentaires. Je ne voulais vraiment pas réécrire manuellement, mais aller en justice pour avoir violé des licences est encore pire. Je suis donc allé au github et j'ai directement demandé à Codrut Stancu. Voici ce qu'il a répondu :

»La réutilisation du code OpenJDK, par exemple, le copier-coller est une chose normale du point de vue des licences. Cependant, il y a une très bonne raison à cela. Si une fonctionnalité peut être implémentée en réutilisant le code JDK sans copier, par exemple en le patchant avec une substitution, ce sera bien mieux. "

Cela ressemble à une autorisation officielle de copier-coller!

Nous avons parlé normalement ...


J'ai commencé à porter ce morceau de code, mais j'ai rencontré ma paresse. Pour tester macOS pour différentes versions, vous devez en trouver au moins un avec le Mountain Lion nécrophile 10.8. J'ai deux de mes appareils Apple disponibles et un d'un ami, et je peux le déployer sur un VMWare d'essai.

Mais la paresse. Et cette paresse m'a sauvé.

Je suis allé dans la salle de chat et j'ai demandé à Chris Seaton quelle chaîne d'outils était la plus appropriée pour l'assemblage. Quelle version du système d'exploitation est prise en charge, compilateur C ++, etc.

En réponse, il a reçu un silence surpris de la conversation et Chris a répondu qu'il ne comprenait pas l'essence du problème.

Il a fallu un peu de temps pour que Chris puisse comprendre ce que je voulais faire et lui a demandé de ne plus jamais le faire .
Il manque vraiment l'idée de SVM. SVM est pur Java, il n'est pas censé y avoir du code à partir d'OpenJDK. Vous pouvez le lire, le convertir en Java, mais personne ne veut de code C ++ d'OpenJDK. C'est la dernière chose que nous voulons.

L'exemple des bibliothèques de mathématiques ne l'a pas convaincu. Au minimum, ils sont écrits en C, et l'inclusion de C ++ signifierait la connexion d'un tout nouveau langage à la base de code. Et celui qui est fufufu.

Que faut-il faire? Écrivez en Java système .

Et si les appels au SDK de la plateforme C / C ++ ne peuvent pas être évités, il doit s'agir d'un appel système unique encapsulé dans l'API C. Les données sont extraites en Java, puis la logique métier est écrite strictement en Java, même si le Kit de développement Platform SDK dispose de méthodes prêtes à l'emploi pratiques pour le faire différemment du côté C ++.

J'ai soupiré et j'ai commencé à étudier le code source pour comprendre comment cela peut être fait différemment.

Conclusions


  • Discutez avec les gens du chat de tout détail obscur. Ils répondent si les questions ne sont pas complètement idiotes. Bien que cet exemple montre que Chris est prêt à discuter de questions idiotes, même si cela ne lui fait pas gagner du temps personnellement;
  • C ++ n'est pas du tout présent dans le projet. Il n'y a aucune raison de croire que quelqu'un le laissera le traîner sous le plancher;
  • Au lieu de cela, vous devez écrire dans System Java en utilisant C en dernier recours (par exemple, lors de l'appel du SDK de la plate-forme).

Le violoniste n'est pas nécessaire


Un violoniste n'est pas nécessaire, cher. Il ne mange que trop de carburant.

Ensuite, j'ai été submergé de tristesse, car regardez ici. Si sous Windows, nous avons <sys/utsname.h> , et nous espérons bêtement pour sa réponse - c'est facile et simple.

Mais s'il n'est pas là, que devra-t-il faire?

  • Appeler les commandes intégrées cmd ou les utilitaires Windows? Publier du texte en russe qui doit être analysé. C'est tout en bas, et cela peut ne pas coïncider avec ce que le vrai OpenJDK répondra à cet endroit.
  • Prendre du registre? Même ici, il y a des nuances, par exemple, lors du passage de Windows 7 à 10, la méthode de stockage des numéros numériques dans le Registre a changé, et dans Windows 10, la version doit être collée avec les mains des composants majeurs et mineurs, ou simplement répondre qu'il s'agit de Windows 10 à un chiffre. Laquelle de ces méthodes est la plus correcte (ne fera pas repentir les culs des utilisateurs) n'est pas claire.

Heureusement, mon angoisse a été interrompue par la quête de traction de Paul Woegerer, qui a tout réglé.

Fait intéressant, au début, tout a été réparé dans l'assistant ( os.version cessé de donner la valeur null dans le test), et ce n'est qu'alors que j'ai remarqué une pull pull. Le problème est que ce commit n'est pas marqué sur le github comme une requête de pull - c'est un commit simple avec l'inscription PullRequest: graal/1885 dans le commentaire. Le fait est que les mecs d'Oracle Labs n'utilisent pas Github, ils en ont seulement besoin pour interagir avec des committers externes. Tous ceux d'entre nous qui n'ont pas eu la chance de travailler chez Oracle Labs doivent s'abonner aux notifications de nouveaux commits dans le référentiel et les lire tous.

Mais maintenant, vous pouvez vous détendre et voir comment implémenter correctement cette fonctionnalité.

Voyons de quel genre de bête il s'agit, System Java.

Comme je l'ai dit plus tôt, tout est simple, comme le dos d'une pelle, quand ils essaient de vous casser les dents. Et tout aussi douloureux. Jetez un œil à la citation de la piscine:

 @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; } 

En d'autres termes, nous écrivons en Java mot pour mot ce que nous écririons en C.

DarwinExecutableName à l' 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; } } 

Tous ces CIntPointer , CCharPointer , PinnedObject , quoi.

À mon goût, c'est inconfortable et moche. Vous devez travailler manuellement avec des pointeurs qui ressemblent à des classes Java. Vous devez appeler la release appropriée à temps pour que la mémoire ne coule pas.

Mais s'il vous semble que ce sont des mesures injustifiées , alors vous pouvez regarder à nouveau l' implémentation de GC dans .NET et être horrifié de ce à quoi mène le C ++ si vous n'arrêtez pas à temps. Je vous rappelle qu'il s'agit d'un énorme fichier CPP plus grand qu'un mégaoctet. Il existe quelques descriptions de son travail, mais elles sont clairement insuffisantes pour être comprises par un contributeur externe. Le code ci-dessus, bien qu'assez méchant, est tout à fait compréhensible et analysé au moyen d'une analyse statique pour Java.

Quant à l'essence du commit, j'ai des questions pour lui. Et au moins le support de Windows n'y est pas implémenté. Lorsqu'un codegen apparaît pour Windows, je vais essayer de me charger de cette tâche.

Conclusions


  • Besoin d'écrire en Java système. Louange, appelle le pain sucré. Il n'y a toujours pas d'options;
  • Abonnez-vous aux notifications du référentiel sur GitHub et lisez les commits, sinon les PR importants passeront;
  • Si possible, renseignez-vous sur toute caractéristique importante de ceux qui sont responsables de ce domaine. Il y a beaucoup de choses qui sont mises en œuvre, mais elles ne sont pas encore connues du grand public. Il y a une chance d'inventer un vélo, et bien pire que celui fabriqué par les gars d'Oracle Labs;
  • Lorsque vous abordez une fonctionnalité, assurez-vous d'en informer le responsable du github. S'il ne répond pas - écrivez une lettre, les adresses de tous les membres de l'équipe sont facilement googlé.

Épilogue


Cette bataille se termine, mais pas la guerre du tout.

Combattant, attendez avec sensibilité les nouveaux articles sur Habré et rentrez dans nos rangs !

Je tiens à vous rappeler qu'Oleg Shelaev, le seul évangéliste officiel GraalVM d'Oracle, viendra à la prochaine conférence Joker . Pas seulement "le seul russophone", mais "le seul en général". Le titre du rapport ( «Compilation Java à l'avance avec GraalVM» ) indique que SubstrateVM ne peut pas s'en passer.

Soit dit en passant, Oleg a récemment reçu une arme de service - un compte rendu sur Habré, shelajev-oleg . Il n'y a pas encore de messages, mais vous pouvez lancer ce nom d'utilisateur.

Vous pouvez parler avec Oleg et Oleg dans notre chat room-chat en Telegram: @graalvm_ru . Contrairement à ishshuyev sur Github, vous pouvez y communiquer sous n'importe quelle forme, et personne ne sera interdit ( mais ce n'est pas exact ).

Je vous rappelle également que chaque semaine, avec le podcast Debriefing, nous faisons la sortie du Java Digest. Par exemple, c'était le dernier condensé . De temps en temps, les informations sur GraalVM sautent là-bas (en fait, je ne transforme pas le problème en un communiqué de presse GraalVM juste pour le respect du public :-)

Merci d'avoir lu ceci - et à bientôt!

Source: https://habr.com/ru/post/fr420455/


All Articles