PHP扩展开发完整教程(下)

本文深入探讨PHP中的面向对象编程,包括对象实例化、方法调用、属性读写及更新,同时解析PHP流处理机制,涵盖fopen、fsockopen、opendir等函数的实现与流API操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第11章 PHP中的面向对象

  1. 实例化一个对象并且调用它的方法
php public function hello() { 
	echo "hello world!\n"; }
}

function test_call() { 
	$obj = new baby(); 
	$obj->hello(); 
}

下面我们在扩展中实现以上test_call函数。

zend_class_entry *baby_ce;
ZEND_FUNCTION(test_call)
{
	zval *obj;
	MAKE_STD_ZVAL(obj);
	object_init_ex(obj, baby_ce);
	
	//如果确认此类没有构造函数就不用调用了。
	walu_call_user_function(NULL, obj, "__construct", "");
	
	walu_call_user_function(NULL, obj, "hello", "");
	zval_ptr_dtor(&obj);
	return;
}

ZEND_METHOD(baby, __construct)
{
	printf("a new baby!\n");
}

ZEND_METHOD(baby, hello)
{
	printf("hello world!!!!!\n");
}
static zend_function_entry baby_method[]={
	ZEND_ME(baby, __construct, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR)
	ZEND_ME(baby, hello, NULL, ZEND_ACC_PUBLIC)
	{NULL, NULL, NULL}
};
ZEND_MINIT_FUNCTION(test)
{
	zend_class_entry ce;
	INIT_CLASS_ENTRY(ce, "baby", baby_method);
	baby_ce = zend_register_internal_class(&ce TSRMLS_CC);
	return SUCCESS;
}
  1. 读写对象的属性
ZEND_API zval *zend_read_property(zend_class_entry *scope, zval *object, char *name, int name_length, zend_bool silent TSRMLS_DC);
ZEND_API zval *zend_read_property(zend_class_entry *scope, zval *object, const char *name, size_t name_length, zend_bool silent, zval *rv) ;//php7

ZEND_API zval *zend_read_static_property(zend_class_entry *scope, char *name, int name_length, zend_bool silent TSRMLS_DC);
ZEND_API zval *zend_read_static_property(zend_class_entry *scope, const char *name, size_t name_length, zend_bool silent);//php7

silent参数:

0: 如果属性不存在,则抛出一个notice错误。
1: 如果属性不存在,不报错。
如果所查的属性不存在,那么此函数将返回IS_NULL类型的zval
3. 更新对象的属性

 ZEND_API void zend_update_property(zend_class_entry *scope, zval *object, char *name, int name_length, zval *value TSRMLS_DC); ZEND_API void zend_update_property(zend_class_entry *scope, zval *object, const char *name, size_t name_length, zval *value);//php7
ZEND_API int zend_update_static_property(zend_class_entry *scope, char *name, int name_length, zval *value TSRMLS_DC); ZEND_API int zend_update_static_property(zend_class_entry *scope, const char *name, size_t name_length, zval *value);//php7

如果对象或者类中没有相关的属性,函数将自动的添加上。
4. 假设我们已经在扩展中定义好下面的类

class baby
{
	public $age;
	public static $area;
	
	public function __construct($age, $area)
	{
		$this->age = $age;
		self::$area = $area;
		
		var_dump($this->age, self::$area);
	}
}

内核写法

PHP_METHOD(baby, __construct)
{
	zval *age, *area, rv;
	zend_class_entry *ce;
	ce = Z_OBJCE_P(getThis());
	
	if( zend_parse_parameters(ZEND_NUM_ARGS(), "zz", &age, &area) == FAILURE )
	{
		printf("Error\n");
		RETURN_NULL();
	}
	zend_update_property(ce, getThis(), "age", sizeof("age")-1, age);
	zend_update_static_property(ce, "area", sizeof("area")-1, area );
	
	age = NULL;
	area = NULL;
	
	age = zend_read_property(ce, getThis(), "age", sizeof("age")-1, 0, &rv );
	php_var_dump(age, 1);
	
	area = zend_read_static_property(ce, "area", sizeof("area")-1, 0, &rv );
	php_var_dump(area, 1);
	
}

第12章 启动与终止的那点事

  1. 模块,请求的init方法中, 每个启动或者关闭的方法在return SUCCESS时退出。如果其中任何的函数return FAILURE,PHP为了避免出现严重问题而将请求中止。
  2. 配置扩展的信息
PHP_MINFO_FUNCTION(sample4) {
    php_info_print_table_start();
    php_info_print_table_row(2, "Sample4 Module", "enabled");
    php_info_print_table_row(2, "version", PHP_SAMPLE4_EXTVER);
    php_info_print_table_end();
}
  1. php_info_html_esc函数是php_escape_html_entities()的一个封装,htmlentites() 函数的底层实现。该函数返回的字符串通过emalloc()创建,并在使用后必须使用 efree()函数释放掉。
char *php_info_html_esc(char *str TSRMLS_DC)
  1. 输出开/关表格式所需的标签。HTML输出是与CLI输出一样,表现为一个简单的换行
void php_info_print_table_start(void)
void php_info_print_table_end(void)
  1. 输出表头行。
void php_info_print_table_header(int cols, ...)
void php_info_print_table_colspan_header(int cols, char *header)

第一个函数在可变参数列表中的char *元素外面的每一列都会输出一对th标签,第二个函数会在指定列数外面输出一对th标签。

void php_info_print_table_row(int cols, ...)
void php_info_print_table_row_ex(int cols, char *class, ...)

第一个函数在可变参数列表中的char *元素外面的每一行都会输出一对td标签,第二个函数会在指定列数外面输出一对td标签。当不在HTML中 输出的时候,两个函数将没有任何差别。

void php_info_print_hr(void)

这种函数将在HTML中输出一个br标签,或者一个表示行开始和结束的水平线
我们常用的PHPWRITE()和php_printf()函数可以在在MINFO函数中使用,你应该注意正确的信息输出取决于当前的SAPI判断是用纯文本还是HTML的方式输出 要做到这一点,只需要检查sapi_module结构中的phpinfo_as_text属性

PHP_MINFO_FUNCTION(sample4) {
    php_info_print_table_start();
    php_info_print_table_row(2, "Sample4 Module", "enabled");
    php_info_print_table_row(2, "version", PHP_SAMPLE4_EXTVER);
    if (sapi_module.phpinfo_as_text) {
        /* No HTML for you */
        php_info_print_table_row(2, "By",
            "Example Technologies\nhttp://www.example.com");
    } else {
        /* HTMLified version */
        php_printf("<tr>"
            "<td class=\"v\">By</td>"
            "<td class=\"v\">"
            "<a href=\"http://www.example.com\""
            " alt=\"Example Technologies\">"
            "<img src=\"http://www.example.com/logo.png\" />"
            "</a></td></tr>");
        php_info_print_table_end();
    }
}
  1. 你可以通过define()函数来定义一个常量。在内核中,我们将会使用REGISTER_*_CONSTANT()的 家族函数来使用常量。
    一般会把常量定义在MINIT中
PHP_MINIT_FUNCTION(sample4) {
    REGISTER_STRING_CONSTANT("SAMPLE4_VERSION",
            PHP_SAMPLE4_EXTVER, CONST_CS | CONST_PERSISTENT);
    return SUCCESS;
}

第一个参数是你要定义的这个常量的名字,常量的名称只能为文字,大家可以尝试使用一个char *的变量,这将导致sizeof计算出错误 的字符串长度。
CONST_CS标识是否大小写敏感,一般情况下CONST_CS标识是默认使用的。对于一些特殊的 情况,比如TRUE,FALSE,NULL等等,这个参数将被省略。
在|后的标识位中的标识符说明了该常量的作用域和生命周期
7. 下面列出的4个创建常量常用的函数,有一个共同需要注意的地方,常量名称一定要用文字而不是char *类型的变量

REGISTER_LONG_CONSTANT(char *name, long lval, int flags)
REGISTER_DOUBLE_CONSTANT(char *name, double dval, int flags)
REGISTER_STRING_CONSTANT(char *name, char *value, int flags)
REGISTER_STRINGL_CONSTANT(char *name,char *value, int value_len, int flags)
  1. 首先,我们需要在扩展的头文件中(默认是php_*.h)中定义所有的全局变量
ZEND_BEGIN_MODULE_GLOBALS(sample4)
    unsigned long counter;
ZEND_END_MODULE_GLOBALS(sample4)

用ZEND_BEGIN_MODULE_GLOBALS和ZEND_END_MODULE_GLOBALS宏将定义的全局变量包起来。将上例中的宏展开后,是下面这个样子:

typedef struct _zend_sample4_globals {
    unsigned long counter;
} zend_sample4_globals;

如果你还有其他的全局变量需要定义,只需加在两个宏之间就可以了,接下来我们该在simple4.c中声明我们在头文件中定义的这些全局变量了:

ZEND_DECLARE_MODULE_GLOBALS(sample4);
  1. 我们可以直接通过sample4_globals.counter来获取计数器的值。在线程安全的版本中,另一种方法是声明一个整数:
//非线程安全
zend_sample4_globals sample4_globals;
//线程 安全的
int sample4_globals_id;
#ifdef ZTS
    ts_allocate_id(
            &sample4_globals_id,
            sizeof(zend_sample4_globals),
            NULL, NULL);
#endif

根据其定义的信息,将为每个新线程的独立存储空间分配内存块,这种方法需要包裹在#ifdef中,以防止它在没有启动Zend Thread Safety(ZTS)时执行
10. 在非线程的环境中,会将一个zend_sample4_globals结构的副本保存在指定进程中。你可以指定他的默认值,或者在MINIT或者RINIT中分配资源来初始化它。要记得 在对应的MSHUTDOWN或者RSHUTDOWN中及时释放这些资源。

#ifdef ZTS
#include "TSRM.h"
#define SAMPLE4_G(v) TSRMG(sample4_globals_id, zend_sample4_globals*, v)
#else
#define SAMPLE4_G(v) (sample4_globals.v)
#endif

PHP_FUNCTION(sample4_counter) {
    RETURN_LONG(++SAMPLE4_G(counter));
}

11, 以下是*G()系列宏

EG()	这个宏可以用来访问符号表,函数,资源信息和常量。
CG()	用来访问核心全局变量。
PG()	PHP全局变量。我们知道php.ini会映射一个或者多个PHP全局结构。举几个使用这个宏的例子:PG(register_globals), PG(safe_mode), PG(memory_limit)
FG()	文件全局变量。大多数文件I/O或相关的全局变量的数据流都塞进标准扩展出口结构。
  1. 在PHP中有一种“特殊”的全局变量,通常我们把它们称作超级全局变量,常见的比如 G E T 、 _GET、 GET_POST、$_FILE等等,他们会在编译之前就声明,所以在普通的脚本中,可能无法定义其它的超级全局变量,我们来看下session扩展的MINIT函数实现
PHP_MINIT_FUNCTION(session) {
        zend_register_auto_global("_SESSION",
                            sizeof("_SESSION") - 1,
                            NULL TSRMLS_CC);
        return SUCCESS;
}

这里的第二个参数,sizeof("_SESSION") - 1,一定要排除标识字符串结束的\0符


第13章 INI设置

  1. 定义一个 INI 的设置,让它的默认值为Hello World!,像下面这样:
#include "php_ini.h" 
PHP_INI_BEGIN()
	PHP_INI_ENTRY("sample4.greeting", "Hello World", PHP_INI_ALL, NULL)
PHP_INI_END()

PHP_INI_ENTRY 这个宏里面设置的前面的两个参数,分别代表着INI设置的名称和它的默认值。第二个参数决定设置是否允许被修改,以及它能被修改的作用域。最后一个参数是一个回调函数,当INI的值被修改时候触发此回调函数。

PHP_INI_PERDIR	指令可以在php.ini、httpd.conf或.htaccess文件中修改
PHP_INI_SYSTEM	指令可以在php.ini 和 httpd.conf 文件中修改
PHP_INI_USER	指令可以在用户脚本中修改
PHP_INI_ALL	指令可以在任何地方修改
  1. 已经声明了你的INI的设置,你现在准备将它用在你的问候函数之中
PHP_FUNCTION(sample4_hello_world) 
{
	const char *greeting = INI_STR("sample4.greeting");
    php_printf("%s\n", greeting); 
}
  1. 并不是所有的INI值都是基于字符串的;也有其他的一些用于整数、浮点数、或布尔值的宏,例如
long lval = INI_INT("sample4.intval"); 
double dval = INI_FLT("sample4.fltval"); 
zend_bool bval = INI_BOOL("sample4.boolval");
  1. 通常你想知道你当前的INI设置的值;恭喜你,ZEND内核刚好就存在一组这样的宏为你提供查询每种类型的INI的默认值
const char *strval = INI_ORIG_STR("sample4.stringval"); 
long lval = INI_ORIG_INT("sample4.intval");
double dval = INI_ORIG_FLT("sample4.fltval"); 
zend_bool bval = INI_ORIG_BOOL("sample4.boolval");
  1. 访问级别
SYSTEM	设置被放在php.ini中,或者在Apache的http.conf配置文件中的和,
	它在apache启动时候生效,被认为是设置的全局变量
PERDIR 一些设置被放在Apache的http.conf的或者块中,或者.htaccess文件之中
USER	一旦脚本开始执行,唯一的改变INI设置的方法就是利用用户方法:ini_set()
  1. 无论INI设置在什么时候被修改,无论是通过ini_set()方法来修改还是在一个perdir指令执行期间来修改,zend引擎都会通过一个OnModify的回调来检查它
ZEND_INI_MH(php_sample4_modify_greeting) 
{
	if (new_value_length == 0) { 
    	return FAILURE;
    }
    return SUCCESS; 
}
PHP_INI_BEGIN()
	PHP_INI_ENTRY("sample4.greeting", "Hello World",
    	PHP_INI_ALL, php_sample4_modify_greeting) 
PHP_INI_END()
  1. ZEND引擎有一个统一的宏来输出这些ini内容,它可以被放置在PHP_MINFO_FUNCTION()块中:
PHP_MINFO_FUNCTION(sample4) 
{
	DISPLAY_INI_ENTRIES(); 
}

第14章 流的访问

  1. 有四种不同的路径去打开一个流. 从用户空间角度来看, 这四种不同的类别如下(函数列表只代表示例, 不是完整列表)
<?php
/* fopen包装
* 操作文件/URI方式指定远程文件类资源 */
$fp = fopen($url, $mode);
$data = file_get_contents($url);
file_put_contents($url, $data);
$lines = file($url);
/* 传输
* 基于套接字的顺序I/O */
$fp = fsockopen($host, $port);
$fp = stream_socket_client($uri);
$fp = stream_socket_server($uri, $options);

/* 目录流 */
$dir = opendir($url);
$files = scandir($url);
$obj = dir($url);

/* "特殊"的流 */
$fp = tmpfile();
$fp = popen($cmd);
proc_open($cmd, $pipes);
  1. 下面是我们实现的fopen()函数
PHP_FUNCTION(sample5_fopen)
{
    php_stream *stream;
    char *path, *mode;
    int path_len, mode_len;
    int options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
    
	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",
	    &path, &path_len, &mode, &mode_len) == FAILURE) {
	    return;
	}
	stream = php_stream_open_wrapper(path, mode, options, NULL);
	if (!stream) {
	    RETURN_FALSE;
	}
	php_stream_to_zval(stream, return_value);
}

php_stream_open_wrapper()的目的应该是完全绕过底层. path指定要读写文件名或URL, 读写行为依赖于mode的值
3. 尽管传输流和fopen包装流是相同的组件组成的, 但它的注册策略和其他的流不同. 从某种程度上来说, 这是因为用户空间对它们的访问方式的不同造成的, 它们需要实现基于套接字的其他因子
4. 从扩展开发者角度来看, 打开传输流的过程是相同的. 下面是对fsockopen()的实现:

PHP_FUNCTION(sample5_fsockopen)
    php_stream *stream;
    char *host, *transport, *errstr = NULL;
    int host_len, transport_len, implicit_tcp = 1, errcode = 0;
    long port = 0;
    int options = ENFORCE_SAFE_MODE;
    int flags = STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l",
                &host, &host_len, &port) == FAILURE) {
        return;
    }
    if (port) {
        int implicit_tcp = 1;
        if (strstr(host, "://")) {
            /* A protocol was specified,
             * no need to fall back on tcp:// */
            implicit_tcp = 0;
        }
        transport_len = spprintf(&transport, 0, "%s%s:%d",
                implicit_tcp ? "tcp://" : "", host, port);
    } else {
        /* When port isn't specified
         * we can safely assume that a protocol was
         * (e.g. unix:// or udg://) */
        transport = host;
        transport_len = host_len;
    }
    stream = php_stream_xport_create(transport, transport_len,
            options, flags,
            NULL, NULL, NULL, &errstr, &errcode);
    if (transport != host) {
        efree(transport);
    }
    if (errstr) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING, "[%d] %s",
                errcode, errstr);
        efree(errstr);
    }
    if (!stream) {
        RETURN_FALSE;
    }
    php_stream_to_zval(stream, return_value);
}

这个函数的基础构造和前面的fopen示例是一样的. 不同在于host和端口号使用不同的参数指定, 接着为了给出一个传输流URL就必须将它们合并到一起. 在产生了一个有意义的路径后, 将它传递给php_stream_xport_create()函数

php_stream *php_stream_xport_create(char *xport, int xport_len,
    int options, int flags,
    const char *persistent_id,
    struct timeval *timeout,
    php_stream_context *context,
    char **errstr, int *errcode);

每个参数的含义如下:

xport 基于URI的传输描述符. 对于基于inet的套接字流, 它可以是tcp://127.0.0.1:80, udp://10.0.0.1:53, ssl://169.254.13.24:445等. 此外, UNIX域传输协议unix:///path/to/socket,udg:///path/to/dgramsocket等都是合法的. xport_len指定了xport的长度, 因此xport是二进制安全的.

  1. fopen包装器支持目录访问, 比如file://和ftp://, 下面是对opendir()的实现:
PHP_FUNCTION(sample5_opendir)
{
    php_stream *stream;
    char *path;
    int path_len, options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s",
        &path, &path_len) == FAILURE) {
        return;
    }
    stream = php_stream_opendir(path, options, NULL);
    if (!stream) {
        RETURN_FALSE;
    }
    php_stream_to_zval(stream, return_value);
}
  1. 还有一些特殊类型的流不能归类到fopen/transport/directory中. 它们中每一个都有自己独有的API:
php_stream *php_stream_fopen_tmpfile(void);
php_stream *php_stream_fopen_temporary_file(const char *dir, const char *pfx, char **opened_path);

创建一个可seek的缓冲区流用于读写. 在关闭时, 这个流使用的所有临时资源, 包括所有的缓冲区(无论是在内存还是磁盘), 都将被释放. 使用这一组API中的后一个函数, 允许临时文件被以特定的格式命名放到指定路径. 这些内部API调用被用户空间的tmpfile()函数隐藏.
7. 以下3个API方法接受已经打开的FILE *资源或文件描述符ID, 使用流API的某种操作包装. fd格式的接口不会搜索匹配你前面看到过的fopen函数打开的资源, 但是它会注册持久化的资源

php_stream *php_stream_fopen_from_fd(int fd, const char *mode, const char *persistent_id);
php_stream *php_stream_fopen_from_file(FILE *file, const char *mode);
php_stream *php_stream_fopen_from_pipe(FILE *file, const char *mode);
  1. 在你打开一个流之后, 就可以在它上面执行I/O操作了. 使用哪种协议包装API创建了流并不重要, 它们都使用相同的访问API.
  2. 流的读写可以使用下面的API函数组合完成, 它们多数都是遵循POSIX I/O中对应的API规范的:
int php_stream_getc(php_stream *stream);

从数据流中接收一个字符. 如果流上再没有数据, 则返回EOF.

size_t php_stream_read(php_stream *stream, char *buf, size_t count);

从指定流中读取指定字节的数据. buf必须预分配至少count字节的内存空间. 这个函数将返回从数据流实际读到缓冲区中的数据字节数.

char *php_stream_get_line(php_stream *stream, char *buf, size_t maxlen, size_t *returned_len);
char *php_stream_gets(php_stream *stream, char *buf, size_t maxlen);

这两个函数从stream中读取最多maxlen个字符, 直到碰到换行符或流结束. buf可以是一个指向预分配的至少maxlen字节的内存空间的指针, 也可以是NULL, 当它是NULL时,则会自动的创建一个动态大小的缓冲区, 用从流中实际读出的数据填充, 成功后函数返回指向缓冲区的指针, 失败则返回NULL. 如果returned_len传递了非NULL值, 则在返回时它将被设置为实际从流中读取的字节数.

char *php_stream_get_record(php_stream *stream, size_t maxlen, size_t *returned_len, char *delim, size_t delim_len TSRMLS_DC);

//php7
PHPAPI zend_string *php_stream_get_record(php_stream *stream, size_t maxlen, const char *delim, size_t delim_len);

和php_stream_get_line()类似, 这个函数将读取最多maxlen, 或到达EOF/行结束第一次出现的位置. 但是它也有和php_stream_get_line()的不同指出, 这个函数允许指定任意的停止读取标记.
10. 从php流中读取目录项和上面从普通文件中读取普通数据相同. 这些数据放到了固定大小的dirents块中. 内部的php_stream_dirent结构体如下, 它与POSIX定义的dirent结构体一致:

typedef struct _php_stream_dirent {
    char d_name[MAXPATHLEN];
} php_stream_dirent;

实际上你可以直接使用php_stream_read()函数读取数据到这个结构体中:

struct dirent entry;
    if (php_stream_read(stream, (char*)&entry, sizeof(entry)) == sizeof(entry)) {
        /* 成功从目录流中读取到一项 */
        php_printf("File: %s\n", entry.d_name);
    }
  1. 由于从目录流中读取是很常见的操作, php流包装层暴露了一个API, 它将记录大小的检查和类型转换处理封装到了一次调用中:
php_stream_dirent *php_stream_readdir(php_stream *dirstream, php_stream_dirent *entry);

如果成功读取到目录项, 则传入的entry指针将被返回, 否则返回NULL标识错误

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值