TIPI:深入理解PHP项目提供PDF版本下载及网站更新

去年开始的新项目,主要分享PHP的内部实现,www.php-internal.com 从开始到现在也花了不少的功夫在上面,目前还有很多的工作要继续做。

刚刚进行了一次升级,主要的工作还是pdf版本的提供,不过目前的版本在排版上还有一些工作要做,更新内容如下,

[新增内容及更新]提供PDF版下载,增加HashTable等数据结构内容及网站功能的优化

TIPI团队在第一次发布后收到了大量的反馈, 多谢大家的支持, 大家也给了很多的意见和建议. 比如在TIPI中出现的一些笔误以及一些小bug,在这次都修改了. 更新的主要内容有:

增加了PDF版的下载,终于TIPI有了第一个可移动的版本。
第三章增加了HashTable的设计及实现.
重构了第三章第六小节,增加了变量的赋值和销毁
修复了热心读者反馈的一些笔误.
对前几章进行了大量的优化和内容丰富.
对阅读工具栏工具栏进行了优化,在滚动页面时工具栏将固定在页面最上方,方便读者在页面内进行跳转.
点击这里进行下载。

TIPI团队

复制SSH会话,避免多次密码输入

就当时记事吧. 这不是怎样配置ssh密钥避免密码输入的实例, 当然经常ssh登陆的你肯定也配置过了.

我们公司的内网环境比较特殊, 为了安全性做了各种认证, 联入网络需要准入一下, 准入需要使用密码+随即密码的方式认证, 是挺安全的, 可以对于我们来说其实很痛苦,每次都要输入一下密码,因为包含了随即密码,我们无法使用脚本来方便的准入. 我们联入公司的开发机需要通过一台特殊的服务器来将我们的登陆转发, 也就是登陆到中转机,然后通过中转机在ssh.同样登陆中转机也是需要这个随机密码的. 吐槽完毕.

工作中经常需要在多台服务之间ssh登陆, screen 是一个不错的选择, 不过有时候还是需要打开另一个窗口再次登陆, 这时我又得再次输入那个随机密码, 如果你使用windows并且使用SecureCRT那你可以不用继续往下看了,SecureCRT可以简单的复制回话, 这个功能很贴心. 如果使用Linux&Mac OS那就继续往下看.

在/etc/ssh_config 文件中加入

Host *
ControlMaster auto
ControlPath ~/.ssh/master-%r@%h:%p

下次登陆同一站点的时候就会自动复用已有的回话. 可以只输入一次密码开N个窗口了. 这个和密钥要解决的不是同一个问题.

TIPI项目正式发布&恭喜er童鞋大婚

首先恭喜一下er童鞋的大婚吧.

接着er大婚的喜气,项目决定在今天发布我们的TIPI项目,项目从成立到现在,一直有条不紊的前进. 现在项目也进行的差不多,可以公开给大家了. 项目的第一个tag,其实是在20分钟前才提交完.终于能如期发布.
下面是我们的发布公告.  欢迎围观:)

————————-

我们的朋友,TIPI团队成员,博客哥,erhttp://www.zhangabc.com同学在今天这个春光灿烂,春暖花开,春心荡漾,春情澎湃的大好日子里,兴高采烈的走入了婚姻的殿堂。 在这样一个让人激动不已,激情四射的日子,TIPI团队决定发布深入理解PHP内核项目的第一阶段成果。

大概在半年前,我们在网上相聚,莫名的邂逅,有了我们这样的一个团队。我们有激情,有想法,有行动,也有了我们这个项目。 开始的艰难,没有时间的痛苦,坚持,从而有了今天的发布。一路走来,有辛苦,也有收获,至少记录了我们的青春,至少做了我们想做的!

深入理解PHP内核(TIPI)项目是一个开源的,分析PHP内核的系列文章项目。整个项目是基于PHP5.3版本的源码。 它包括PHP语言中我们常用的变量,函数,类,对象等的实现原理,也包括PHP的虚拟机,内存管理机制,线程安全,错误异常,文件流和PHP5.3新增加的垃圾收集机制,命名空间等。 除了PHP语言本身的特性外,还包括PHP扩展的相关信息。我们希望这个项目可以帮助更多的PHPer可以更加了解PHP语言本身,知其然知其所以然!

第一阶段,我们发布了前四章,从环境的搭建,源码的阅读方式到对于PHP源码的整体把握,再到对于变量和函数的详细解说。随着项目的进展,我们本身对于PHP内核的理解也加深了许多。 后续我们将以章为单位发布后续的章节。现在第5章正在撰写…

在线阅读入口>>>

TIPI团队序

博客哥三者,今聚首于网络一偶,共谋TIPI大计,与诸君共享技术之事: 向来穷PHP内核之事者或多,却鲜有分享之举。哥三者,常流连于中外博客也,若得一佳作,即欣喜若狂,本乐分享,及有学习总结之心,欲为PHP内核之事穷全身之力。

  • reeze,博客哥者,好苹果,好开源, 陶醉于Web开发及架构, 为Ruby之美所折服, 甚爱iOS及其开发, 好一切善美之事物.
  • er,博客哥者,稀饭Linux, Web, 2.0, Ajax, C, PHP, Javascript, CSS等。乃一以代码为乐之码农也。
  • phppan,博客哥者,好书,好PHP,亲于PHP,C,Ajax,程序架构等

是以三人之力行分享之事,转GIT,习markdown,论项目之计于深夜,何怕事之不成?务使PHP内核之事向众人知。 为此特示。

项目大事记

  • 2010/12/28 14:47 pan向reeze提议写一个PHP内核系列文章,一拍即合.
  • 2010/12/28 15:10 er同学加入.组织正式形成.
  • 2010/12/30 11:11 pan发出<<深入理解PHP内核>>第一份完整目录草稿.
  • 2010/12/31 21:14 举行第一次三方会谈,结合pan和reeze的目录草稿确定了正式目录. 标志着TIPI团队项目的正式确立.. (鼓掌).
  • 2011/01/01 05:08 reeze向github版本库提交了完整的项目, TIPI项目开始进入实施阶段
  • 2011/01/06 15:22 经过哥三激烈的讨论后做出艰难的决定,我们的项目域名正式确定为php-internal.com.(撒花无数).
  • 2011/02/14 23:32 在这个几人欢喜几人愁,充满花香的日子里, 哥三在深夜确定了TIPI项目的第一次整体发布流程,并且定稿了前三章的大纲以及确定了发布前的调整工作。
  • 2011/02/25 02:53 虽然我们还没有正式开始推广TIPI, 但已经有人开始关注TIPI了. 恭喜icodeuhttp://blog.icodeu.com同学成为我们第一位留下脚印的同学(看留言时间,也是个夜猫子啊.)
  • 2011/03/10 11:22 经过TIPI团队的慎重考虑, TIPI团队新增一员大将:honestqiao同学, 欢迎他的加入!
  • 2011/03/20 20:00 今天是TIPI团队成员er同学的大婚之日,团队决定在这个喜庆的日子将我们第一阶段的成果对外发布。让我们恭喜这对新人和我们的TIPI团队。

特别鸣谢

我们需要感谢我们家里的领导,没有有她们的支持,也就没有我们今天的发布,感谢她们的包容,感谢她们的照顾,感谢她们的理解和支持。谢谢!

[PHP-Internal]定制PHP语法获取PHP变量的变量名

上一篇”获取PHP变量名扩展的“文章中通过一个PHP扩展的方式,实现了获取变量的变量名实现. 这次将通过为PHP语言增加语法结构的方式来实现这样一个功能。
PHP的语法实现是通过lex以及yacc 实现的。 lex负责词法分析,yacc负责语法分析。
语法实现的文件有两个:

$PHP_SRC/Zend/zend_language_scanner.l 词法定义 一般的语法结构错误在这里检查。比如
$PHP_SRC/Zend/zend_language_parser.y 语法定义 而类似 函数定义缺少function关键字的错误。在这个环节报出来。运行时的错误(函数不存在,类找不到之类的)或异常在opcode执行期间处理

为了给PHP增加语法结构就得从语法分析开始。 如果没有接触过lex&yacc,可以先去看看后面提到的lex/yacc学习链接。

我们要实现的语法结构和print有点像, 这两个结构都不是PHP函数调用,都有返回值.那我们先看看print怎么实现的吧(为什么是print而不是echo, 因为我们的需求是需要返回变量的变量名,需要有返回值, print有返回值, 而echo没有,不是NULL, 如果你尝试给将将echo的返回值赋给一个变了会出现语法错误,注意是语法错误.). 首先我们看看print结构是怎么实现的吧:

$str = ' var_dump(token_get_all($str));

PHP被切分为一个一个的token 比如 print 就被分析为一个个的token

array(6) {
[0]=>
array(3) {
[0]=>
int(368) // PHP脚本开始标记 T_OPEN_TAG
[1]=>
string(6) " [2]=>
int(1)
}
[1]=>
array(3) {
[0]=>
int(266) // 对应于 token的 Zend/zend_language_parser.h T_PRINT
[1]=>
string(5) "print"
[2]=>
int(1)
}
[2]=>
string(1) "("
[3]=>
array(3) {
[0]=>
int(315) // T_CONSTANT_ENCAPSED_STRING 常量字符串
[1]=>
string(8) ""result""
[2]=>
int(1)
}
[4]=>
string(1) ")"
[5]=>
string(1) ";"
}

token化了以后就交由 Zend/zend_language_parse.y处理了,根据y文件里的规则进行opcode编译. 将这些token编译为opcode. 编译完了以后再执行.
如果你熟悉编译原理的话,这些东西看起来就没有什么问题了,如果还不熟悉,可以看看 Yacc 与Lex 快速入门 或者下载《Lex与Yacc》中文第二版(带源码) 花不了你多少时间就可以看懂Zend/language_scanner.l以及 language_parser.y文件了.


"exit" {
return T_EXIT;
}

"die" {
return T_EXIT;
}

"function" {
return T_FUNCTION;
}

"const" {
return T_CONST;
}

"return" {
return T_RETURN;
}

"try" {
return T_TRY;
}

"catch" {
return T_CATCH;
}

"throw" {
return T_THROW;
}

... 下面是词法分析中变量的词法分析代码.


/* Make sure a label character follows "->", otherwise there is no property
* and "->" will be taken literally
*/
"$"{LABEL}"->"[a-zA-Z_x7f-xff] {
yyless(yyleng - 3);
yy_push_state(ST_LOOKING_FOR_PROPERTY TSRMLS_CC);
zend_copy_value(zendlval, (yytext+1), (yyleng-1));
zendlval->type = IS_STRING;
return T_VARIABLE;
}

/* A [ always designates a variable offset, regardless of what follows
*/
"$"{LABEL}"[" {
yyless(yyleng - 1);
yy_push_state(ST_VAR_OFFSET TSRMLS_CC);
zend_copy_value(zendlval, (yytext+1), (yyleng-1));
zendlval->type = IS_STRING;
return T_VARIABLE;
}

"$"{LABEL} {
zend_copy_value(zendlval, (yytext+1), (yyleng-1));
zendlval->type = IS_STRING;
return T_VARIABLE;
}

这里把这见关键字token化为特定的token,变量的词法分析有一些不一样. 变量在token化的时候会把变量的名字(yytext)保存起来.这对于我们后续将变量的名字返回出来比较有用.

在Zend/zend_language_parser.y中

104 %token T_CASE
105 %token T_DEFAULT
106 %token T_BREAK
107 %token T_CONTINUE
108 %token T_GOTO
109 %token T_FUNCTION
110 %token T_CONST
111 %token T_RETURN
...
220 unticked_statement:
....

238 | T_CONTINUE ';' { zend_do_brk_cont(ZEND_CONT, NULL TSRMLS_CC); }
239 | T_CONTINUE expr ';' { zend_do_brk_cont(ZEND_CONT, &$2 TSRMLS_CC); }
240 | T_RETURN ';' { zend_do_return(NULL, 0 TSRMLS_CC); }
241 | T_RETURN expr_without_variable ';' { zend_do_return(&$2, 0 TSRMLS_CC); }
242 | T_RETURN variable ';' { zend_do_return(&$2, 1 TSRMLS_CC); }
...
651 | T_PRINT expr { zend_do_print(&$$, &$2 TSRMLS_CC); }

最后一行的规则匹配一个T_PRINT标记,然后是一个表达式的标记. 匹配到后则执行zend_do_print方法.其中的参数$$表示返回值, $2表示第二个标记也就是表达式expr


回到我们的需求,我希望给php增加一个语法结构.叫做var_name我希望能这样使用:


$a_named_variable = "http://reeze.cn/tags/php";
echo var_name($a_named_variable); // "a_named_variable";
?>

按照上面的这写语法编译的方式. 那么我们需要增加一个token用来标示var_name 现在我就叫他T_VARIABLE_NAME吧. 后面的变量在php中已经有了响应的处理方式,比如那个变量的token T_VARIABLE, 那么我需要在Zend/zend_language_scanner.l中处理这个token, 我在里面增加了如下内容, 这样就可以匹配脚本中的var_name了.


"var_name" {
return T_VARIABLE_NAME;
}

匹配好了以后需要在Zend/zend_language_parser.y

internal_functions_in_yacc:
T_ISSET '(' isset_variables ')' { $$ = $3; }
| T_EMPTY '(' variable ')' { zend_do_isset_or_isempty(ZEND_ISEMPTY, &$$, &$3 TSRMLS_CC); }
| T_INCLUDE expr { zend_do_include_or_eval(ZEND_INCLUDE, &$$, &$2 TSRMLS_CC); }
| T_INCLUDE_ONCE expr { zend_do_include_or_eval(ZEND_INCLUDE_ONCE, &$$, &$2 TSRMLS_CC); }
| T_EVAL '(' expr ')' { zend_do_include_or_eval(ZEND_EVAL, &$$, &$3 TSRMLS_CC); }
| T_REQUIRE expr { zend_do_include_or_eval(ZEND_REQUIRE, &$$, &$2 TSRMLS_CC); }
| T_REQUIRE_ONCE expr { zend_do_include_or_eval(ZEND_REQUIRE_ONCE, &$$, &$2 TSRMLS_CC); }
| T_VARIABLE_NAME '(' T_VARIABLE ')' { zend_do_variable_name(&$$, &$3 TSRMLS_CC); }
| T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); }
;

增加了两行 最后需要在Zend/zend_compile.c中增加匹配到这个规则所做的动作. 最后两个规则表示可以通过var_name($variable); 或者var_name $variable的方式使用. 类似于echo.
好了.下面这个方法就是用来编译opcode的方法了.

// 这里就是编译opcode
void zend_do_variable_name(znode *result, znode *variable TSRMLS_DC) /* {{{ */
{
// 生成一条zend_op
zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

// 因为我们需要有返回值, 并且返回值只作为中间值.所以就是一个临时变量
opline->result.op_type = IS_TMP_VAR;
opline->result.u.var = get_temporary_variable(CG(active_op_array));

// 很多人说echo和strlen这类函数的区别就像C中的宏和函数的区别一样
// 其实在PHP中并不是如此,这里的区别只是opcode的值不一样,如果是函数的话
// opcode将会是ZEND_DO_FCALL.
// 真是因为需要执行,下面的ZEND_VARIABLE_NAME是我增加的一个opcode类型.见Zend/zend_vm_opcodes.h
opline->opcode = ZEND_VARIABLE_NAME;
// 我们把var_name($var)中作为操作数传递进来
// 这里的variable是哪里来的呢? 见我在Zend/zend_language_parser.y中增加的
// T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); } 对应于 T_VARIABLE
opline->op1 = *variable;

// 我们只需要一个操作数就好了
SET_UNUSED(opline->op2);

*result = opline->result;
}

在Zend/zend_vm_opcodes.h中我增加了:

157 /* Added by reeze */
158 #define ZEND_VARIABLE_NAME 154

至于为什么是154就没什么好说的了. 只是依照顺序opcode 的顺序依次增加.

好了.到现在opcode已经编译好了. 大家都知道opcode是一条一条执行的.那么现在我们就需要进入执行阶段了.我们看看opcode是怎么执行的吧
入口在Zend/zend_vm_execute.h
zend_op的结构为:


struct _zend_op {
opcode_handler_t handler;
znode result;
znode op1;
znode op2;
ulong extended_value;
uint lineno;
zend_uchar opcode;
};

其中的handler就是在opcode在执行的时候需要执行的处理函数.

static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)
{
static const int zend_vm_decode[] = {
_UNUSED_CODE, /* 0 */
_CONST_CODE, /* 1 = IS_CONST */
_TMP_CODE, /* 2 = IS_TMP_VAR */
_UNUSED_CODE, /* 3 */
_VAR_CODE, /* 4 = IS_VAR */
_UNUSED_CODE, /* 5 */
_UNUSED_CODE, /* 6 */
_UNUSED_CODE, /* 7 */
_UNUSED_CODE, /* 8 = IS_UNUSED */
_UNUSED_CODE, /* 9 */
_UNUSED_CODE, /* 10 */
_UNUSED_CODE, /* 11 */
_UNUSED_CODE, /* 12 */
_UNUSED_CODE, /* 13 */
_UNUSED_CODE, /* 14 */
_UNUSED_CODE, /* 15 */
_CV_CODE /* 16 = IS_CV */
};
// 映射关系.
return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type]];
}
// 为 opcode设定处理函数.
ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
{
op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
}

// 为此增加执行的处理函数
static int ZEND_FASTCALL ZEND_VARIABLE_NAME_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);

// PHP中所有的变量在内部都是存储在zval结构中的.
zval *result = &EX_T(opline->result.u.var).tmp_var;

// 把变量的名字赋给临时返回值
Z_STRVAL(*result) = estrndup(opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len);
Z_STRLEN(*result) = opline->op1.u.constant.value.str.len;
Z_TYPE(EX_T(opline->result.u.var).tmp_var) = IS_STRING;

ZEND_VM_NEXT_OPCODE();
}

为了能让opcode映射到这个处理函数.需要在void zend_init_opcodes_handlers(void)函数中根据映射关系添加函数.
根据static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)中的映射关系.在末尾增加了ZEND_VARIABLE_NAME_HANDLER函数指针.

现在好了.把Zend/zend_language*.c文件删除.然后重新make一下. 执行一下重新编译好的php ($PHP_SRC/sapi/cli/php). 因为这修改了php本身所以必须重新编译php.

PS: 重新编译需要lex&yacc或者类似的变体(flex&bison). 还需要安装re2c. 否则也是无法正常编译的. 下面是测试的php脚本.


echo var_name($name); // "name"
echo var_name($variable); // "variable"
echo var_name($afdsafs=10); // Syntax Error: 这次的实现和php扩展的方式不一样. 如果像这样的调用会出现语法错误.当然这个也是可以解决的.不过这就不是这次的重点了
?>



所有的修改见文章最后的diff,我是基于PHP5.3.3修改的, 所以你的PHP代码版本最好是5.3分支的. 你可以从官方下载, 也可以从github的镜像下载. 如果你只想试试看是什么效果. 可以下载这个文件. 至于怎么给代码打补丁.网上搜一下patch命令怎么用把.也可以直接man patch:).

当然我并不推荐自己在生产环境修改一个自己的php分支. 如果真的觉得你对语法的修改对大家都有用.可以写一个RFC给php相关的邮件组.或者你觉得其他语言有非常有用的特性能为php所用,推荐订阅PHP的邮件列表http://www.php.net/mailing-lists.php 的Internal list http://marc.info/?l=php-internals,在这里能看到PHP语言的演变和他们对PHP的一些讨论以及最新的PHP的会具有的特性.

还是直接附上diff吧:

diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index ddae339..707192c 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -966,6 +966,34 @@ void zend_check_writable_variable(const znode *variable) /* {{{ */
}
/* }}} */

+
+// 这里就是编译opcode
+void zend_do_variable_name(znode *result, znode *variable TSRMLS_DC) /* {{{ */
+{
+ // 生成一条zend_op
+ zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
+
+ // 因为我们需要有返回值, 并且返回值只作为中间值.所以就是一个临时变量
+ opline->result.op_type = IS_TMP_VAR;
+ opline->result.u.var = get_temporary_variable(CG(active_op_array));
+
+ // 很多人说echo和strlen这类函数的区别就像C中的宏和函数的区别一样
+ // 其实在PHP中并不是如此,这里的区别只是opcode的值不一样,如果是函数的话
+ // opcode将会是ZEND_DO_FCALL.
+ // 真是因为需要执行,下面的ZEND_VARIABLE_NAME是我增加的一个opcode类型.见Zend/zend_vm_opcodes.h
+ opline->opcode = ZEND_VARIABLE_NAME;
+ // 我们把var_name($var)中作为操作数传递进来
+ // 这里的variable是哪里来的呢? 见我在Zend/zend_language_parser.y中增加的
+ // T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); } 对应于 T_VARIABLE
+ opline->op1 = *variable;
+
+ // 我们只需要一个操作数就好了
+ SET_UNUSED(opline->op2);
+
+ *result = opline->result;
+}
+/* }}} */
+
void zend_do_begin_variable_parse(TSRMLS_D) /* {{{ */
{
zend_llist fetch_list;
diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y
index 682b594..239c742 100644
--- a/Zend/zend_language_parser.y
+++ b/Zend/zend_language_parser.y
@@ -149,6 +149,8 @@
%token T_DIR
%token T_NS_SEPARATOR

+%token T_VARIABLE_NAME
+
%% /* Rules */

start:
@@ -987,6 +989,8 @@ internal_functions_in_yacc:
| T_EVAL '(' expr ')' { zend_do_include_or_eval(ZEND_EVAL, &$$, &$3 TSRMLS_CC); }
| T_REQUIRE expr { zend_do_include_or_eval(ZEND_REQUIRE, &$$, &$2 TSRMLS_CC); }
| T_REQUIRE_ONCE expr { zend_do_include_or_eval(ZEND_REQUIRE_ONCE, &$$, &$2 TSRMLS_CC); }
+ | T_VARIABLE_NAME '(' T_VARIABLE ')' { zend_do_variable_name(&$$, &$3 TSRMLS_CC); }
+ | T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); }
;

isset_variables:
diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l
index a4faff3..157101c 100644
--- a/Zend/zend_language_scanner.l
+++ b/Zend/zend_language_scanner.l
@@ -1110,6 +1110,10 @@ NEWLINE ("r"|"n"|"rn")
return T_ISSET;
}

+"var_name" {
+ return T_VARIABLE_NAME;
+}
+
"empty" {
return T_EMPTY;
}
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 45f5c62..bffa443 100644
--- a/Zend/zend_vm_execute.h
+++ b/Zend/zend_vm_execute.h
@@ -3065,6 +3065,21 @@ static int ZEND_FASTCALL ZEND_CONCAT_SPEC_CONST_TMP_HANDLER(ZEND_OPCODE_HANDLER
ZEND_VM_NEXT_OPCODE();
}

+static int ZEND_FASTCALL ZEND_VARIABLE_NAME_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
+{
+ zend_op *opline = EX(opline);
+
+ // PHP中所有的变量在内部都是存储在zval结构中的.
+ zval *result = &EX_T(opline->result.u.var).tmp_var;
+
+ // 把变量的名字赋给临时返回值
+ Z_STRVAL(*result) = estrndup(opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len);
+ Z_STRLEN(*result) = opline->op1.u.constant.value.str.len;
+ Z_TYPE(EX_T(opline->result.u.var).tmp_var) = IS_STRING;
+
+ ZEND_VM_NEXT_OPCODE();
+}
+
static int ZEND_FASTCALL ZEND_IS_IDENTICAL_SPEC_CONST_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
@@ -33867,7 +33882,33 @@ void zend_init_opcodes_handlers(void)
ZEND_NULL_HANDLER,
ZEND_NULL_HANDLER,
ZEND_NULL_HANDLER,
- ZEND_NULL_HANDLER
+ ZEND_NULL_HANDLER,
+ /* Added by reeze */
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER
};
zend_opcode_handlers = (opcode_handler_t*)labels;
}
diff --git a/Zend/zend_vm_opcodes.h b/Zend/zend_vm_opcodes.h
index d048a85..14276a5 100644
--- a/Zend/zend_vm_opcodes.h
+++ b/Zend/zend_vm_opcodes.h
@@ -153,3 +153,6 @@
#define ZEND_USER_OPCODE 150
#define ZEND_JMP_SET 152
#define ZEND_DECLARE_LAMBDA_FUNCTION 153
+
+/* Added by reeze */
+#define ZEND_VARIABLE_NAME 154

[Plugin]豆瓣秀 For WordPress

我喜欢在博客中显示我在豆瓣上的一些信息,比如想看哪些书哪些电影神马的。刚开始用的是 Robin的 http://www.robb.com.cn/plugins/ 的 WP-DoubanShow插件,他用的是豆瓣API。这个插件需要手动修改主题模板文件。刚开始用的挺好,修改就修改吧。不过后来换过几次主题后发现每次修改模板文件还挺“脏”的。遂问robin能否修改成widget的方式,他说官方推出了一个豆瓣秀功能http://www.douban.com/service/badgemaker, 所以不继续维护了。看过官方的说明。发现要在Wordpress中用也只能手动修改模板,不过官方提供了一个生成js的设置项,可以根据需要生成相应的脚本,选项也还算简单。 在网上搜了一番,没有给wordpress用的插件,所以自己写了一个,设置项和官方的一样。下面简单的说明一下:

  1. 第一步:下载插件文件: DoubanShow.zip, 或者在管理界面中添加插件, 搜索douban即可看到”豆瓣秀For WordPress” 选择安装, 如果这样的话,下面的上传步骤就不需要了
  2. 第二步:上传安装。 后台管理的  插件 -> 添加插件 -> 上传中上传下载的文件。

  3. 上传完后记得“启用插件”

  4. 第三步:在后台管理的 外观 -> 小工具 中选择”豆瓣秀“ 拖到右侧你想放置的位置。 然后点击拖过去的豆瓣秀箭头。出现如下设置:

    标题默认为空,就是不显示标题。也可以设置成你想要的标题。然后要设置好你的豆瓣ID,记住不是豆瓣的登录用户名。 设置好以后。去你的页面看看效果吧,也可以看我博客页面右下角。

PHP Puzzle(一): 有趣的变量作用域-PHP中global和Javascript中的var关键字

昨天在网上看到几道有意思的PHP题, 下面这道题让我想起了对应的Javascript版本.

function multiply($b) {
$a = 100;
global $a;
return $a * $b;
}
echo multiply(100);

这段代码运行结果是什么呢? 别急着执行这段代码,先想想你的结果.然后再对比一下吧.

我们看先看看global的定义 http://www.php.net/manual/en/language.variables.scope.php 这里也没有太为规范的解释.只是说可以通过global关键字来访问全局变量. 这里还涉及到一个类型转换的问题.

大家都知道PHP脚本是编译为opcode逐语句执行的. 那么现在要一句语句解释就很容易了.

function multiply($b) {
$a = 100; // 定义局部变量$a
global $a; // 访问全局变量$a, $a变量现在的是全局变量了
return $a * $b; // 返回$a和$b的乘积
}
echo multiply(100);

这里可能比较困惑的的是现在变量$a到底是局部变量还是全局变量了.因为global在定义局部变量之后.所以$a变为了全局变量,而在最后输出结果的时候$a并没有值.所以最后在相乘的时候是 NULL * 100; 也就是0了;可能会有人有疑问, 后面只是把$a变为了全局变量, 他的值应该不变的啊. 让我通过下面的例子来看把:

function func($d) {
$a = 100;
global $a;

var_dump(get_defined_vars()); // get_defined_vars()返回当前作用域的所有变量信息
}

func();

array(2) {
["d"]=>
int(0)
["a"]=>
&NULL
}

变量a是NULL的一个引用,因为全局作用域内没有a这个变量. 所以即使在函数前面定义了一个a变量,但是它的值已经指向了全局作用域了.
实际上 global关键字首先从全局符号表中查找变量名叫做a的变量,并把这个变量值设置为当前作用域的符号表中的a变量(更新了当前变量的值). 如果全局作用域内没有这个变量则会在全局作用域内增加这个变量, 实现代码见: $PHP_SRC/Zend/zend_vm_execute.h

static int ZEND_FASTCALL zend_fetch_var_address_helper_SPEC_CONST(int type, ZEND_OPCODE_HANDLER_ARGS) {

// ...
if (zend_hash_find(target_symbol_table, varname->value.str.val, varname->value.str.len+1, (void **) &retval) == FAILURE) {
switch (type) {
case BP_VAR_R:
case BP_VAR_UNSET:
zend_error(E_NOTICE,"Undefined variable: %s", Z_STRVAL_P(varname));
/* break missing intentionally */
case BP_VAR_IS:
retval = &EG(uninitialized_zval_ptr);
break;
case BP_VAR_RW:
zend_error(E_NOTICE,"Undefined variable: %s", Z_STRVAL_P(varname));
/* break missing intentionally */
case BP_VAR_W: {
zval *new_zval = &EG(uninitialized_zval);

Z_ADDREF_P(new_zval);
zend_hash_update(target_symbol_table, varname->value.str.val, varname->value.str.len+1, &new_zval, sizeof(zval *), (void **) &retval);
}
break;
EMPTY_SWITCH_DEFAULT_CASE()
}
}

//...
}

看了这个解释大家可能觉得理所当然.一句一句执行的嘛. 看完了PHP中全局作用域的例子,咱们再看看类似的Javascript中的局部变量的版本吧

var a = 1;
function multiply(b)
{
a = 100;
var a;

return a * b;
}
alert(a);
alert(multiply(100));

那这段代码的输出将会是多少呢?
如果还是同样的思路,结果可能是你的期望完全不一样的结果. 这里的var定义变量和php中global不是一样的东西, php中的global是会在运行时执行的.而Javascript中的var在运行之前就已经"处理"好了.在运行之前的"语法分析"(没有看过Javascript引擎的实现.姑且这么分把)过程中,multiply函数中出现了var a;则把变量a加到函数体内的"局部变量表"中了.在运行过程中并不会执行var a;这一句. 这也是Javascript"怪异"的地方.定义变量的位置并没有关系.所以在函数内定义局部变量最好放在函数体的前面.

所以第一个alert输出的1, 函数的执行并没有改版全局范围内的a变量; 第二就没有什么问题了, 是10000;

怎样获取PHP变量的变量名之PHP实现

上一篇文章里提到是用PHP扩展实现获取变量的变量名的方法. 今天发现有一个PHP实现的版本 . 实现方法来自:http://mach13.com/how-to-get-a-variable-name-as-a-string-in-php

刚开始以为这个方法好使, 仔细想想其实也是有问题的.

这个解决方法是用的PHP里的get_defined_vars()方法,该方法返回当前作用域内的所有变量信息.也是和$GLOBALS一样,以变量名 => 值的方式返回.
他的代码很简单:


function var_name (&$iVar, &$aDefinedVars)
{
foreach ($aDefinedVars as $k=>$v)
$aDefinedVars_0[$k] = $v;

$iVarSave = $iVar;
$iVar =!$iVar; // 将当前变量的值取反

$aDiffKeys = array_keys (array_diff_assoc ($aDefinedVars_0, $aDefinedVars)); // 对比取反前后的变量
$iVar = $iVarSave; // 恢复当前变量的值

return $aDiffKeys[0];
}

?>

它通过引用的方式改变当前变量的值, 然后通过对比前后两个数组的差异来获取值被改变了的变量.然后返回其名字.经过测试这的确是一个方法.相对我实现的方法. 它提供的方法移植性较好, 不需要赖以扩展. 而这个php版本的实现, 必须传递一个get_defined_vars()的参数, 我实现的那个扩展,则不需要. 对于类似 var_name($a=10,get_defined_vars()); 的调用,该方法无法正常获得变量名.

这个今天又仔细想了想,下面提供的方法是有问题的.. 他解决问题的方法是通过修改变量的值, 并对比前后所有的变量来找出值发生变化的变量. 而实际上.修改了其中一个变量另一个变量的值也会发生变化: 这就是引用, 如下

$a = 10;
$b = &$a;

echo var_name($b, get_defined_vars()); // 这回返回a, 而不是b. 和预期的并不一样.

PS: 如果你真的需要这种方法. 请重新思考一下你的需要真的需要这样的方法么?

[PHP-Internal]怎么样获取PHP变量的变量名之扩展实现

很长时间没有更新博客了. 一来最近工作比较忙,没有时间好好研究问题, 二是觉得没有很好的材料可以写. 也有一些没有彻底研究透的问题,写着写着没有了头绪,都扔在了草稿箱里了. 这次顺带也要更新一下博客的模版了, 现在的这个模版主体有点窄,不适合阅读. 我这个博客现在,以后主要还是写一些技术的东西.还是换一个眼睛友好的主题吧.

本文要解决的是从去年就一直在考虑的一个PHP的问题: 怎么样获取PHP变量的变量名. 一直以来都没有好好的研究.最近断断续续的开始看PHP源代码.并尝试解决. 直到两星期前把问题都解决了才开始把这些东西都记下来.

如果有兴趣先看看这个功能是怎么实现的. 可以先点击这里下载代码.

1.问题:能在PHP中获取php变量本身的名字么?

一年多前做一个模版引擎的什么时候有了这样一个需求: 获取变量的变量名. 比如:

$some_variable_name = "blahblah";
//...
echo get_var_name($some_variable_name); // 这里期望输出"some_variable_name";
?>

如果你也有这样的需求. 你对需求的理解绝对有问题.  不过后来想想这需求虽然不合理. 但是如果我偏有这样不合理的需求, 我有办法真的能满足么?

2.有哪些解决方法

在遇到这个问题之前,没有太系统的去看过PHP的C实现. 从问题提出到目前为止,我想到了如下几种方法:

  • 直接写一个PHP函数来获取.比如:

    function get_var_name($var) {
    // 但是... 我怎么的到变量的名字呢...
    // echo ? How To?
    }

    用过$GLOBALS变量的人应该知道可以通过 $GLOBALS['var']的方式来获取变量$var的值. 这样的话,我应该就能这样实现了

    function get_var_name($var) {
    foreach($GLOBALS as $var_name => $var_value) {
    if($var === $var_value) {
    return $var_name;
    }
    }
    }

    这个是不可行的. 首先, 这个方法只能返回全局作用域内的变量. 如果在函数体内调用这个函数会有问题. 并且通过值比较也完全不可靠.
  • 随后我开始看PHP的内部实现.知道了在PHP执行过程中所有的变量都是存放在符号表(symbol_table)中, 和$GLOBALS变量类似, 以变量名 =>值的方式存储.. 并且在不同的作用域内有不同的active_symbol_table, 这样的话就不存在作用域的问题了, 那我们是不是可以从当前的符号表中来根据传递进来的变量值来进行比较呢. 在符号表的值是存放在一个指向zval结构的指针. 那我们是否可能通过比较指针地址的方式来查找保存该值的变量名呢? 其实这也是行不通的. 因为在PHP内部可能有多个变量指向同一个内部值.也就是引用计数. 看来通过符号表还是解决不了问题.
  • 通过对PHP内部实现的进一步学习发现在脚本运行的时候还是有很多其他丰富的内部信息可以利用.比如如下的脚本运行时全局变量.  这也是解决这个问题的突破口所在, 本文将根据这些运行时信息来编写一个实现该功能的扩展.


    struct _zend_executor_globals {
    zval **return_value_ptr_ptr;

    zval uninitialized_zval;
    zval *uninitialized_zval_ptr;

    zval error_zval;
    zval *error_zval_ptr;

    zend_ptr_stack arg_types_stack;

    /* symbol table cache */
    HashTable *symtable_cache[SYMTABLE_CACHE_SIZE];
    HashTable **symtable_cache_limit;
    HashTable **symtable_cache_ptr;

    zend_op **opline_ptr;

    HashTable *active_symbol_table; // 当前作用域的变量符号表
    HashTable symbol_table; /* main symbol table */ // 全局符号表

    HashTable included_files; /* files already included */

    //..
    };

  • - 最后肯定能实现的一种方式是为PHP增加一个类似echo的语法结构. 这种方式的侵入性最大, 在这篇日志中将不讨论这种实现方式, 我将在下一篇日志中介绍通过修改PHP语法的方式来支持开篇所提出的问题.
    • 3.扩展实现

      比如模块提供一个叫做get_var_name()的函数来获取变量名字.  如果大家有写过PHP扩展的经验的话,应该看过类似如下的函数实现(取自php json扩展$PHP_SRC/ext/json/json.c):

      /* {{{ proto string json_encode(mixed data [, int options])
      Returns the JSON representation of a value */
      static PHP_FUNCTION(json_encode)
      {
      zval *parameter;
      smart_str buf = {0};
      long options = 0;

      if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &parameter, &options) == FAILURE) { // 这里将传入的参数取出来. 参考文档 http://www.php.net/manual/en/internals2.funcs.php
      return;
      }

      php_json_encode(&buf, parameter, options TSRMLS_CC);

      ZVAL_STRINGL(return_value, buf.c, buf.len, 1);

      smart_str_free(&buf);
      }
      /* }}} */

      这中实现在函数体内可以通过zend_parse_parameter的方式来获取传递进来的变量, 但这样只能获取到变量的值. 却无法得到其他更多的信息.,我们往低层看看在PHP中函数是怎么调用,参数是怎么传递的.

      3.1 PHP中函数的调用

      在研究函数怎么调用之前, 我们需要看看PHP代码是怎么执行的.

      大致可以分为2个步骤:

      - 词法分析,语法分析然后编译成opcode

      - 执行opcode

      PHP函数的执行也只能在opcode执行阶段执行.

      这里之前要介绍一个查看OPCODE的绝佳工具 vld(http://pecl.php.net/package/vld)

      装好这扩展。可以在命令行下查看php脚本编译后的opcode

      我们看看下面这个php脚本被编译后opcode是什么样的.

      $str = "http://reeze.cn";
      $len = strlen($str);

      $len2 = strlen($str2=10);

      echo $len;
      echo $len2;
      ?>
      $ php -dvld.active=1 func_call.php


      也可以再增加一个参数  -dvld.verbosity=3, 这样将会显示更多的信息.

      它被编译为上面的10条opcode命令.

      op的名称一看也能看出什么意思. .. 其中以 “!”开头的数字表示编译后的变量,, 以”~”开头的变量表示零时变量.

      上面可可以看出如果函数调用存在参数的话,在DO_FCALL之前会执行SEND_VAR 或者  SEND_VAR_NO_REF指令。并且这些指令后面操作的是编译过变量或者一个临时变量.

      在PHP中调用时我们是可以访问到DO_FCALL这个操作的opcode信息 。可以通过 EG(active_opline_ptr) 获取到当前指令

      PHP中存在一系列*G宏, EG 则为在执行opcode时的全局变量。
      见文件: $PHP_SRC/Zend/zend_globals.h

      struct _zend_executor_globals {
      // ...
      zend_op **opline_ptr; // 指向当前正在执行的zend_op对象
      HashTable *active_symbol_table;
      HashTable symbol_table; /* main symbol table */
      HashTable included_files; /* files already included */
      jmp_buf *bailout;
      int error_reporting;
      int orig_error_reporting;
      int exit_status;
      zend_op_array *active_op_array;
      // ...
      };

      struct _zend_op_array {
      /* Common elements */
      zend_uchar type;
      char *function_name;
      zend_class_entry *scope;
      zend_uint fn_flags;
      union _zend_function *prototype;
      zend_uint num_args;
      zend_uint required_num_args;
      zend_arg_info *arg_info;
      zend_bool pass_rest_by_reference;
      unsigned char return_reference;
      /* END of common elements */

      zend_bool done_pass_two;

      zend_uint *refcount;

      zend_op *opcodes; // zend_op数组.
      zend_uint last, size;

      zend_compiled_variable *vars; // 所有编译后的变量信息Since PHP5.1 这是一个数组
      int last_var, size_var; // last_var 最后一个编译变量的索引

      // ...
      };


      当前执行的op_array中保存所有编译变量的信息, 再看看zend_compiled_variable的结构吧。

      typedef struct _zend_compiled_variable {
      char *name;
      int name_len;
      ulong hash_value;
      } zend_compiled_variable;

      这正是我想获取的变量名称.

      我们可以通过全局变量EG(opline_ptr)指针获取到当前执行的zend_op, zend_op的结构如下:

      struct _zend_op {
      opcode_handler_t handler; // 处理该OPCODE的处理函数
      znode result; // 该opcode执行的结果
      znode op1; // 有的opcode需要1个,有的需要两个操作数。
      znode op2;
      ulong extended_value;
      uint lineno;
      zend_uchar opcode; // 该opcode的值 见$PHP_SRC/Zend/zend_vm_opcodes.h
      };

      这也就是我们函数调用时执行的opcode.我们现在可以获取到DO_FCALL时的opcode,  通过VLD察看opcode工具很容易就知道函数调用之前,如果函数有参数的话,在DO_FCALL之前一定有SEND_VAR或者SEND_VAR_NO_REF指令, 指针后退一个则一定是指向SEND_VAR或SEND_VAR_NO_REF指令的。 这样的话我们根据DO_FCALL获取到的zend_op指令后退不久可以获取SEND_VAR指令了么. SEND_VAR指令会操作compiled_var,这样我们就能得到变量的信息了..

      看看znode都有哪些信息:

      typedef struct _znode {
      int op_type;
      union {
      zval constant;
      zend_uint var; // 这个var就是当前变量在zend_op_array.vars 中的compiled_variable数组中的索引.不过这个索要并不是字面上的. 详情请看最后的代码实现.
      zend_uint opline_num; /* Needs to be signed */
      zend_op_array *op_array;
      zend_op *jmp_addr;
      struct {
      zend_uint var; /* dummy */
      zend_uint type;
      } EA;
      } u;
      } znode;

      如在在上面的注释. 通过获取znode.u.var的值就可以获取到变量的信息了.

      这样的话.程序的实现也就简单了.

      下面是实现:

      /* {{{ get_var_name
      *
      * 这个扩展要求PHP >= 5.1
      * 因为依赖PHP 5.1引入的compiled variable
      *
      * 在PHP空间导出一个get_var_name函数.
      * echo get_var_name($var_name); // expect: var_name
      * echo get_var_name($lineno=100); // expect: lineno
      */
      PHP_FUNCTION(get_var_name)
      {
      int len;
      char *strg = "";

      if(ZEND_NUM_ARGS() < 1) {
      return;
      }

      /* 显示所有的编译变量
      int i;
      zend_compiled_variable *vars = EG(active_op_array)->vars;
      for(i=0; i < EG(active_op_array)->last_var; ++i) { // last_var 最后一个编译变量的索引
      spprintf(&strg, 0, "%snVar:%sn", strg, EG(active_op_array)->vars[i].name);
      ++vars;
      }
      */

      zend_op *pre_opline_ptr = *EG(opline_ptr);
      pre_opline_ptr--;

      // 支持这类的调用: get_var_name($a="VALUE"); // expect: a
      // 这里增加在赋值的情况下也能正确返回变量的名字的处理方法, 如果方法参数是赋值的的话, 编译的OPCODE 中SEND_VAR之前将会
      // 有一个ZEND_ASSIGN 操作, 并且ZEND_ASSIGN操作的返回值被使用.比如: $c = $d + 1; $d + 1的返回值就被使用了. 就可以确认
      // 是前面的调用方式
      zend_op *pre_pre_online_ptr = pre_opline_ptr - 1;
      if(pre_pre_online_ptr && pre_pre_online_ptr->opcode == ZEND_ASSIGN && !(pre_pre_online_ptr->result.u.EA.type & EXT_TYPE_UNUSED)) {
      // 通过赋值之前的zend_op来获取变量信息
      pre_opline_ptr = pre_pre_online_ptr;
      }

      int index;
      // 比如get_var_name($name); 这时SEND_VAR OPCODE的op1操作数类型就是IS_CV 也就是IS Compiled Variable
      // 只有compiled variable才是直接存储索引的. PHP >= 5.1
      if(pre_opline_ptr->op1.op_type == IS_CV) {
      index = pre_opline_ptr->op1.u.var;

      }
      else {
      // 请参考VLD的源代码 $VLD_SRC/srm_oparray.c LINE:320 vld_dump_znode函数
      index = pre_opline_ptr->op1.u.var / sizeof(temp_variable);
      }

      zend_compiled_variable var = EG(active_op_array)->vars[index];
      len = spprintf(&strg, 0, "%s", strg, var.name);

      RETURN_STRINGL(strg, len, 0);
      }
      /* }}} */

      点击这里下载代码

解决Mac下终端中使用screen管理回话但看不到滚动条等问题

工作中开发基本都是ssh到远程服务器上vim开发,有时候因为网络原因或者不小心把终端给关了或者网络出现问题以后就痛苦了,因为vim默认会用swp文件保存修改,我又不想关掉这个功能,一不小心真丢数据就划不来了,非法关闭vim之后再次打开就会提示是否恢复文件,自从发现screen这个绝佳的绘画管理工具,就深深的爱上了它,IBM的这篇文章有详细的介绍 “linux 技巧:使用screen 管理你的远程会话”。在使用过程中发现一些问题,默认快捷键是Ctrl + A, 用惯了命令行的人肯定会用Ctrl +A , Ctrl + E来定位当前输入的命令吧. 在~/.screenrc中修改一下就可以了,下面是我的.screenrc

vbell_msg "" # 比如在tab自动提示的时候,默认总是会出现一个Wuff,觉得很烦,关掉
escape ``  # instead of Control-a  使用 反引号当快捷键,因为用的相对较少
termcapinfo xterm* ti@:te@  # 这个就是解决mac下看不到滚动条的问题了

解决方法来源见:http://stackoverflow.com/questions/1039442/mac-os-x-terminal-apps-buffer-and-screen-command

让你的网站也像Gmail一样支持文件拖放上传-HTML5之File API

如果你比较好奇,可以先从这里下载所有代码,也可以点击这里查看chrome下上传的demo,点这里查看firefox下的demo

前不久Gmail推出了支持拖拽的附件上传功能,试用了下还真不错,其实很久以前就在想能有直接拖拽附件的功能,多亏有了HTML5,Web应用越来越像客户端的应用了。

在好奇心驱使下,想了解一下Gmail到底是怎么做到的,了解了一下最新的HTML5 File API草案,这个接口主要提供的就是提供对文件对象的访问,别想歪了,这个接口是无法随意的访问系统里的文件的。他能做的就是访问<input type=”file” />标签里所选择的文件,这些文件可以通过用户手动选择,或者是HTML5的拖放接口选中的文件。有兴趣的童鞋可以看看这个规范,还算比较简单。

下面简单看看接口定义几个对象。

FileList、File对象。

在HTML5中的<input type=”file” />标签中增加了mutilple属性,允许进行多文件选择。大家应该都知道一般上传标签中是不允许选择多个文件的。 新增的这个属性就是允许进行多个文件的选择(这个在桌面应用中也很常见)。<input type=”file” multiple=”multiple” id=”file” />

下面是在Firebug中的输出

>>> var f = document.getElementById("file")
>>> f.files
FileList0=File length=1 // 选中的文件数量
>>> f.files[0]
FilefileName=es.dll fileSize=271360

FileList对象就是用户选择的所有文件的对象表示,如果是通过input标签选择的,就可以通过上面代码所示的方法进行访问,File对象就可以刚才选择的某个文件的信息,如上面的代码所示,主要可以得到所选中的文件名以及文件大小信息。

你可能在想只能得到这些信息到底有什么用呢?都没有办法读取文件内容,这就得提到规范中的FileReader接口了,这个接口就是用来读取File对象文件的。

File API规范中提到File API主要是和其他的接口协同合作。比如XMLHttpRequest (这个新接口支持通过xhr的send()方法发送File对象), DataTransfer(也就是HTML5中的拖拽接口 ), 以及Web Worksers(这个主要是异步脚本执行,相当于给JS提供了“多线程”脚本执行能力,并且支持通过postMessage()进行“线程间通信”),感兴趣的,可以看看这篇日志,以及这篇

目前能实现这样的效果的方式主要有如下几种:

  • Gmail中提到的这两个浏览器都支持拖放接口,托放以后可以直接通过托放事件的DataTransfer属性访问到本次托放是关联的文件对象列表FileList,然后通过XMLHttpRequest的send方法将File对象发送到服务器
  • 在Chrome下支持直接将文件拖放到文件选择控件上,就相当于直接选择了文件。这时可以通过<input type=”file” />DOM对象的files属性访问到被托放进来的文件列表对象,然后也可以通过Ajax将文件对象发送到服务器,通过将文件选择控件透明度降低也可以实现Gmail类似的效果。 在Chrome因为可以直接通过托拽的方式让文件选择控件“赋值”,此时也可以通过一个iframe加表单的方式将数据发送到服务器。
  • 在Firefox3.6下可以通过FileReader直接读取到文件的内容,然后直接将文件内容发送到服务器端(可以参考这个例子,这是个不完整的例子,直接浏览是看不到效果的,查看源代码你就会懂的。)

下面就来看看Gmail到底是怎么做到的吧。

本来想通过Firebug的概况功能来捕捉到在托拽期间的脚本执行情况,比如:


但是脚本执行里压根没有找到ajax相关的函数调用,可能是因为firebug还不支持监控页面里嵌入的iframe中的脚本执行跟踪,这也说明本次上传肯定是在某个iframe中完成的。,那就直接监听网络吧,托拽上传一个附件时查看网络情况,发现附件是通过下面的ajax post过去的:

大家注意看,是通过ajax post方式将附件POST到服务器的,


可以看出Gmail在firefox下不是通过表单直接提交实现的。在chrome下的开发人员工具有点简单,无法看到网络情况,我也懒的再去抓包看了,估计是使用透明<input type=”file” />+ajax方式实现的。

在Gmail支持托拽的声明中提到目前只支持Chrome 2+以及FireFox3.6+。虽然这两个浏览器都支持HTML5,但是对于所有规范的支持程度都是不一样的,并且规范也还不是正式规范。在Firefox3.6的release note中提到:

Support for new DOM and HTML5 specifications including the Drag & Drop API and the File API, which allow for more interactive web pages.

开始支持了HTML5的拖拽接口以及File API。


下面根据浏览器以及HTML5的规范整理出两个浏览器下实现类似Gmail 上传附件的代码。

点击这里下载所有代码,有兴趣的童鞋查看源代码就知道怎么回事了,有一定的注释:)

也可以点击这里查看chrome下上传的demo,点这里查看firefox下的demo,之所以分开是为了简单起见,当然你真的想要给你的网站提供托拽上传功能,你就得自己去同时兼容这两个浏览器啦,相信这也不是件困难的事情:)