php包裹第三方的扩展

如何编写PHP扩展(2)
2008年06月02日 星期一 13:10
包裹第三方的扩展



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



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

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

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



FILE *fopen(const char *path, const char *mode);

int fclose(FILE *stream);

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

int feof(FILE *stream);



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

以下是PHP风格的API:



resource file_open(string filename, string mode)

file_open()接收两个字符串(文件名和模式),返回一个文件的资源句柄。

bool file_close(resource filehandle)

file_close()接收一个资源句柄,返回真/假指示是否操作成功。

string file_read(resource filehandle, int size)

file_read()接收一个资源句柄和读入的总字节数,返回读入的字符串。

bool file_write(resource filehandle, string buffer)

file_write接收一个资源句柄和被写入的字符串,返回真/假指示是否操作成功。

bool file_eof(resource filehandle)

file_eof()接收一个资源句柄,返回真/假指示是否到达文件的尾部。



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



resource file_open(string filename, string mode)

bool file_close(resource filehandle)

string file_read(resource filehandle, int size)

bool file_write(resource filehandle, string buffer)

bool file_eof(resource filehandle)



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



./ext_skel --extname=myfile --proto=myfile.def



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



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

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

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



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



int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number)



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

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



static void myfile_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)

{

FILE *fp = (FILE *) rsrc->ptr;

fclose(fp);

}



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



PHP_MINIT_FUNCTION(myfile)

{

/* If you have INI entries, uncomment these lines

ZEND_INIT_MODULE_GLOBALS(myfile, php_myfile_init_globals,NULL);

REGISTER_INI_ENTRIES();

*/

le_myfile = zend_register_list_destructors_ex(myfile_dtor,NULL,"standard-c-file", module_number);

return SUCCESS;

}



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 *,我们需要利用资源机制注册它。下面的主要宏实现注册功能:



ZEND_REGISTER_RESOURCE(rsrc_result, rsrc_pointer, rsrc_type);



参考表格对宏参数的解释



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()应该非常简单,看起来像下面的样子:



PHP_FUNCTION(file_open)

{

char *filename = NULL;

char *mode = NULL;

int argc = ZEND_NUM_ARGS();

int filename_len;

int mode_len;

FILE *fp;

if (zend_parse_parameters(argc TSRMLS_CC, "ss", &filename,&filename_len, &mode, &mode_len) == FAILURE) {

return;

}

fp = VCWD_FOPEN(filename, mode);

if (fp == NULL) {

RETURN_FALSE;

}

ZEND_REGISTER_RESOURCE(return_value, fp, le_myfile);

}



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



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



ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id, default_id,

resource_type_name, resource_type);



ZEND_FETCH_RESOURCE 宏参数

参数
   

含义

rsrc
   

资源值保存到的变量名。它应该和资源有相同类型。

rsrc_type
   

rsrc的类型,用于在内部把资源转换成正确的类型

passed_id
   

寻找的资源值(例如zval **)

default_id
   

如果该值不为-1,就使用这个id。用于实现资源的默认值。

resource_type_name
   

资源的一个简短名称,用于错误信息。

resource_type
   

注册资源的资源类型id

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



PHP_FUNCTION(file_eof)

     

int argc = ZEND_NUM_ARGS();

zval *filehandle = NULL;

FILE *fp;

if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) ==FAILURE) {

return;

       }

ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-c-file",le_myfile);

if (fp == NULL) {

RETURN_FALSE;

       }

if (feof(fp) <= 0) {

/* Return eof also if there was an error */

RETURN_TRUE;

       }

RETURN_FALSE;

     



删除一个资源 通常使用下面这个宏删除一个资源:



int zend_list_delete(int id)



传 递给宏一个资源id,返回SUCCESS或者FAILURE。如果资源存在,优先从Zend资源列队中删除,该过程中会调用该资源类型的已注册资源清理函 数。因此,在我们的例子中,不必取得文件指针,调用fclose()关闭文件,然后再删除资源。直接把资源删除掉即可。

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



PHP_FUNCTION(file_close)

{

int argc = ZEND_NUM_ARGS();

zval *filehandle = NULL;

if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) == FAILURE) {

return;

       }

if (zend_list_delete(Z_RESVAL_P(filehandle)) == FAILURE) {

RETURN_FALSE;

       }

RETURN_TRUE;

       }



你 肯定会问自己Z_RESVAL_P()是做什么的。当我们使用zend_parse_parameters()从参数列表中取得资源的时候,得到的是 zval的形式。为了获得资源id,我们使用Z_RESVAL_P()宏得到id,然后把id传递给zend_list_delete()。

       有一系列宏用于访问存储于zval值(参考表的宏列表)。尽管在大多数情况下zend_parse_parameters()返回与c类型相应的值,我们仍希望直接处理zval,包括资源这一情况。



Zval访问宏




   

访问对象
   

C 类型

Z_LVAL, Z_LVAL_P,

Z_LVAL_PP


   

整型值
   

long

Z_BVAL, Z_BVAL_P,

Z_BVAL_PP


   

布尔值
   

zend_bool

Z_DVAL, Z_DVAL_P,

Z_DVAL_PP


   

浮点值
   

double

Z_STRVAL, Z_STRVAL_P,

Z_STRVAL_PP


   

字符串值
   

char *

Z_STRLEN, Z_STRLEN_P, Z_STRLEN_PP


   

字符串长度值
   

int

Z_RESVAL, Z_RESVAL_P,Z_RESVAL_PP
   

资源值
   

long

Z_ARRVAL, Z_ARRVAL_P,

Z_ARRVAL_PP


   

联合数组
   

HashTable *

Z_TYPE, Z_TYPE_P,

Z_TYPE_PP


   

Zval类型
   

Enumeration (IS_NULL, IS_LONG, IS_DOUBLE, IS_STRING, IS_ARRAY, IS_OBJECT, IS_BOOL, IS_RESOURCE)



Z_OBJPROP,

Z_OBJPROP_P,

Z_OBJPROP_PP


   

对象属性hash(本章不会谈到)
   

HashTable *

Z_OBJCE, Z_OBJCE_P,

Z_OBJCE_PP


   

对象的类信息(本章不会谈到)
   

zend_class_entry



用 于访问zval值的宏        所有的宏都有三种形式:一个是接受zval s,另外一个接受zval *s,最后一个接受zval **s。它们的区别是在命名上,第一个没有后缀,zval *有后缀_P(代表一个指针),最后一个 zval **有后缀_PP(代表两个指针)。

       现在,你有足够的信息来独立完成 file_read()和 file_write()函数。这里是一个可能的实现:



PHP_FUNCTION(file_read)

{

int argc = ZEND_NUM_ARGS();

long size;

zval *filehandle = NULL;

FILE *fp;

char *result;

size_t bytes_read;

if (zend_parse_parameters(argc TSRMLS_CC, "rl", &filehandle,&size) == FAILURE) {

return;

}

ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile);

result = (char *) emalloc(size+1);

bytes_read = fread(result, 1, size, fp);

result[bytes_read] = '/0';

RETURN_STRING(result, 0);

}



PHP_FUNCTION(file_write)

{

char *buffer = NULL;

int argc = ZEND_NUM_ARGS();

int buffer_len;

zval *filehandle = NULL;

FILE *fp;

if (zend_parse_parameters(argc TSRMLS_CC, "rs", &filehandle,&buffer, &buffer_len) == FAILURE) {

return;

}

ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile);

if (fwrite(buffer, 1, buffer_len, fp) != buffer_len) {

RETURN_FALSE;

}

RETURN_TRUE;

}

测试扩展 你现在可以编写一个测试脚本来检测扩展是否工作正常。下面是一个示例脚本,该脚本打开文件test.txt,输出文件类容到标准输出,建立一个拷贝test.txt.new。



<?php

$fp_in = file_open("test.txt", "r") or die("Unable to open input file/n");

$fp_out = file_open("test.txt.new", "w") or die("Unable to open output file/n");

while (!file_eof($fp_in)) {

$str = file_read($fp_in, 1024);

print($str);

file_write($fp_out, $str);

}

file_close($fp_in);

file_close($fp_out);

?>




全局变量

你 可能希望在扩展里使用全局C变量,无论是独自在内部使用或访问php.ini文件中的INI扩展注册标记(INI在下一节中讨论)。因为PHP是为多线程 环境而设计,所以不必定义全局变量。PHP提供了一个创建全局变量的机制,可以同时应用在线程和非线程环境中。我们应当始终利用这个机制,而不要自主地定 义全局变量。用一个宏访问这些全局变量,使用起来就像普通全局变量一样。

       用于生成myfile工程骨架文件的ext_skel脚本创建了必要的代码来支持全局变量。通过检查php_myfile.h文件,你应当发现类似下面的被注释掉的一节,



ZEND_BEGIN_MODULE_GLOBALS(myfile)

int global_value;

char *global_string;

ZEND_END_MODULE_GLOBALS(myfile)



你 可以把这一节的注释去掉,同时添加任何其他全局变量于这两个宏之间。文件后部的几行,骨架脚本自动地定义一个MYFILE_G(v)宏。这个宏应当被用于 所有的代码,以便访问这些全局变量。这就确保在多线程环境中,访问的全局变量仅是一个线程的拷贝,而不需要互斥的操作。

       为了使全局变量有效,最后需要做的是把myfile.c:



ZEND_DECLARE_MODULE_GLOBALS(myfile)



注释去掉。

你也许希望在每次PHP请求的开始初始化全局变量。另外,做为一个例子,全局变量已指向了一个已分配的内存,在每次PHP请求结束时需要释放内存。为了达到这些目的,全局变量机制提供了一个特殊的宏,用于注册全局变量的构造和析构函数(参考表对宏参数的说明):



ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)



表 ZEND_INIT_MODULE_GLOBALS 宏参数

参数
   

含义

module_name


   

与传递给ZEND_BEGIN_MODULE_GLOBALS()宏相同的扩展名称。

globals_ctor


   

构造函数指针。在myfile扩展里,函数原形与void php_myfile_init_globals(zend_myfile_globals *myfile_globals)类似

globals_dtor


   

析构函数指针。例如,php_myfile_init_globals(zend_myfile_globals *myfile_globals)



你可以在myfile.c里看到如何使用构造函数和ZEND_INIT_MODULE_GLOBALS()宏的示例。



添加自定义INI指令



       INI文件(php.ini)的实现使得PHP扩展注册和监听各自的INI条目。如果这些INI条目由php.ini、Apache的htaccess或 其他配置方法来赋值,注册的INI变量总是更新到正确的值。整个INI框架有许多不同的选项以实现其灵活性。我们涉及一些基本的(也是个好的开端),借助 本章的其他材料,我们就能够应付日常开发工作的需要。

通过在PHP_INI_BEGIN()/PHP_INI_END()宏之间的STD_PHP_INI_ENTRY()宏注册PHP INI指令。例如在我们的例子里,myfile.c中的注册过程应当如下:



PHP_INI_BEGIN()

STD_PHP_INI_ENTRY("myfile.global_value", "42", PHP_INI_ALL, OnUpdateInt, global_value, zend_myfile_globals, myfile_globals)

STD_PHP_INI_ENTRY("myfile.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_myfile_globals, myfile_globals)

PHP_INI_END()



除了STD_PHP_INI_ENTRY()其他宏也能够使用,但这个宏是最常用的,可以满足大多数需要(参看表对宏参数的说明):



STD_PHP_INI_ENTRY(name, default_value, modifiable, on_modify, property_name, struct_type, struct_ptr)



STD_PHP_INI_ENTRY 宏参数表



参数
   

含义

name
   

INI条目名

default_value
   

如果没有在INI文件中指定,条目的默认值。默认值始终是一个字符串。

modifiable
   

设定在何种环境下INI条目可以被更改的位域。可以的值是:

• PHP_INI_SYSTEM. 能够在php.ini或http.conf等系统文件更改

• PHP_INI_PERDIR. 能够在 .htaccess中更改

• PHP_INI_USER. 能够被用户脚本更改

• PHP_INI_ALL. 能够在所有地方更改

on_modify
   

处理INI条目更改的回调函数。你不需自己编写处理程序,使用下面提供的函数。包括:

• OnUpdateInt

• OnUpdateString

• OnUpdateBool

• OnUpdateStringUnempty

• OnUpdateReal

property_name
   

应当被更新的变量名

struct_type
   

变量驻留的结构类型。因为通常使用全局变量机制,所以这个类型自动被定义,类似于zend_myfile_globals。

struct_ptr
   

全局结构名。如果使用全局变量机制,该名为myfile_globals。



最 后,为了使自定义INI条目机制正常工作,你需要分别去掉PHP_MINIT_FUNCTION(myfile)中的 REGISTER_INI_ENTRIES()调用和PHP_MSHUTDOWN_FUNCTION(myfile)中的 UNREGISTER_INI_ENTRIES()的注释。

       访问两个示例全局变量中的一个与在扩展里编写MYFILE_G(global_value) 和MYFILE_G(global_string)一样简单。

       如果你把下面的两行放在php.ini中,MYFILE_G(global_value)的值会变为99。



; php.ini – The following line sets the INI entry myfile.global_value to 99.

myfile.global_value = 99



线程安全资源管理宏



现在,你肯定注意到以TSRM(线程安全资源管理器)开头的宏随处使用。这些宏提供给扩展拥有独自的全局变量的可能,正如前面提到的。

当 编写PHP扩展时,无论是在多进程或多线程环境中,都是依靠这一机制访问扩展自己的全局变量。如果使用全局变量访问宏(例如MYFILE_G()宏),需 要确保TSRM上下文信息出现在当前函数中。基于性能的原因,Zend引擎试图把这个上下文信息作为参数传递到更多的地方,包括 PHP_FUNCTION()的定义。正因为这样,在PHP_FUNCTION()内当编写的代码使用访问宏(例如MYFILE_G()宏)时,不需要做 任何特殊的声明。然而,如果PHP函数调用其他需要访问全局变量的C函数,要么把上下文作为一个额外的参数传递给C函数,要么提取上下文(要慢点)。

       在需要访问全局变量的代码块开头使用TSRMLS_FETCH()来提取上下文。例如:



void myfunc()

{

TSRMLS_FETCH();

MYFILE_G(myglobal) = 2;

}



如 果希望让代码更加优化,更好的办法是直接传递上下文给函数(正如前面叙述的,PHP_FUNCTION()范围内自动可用)。可以使用 TSRMLS_C(C表示调用Call)和TSRMLS_CC(CC边式调用Call和逗号Comma)宏。前者应当用于仅当上下文作为一个单独的参数, 后者应用于接受多个参数的函数。在后一种情况中,因为根据取名,逗号在上下文的前面,所以TSRMLS_CC不能是第一个函数参。

在函数原形中,可以分别使用TSRMLS_D和TSRMLS_DC宏声名正在接收上下文。

       下面是前一例子的重写,利用了参数传递上下文。



void myfunc(TSRMLS_D)

{

MYFILE_G(myglobal) = 2;

}

PHP_FUNCTION(my_php_function)

{



myfunc(TSRMLS_C);



}



总 结



现在,你已经学到了足够的东西来创建自己的扩展。本章讲述了一些重要的基础来编写和理解PHP扩展。Zend引擎提供的扩展API相当丰富,使你能够开发面向对象的扩展。几乎没有文档谈几许多高级特性。当然,依靠本章所学的基础知识,你可以通过浏览现有的原码学到很多。

       更多关于信息可以在PHP手册的扩展PHP章节http://www.php.net/manual/en/zend.php中找到。另外,你也可以考虑 加入PHP开发者邮件列表internals@ lists.php.net,该邮件列表围绕开发PHP 本身。你还可以查看一下新的扩展生成工具——PECL_Gen(http://pear.php.net/package/PECL_Gen),这个工具 正在开发之中,比起本章使用的ext_skel有更多的特性。



词汇表



binary safe 二进制安全

context 上下文

extensions 扩展

entry 条目

skeleton 骨架

Thread-Safe Resource Manager TSRM 线程安全资源管理器



Contact info:

Email: taft # wjl.cn

[1] 可参考译者写的

[2] 译者:可以使用phpcli程序在控制台里执行php文件。

[3] 译者:可以查看到生成的FETCH_RESOURCE()宏参数是一些’???’。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值