用C/C++扩展你的PHP - Go语言中文社区

用C/C++扩展你的PHP


28 Apr 09 用C/C++扩展你的PHP

http://www.laruence.com/2009/04/28/719.html

<ol><li>原文出自: <PHP 5 Power Programming> Chapter 15</li><li>作者:Andi Gutmans, Stig Sæther Bakken, Derick Rethans</li><li>翻译:taft at wjl.cn(http://blog.csdn.net/taft/)</li><li>校对:laruence at yahoo.com.cn</li><li>最后更新日期:2009/04/29</li></ol>

简 介

英文版下载: PHP 5 Power Programming

PHP取得成功的一个主要原因之一是她拥有大量的可用扩展。web开发者无论有何种需求,这种需求最有可能在PHP发行包里找到。PHP发行包包括支持各种数据库,图形文件格式,压缩,XML技术扩展在内的许多扩展。

扩展API的引入使PHP3取得了巨大的进展,扩展API机制使PHP开发社区很容易的开发出几十种扩展。现在,两个版本过去了,API仍然和PHP3时的非常相似。扩展主要的思想是:尽可能的从扩展编写者那里隐藏PHP的内部机制和脚本引擎本身,仅仅需要开发者熟悉API。

有两个理由需要自己编写PHP扩展。第一个理由是:PHP需要支持一项她还未支持的技术。这通常包括包裹一些现成的C函数库,以便提供PHP接口。例如,如果一个叫FooBase的数据库已推出市场,你需要建立一个PHP扩展帮助你从PHP里调用FooBase的C函数库。这个工作可能仅由一个人完成,然后被整个PHP社区共享(如果你愿意的话)。第二个不是很普遍的理由是:你需要从性能或功能的原因考虑来编写一些商业逻辑。

如果以上的两个理由都和你没什么关系,同时你感觉自己没有冒险精神,那么你可以跳过本章。

本章教你如何编写相对简单的PHP扩展,使用一部分扩展API函数。对于大多数打算开发自定义PHP扩展开发者而言,它含概了足够的资料。学习一门编程课程的最好方法之一就是动手做一些极其简单的例子,这些例子正是本章的线索。一旦你明白了基础的东西,你就可以在互联网上通过阅读文挡、原代码或参加邮件列表新闻组讨论来丰富自己。因此,本章集中在让你如何开始的话题。在UNIX下一个叫ext_skel的脚本被用于建立扩展的骨架,骨架信息从一个描述扩展接口的定义文件中取得。因此你需要利用UNIX来建立一个骨架。Windows开发者可以使用Windows ext_skel_win32.php代替ext_skel。

然而,本章关于用你开发的扩展编译PHP的指导仅涉及UNIX编译系统。本章中所有的对API的解释与UNIX和Windows下开发的扩展都有联系。

当你阅读完这章,你能学会如何

  • 建立一个简单的商业逻辑扩展。
  • 建议个C函数库的包裹扩展,尤其是有些标准C文件操作函数比如fopen()

快速开始

本节没有介绍关于脚本引擎基本构造的一些知识,而是直接进入扩展的编码讲解中,因此不要担心你无法立刻获得对扩展整体把握的感觉。假设你正在开发一个网站,需要一个把字符串重复n次的函数。下面是用PHP写的例子:

<ol><li><span class="sh_keyword">function</span> <span class="sh_function">self_concat</span><span class="sh_symbol">(</span><span class="sh_variable">$string</span><span class="sh_symbol">,</span> <span class="sh_variable">$n</span><span class="sh_symbol">)</span><span class="sh_cbracket">{</span></li><li>    <span class="sh_variable">$result</span> <span class="sh_symbol">=</span> <span class="sh_string">""</span><span class="sh_symbol">;</span></li><li>    <span class="sh_keyword">for</span><span class="sh_symbol">(</span><span class="sh_variable">$i</span> <span class="sh_symbol">=</span> <span class="sh_number">0</span><span class="sh_symbol">;</span> <span class="sh_variable">$i</span> <span class="sh_symbol"><</span> <span class="sh_variable">$n</span><span class="sh_symbol">;</span> <span class="sh_variable">$i</span><span class="sh_symbol">++)</span><span class="sh_cbracket">{</span></li><li>        <span class="sh_variable">$result</span> <span class="sh_symbol">.=</span> <span class="sh_variable">$string</span><span class="sh_symbol">;</span></li><li>    <span class="sh_cbracket">}</span></li><li>    <span class="sh_keyword">return</span> <span class="sh_variable">$result</span><span class="sh_symbol">;</span></li><li><span class="sh_cbracket">}</span></li><li> </li><li><span class="sh_function">self_concat</span><span class="sh_symbol">(</span><span class="sh_string">"One"</span><span class="sh_symbol">,</span> <span class="sh_number">3</span><span class="sh_symbol">)</span> returns <span class="sh_string">"OneOneOne"</span><span class="sh_symbol">.</span></li><li> </li><li><span class="sh_function">self_concat</span><span class="sh_symbol">(</span><span class="sh_string">"One"</span><span class="sh_symbol">,</span> <span class="sh_number">1</span><span class="sh_symbol">)</span> returns <span class="sh_string">"One"</span><span class="sh_symbol">.</span></li></ol>

假设由于一些奇怪的原因,你需要时常调用这个函数,而且还要传给函数很长的字符串和大值n。这意味着在脚本里有相当巨大的字符串连接量和内存重新分配过程,以至显著地降低脚本执行速度。如果有一个函数能够更快地分配大量且足够的内存来存放结果字符串,然后把$string重复n次,就不需要在每次循环迭代中分配内存。

为扩展建立函数的第一步是写一个函数定义文件,该函数定义文件定义了扩展对外提供的函数原形。该例中,定义函数只有一行函数原形self_concat() :

<ol><li><span class="sh_usertype">string</span><span class="sh_normal"> </span><span class="sh_function">self_concat</span><span class="sh_symbol">(</span><span class="sh_usertype">string</span><span class="sh_normal"> </span>str<span class="sh_symbol">,</span> <span class="sh_type">int</span> n<span class="sh_symbol">)</span></li></ol>

函数定义文件的一般格式是一个函数一行。你可以定义可选参数和使用大量的PHP类型,包括: bool, float, int, array等。

保存为myfunctions.def文件至PHP原代码目录树下。

该是通过扩展骨架(skeleton)构造器运行函数定义文件的时机了。该构造器脚本叫ext_skel,放在PHP原代码目录树的ext/目录下(PHP原码主目录下的README.EXT_SKEL提供了更多的信息)。假设你把函数定义保存在一个叫做myfunctions.def的文件里,而且你希望把扩展取名为myfunctions,运行下面的命令来建立扩展骨架

<ol><li>./ext_skel --extname=myfunctions --proto=myfunctions.def</li></ol>

这个命令在ext/目录下建立了一个myfunctions/目录。你要做的第一件事情也许就是编译该骨架,以便编写和测试实际的C代码。编译扩展有两种方法:

  • 作为一个可装载模块或者DSO(动态共享对象)
  • 静态编译到PHP
PHP扩展开发导图

PHP扩展开发导图

因为第二种方法比较容易上手,所以本章采用静态编译。如果你对编译可装载扩展模块感兴趣,可以阅读PHP原代码根目录下的README.SELF-CONTAINED_EXTENSIONS文件。为了使扩展能够被编译,需要修改扩展目录ext/myfunctions/下的config.m4文件。扩展没有包裹任何外部的C库,你需要添加支持–enable-myfunctions配置开关到PHP编译系统里(–with-extension 开关用于那些需要用户指定相关C库路径的扩展)。可以去掉自动生成的下面两行的注释来开启这个配置。

<ol><li>./ext_skel --extname=myfunctions --proto=myfunctions.def</li><li>PHP_ARG_ENABLE(myfunctions, whether to enable myfunctions support,</li><li> </li><li>[ --enable-myfunctions                Include myfunctions support])</li></ol>

现在剩下的事情就是在PHP原代码树根目录下运行./buildconf,该命令会生成一个新的配置脚本。通过查看./configure –help输出信息,可以检查新的配置选项是否被包含到配置文件中。现在,打开你喜好的配置选项开关和–enable-myfunctions重新配置一下PHP。最后的但不是最次要的是,用make来重新编译PHP。

ext_skel应该把两个PHP函数添加到你的扩展骨架了:打算实现的self_concat()函数和用于检测myfunctions 是否编译到PHP的confirm_myfunctions_compiled()函数。完成PHP的扩展开发后,可以把后者去掉。

<ol><li><span class="sh_symbol"><?php</span></li><li><span class="sh_keyword">print</span> <span class="sh_function">confirm_myfunctions_compiled</span><span class="sh_symbol">(</span><span class="sh_string">"myextension"</span><span class="sh_symbol">);</span></li><li><span class="sh_symbol">?></span></li></ol>

运行这个脚本会出现类似下面的输出:

<ol><li>"Congratulations! You have successfully modified ext/myfunctions</li><li> </li><li>config.m4. Module myfunctions is now compiled into PHP."</li></ol>

另外,ext_skel脚本生成一个叫myfunctions.php的脚本,你也可以利用它来验证扩展是否被成功地编译到PHP。它会列出该扩展所支持的所有函数。

现在你学会如何编译扩展了,该是真正地研究self_concat()函数的时候了。
下面就是ext_skel脚本生成的骨架结构:

<ol><li><span class="sh_comment">/* {{{ proto string self_concat(string str, int n)</span></li><li> </li><li><span class="sh_comment">*/</span></li><li> </li><li><span class="sh_function">PHP_FUNCTION</span><span class="sh_symbol">(</span>self_concat<span class="sh_symbol">)</span></li><li><span class="sh_cbracket">{</span></li><li> </li><li><span class="sh_type">char</span> <span class="sh_symbol">*</span>str <span class="sh_symbol">=</span> NULL<span class="sh_symbol">;</span></li><li> </li><li><span class="sh_type">int</span> argc <span class="sh_symbol">=</span> <span class="sh_function">ZEND_NUM_ARGS</span><span class="sh_symbol">();</span></li><li> </li><li><span class="sh_type">int</span> str_len<span class="sh_symbol">;</span></li><li> </li><li><span class="sh_type">long</span> n<span class="sh_symbol">;</span></li><li> </li><li><span class="sh_keyword">if</span> <span class="sh_symbol">(</span><span class="sh_function">zend_parse_parameters</span><span class="sh_symbol">(</span><span class="sh_usertype">argc</span><span class="sh_normal"> </span>TSRMLS_CC<span class="sh_symbol">,</span> <span class="sh_string">"sl"</span><span class="sh_symbol">,</span> <span class="sh_symbol">&</span>str<span class="sh_symbol">,</span> <span class="sh_symbol">&</span>str_len<span class="sh_symbol">,</span> <span class="sh_symbol">&</span>n<span class="sh_symbol">)</span> <span class="sh_symbol">==</span> FAILURE<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_keyword">return</span><span class="sh_symbol">;</span></li><li> </li><li><span class="sh_function">php_error</span><span class="sh_symbol">(</span>E_WARNING<span class="sh_symbol">,</span> <span class="sh_string">"self_concat: not yet implemented"</span><span class="sh_symbol">);</span></li><li> </li><li><span class="sh_cbracket">}</span></li><li> </li><li><span class="sh_comment">/* }}} */</span></li></ol>

自动生成的PHP函数周围包含了一些注释,这些注释用于自动生成代码文档和vi、Emacs等编辑器的代码折叠。函数自身的定义使用了宏PHP_FUNCTION(),该宏可以生成一个适合于Zend引擎的函数原型。逻辑本身分成语义各部分,取得调用函数的参数和逻辑本身。

为了获得函数传递的参数,可以使用zend_parse_parameters()API函数。下面是该函数的原型:

<ol><li><span class="sh_function">zend_parse_parameters</span><span class="sh_symbol">(</span><span class="sh_type">int</span> <span class="sh_usertype">num_args</span><span class="sh_normal"> </span>TSRMLS_DC<span class="sh_symbol">,</span> <span class="sh_type">char</span> <span class="sh_symbol">*</span>type_spec<span class="sh_symbol">,</span> …<span class="sh_symbol">);</span></li></ol>

第一个参数是传递给函数的参数个数。通常的做法是传给它ZEND_NUM_ARGS()。这是一个表示传递给函数参数总个数的宏。第二个参数是为了线程安全,总是传递TSRMLS_CC宏,后面会讲到。第三个参数是一个字符串,指定了函数期望的参数类型,后面紧跟着需要随参数值更新的变量列表。因为PHP采用松散的变量定义和动态的类型判断,这样做就使得把不同类型的参数转化为期望的类型成为可能。例如,如果用户传递一个整数变量,可函数需要一个浮点数,那么zend_parse_parameters()就会自动地把整数转换为相应的浮点数。如果实际值无法转换成期望类型(比如整形到数组形),会触发一个警告。

下表列出了可能指定的类型。我们从完整性考虑也列出了一些没有讨论到的类型。

类型指定符 对应的C类型 描述
l long 符号整数
d double 浮点数
s char *, int 二进制字符串,长度
b zend_bool 逻辑型(1或0)
r zval * 资源(文件指针,数据库连接等)
a zval * 联合数组
o zval * 任何类型的对象
O zval * 指定类型的对象。需要提供目标对象的类类型
z zval * 无任何操作的zval

为了容易地理解最后几个选项的含义,你需要知道zval是Zend引擎的值容器[1]。无论这个变量是布尔型,字符串型或者其他任何类型,其信息总会包含在一个zval联合体中。本章中我们不直接存取zval,而是通过一些附加的宏来操作。下面的是或多或少在C中的zval, 以便我们能更好地理解接下来的代码。

<ol><li><span class="sh_keyword">typedef</span> <span class="sh_keyword">union</span> _zval<span class="sh_cbracket">{</span></li><li>     <span class="sh_type">long</span> lval<span class="sh_symbol">;</span></li><li>     <span class="sh_type">double</span> dval<span class="sh_symbol">;</span></li><li>     <span class="sh_keyword">struct</span> <span class="sh_cbracket">{</span></li><li>          <span class="sh_type">char</span> <span class="sh_symbol">*</span>val<span class="sh_symbol">;</span></li><li>          <span class="sh_type">int</span> len<span class="sh_symbol">;</span></li><li>     <span class="sh_cbracket">}</span>str<span class="sh_symbol">;</span></li><li> </li><li>     <span class="sh_usertype">HashTable</span><span class="sh_normal"> </span><span class="sh_symbol">*</span>ht<span class="sh_symbol">;</span></li><li>     <span class="sh_usertype">zend_object_value</span><span class="sh_normal"> </span>obj<span class="sh_symbol">;</span></li><li> </li><li><span class="sh_cbracket">}</span>zval<span class="sh_symbol">;</span></li></ol>

在我们的例子中,我们用基本类型调用zend_parse_parameters(),以本地C类型的方式取得函数参数的值,而不是用zval容器。

为了让zend_parse_parameters()能够改变传递给它的参数的值,并返回这个改变值,需要传递一个引用。仔细查看一下self_concat():

<ol><li><span class="sh_keyword">if</span> <span class="sh_symbol">(</span><span class="sh_function">zend_parse_parameters</span><span class="sh_symbol">(</span><span class="sh_usertype">argc</span><span class="sh_normal"> </span>TSRMLS_CC<span class="sh_symbol">,</span> <span class="sh_string">"sl"</span><span class="sh_symbol">,</span> <span class="sh_symbol">&</span>str<span class="sh_symbol">,</span> <span class="sh_symbol">&</span>str_len<span class="sh_symbol">,</span> <span class="sh_symbol">&</span>n<span class="sh_symbol">)</span> <span class="sh_symbol">==</span> FAILURE<span class="sh_symbol">)</span></li><li><span class="sh_keyword">return</span><span class="sh_symbol">;</span></li></ol>

注意到自动生成的代码会检测函数的返回值FAILUER(成功即SUCCESS)来判断是否成功。如果没有成功则立即返回,并且由zend_parse_parameters()负责触发警告信息。因为函数打算接收一个字符串l和一个整数n,所以指定 ”sl” 作为其类型指示符。s需要两个参数,所以我们传递参考char * 和 int (str 和 str_len)给zend_parse_parameters()函数。无论什么时候,记得总是在代码中使用字符串长度str_len来确保函数工作在二进制安全的环境中。不要使用strlen()和strcpy(),除非你不介意函数在二进制字符串下不能工作。二进制字符串是包含有nulls的字符串。二进制格式包括图象文件,压缩文件,可执行文件和更多的其他文件。”l” 只需要一个参数,所以我们传递给它n的引用。尽管为了清晰起见,骨架脚本生成的C变量名与在函数原型定义文件中的参数名一样;这样做不是必须的,尽管在实践中鼓励这样做。

回到转换规则中来。下面三个对self_concat()函数的调用使str, str_len和n得到同样的值:

<ol><li><span class="sh_function">self_concat</span><span class="sh_symbol">(</span><span class="sh_string">"321"</span><span class="sh_symbol">,</span> <span class="sh_number">5</span><span class="sh_symbol">);</span></li><li> </li><li><span class="sh_function">self_concat</span><span class="sh_symbol">(</span><span class="sh_number">321</span><span class="sh_symbol">,</span> <span class="sh_string">"5"</span><span class="sh_symbol">);</span></li><li> </li><li><span class="sh_function">self_concat</span><span class="sh_symbol">(</span><span class="sh_string">"321"</span><span class="sh_symbol">,</span> <span class="sh_string">"5"</span><span class="sh_symbol">);</span></li><li> </li><li>str points to <span class="sh_usertype">the</span><span class="sh_normal"> </span>string <span class="sh_string">"321"</span><span class="sh_symbol">,</span> str_len equals <span class="sh_number">3</span><span class="sh_symbol">,</span> and n equals <span class="sh_number">5</span><span class="sh_symbol">.</span></li><li> </li><li>str 指向字符串<span class="sh_string">"321"</span>,str_len等于<span class="sh_number">3</span>,n等于<span class="sh_number">5</span>。</li></ol>

在我们编写代码来实现连接字符串返回给PHP的函数前,还得谈谈两个重要的话题:内存管理、从PHP内部返回函数值所使用的API。

内存管理

用于从堆中分配内存的PHP API几乎和标准C API一样。在编写扩展的时候,使用下面与C对应(因此不必再解释)的API函数:

<ol><li><span class="sh_function">emalloc</span><span class="sh_symbol">(</span><span class="sh_usertype">size_t</span><span class="sh_normal"> </span>size<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_function">efree</span><span class="sh_symbol">(</span><span class="sh_type">void</span> <span class="sh_symbol">*</span>ptr<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_function">ecalloc</span><span class="sh_symbol">(</span><span class="sh_usertype">size_t</span><span class="sh_normal"> </span>nmemb<span class="sh_symbol">,</span> <span class="sh_usertype">size_t</span><span class="sh_normal"> </span>size<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_function">erealloc</span><span class="sh_symbol">(</span><span class="sh_type">void</span> <span class="sh_symbol">*</span>ptr<span class="sh_symbol">,</span> <span class="sh_usertype">size_t</span><span class="sh_normal"> </span>size<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_function">estrdup</span><span class="sh_symbol">(</span><span class="sh_keyword">const</span> <span class="sh_type">char</span> <span class="sh_symbol">*</span>s<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_function">estrndup</span><span class="sh_symbol">(</span><span class="sh_keyword">const</span> <span class="sh_type">char</span> <span class="sh_symbol">*</span>s<span class="sh_symbol">,</span> <span class="sh_type">unsigned</span> <span class="sh_type">int</span> length<span class="sh_symbol">);</span></li></ol>

在这一点上,任何一位有经验的C程序员应该象这样思考一下:“什么?标准C没有strndup()?”是的,这是正确的,因为GNU扩展通常在Linux下可用。estrndup()只是PHP下的一个特殊函数。它的行为与estrdup()相似,但是可以指定字符串重复的次数(不需要结束空字符),同时是二进制安全的。这是推荐使用estrndup()而不是estrdup()的原因。

在几乎所有的情况下,你应该使用这些内存分配函数。有一些情况,即扩展需要分配在请求中永久存在的内存,从而不得不使用malloc(),但是除非你知道你在做什么,你应该始终使用以上的函数。如果没有使用这些内存函数,而相反使用标准C函数分配的内存返回给脚本引擎,那么PHP会崩溃。

这些函数的优点是:任何分配的内存在偶然情况下如果没有被释放,则会在页面请求的最后被释放。因此,真正的内存泄漏不会产生。然而,不要依赖这一机制,从调试和性能两个原因来考虑,应当确保释放应该释放的内存。剩下的优点是在多线程环境下性能的提高,调试模式下检测内存错误等。

还有一个重要的原因,你不需要检查这些内存分配函数的返回值是否为null。当内存分配失败,它们会发出E_ERROR错误,从而决不会返回到扩展。

从PHP函数中返回值

扩展API包含丰富的用于从函数中返回值的宏。这些宏有两种主要风格:第一种是RETVAL_type()形式,它设置了返回值但C代码继续执行。这通常使用在把控制交给脚本引擎前还希望做的一些清理工作的时候使用,然后再使用C的返回声明 ”return” 返回到PHP;后一个宏更加普遍,其形式是RETURN_type(),他设置了返回类型,同时返回控制到PHP。下表解释了大多数存在的宏。

设置返回值并且结束函数 设置返回值 宏返回类型和参数
RETURN_LONG(l) RETVAL_LONG(l) 整数
RETURN_BOOL(b) RETVAL_BOOL(b) 布尔数(1或0)
RETURN_NULL() RETVAL_NULL() NULL
RETURN_DOUBLE(d) RETVAL_DOUBLE(d) 浮点数
RETURN_STRING(s, dup) RETVAL_STRING(s, dup) 字符串。如果dup为1,引擎会调用estrdup()重复s,使用拷贝。如果dup为0,就使用s
RETURN_STRINGL(s, l, dup) RETVAL_STRINGL(s, l, dup) 长度为l的字符串值。与上一个宏一样,但因为s的长度被指定,所以速度更快。
RETURN_TRUE RETVAL_TRUE 返回布尔值true。注意到这个宏没有括号。
RETURN_FALSE RETVAL_FALSE 返回布尔值false。注意到这个宏没有括号。
RETURN_RESOURCE(r) RETVAL_RESOURCE(r) 资源句柄。

完成self_concat()

现在你已经学会了如何分配内存和从PHP扩展函数里返回函数值,那么我们就能够完成self_concat()的编码:

<ol><li><span class="sh_comment">/* {{{ proto string self_concat(string str, int n)</span></li><li> </li><li><span class="sh_comment">*/</span></li><li> </li><li><span class="sh_function">PHP_FUNCTION</span><span class="sh_symbol">(</span>self_concat<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_cbracket">}</span></li><li> </li><li><span class="sh_type">char</span> <span class="sh_symbol">*</span>str <span class="sh_symbol">=</span> NULL<span class="sh_symbol">;</span></li><li> </li><li><span class="sh_type">int</span> argc <span class="sh_symbol">=</span> <span class="sh_function">ZEND_NUM_ARGS</span><span class="sh_symbol">();</span></li><li> </li><li><span class="sh_type">int</span> str_len<span class="sh_symbol">;</span></li><li> </li><li><span class="sh_type">long</span> n<span class="sh_symbol">;</span></li><li> </li><li><span class="sh_type">char</span> <span class="sh_symbol">*</span>result<span class="sh_symbol">;</span> <span class="sh_comment">/* Points to resulting string */</span></li><li> </li><li><span class="sh_type">char</span> <span class="sh_symbol">*</span>ptr<span class="sh_symbol">;</span> <span class="sh_comment">/* Points at the next location we want to copy to */</span></li><li> </li><li><span class="sh_type">int</span> result_length<span class="sh_symbol">;</span> <span class="sh_comment">/* Length of resulting string */</span></li><li> </li><li><span class="sh_keyword">if</span> <span class="sh_symbol">(</span><span class="sh_function">zend_parse_parameters</span><span class="sh_symbol">(</span><span class="sh_usertype">argc</span><span class="sh_normal"> </span>TSRMLS_CC<span class="sh_symbol">,</span> <span class="sh_string">"sl"</span><span class="sh_symbol">,</span> <span class="sh_symbol">&</span>str<span class="sh_symbol">,</span> <span class="sh_symbol">&</span>str_len<span class="sh_symbol">,</span> <span class="sh_symbol">&</span>n<span class="sh_symbol">)</span> <span class="sh_symbol">==</span> FAILURE<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_keyword">return</span><span class="sh_symbol">;</span></li><li> </li><li><span class="sh_comment">/* Calculate length of result */</span></li><li> </li><li>result_length <span class="sh_symbol">=</span> <span class="sh_symbol">(</span>str_len <span class="sh_symbol">*</span> n<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_comment">/* Allocate memory for result */</span></li><li> </li><li>result <span class="sh_symbol">=</span> <span class="sh_symbol">(</span><span class="sh_type">char</span> <span class="sh_symbol">*)</span> <span class="sh_function">emalloc</span><span class="sh_symbol">(</span>result_length <span class="sh_symbol">+</span> <span class="sh_number">1</span><span class="sh_symbol">);</span></li><li> </li><li><span class="sh_comment">/* Point at the beginning of the result */</span></li><li> </li><li>ptr <span class="sh_symbol">=</span> result<span class="sh_symbol">;</span></li><li> </li><li><span class="sh_keyword">while</span> <span class="sh_symbol">(</span>n<span class="sh_symbol">--)</span> <span class="sh_cbracket">{</span></li><li> </li><li><span class="sh_comment">/* Copy str to the result */</span></li><li> </li><li><span class="sh_function">memcpy</span><span class="sh_symbol">(</span>ptr<span class="sh_symbol">,</span> str<span class="sh_symbol">,</span> str_len<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_comment">/* Increment ptr to point at the next position we want to write to */</span></li><li> </li><li>ptr <span class="sh_symbol">+=</span> str_len<span class="sh_symbol">;</span></li><li> </li><li><span class="sh_cbracket">}</span></li><li> </li><li><span class="sh_comment">/* Null terminate the result. Always null-terminate your strings</span></li><li> </li><li><span class="sh_comment">even if they are binary strings */</span></li><li> </li><li><span class="sh_symbol">*</span>ptr <span class="sh_symbol">=</span> <span class="sh_string">'</span><span class="sh_specialchar"></span><span class="sh_string">'</span><span class="sh_symbol">;</span></li><li> </li><li><span class="sh_comment">/* Return result to the scripting engine without duplicating it*/</span></li><li> </li><li><span class="sh_function">RETURN_STRINGL</span><span class="sh_symbol">(</span>result<span class="sh_symbol">,</span> result_length<span class="sh_symbol">,</span> <span class="sh_number">0</span><span class="sh_symbol">);</span></li><li> </li><li><span class="sh_cbracket">}</span> </li><li> </li><li><span class="sh_comment">/* }}} */</span></li></ol>

现在要做的就是重新编译一下PHP,这样就完成了第一个PHP函数。

让我门检查函数是否真的工作。在最新编译过的PHP树下执行[2]下面的脚本:

<ol><li><span class="sh_symbol"><?php</span></li><li><span class="sh_keyword">for</span> <span class="sh_symbol">(</span><span class="sh_variable">$i</span> <span class="sh_symbol">=</span> <span class="sh_number">1</span><span class="sh_symbol">;</span> <span class="sh_variable">$i</span> <span class="sh_symbol"><=</span> <span class="sh_number">3</span><span class="sh_symbol">;</span> <span class="sh_variable">$i</span><span class="sh_symbol">++)</span><span class="sh_cbracket">{</span></li><li>     <span class="sh_keyword">print</span> <span class="sh_function">self_concat</span><span class="sh_symbol">(</span><span class="sh_string">"ThisIsUseless"</span><span class="sh_symbol">,</span> <span class="sh_variable">$i</span><span class="sh_symbol">);</span></li><li>     <span class="sh_keyword">print</span> <span class="sh_string">"n"</span><span class="sh_symbol">;</span></li><li><span class="sh_cbracket">}</span></li><li><span class="sh_symbol">?></span></li></ol>

你应该得到下面的结果:

<ol><li>ThisIsUseless</li><li> </li><li>ThisIsUselessThisIsUseless</li><li> </li><li>ThisIsUselessThisIsUselessThisIsUseless</li></ol>

实例小结

你已经学会如何编写一个简单的PHP函数。回到本章的开头,我们提到用C编写PHP功能函数的两个主要的动机。第一个动机是用C实现一些算法来提高性能和扩展功能。前一个例子应该能够指导你快速上手这种类型扩展的开发。第二个动机是包裹三方函数库。我们将在下一步讨论。

包裹第三方的扩展

本节中你将学到如何编写更有用和更完善的扩展。该节的扩展包裹了一个C库,展示了如何编写一个含有多个互相依赖的PHP函数扩展。

动机

也许最常见的PHP扩展是那些包裹第三方C库的扩展。这些扩展包括MySQL或Oracle的数据库服务库,libxml2的 XML技术库,ImageMagick 或GD的图形操纵库。

在本节中,我们编写一个扩展,同样使用脚本来生成骨架扩展,因为这能节省许多工作量。这个扩展包裹了标准C函数fopen(), fclose(), fread(), fwrite()和 feof().

扩展使用一个被叫做资源的抽象数据类型,用于代表已打开的文件FILE*。你会注意到大多数处理比如数据库连接、文件句柄等的PHP扩展使用了资源类型,这是因为引擎自己无法直接“理解”它们。我们计划在PHP扩展中实现的C API列表如下:

<ol><li><span class="sh_usertype">FILE</span><span class="sh_normal"> </span><span class="sh_symbol">*</span><span class="sh_function">fopen</span><span class="sh_symbol">(</span><span class="sh_keyword">const</span> <span class="sh_type">char</span> <span class="sh_symbol">*</span>path<span class="sh_symbol">,</span> <span class="sh_keyword">const</span> <span class="sh_type">char</span> <span class="sh_symbol">*</span>mode<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_type">int</span> <span class="sh_function">fclose</span><span class="sh_symbol">(</span><span class="sh_usertype">FILE</span><span class="sh_normal"> </span><span class="sh_symbol">*</span>stream<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_usertype">size_t</span><span class="sh_normal"> </span><span class="sh_function">fread</span><span class="sh_symbol">(</span><span class="sh_type">void</span> <span class="sh_symbol">*</span>ptr<span class="sh_symbol">,</span> <span class="sh_usertype">size_t</span><span class="sh_normal"> </span>size<span class="sh_symbol">,</span> <span class="sh_usertype">size_t</span><span class="sh_normal"> </span>nmemb<span class="sh_symbol">,</span> <span class="sh_usertype">FILE</span><span class="sh_normal"> </span><span class="sh_symbol">*</span>stream<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_usertype">size_t</span><span class="sh_normal"> </span><span class="sh_function">fwrite</span><span class="sh_symbol">(</span><span class="sh_keyword">const</span> <span class="sh_type">void</span> <span class="sh_symbol">*</span>ptr<span class="sh_symbol">,</span> <span class="sh_usertype">size_t</span><span class="sh_normal"> </span>size<span class="sh_symbol">,</span> <span class="sh_usertype">size_t</span><span class="sh_normal"> </span>nmemb<span class="sh_symbol">,</span> <span class="sh_usertype">FILE</span><span class="sh_normal"> </span><span class="sh_symbol">*</span>stream<span class="sh_symbol">);</span></li><li> </li><li><span class="sh_type">int</span> <span class="sh_function">feof</span><span class="sh_symbol">(</span><span class="sh_usertype">FILE</span><span class="sh_normal"> </span><span class="sh_symbol">*</span>stream<span class="sh_symbol">);</span></li></ol>

我们实现这些函数,使它们在命名习惯和简单性上符合PHP脚本。如果你曾经向PHP社区贡献过代码,你被期望遵循一些公共习俗,而不是跟随C库里的API。并不是所有的习俗都写在PHP代码树的CODING_STANDARDS文件里。这即是说,此功能已经从PHP发展的很早阶段即被包含在PHP中,并且与C库API类似。PHP安装已经支持fopen(), fclose()和更多的PHP函数。

以下是PHP风格的API:

<ol><li><span class="sh_usertype">resource</span><span class="sh_normal"> </span><span class="sh_function">file_open</span><span class="sh_symbol">(</span><span class="sh_usertype">string</span><span class="sh_normal"> </span>filename<span class="sh_symbol">,</span> <span class="sh_usertype">string</span><span class="sh_normal"> </span>mode<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_function">file_open</span><span class="sh_symbol">()</span> <span class="sh_comment">//接收两个字符串(文件名和模式),返回一个文件的资源句柄。</span></li><li> </li><li><span class="sh_type">bool</span> <span class="sh_function">file_close</span><span class="sh_symbol">(</span><span class="sh_usertype">resource</span><span class="sh_normal"> </span>filehandle<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_function">file_close</span><span class="sh_symbol">()</span> <span class="sh_comment">//接收一个资源句柄,返回真/假指示是否操作成功。</span></li><li> </li><li><span class="sh_usertype">string</span><span class="sh_normal"> </span><span class="sh_function">file_read</span><span class="sh_symbol">(</span><span class="sh_usertype">resource</span><span class="sh_normal"> </span>filehandle<span class="sh_symbol">,</span> <span class="sh_type">int</span> size<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_function">file_read</span><span class="sh_symbol">()</span> <span class="sh_comment">//接收一个资源句柄和读入的总字节数,返回读入的字符串。</span></li><li> </li><li><span class="sh_type">bool</span> <span class="sh_function">file_write</span><span class="sh_symbol">(</span><span class="sh_usertype">resource</span><span class="sh_normal"> </span>filehandle<span class="sh_symbol">,</span> <span class="sh_usertype">string</span><span class="sh_normal"> </span>buffer<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_function">file_write</span><span class="sh_symbol">()</span>   <span class="sh_comment">//接收一个资源句柄和被写入的字符串,返回真/假指示是否操作成功。</span></li><li> </li><li><span class="sh_type">bool</span> <span class="sh_function">file_eof</span><span class="sh_symbol">(</span><span class="sh_usertype">resource</span><span class="sh_normal"> </span>filehandle<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_function">file_eof</span><span class="sh_symbol">()</span>     <span class="sh_comment">//接收一个资源句柄,返回真/假指示是否到达文件的尾部。</span></li></ol>

因此,我们的函数定义文件——保存为ext/目录下的myfile.def——内容如下:

<ol><li><span class="sh_usertype">resource</span><span class="sh_normal"> </span><span class="sh_function">file_open</span><span class="sh_symbol">(</span><span class="sh_usertype">string</span><span class="sh_normal"> </span>filename<span class="sh_symbol">,</span> <span class="sh_usertype">string</span><span class="sh_normal"> </span>mode<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_type">bool</span> <span class="sh_function">file_close</span><span class="sh_symbol">(</span><span class="sh_usertype">resource</span><span class="sh_normal"> </span>filehandle<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_usertype">string</span><span class="sh_normal"> </span><span class="sh_function">file_read</span><span class="sh_symbol">(</span><span class="sh_usertype">resource</span><span class="sh_normal"> </span>filehandle<span class="sh_symbol">,</span> <span class="sh_type">int</span> size<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_type">bool</span> <span class="sh_function">file_write</span><span class="sh_symbol">(</span><span class="sh_usertype">resource</span><span class="sh_normal"> </span>filehandle<span class="sh_symbol">,</span> <span class="sh_usertype">string</span><span class="sh_normal"> </span>buffer<span class="sh_symbol">)</span></li><li> </li><li><span class="sh_type">bool</span> <span class="sh_function">file_eof</span><span class="sh_symbol">(</span><span class="sh_usertype">resource</span><span class="sh_normal"> </span>filehandle<span class="sh_symbol">)</span></li></ol>

下一步,利用ext_skel脚本在ext./ 原代码目录执行下面的命令:

<ol><li>./ext_skel --extname=myfile --proto=myfile.def</li></ol>

然后,按照前一个例子的关于编译新建立脚本的步骤操作。你会得到一些包含FETCH_RESOURCE()宏行的编译错误,这样骨架脚本就无法顺利完成编译。为了让骨架扩展顺利通过编译,把那些出错行[3]注释掉即可。

资源

资源是一个能容纳任何信息的抽象数据结构。正如前面提到的,这个信息通常包括例如文件句柄、数据库连接结构和其他一些复杂类型的数据。

使用资源的主要原因是因为:资源被一个集中的队列所管理,该队列可以在PHP开发人员没有在脚本里面显式地释放时可以自动地被释放。

举个例子,考虑到编写一个脚本,在脚本里调用mysql_connect()打开一个MySQL连接,可是当该数据库连接资源不再使用时却没有调用mysql_close()。在PHP里,资源机制能够检测什么时候这个资源应当被释放,然后在当前请求的结尾或通常情况下更早地释放资源。这就为减少内存泄漏赋予了一个“防弹”机制。如果没有这样一个机制,经过几次web请求后,web服务器也许会潜在地泄漏许多内存资源,从而导致服务器当机或出错。

注册资源类型

如何使用资源?Zend引擎让使用资源变地非常容易。你要做的第一件事就是把资源注册到引擎中去。使用这个API函数:

<ol><li><span class="sh_type">int</span> <span class="sh_function">zend_register_list_destructors_ex</span><span class="sh_symbol">(</span><span class="sh_usertype">rsrc_dtor_func_t</span><span class="sh_normal"> </span>ld<span class="sh_symbol">,</span> <span class="sh_usertype">rsrc_dtor_func_t</span><span class="sh_normal"> </span>pld<span class="sh_symbol">,</span> <span class="sh_type">char</span> <span class="sh_symbol">*</span>type_name<span class="sh_symbol">,</span> <span class="sh_type">int</span> module_number<span class="sh_symbol">)</span></li></ol>

这个函数返回一个资源类型id,该id应当被作为全局变量保存在扩展里,以便在必要的时候传递给其他资源API。ld:该资源释放时调用的函数。pld用于在不同请求中始终存在的永久资源,本章不会涉及。type_name是一个具有描述性类型名称的字符串,module_number为引擎内部使用,当我们调用这个函数时,我们只需要传递一个已经定义好的module_number变量。

回到我们的例子中来:我们会添加下面的代码到myfile.c原文件中。该文件包括了资源释放函数的定义,此资源函数被传递给zend_register_list_destructors_ex()注册函数(资源释放函数应该提早添加到文件中,以便在调用zend_register_list_destructors_ex()时该函数已被定义):

<ol><li><span class="sh_keyword">static</span> <span class="sh_type">void</span> <span class="sh_function">myfile_dtor</span><span class="sh_symbol">(</span>zend_rsrc_list_entry <span class="sh_symbol">*</span><span class="sh_usertype">rsrc</span><span class="sh_normal"> </span>TSRMLS_DC<span class="sh_symbol">)</span><span class="sh_cbracket">{</span></li><li>     <span class="sh_usertype">FILE</span><span class="sh_normal"> </span><span class="sh_symbol">*</span>fp <span class="sh_symbol">=</span> <span class="sh_symbol">(</span>FILE <span class="sh_symbol">*)</span> rsrc<span class="sh_symbol">-></span>ptr<span class="sh_symbol">;</span></li><li>     <span class="sh_function">fclose</span><span class="sh_symbol">(</span>fp<span class="sh_symbol">);</span></li><li><span class="sh_cbracket">}</span></li></ol>

把注册行添加到PHP_MINIT_FUNCTION()后,看起来应该如下面的代码:

<ol><li><span class="sh_function">PHP_MINIT_FUNCTION</span><span class="sh_symbol">(</span>myfile<span class="sh_symbol">)</span><span class="sh_cbracket">{</span></li><li>     <span class="sh_comment">/* If you have INI entries, uncomment these lines</span></li><li><span class="sh_comment">     ZEND_INIT_MODULE_GLOBALS(myfile, php_myfile_init_globals,NULL);</span></li><li> </li><li><span class="sh_comment">     REGISTER_INI_ENTRIES();</span></li><li><span class="sh_comment">     */</span></li><li> </li><li>     le_myfile <span class="sh_symbol">=</span> <span class="sh_function">zend_register_list_destructors_ex</span><span class="sh_symbol">(</span>myfile_dtor<span class="sh_symbol">,</span>NULL<span class="sh_symbol">,</span><span class="sh_string">"standard-c-file"</span><span class="sh_symbol">,</span> module_number<span class="sh_symbol">);</span></li><li> </li><li>     <span class="sh_keyword">return</span> SUCCESS<span class="sh_symbol">;</span></li><li><span class="sh_cbracket">}</span></li></ol>

l 注意到le_myfile是一个已经被ext_skel脚本定义好的全局变量。

PHP_MINIT_FUNCTION()是一个先于模块(扩展)的启动函数,是暴露给扩展的一部分API。下表提供可用函数简要的说明。

函数声明宏 语义
PHP_MINIT_FUNCTION() 当PHP被装载时,模块启动函数即被引擎调用。这使得引擎做一些例如资源类型,注册INI变量等的一次初始化。
PHP_MSHUTDOWN_FUNCTION() 当PHP完全关闭时,模块关闭函数即被引擎调用。通常用于注销INI条目
PHP_RINIT_FUNCTION() 在每次PHP请求开始,请求前启动函数被调用。通常用于管理请求前逻辑。
PHP_RSHUTDOWN_FUNCTION() 在每次PHP请求结束后,请求前关闭函数被调用。经常应用在清理请求前启动函数的逻辑。
PHP_MINFO_FUNCTION() 调用phpinfo()时模块信息函数被呼叫,从而打印出模块信息。

新建和注册新资源 我们准备实现file_open()函数。当我们打开文件得到一个FILE *,我们需要利用资源机制注册它。下面的主要宏实现注册功能:

<ol><li><span class="sh_function">ZEND_REGISTER_RESOURCE</span><span class="sh_symbol">(</span>rsrc_result<span class="sh_symbol">,</span> rsrc_pointer<span class="sh_symbol">,</span> rsrc_type<span class="sh_symbol">);</span></li></ol>

参考表格对宏参数的解释

ZEND_REGISTER_RESOURCE 宏参数

宏参数 参数类型
rsrc_result zval *, which should be set with the registered resource information. zval * 设置为已注册资源信息
rsrc_pointer Pointer to our resource data. 资源数据指针
rsrc_type The resource id obtained when registering the resource type. 注册资源类型时获得的资源id

文件函数

现在你知道了如何使用ZEND_REGISTER_RESOURCE()宏,并且准备好了开始编写file_open()函数。还有一个主题我们需要讲述。

当PHP运行在多线程服务器上,不能使用标准的C文件存取函数。这是因为在一个线程里正在运行的PHP脚本会改变当前工作目录,因此另外一个线程里的脚本使用相对路径则无法打开目标文件。为了阻止这种错误发生,PHP框架提供了称作VCWD (virtual current working directory 虚拟当前工作目录)宏,用来代替任何依赖当前工作目录的存取函数。这些宏与被替代的函数具备同样的功能,同时是被透明地处理。在某些没有标准C函数库平台的情况下,VCWD框架则不会得到支持。例如,Win32下不存在chown(),就不会有相应的VCWD_CHOWN()宏被定义。

VCWD列表

标准C库 VCWD宏
getcwd() VCWD_GETCWD()
fopen() VCWD_FOPEN
open() VCWD_OPEN() //用于两个参数的版本
open() VCWD_OPEN_MODE() //用于三个参数的open()版本
creat() VCWD_CREAT()
chdir() VCWD_CHDIR()
getwd() VCWD_GETWD()
realpath() VCWD_REALPATH()
rename() VCWD_RENAME()
stat() VCWD_STAT()
lstat() VCWD_LSTAT()
unlink() VCWD_UNLINK()
mkdir() VCWD_MKDIR()
rmdir() VCWD_RMDIR()
opendir() VCWD_OPENDIR()
popen() VCWD_POPEN()
access() VCWD_ACCESS()
utime() VCWD_UTIME()
chmod() VCWD_CHMOD()
chown() VCWD_CHOWN()

编写利用资源的第一个PHP函数

实现file_open()应该非常简单,看起来像下面的样子:

<ol><li><span class="sh_function">PHP_FUNCTION</span><span class="sh_symbol">(</span>file_open<span class="sh_symbol">)</span><span class="sh_cbracket">{</span></li><li>     <span class="sh_type">char</span> <span class="sh_symbol">*</span>filename <span class="sh_symbol">=</span> NULL<span class="sh_symbol">;</span></li><li>     <span class="sh_type">char</span> <span class="sh_symbol">*</span>mode <span class="sh_symbol">=</span> NULL<span class="sh_symbol">;</span></li><li>     <span class="sh_type">int</span> argc <span class="sh_symbol">=</span> <span class="sh_function">ZEND_NUM_ARGS</span><span class="sh_symbol">();</span></li><li>     <span class="sh_type">int</span> filename_len<span class="sh_symbol">;</span></li><li>     <span class="sh_type">int</span> mode_len<span class="sh_symbol">;</span></li><li>     <span class="sh_usertype">FILE</span><span class="sh_normal"> </span><span class="sh_symbol">*</span>fp<span class="sh_symbol">;</span></li><li> </li><li>     <span class="sh_keyword">if</span> <span class="sh_symbol">(</span><span class="sh_function">zend_parse_parameters</span><span class="sh_symbol">(</span><span class="sh_usertype">argc</span><span class="sh_normal"> </span>TSRMLS_CC<span class="sh_symbol">,</span> <span class="sh_string">"ss"</span><span class="sh_symbol">,</span> <span class="sh_symbol">&</span>filename<span class="sh_symbol">,&</span>filename_len<span class="sh_symbol">,</span> <span class="sh_symbol">&</span>mode<span class="sh_symbol">,</span> <span class="sh_symbol">&</span>mode_len<span class="sh_symbol">)</span> <span class="sh_symbol">==</span> FAILURE<span class="sh_symbol">)</span> <span class="sh_cbracket">{</span></li><li>          <span class="sh_keyword">return</span><span class="sh_symbol">;</span></li><li>     <span class="sh_cbracket">}</span></li><li> </li><li>     fp <span class="sh_symbol">=</span> <span class="sh_function">VCWD_FOPEN</span><span class="sh_symbol">(</span>filename<span class="sh_symbol">,</span> mode<span class="sh_symbol">);</span></li><li> </li><li>     <span class="sh_keyword">if</span> <span class="sh_symbol">(</span>fp <span class="sh_symbol">==</span> NULL<span class="sh_symbol">)</span> <span class="sh_cbracket">{</span></li><li>          RETURN_FALSE<span class="sh_symbol">;</span></li><li>     <span class="sh_cbracket">}</span></li><li> </li><li>     <span class="sh_function">ZEND_REGISTER_RESOURCE</span><span class="sh_symbol">(</span>return_value<span class="sh_symbol">,</span> fp<span class="sh_symbol">,</span> le_myfile<span class="sh_symbol">);</span></li><li><span class="sh_cbracket">}</span></li></ol>

你可能会注意到资源注册宏的第一个参数return_value,可此地找不到它的定义。这个变量自动的被扩展框架定义为zval * 类型的函数返回值。先前讨论的、能够影响返回值的RETURN_LONG() 和RETVAL_BOOL()宏确实改变了return_value的值。因此很容易猜到程序注册了我们取得的文件指针fp,同时设置return_value为该注册资源。

访问资源 需要使用下面的宏访问资源(参看表对宏参数的解释)

<ol><li><span class="sh_function">ZEND_FETCH_RESOURCE</span><span class="sh_symbol">(</span>rsrc<span class="sh_symbol">,</span> rsrc_type<span class="sh_symbol">,</span> passed_id<span class="sh_symbol">,</span> default_id<span class="sh_symbol">,</span> resource_type_name<span class="sh_symbol">,</span> resource_type<span class="sh_symbol">);</span></li></ol>

ZEND_FETCH_RESOURCE 宏参数

参数 含义
rsrc 资源值保存到的变量名。它应该和资源有相同类型。
rsrc_type rsrc的类型,用于在内部把资源转换成正确的类型
passed_id 寻找的资源值(例如zval **)
default_id 如果该值不为-1,就使用这个id。用于实现资源的默认值。
resource_type_name 资源的一个简短名称,用于错误信息。
resource_type 注册资源的资源类型id

使用这个宏,我们现在能够实现file_eof():

<ol><li><span class="sh_function">PHP_FUNCTION</span><span class="sh_symbol">(</span>file_eof<span class="sh_symbol">)</span><span class="sh_cbracket">{</span></li><li>     <span class="sh_type
                        
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/autumn20080101/article/details/50685389
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-01 18:13:05
  • 阅读 ( 1109 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢