Un pic de PHP, Go, FFI si atmosfera de sarbatori

Read this post in English

Share on:

Ah, atmosfera de sarbatori…

Inspirat de o postare in Perl Advent, m-am gandit ca ar fi interesant sa vedem un exemplu cu PHP si Go. Vreau sa mentionez ca acest blog este inspirat intr-o buna masura din postarea mentionata anterior.

Sa presupunem ca vrei sa-i urezi cuiva sarbatori fericite folosind viteza limbajului Go, dar aplicatia ta este scrisa in PHP, ce poti face?

In lumea PHP ar fi momentul ideal, deoarece noua versiune PHP 7.4 vine cu “Foreign Function Interface” (FFI pe scurt).

Echipat cu aceasta noua arma, am instalat PHP 7.4 si am trecut la treaba.

Sa incepem cu super incredibila urare din Go, sa creem fisierul “greeting.go”:

 1package main
 2 
 3import (
 4   "fmt"
 5)
 6 
 7func main() {
 8   WishMerryChristmas();
 9}
10 
11func WishMerryChristmas() {
12   fmt.Println("We wish you a Merry Christmas!");
13}

Si acum sa-l rulam:

1$ go run greeting.go

Ar trebui sa afiseze:

1We wish you a Merry Christmas!

Pana aici e super! Este foarte rapid, frumos si tot ce mai trebuie, dar noua ne-ar trebui sa fie un serviciu, deci sa vedem cum o sa arate in acest caz:

 1package main
 2 
 3import (
 4   "C"
 5   "fmt"
 6)
 7 
 8func main() {}
 9 
10//export WishMerryChristmas
11func WishMerryChristmas() {
12   fmt.Println("We wish you a Merry Christmas!")
13}

Dupa cum se poate vedea, exista cateva diferente:

  • am importat si libraria “C”,
  • am scos apelul functiei din main()
  • am adaugat un comentariu pentru exportul functiei.

Pentru compilare se ruleaza astfel:

1$ go build -o greeting.so -buildmode=c-shared

De mentionat este faptul ca aceasta comanda trebuie rulata de fiecare data cand fisierul Go este modificat.

Rezultatul ar trebuie sa fie compus din doua fisiere:
“greeting.so” si “greeting.h”.

Fisierul de header “greeting.h” contine tipurile de date si definitile functiilor. Daca ai mai lucrat cu C, esti probabil familiarizat cu acest fel de fisiere. In mod normal, tot ce trebuie sa mai facem acum este sa importam fisierul header folosind FFI si sa apelam functia!

Pentru asta am creat un fisier cu numele “greeting.php”:

1<?php
2$ffi = FFI::load("greeting.h");
3$ffi->WishMerryChristmas();

Pare suficient de simplu, tot ce trebuie sa mai facem acum este sa-l rulam folosind comanda:

 1$ php greeting.php 
 2PHP Fatal error:  Uncaught FFI\ParserException: undefined C type '__SIZE_TYPE__' at line 43 in /home/claudiu/php-go/greeting.php:3
 3Stack trace:
 4#0 /home/claudiu/php-go/greeting.php(3): FFI::load()
 5#1 {main}
 6
 7Next FFI\Exception: Failed loading 'greeting.h' in /home/claudiu/php-go/greeting.php:3
 8Stack trace:
 9#0 /home/claudiu/php-go/greeting.php(3): FFI::load()
10#1 {main}
11  thrown in /home/claudiu/php-go/greeting.php on line 3

Nu e tocmai urarea la care ma asteptam…

Dupa ce am sapat destul de intens, am gasit asta intr-o pagina de manual:
C preprocessor directives are not supported, i.e. #include, #define and CPP macros do not work.

Din acest motiv, se pare ca nu vom putea folosi fisierul de header, sau cel putin eu nu am gasit o metoda clara.

Partea buna este ca putem folosi FFI::cdef() care permite sa specificam definitia functiilor. Daca te-am pierdut pe drum, ce incerc de fapt sa fac este sa-i spun PHP-ului care sunt definitile funcțiilor pe care vreau sa folosesc din fisierul “greeting.so”.

Noul cod va fi:

1<?php
2$ffi = FFI::cdef("
3void WishMerryChristmas();
4", __DIR__ . "/greeting.so");
5
6$ffi->WishMerryChristmas();

Iar daca rulam acum:

1$ php greeting.php 
2We wish you a Merry Christmas!

Am progresat mult, iar serviciul isi face treaba foarte bine!

Adaugarea unui parametru int

Urarea arata foarte bine si e super rapida, dar ar fi si mai frumos sa putem specifica de cate ori sa o afisam.
Pentru a face asta, trebuie modificata functia din fisierul “greeting.go”, adaugandu-i un parametru cu numarul afisarilor dorite:

1//export WishMerryChristmas
2func WishMerryChristmas(number int) {
3   for i := 0; i < number; i++ {
4       fmt.Println("We wish you a Merry Christmas!");
5   }
6}

Trebuie rulata din nou comanda de compilare ca mai devreme.

In PHP trebuie sa modificam definitia functiei. Pentru a vedea ce trebuie modificat putem sa ne inspiram din fisierul “greeting.h”. Noua definitie a functiei in fisier este:

1extern void WishMerryChristmas(GoInt p0);

“GoInt”? Ce magie mai e si asta? Daca ne uitam in fisier gasim urmatoarele definitii:

1...
2typedef long long GoInt64;
3...
4typedef GoInt64 GoInt;
5...

De unde concluzionam ca GoInt este de fapt un long.

Cu aceste noi descoperiri putem modifica fisierul PHP in:

1<?php
2 
3$ffi = FFI::cdef("
4void WishMerryChristmas(long);
5", __DIR__ . "/greeting.so");
6 
7$ffi->WishMerryChristmas(3);

Il rulam din nou si vedem:

1$ php greeting.php 
2We wish you a Merry Christmas!
3We wish you a Merry Christmas!
4We wish you a Merry Christmas!

Ah, incepe sa se simta spiritul sarbatorilor!

Adaugarea unui parametru string

Sa afisam o urare de mai multe ori este destul de dragut, dar ar fi mai dragut sa adaugam si un nume.

Noua functie devine:

1//export WishMerryChristmas
2func WishMerryChristmas(name string, number int) {
3   for i := 0; i < number; i++ {
4       fmt.Printf("We wish you a Merry Christmas, %s!\n", name);
5   }
6}

Nu uita sa compilezi, iar apoi vom trece la partea interesanta.

Daca ne uitam in fisierul “greeting.h”, noua definitie a functiei este:

1extern void WishMerryChristmas(GoString p0, GoInt p1);

Stim deja ce este GoInt, dar GoString este un pic mai complicat. Dupa mai multe substituiri am ajuns la structura:

1typedef struct { char* p; long n } GoString;

Este practic un pointer catre o lista de caractere si o dimensiune.

Asta inseamna ca, in fisierul PHP, noua definitie o sa fie:

1$ffi = FFI::cdef("
2typedef struct { char* p; long n } GoString;
3typedef long GoInt;
4void WishMerryChristmas(GoString p0, GoInt p1);
5", __DIR__ . "/greeting.so");

p0 si p1 sunt optionali, dar i-am lasat ca sa semene mai mult cu definitia din fisierul de header.
Similar, GoInt este practic un long, dar l-am lasat acolo din acelasi motiv.

Construirea unui GoString din PHP a fost un pic mai complicata. Principala cauza a fost ca nu am gasit un mod sa fac un “char *” si sa-l initializez in acelasi timp. Alternativa mea a fost sa fac un array de “char” si apoi sa-i fac cast, dupa cum urmeaza:

 1$name = "reader";
 2$strChar = str_split($name);
 3 
 4$c = FFI::new('char[' . count($strChar) . ']');
 5foreach ($strChar as $i => $char) {
 6   $c[$i] = $char;
 7}
 8 
 9$goStr = $ffi->new("GoString");
10$goStr->p = FFI::cast(FFI::type('char *'), $c);
11$goStr->n = count($strChar);
12 
13$ffi->WishMerryChristmas($goStr, 2);

Sa-l incercam:

1$ php greeting.php 
2We wish you a Merry Christmas, reader!
3We wish you a Merry Christmas, reader!

Succes!

In aceasta faza, mi-ar placea sa mut crearea de GoString intr-o functie separata, de dragul lizibilitatii codului.

Noul cod rezultat este:

 1$name = "reader";
 2
 3$goStr = stringToGoString($ffi->new("GoString"), $name);
 4
 5$ffi->WishMerryChristmas($goStr, 2);
 6
 7function stringToGoString($goStr, $name) {
 8    $strChar = str_split($name);
 9
10    $c = FFI::new('char[' . count($strChar) . ']');
11    foreach ($strChar as $i => $char) {
12        $c[$i] = $char;
13    }
14    
15    $goStr->p = FFI::cast(FFI::type('char *'), $c);
16    $goStr->n = count($strChar);
17
18    return $goStr;
19}

Si acum sa-l incercam:

1$ php greeting.php 
2We wish you a Merry Christmas, ��!
3We wish you a Merry Christmas, ��!

Hopa, nu e tocmai bine… pare ca afisam niste memorie reziduala. Dar de ce?

Cautand in documentatia FFI::new am gasit un al doilea parametru, “bool $owned = TRUE”.

Whether to create owned (i.e. managed) or unmanaged data. Managed data lives together with the returned FFI\CData object, and is released when the last reference to that object is released by regular PHP reference counting or GC. Unmanaged data should be released by calling FFI::free(), when no longer needed.

Asta inseamna ca atunci cand intoarcem rezultatul functiei, GC-ul din PHP dezaloca memoria pentru variabila cu string-ul. Este destul de probabil ca acesta sa fie un bug, dar exista o solutie foarte simpla, trebuie doar sa adaugam “false” la crearea de array:

1$c = FFI::new('char[' . count($strChar) . ']', false);

Sa incercam din nou:

1$ php greeting.php 
2We wish you a Merry Christmas, reader!
3We wish you a Merry Christmas, reader!

Si functioneaza!

Concluzie

Poate ca rulatul din PHP a unei librarii scrisa in Go nu este atat de simplu precum importul unui fisier de header, dar cu putina rabdare nu este nici foarte dificil! Marele avantaj este ca librariile scrise in Go, sau in orice alt limbaj de programare care permite acest lucru, pot fi folosite din PHP fara sa mai trebuiasca reimplementarea lor!

Si, încheind pe aceasta nota pozitiva, va urez sarbatori fericite!