一種全新的PHP擴展方式(PHP FFI)

  傳統的方法,當我們需求用一些已有的C言語的庫的才能的時候,我們需求用C言語寫wrapper,把他們包裝成擴展,這個過程當中就需求咱們去學習PHP的擴展怎麼寫,固然如今也有一些方便的方法,好比Zephir.但總仍是有一些學習本錢的,而有了FFI從此,我們就可以直接在PHP腳本中調用C言語寫的庫中的函數了。
  而C言語幾十年的歷史中,積累了大量的優秀的庫,FFI直接讓我們可以方便的享受這個巨大的資源了。言歸正傳,今日我用一個好比來介紹,我們如何運用PHP來調用libcurl,來抓取一個網頁的內容,爲何要用libcurl呢?PHP不是已經有了curl擴展了麼?嗯,首要因爲libcurl的api我比較熟,其次
  呢,正是因爲有了,才比如照,傳統擴展方法和FFI方法直接的易用性不是?
  首要,好比我們就拿當時你看的這篇文章爲例,我如今需求寫一段代碼來抓取它的內容,假如用傳統的PHP的curl擴展,我們大概會這麼寫:
  <?php
  $url="https://www.nxmrx.com/2020/03/11/5475.html";
  $ch=curl_init();
  curl_setopt($ch,CURLOPT_URL,$url);
  curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
  curl_exec($ch);
  curl_close($ch);
  (因爲個人網站是https的,因此會多一個設置SSL_VERIFYPEER的操做)那假如是用FFI呢?
  首要我們下載PHP-FFI,編譯安裝,PHP-FFI需求PHP-7.4以及libffi-3以上。
  而後,我們需求告知PHPFFI我們要調用的函數原型是咋樣的,這個我們可以運用FFI::cdef,它的原型是:
  FFI::cdef([string$cdef=""[,string$lib=null]]):FFI
  具體到這個好比,我們寫一個curl.php,包含一切要聲明的東西,代碼以下:
  $libcurl=FFI::cdef(<<<CTYPE
  void*curl_easy_init();
  intcurl_easy_setopt(void*curl,intoption,...);
  intcurl_easy_perform(void*curl);
  voidcurl_easy_cleanup(void*handle);
  CTYPE
  ,"libcurl.so"
  );
  在string$cdef中,我們可以寫C言語函數式聲明,FFI會parse它,瞭解到我們要在string$lib這個庫中調用的函數的簽名是啥樣的,在這個好比中,我們用到三個libcurl的函數,它們的聲明我們都可以在libcurl的文檔裏找到,好比關於curl_easy_init.
  這裏有個當地是,文檔中寫的是回來值是CURL,但事實上因爲我們的好比中不會引證它,只是傳遞,那就避免麻煩就用void替代。
  可是還有個麻煩的工做是,PHP預界說好了CURLOPT_等option的值,但如今我們需求本身界說,簡單的方法即是檢查curl的頭文件,找到對應的值,而後我們把值給加進去:
  <?php
  constCURLOPT_URL=10002;
  constCURLOPT_SSL_VERIFYPEER=64;
  $libcurl=FFI::cdef(<<<CTYPE
  void*curl_easy_init();
  intcurl_easy_setopt(void*curl,intoption,...);
  intcurl_easy_perform(void*curl);
  voidcurl_easy_cleanup(void*handle);
  CTYPE
  ,"libcurl.so"
  );
  好了,界說部分就算完結了,如今我們完結實際邏輯部分,整個下來的代碼會是:
  <?php
  require"curl.php";
  $url="https://www.laruence.com/2020/03/11/5475.html";
  $ch=$libcurl->curl_easy_init();
  $libcurl->curl_easy_setopt($ch,CURLOPT_URL,$url);
  $libcurl->curl_easy_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
  $libcurl->curl_easy_perform($ch);
  $libcurl->curl_easy_cleanup($ch);
  怎麼樣,比較運用curl擴展的方法,是否是相同簡練呢?
  接下來,我們稍微弄的複雜一點,也即便,假如我們不想要成果直接輸出,而是回來成一個字符串呢,關於PHP的curl擴展來講,我們只需求調用curl_setop把CURLOPT_RETURNTRANSFER爲1,但在libcurl中其實並無直接回來字符串的才能,而是提供了一個WRITEFUNCTION的回掉函函數,在有數據回來的時候,libcurl會調用這個函數.
  如今我們並不能直接把一個PHP函數做爲回調函數通過FFI傳遞給libcurl,那我們會有倆種方法來作:
  1.選用WRITEDATA,默許的libcurl會調用fwrite做爲回調函數,而我們可以通過WRITEDATA給libcurl一個fd,讓它不要寫入stdout,而是寫入到這個fd2.我們本身編寫一個C到簡單函數,通過FFI引
  入進來,傳遞給libcurl.
  我們先用第一種方法,首要我們需求運用fopen,此次我們通過界說個C的頭文件來聲明原型(file.h):
  voidfopen(charfilename,char*mode);
  voidfclose(void*fp);
  像file.h相同,我們把一切的libcurl的函數聲明也放到curl.h中去
  #defineFFI_LIB"libcurl.so"
  void*curl_easy_init();
  intcurl_easy_setopt(void*curl,intoption,...);
  intcurl_easy_perform(void*curl);
  voidcurl_easy_cleanup(CURL*handle);
  留意,我們通過界說了一個FFI_LIB的宏,來告知FFI這些函數來自libcurl.so,當我們用FFI::load加載這個h文件的時候,PHPFFI就會主動載入libcurl.so,好,如今整個代碼會是:
  <?php
  constCURLOPT_URL=10002;
  constCURLOPT_SSL_VERIFYPEER=64;
  constCURLOPT_WRITEDATA=10001;
  $libc=FFI::load("file.h");
  $libcurl=FFI::load("curl.h");
  $url="https://www.laruence.com/2020/03/11/5475.html";
  $tmpfile="/tmp/tmpfile.out";
  $ch=$libcurl->curl_easy_init();
  $fp=$libc->fopen($tmpfile,"a");
  $libcurl->curl_easy_setopt($ch,CURLOPT_URL,$url);
  $libcurl->curl_easy_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
  $libcurl->curl_easy_setopt($ch,CURLOPT_WRITEDATA,$fp);
  $libcurl->curl_easy_perform($ch);
  $libcurl->curl_easy_cleanup($ch);
  $libc->fclose($fp);
  $ret=file_get_contents($tmpfile);
  @unlink($tmpfile);
  但這種方法呢即是需求一個臨時的中轉文件,仍是不夠優雅,如今我們用第二種方法,要用第二種方法,我們需求本身用C寫一個回掉函數傳遞給libcurl:
  #include<stdlib.h>
  #include<string.h>
  #include"write.h"
  size_town_writefunc(voidptr,size_tsize,size_tnmember,voiddata){
  own_write_datad=(own_write_data)data;
  size_ttotal=size*nmember;
  if(d->buf==NULL){
  d->buf=malloc(total);
  if(d->buf==NULL){
  return0;
  }
  d->size=total;
  memcpy(d->buf,ptr,total);
  }else{
  d->buf=realloc(d->buf,d->size+total);
  if(d->buf==NULL){
  return0;
  }
  memcpy(d->buf+d->size,ptr,total);
  d->size+=total;
  }
  returntotal;
  }
  void*init(){
  return&own_writefunc;
  }
  留意此處的init函數,因爲在PHPFFI中,就如今的版別(2020-03-11)我們沒有方法直接得到一個函數指針,因此我們界說了這個函數,回來own_writefunc的地址。
  最後我們界說上面用到的頭文件write.h:
  #defineFFI_LIB"write.so"
  typedefstruct_writedata{
  void*buf;
  size_tsize;
  }own_write_data;
  void*init();
  留意到我們在頭文件中也界說了FFI_LIB,這樣這個頭文件就可以一塊兒被write.c和接下來我們的PHPFFI共同運用了。
  而後我們編譯write函數爲一個動態庫:
  gcc-O2-fPIC-shared-gwrite.c-owrite.so
  好了,如今整個的代碼會變成:
  <?php
  constCURLOPT_URL=10002;
  constCURLOPT_SSL_VERIFYPEER=64;
  constCURLOPT_WRITEDATA=10001;
  constCURLOPT_WRITEFUNCTION=20011;
  $libcurl=FFI::load("curl.h");
  $write=FFI::load("write.h");
  $url="https://www.laruence.com/2020/03/11/5475.html";
  $data=$write->new("own_write_data");
  $ch=$libcurl->curl_easy_init();
  $libcurl->curl_easy_setopt($ch,CURLOPT_URL,$url);
  $libcurl->curl_easy_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
  $libcurl->curl_easy_setopt($ch,CURLOPT_WRITEDATA,FFI::addr($data));
  $libcurl->curl_easy_setopt($ch,CURLOPT_WRITEFUNCTION,$write->init());
  $libcurl->curl_easy_perform($ch);
  $libcurl->curl_easy_cleanup($ch);
  ret=FFI::string($data->buf,$data->size);
  好了,跑一下吧?
  可是究竟直接在PHP中引證外部的so,仍是會有很大的安全問題的,另外你也具備了1000中方法讓PHPcrash,安全起見我們可以選用preload的方法,這種形式下,我們不能在腳本中直接調用
  FFI::cdef,FF::load,只能在通過opcache.preload:
  ffi.enable=preload
  opcache.preload=ffi_preload.inc
  ffi_preload.inc:
  <?php
  FFI::load("curl.h");
  FFI::load("write.h");
  但我們引證載入的FFI呢?爲此我們需求修正一下這倆個.h頭文件,參加FFI_SCOPE,好比curl.h:
  #defineFFI_LIB"libcurl.so"
  #defineFFI_SCOPE"libcurl"
  void*curl_easy_init();
  intcurl_easy_setopt(void*curl,intoption,...);
  intcurl_easy_perform(void*curl);
  voidcurl_easy_cleanup(void*handle);
  對應的我們給write.h也參加FFI_SCOPE爲"write",而後我們的腳本如今看起來應該是這樣:
  <?php
  constCURLOPT_URL=10002;
  constCURLOPT_SSL_VERIFYPEER=64;
  constCURLOPT_WRITEDATA=10001;
  constCURLOPT_WRITEFUNCTION=20011;
  $libcurl=FFI::scope("libcurl");
  $write=FFI::scope("write");
  $url="https://www.laruence.com/2020/03/11/5475.html";
  $data=$write->new("own_write_data");
  $ch=$libcurl->curl_easy_init();
  $libcurl->curl_easy_setopt($ch,CURLOPT_URL,$url);
  $libcurl->curl_easy_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
  $libcurl->curl_easy_setopt($ch,CURLOPT_WRITEDATA,FFI::addr($data));
  $libcurl->curl_easy_setopt($ch,CURLOPT_WRITEFUNCTION,$write->init());
  $libcurl->curl_easy_perform($ch);
  $libcurl->curl_easy_cleanup($ch);
  ret=FFI::string($data->buf,$data->size);
  也即是,我們如今運用FFI::scope來替代FFI::load,引證對應的函數。
  好了,通過這個好比,咱們應該對FFI有了一個比較深化的理解了,有興趣,就去找一個C庫,試試吧?php

相關文章
相關標籤/搜索