Extension PHP et Kotlin Native. Troisième partie, probablement finale

Dans la première partie , des choses assez basiques sont racontées sur la mise en place d'outils et de concepts généraux.

La deuxième partie concerne, pour ainsi dire, la première approche du projectile, des idées, des plans, des plans.

Dans cet article, il y aura un peu plus de hardcore sur les interfaces C et K / N, beaucoup de macros, la douleur, le désespoir et les "rayons de bonté". Bien sûr, il y aura un chapitre avec une histoire sur les réalisations (vous ne vous féliciterez pas ... et en bonus, une histoire sur un fakap épique.

Avertissement: tout ce qui suit est considéré dans le contexte de l'écriture d'une bibliothèque pour PHP.

Chapitre un Interope naive


L'utilisation des fonctions K / N en C est décrite dans la première partie du cycle. En conséquence, je vais vous expliquer ici comment utiliser les fonctions C en K / N.

La documentation officielle est plutôt avare et concise, cependant, pour les projets simples, c'est assez.

En bref, vous devez créer un fichier spécial avec l'extension .def et y spécifier les fichiers d'en-tête nécessaires.

headers = php.h 

Ensuite, alimentez-le dans un programme appelé cinterop .

 # cinterop -def php.def -o php 

À la sortie, vous obtiendrez la bibliothèque libphp.klib contenant le bitcode llvm et diverses méta-informations.

Vous pouvez ensuite utiliser en toute sécurité les fonctions et macros décrites dans le fichier d'en-tête ( #define ), sans oublier de connecter la bibliothèque lors de la compilation.

 # kotlinc -opt -produce static ${SOURCES} -l libphp.klib -o myLib 

Mais il y a une nuance. Et pas un.

Sous la forme décrite ci-dessus, la bibliothèque ne sera pas assemblée


Pourquoi? Mais parce que les lignes suivantes sont présentes dans php.h:

 #include "php_version.h" #include "zend.h" #include "zend_sort.h" #include "php_compat.h" #include "zend_API.h" 

Ici, il convient de noter que llvm est toujours engagé dans la compilation de la bibliothèque, et il a le commutateur -I , et cinterop a le commutateur -copt . Eh bien, vous obtenez le point. En conséquence, une telle commande suffit pour compiler php.h.

 # cinterop -def my.def -o myLib -I${PHP_LIB_ROOT} -copt -I${PHP_LIB_ROOT} \ -copt -I${PHP_LIB_ROOT}/main \ -copt -I${PHP_LIB_ROOT}/Zend \ -copt -I${PHP_LIB_ROOT}/TSRM 

Macros. Je t'aime et je te déteste! Non, je déteste ça.


Tout ce que vous devez savoir sur #define en termes d' #define C> K / N est
Chaque macro C qui se développe en une constante est représentée comme une propriété Kotlin. Les autres macros ne sont pas prises en charge.

Et puis nous rappelons que l'extension PHP est une macro sur une macro et entraîne une macro et essaye de ne pas pleurer.

Mais tout n'est pas si mal. Pour contourner cette situation, les développeurs K / N ont fourni un rouleau de ruban électrique bleu à attacher au fichier def de déclarations personnalisées . Cela ressemble à ceci (par exemple, prenez la macro Z_TYPE_P )

 headers = php.h --- static inline zend_uchar __zp_get_arg_type(zval *z_value) { return Z_TYPE_P(z_value); } 

Maintenant, dans le code K / N, vous pouvez utiliser la fonction __zp_get_arg_type

Chapitre deux Paramètres INI PHP ou macro sous-sous.


C'est un "rayon de bien" vers le code source de PHP.

Il existe 4 macros pour l'extraction des paramètres:

 INI_INT(val) INI_FLT(val) INI_STR(val) INI_BOOL(val) 

val est une chaîne avec le nom du paramètre.

Examinons maintenant l'exemple d' INI_STR pour voir comment cette macro est définie.

 #define INI_STR(name) zend_ini_string_ex((name), sizeof(name)-1, 0, NULL) 

Déjà remarqué son "défaut fatal"?

Sinon, laissez-moi vous dire - c'est la sizeof fonction. Lorsque vous utilisez directement la macro, tout va bien:

 php_printf("The value is : %s", INI_STR("my.ini")); 

Lorsque vous l'utilisez via une fonction proxy à partir d'un fichier .def , le chariot se transforme en citrouille et sizeof (nom) renvoie la taille du pointeur. Échec et mat Kotlin Native.

En fait, il n'y a que deux options de contournement.

  1. N'utilisez pas des macros, mais des fonctions auxquelles elles sont attachées.
  2. L'encapsuleur de code dur fonctionne pour chaque paramètre requis.

La première option est meilleure pour tout le monde que la seconde, sauf pour un point - personne ne garantira que la déclaration macro ne changera pas. Par conséquent, pour mon projet, avec un profond sentiment d'insatisfaction, j'ai choisi la deuxième option.

Chapitre Trois Déboguer? Quel débag?


Acte 1 - Interop.


À un moment donné, après avoir attaché la bande bleue au fichier def de 20 fonctions proxy consécutives, j'ai reçu une merveilleuse erreur.

 Exception in thread "main" java.lang.Error: /tmp/tmp399964332777824085.c:103:38: error: too many arguments to function call, expected 2, have 3 at org.jetbrains.kotlin.native.interop.indexer.UtilsKt.ensureNoCompileErrors(Utils.kt:137) at org.jetbrains.kotlin.native.interop.indexer.IndexerKt.indexDeclarations(Indexer.kt:902) at org.jetbrains.kotlin.native.interop.indexer.IndexerKt.buildNativeIndexImpl(Indexer.kt:892) at org.jetbrains.kotlin.native.interop.indexer.NativeIndexKt.buildNativeIndex(NativeIndex.kt:56) at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.processCLib(main.kt:283) at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.interop(main.kt:38) at org.jetbrains.kotlin.cli.utilities.InteropCompilerKt.invokeInterop(InteropCompiler.kt:100) at org.jetbrains.kotlin.cli.utilities.MainKt.main(main.kt:29) 


Commentez la moitié, recompilez, si le commentaire sur la moitié du reste est répété, nous collectons ... Et étant donné que le processus de compilation des en-têtes est assez long ... (oui, cela semblait plus rapide que de grimper une douzaine de fichiers source et méticuleusement, avec une loupe, réconcilier).

Le deuxième "rayon de bien" va vers JetBrains.


Acte 2 - exécution.


J'obtiens un défaut de segmentation d' exécution. Bon ok, ça arrive. Je monte dans le débogueur. Ummm ... STA?

 Program received signal SIGSEGV, Segmentation fault. kfun:kotlinx.cinterop.toKString@kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVarOf<kotlin.Byte>>.()kotlin.String () at /opt/buildAgent/work/4d622a065c544371/Interop/Runtime/src/main/kotlin/kotlinx/cinterop/Utils.kt:402 402 /opt/buildAgent/work/4d622a065c544371/Interop/Runtime/src/main/kotlin/kotlinx/cinterop/Utils.kt: No such file or directory. 


Chapitre quatre J'ai versé du thé dans votre thé pour que vous puissiez en boire pendant que vous buvez du thé.


Ici, il faut dire comment fonctionne cette merde que je fais.

Vous écrivez un DSL décrivant la future extension PHP, écrivez du code K / N avec l'implémentation de fonctions, classes et méthodes, puis lancez make et, miraculeusement, obtenez une bibliothèque prête à l'emploi qui peut être connectée à PHP.

L'assemblage peut être divisé en 4 étapes:

  1. Création d'un calque entre C et K / N (le même cinterop )
  2. Génération de code d'extension C
  3. Compilation d'une bibliothèque avec logique
  4. Compilation de la bibliothèque cible

Le but est d'ajouter la possibilité de créer des instances d'une classe PHP en code K / N. Par exemple, pour que la classe puisse définir la méthode getInstance() . Et je veux que ce soit pratique à utiliser.

En C, ce problème est résolu une ou deux fois.

 zval *obj = malloc(sizeof(zval)); object_init_ex(obj, myClass); 

Cela semblerait simple - prenez-le et transférez-le à K / N, mais voici myClass ...

Mais myClass est une variable globale de type zend_class_entry* , déclarée dans le code C du projet et avec un nom inconnu à l'avance.

Regardez vos mains. Vous devez compiler la bibliothèque à partir du code K / N, dans lequel il y aura une fonction qui devra avoir accès à myClass , qui est défini dans le code C généré, mais non compilé, à partir duquel cette fonction sera ensuite appelée.

En fin de compte, la mise en œuvre de cette fonctionnalité a conduit à l'ajout de deux nouveaux artefacts: .h et .kt au stade de la génération de code, la complication de l'étape cinterop et le phakap épique, dont je parlerai à la fin.

Chapitre cinq Qu'est-ce qui est en mon nom?


L'histoire de pourquoi:

 enum class ArgumentType { PHP_STRING, PHP_LONG, PHP_DOUBLE, PHP_NULL, ... } 

mieux que:

 enum class ArgumentType { STRING, LONG, DOUBLE, NULL, ... } 

Oui, même pas besoin de l'expliquer. C'est ce que ArgumentType.NULL transforme en fichier d'en-tête de la bibliothèque Kotlin:

 struct { extension_kt_kref_php_extension_dsl_ArgumentType (*get)(); /* enum entry for NULL. */ } NULL; 

Et c'est ainsi que gcc réagit.

 /root/simpleExtension/phpmodule/extension_kt_api.h:113:17: error: expected identifier or '(' before 'void' } NULL; ^ 

Le rideau! Attention aux noms.

L'avant-dernier chapitre. Vous ne vous louerez pas - personne ne louera.


Dans l'ensemble, j'ai atteint mes objectifs. Immergé dans le sujet, le "framework" pour écrire des extensions PHP sur Kotlin Native, en général, est prêt. Il reste à ajouter certaines fonctionnalités, et non les plus critiques, et polir.

Le projet lui-même et, je l'espère, une bonne documentation pour lui, peuvent être consultés sur le github .

Que puis-je dire à propos de K / N? Seulement bon. L'écrire est un plaisir, et les petits bancs et la rugosité peuvent bien être attribués au fait qu'il n'est même pas sorti du berceau :)

Le dernier chapitre. Rayons de bien, sans guillemets.


Et maintenant, très sérieusement et avec un profond respect, je tiens à remercier les gars de JetBrains et les résidents de la chaîne slack Kotlin Native. Tu es super!

Et merci tout particulièrement à Nicholas Igotti .



Bonus Epic Fakap.


Le contexte est décrit dans le quatrième chapitre.

En fait, lorsque tout a été ajouté à un état dans lequel il a été compilé sans erreur, un problème est survenu - pendant les tests, PHP m'a été ouvert d'un côté complètement inconnu.

 # php -dextension=./phpmodule/modules/extension.so -r "var_dump(ExampleClass::getInstance());" *RECURSION* # 

"Figas!" - J'ai pensé, je suis entré dans le code source PHP et j'ai trouvé un tel morceau.

 case IS_OBJECT: if (Z_IS_RECURSIVE_P(struc)) { PUTS("*RECURSION*\n"); return; } 

Ajout du débogage:

 printf("%u", Z_IS_RECURSIVE_P(struc)) 

conduit à:

 undefined symbol: Z_IS_RECURSIVE_P in Unknown on line 0 

"Figas!" J'ai encore pensé.

À ce moment-là, quand j'ai deviné regarder le php.h (7.1.8) réellement utilisé sur l'hôte linux, et non celui qui a été tiré du github du master brunch (7.3.x), une journée s'est écoulée. Honte.

Mais, il s'est avéré que ce n'était pas une bobine.

Le code de vérification de récursivité correct, à toutes les étapes de la vie de l'objet que je contrôlais, indiquait que tout devait fonctionner. Et cela signifie que vous devez regarder attentivement ces endroits que je ne contrôle pas. Il y avait exactement une telle chose - dans laquelle mon objet est retourné à la fonction var_dump

 RETURN_OBJ( example_symbols()->kotlin.root.php.extension.proxy.objectToZval( example_symbols()->kotlin.root.exampleclass.getInstance(/* */) ) ) 

Ouvrez la macro RETURN_OBJ à la fin. Éloignez-vous des moniteurs nerveux et enceintes!

 1) RETURN_OBJ(r) 2) { RETVAL_OBJ(r); return; } 3) { ZVAL_OBJ(return_value, r); return; } 4) { do { zval *__z = (return_value); Z_OBJ_P(__z) = (r); Z_TYPE_INFO_P(__z) = IS_OBJECT_EX; } while (0); return; } 5) { do { zval *__z = (return_value); Z_OBJ(*(__z)) = (r); Z_TYPE_INFO(*(__z)) = (IS_OBJECT | (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT)); } while (0); return; } 6) { do { zval *__z = (return_value); (*(__z)).value.obj = (r); (*(__z)).u1.type_info = (8 | ((1<<0) << 8)); } while (0); return; } 

Ici, j'ai eu honte pour la deuxième fois. Moi, complètement sur mon œil bleu, zval* poussé zval* à l'endroit où zend_object* attendait et zend_object* passé près de deux jours à chercher l'erreur.

Merci à tous Kotlin! :)

PS. S'il y a une bonne âme qui lit mon anglais maladroit et corrige la documentation - il n'y aura aucune limite à ma gratitude.

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


All Articles