一种全新的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

相关文章
相关标签/搜索