一种全新的PHP扩展方式(PHP FFI)

  传统的方法,当咱们需求用一些已有的C言语的库的才能的时候,咱们需求用C言语写wrapper,把他们包装成扩展,这个过程中就需求我们去学习PHP的扩展怎么写,当然现在也有一些方便的方法,比如Zephir.但总还是有一些学习本钱的,而有了FFI今后,咱们就能够直接在PHP脚本中调用C言语写的库中的函数了。
  而C言语几十年的历史中,积累了大量的优秀的库,FFI直接让咱们能够方便的享受这个巨大的资源了。言归正传,今日我用一个比如来介绍,咱们如何运用PHP来调用libcurl,来抓取一个网页的内容,为什么要用libcurl呢?PHP不是已经有了curl扩展了么?嗯,首要由于libcurl的api我比较熟,其次
  呢,正是由于有了,才好比照,传统扩展方法和FFI方法直接的易用性不是?
  首要,比如咱们就拿当时你看的这篇文章为例,我现在需求写一段代码来抓取它的内容,假如用传统的PHP的curl扩展,咱们大概会这么写:
    $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(<<  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的头文件,找到对应的值,然后咱们把值给加进去:
    constCURLOPT_URL=10002;
  constCURLOPT_SSL_VERIFYPEER=64;
  $libcurl=FFI::cdef(<<  void*curl_easy_init();
  intcurl_easy_setopt(void*curl,intoption,...);
  intcurl_easy_perform(void*curl);
  voidcurl_easy_cleanup(void*handle);
  CTYPE
  ,"libcurl.so"
  );
  好了,界说部分就算完结了,现在咱们完结实际逻辑部分,整个下来的代码会是:
    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,好,现在整个代码会是:
    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
  #include
  #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
  好了,现在整个的代码会变成:
    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:
    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",然后咱们的脚本现在看起来应该是这样:
    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,后端)