權利聲明
此譯本在不獲利的情況下, 可以無限制自由傳播.
除了幾個"預覽"的例外, 你迄今處理的擴展函數都很簡單, 只有返回. 然而, 多數函數並非只有一個目的. 你通常會傳遞一些參數, 並希望接收到基於值和其他附加處理的有用的響應.
zend_parse_parameters()的自動類型轉換
和上一章你看到的返回值一樣, 參數的值也是圍繞著對zval引用的間訪展開的. 取得這些zval*的值最簡單的方法就是使用zend_parse_parameters()函數.
呼叫zend_parse_parameters()幾乎總是以ZEND_NUM_ARGS()宏接著是無所不在的TSLSRM_CC . ZEND_NUM_ARGS()從名字上可以猜到, 它返回int型的實際傳遞的參數個數. 由於zend_parse_parameters()內部工作的方法, 你可能不需要直接了解這個值, 因此現在只需要傳遞它.
zend_parse_parameters()的下一個參數是格式字串參數, 它是由Zend引擎支援的基礎型別描述字元組成的字元序列, 用來描述要接受的函數參數. 下表是基礎的型別字元:
zend_parse_parameters()剩下的參數依賴於你的格式字串中所指定的型別描述. 對於簡單型別, 直接解引用為C語言的基礎型別. 例如, long資料型別如下解出:
PHP_FUNCTION(sample_getlong) { long foo; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &foo) == FAILURE) { RETURN_NULL(); } php_printf("The integer value of the parameter you " "passed is: %ld\n", foo); RETURN_TRUE; }
儘管integer與long的儲存空間通常是一樣大的, 但它們並不完全一致. 嘗試將一個int類型的資料解引用到long *參數可能會帶來不期望的結果, 尤其是在64位元平台上更加容易出錯. 因此, 請嚴格使用下表中列出的資料類型.
注意, 所有其他的複雜類型實際上都解析為簡單的zval. 這樣做的原因和不使用RETURN_*()巨集返回複雜資料類型一樣, 都是受限於無法真正的模擬C空間中的這些結構. zend_parse_parameters()能為你的函數所做的, 是確保你接收到的zval *是正確的類型. 如果需要, 它甚至會執行隱式的型別轉換, 例如將陣列轉換為stdClass的物件.
s和O型別需要單獨說明, 因為它們一次呼叫需要兩個參數. 在第10章"php4物件"和第11章"php5物件"中你將更進一部的了解O. 對於s這個類型, 我們對第5章"你的第一個擴展"的sample_hello_world()函數進行一次擴展, 讓它可以跟指定的人名打招呼.
function sample_hello_world($name) { echo "Hello $name!\n"; } /* 在C中, 你将使用zend_parse_parameters()函数接受一个字符串 */ PHP_FUNCTION(sample_hello_world) { char *name; int name_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE) { RETURN_NULL(); } php_printf("Hello "); PHPWRITE(name, name_len); php_printf("!\n"); }
zend_parse_parameters()函數可能由於函數傳遞的參數太少不能滿足格式字串, 或因為其中某個參數不能轉換為請求的類型而失敗. 這種情況下, 它將自動的輸出錯誤訊息, 因此你的擴展不需要這樣做.
要請求超過1個參數, 就需要擴充格式串, 包括其他字符, 並將zend_parse_parameters()調用的後面其他參數壓棧. 參數和它們在用戶空間函數定義的行為一致, 從左向右解析.
function sample_hello_world($name, $greeting) { echo "Hello $greeting $name!\n"; } sample_hello_world('John Smith', 'Mr.'); Or: PHP_FUNCTION(sample_hello_world) { char *name; int name_len; char *greeting; int greeting_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &name, &name_len, &greeting, &greeting_len) == FAILURE) { RETURN_NULL(); } php_printf("Hello "); PHPWRITE(greeting, greeting_len); php_printf(" "); PHPWRITE(name, name_len); php_printf("!\n"); }
除了基礎類型, 還有3個元字元用於修改參數處理方式. 如下表所示:
可選參數
我們再來看一看修訂版的sample_hello_world()範例, 下一步是增加一個可選的$greeting參數:
function sample_hello_world($name, $greeting='Mr./Ms.') { echo "Hello $greeting $name!\n"; }
sample_hello_world()現在可以只用$name參數調用, 也可以同時使用兩個參數調用.
sample_hello_world('Ginger Rogers','Ms.'); sample_hello_world('Fred Astaire');
當不傳遞第二個參數時,使用預設值. 在C語言實作中, 可選參數也是以類似的方式指定.
要完成這個功能, 我們需要使用zend_parse_parameters()格式串中的管道符(|). 管道符左側的參數從呼叫棧解析, 所有管道符右邊的參數如果在調用棧中沒有提供, 則不會被修改(zend_parse_parameters()格式串後面對應的參數). 例如:
PHP_FUNCTION(sample_hello_world) { char *name; int name_len; char *greeting = "Mr./Mrs."; int greeting_len = sizeof("Mr./Mrs.") - 1; /* 如果调用时没有传递第二个参数, 则greeting和greeting_len保持不变. */ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|s", &name, &name_len, &greeting, &greeting_len) == FAILURE) { RETURN_NULL(); } php_printf("Hello "); PHPWRITE(greeting, greeting_len); php_printf(" "); PHPWRITE(name, name_len); php_printf("!\n"); }
除非調用時提供了可選參數的值, 否則不會修改它的初始值, 因此, 為可選參數設定初始的預設值非常重要. 多數情況下, 它的初始預設值是NULL/0, 不過有時候, 例如上面的例子, 預設的是有意義的其他值.
IS_NULL Vs. NULL
每個zval, 即使是非常簡單的IS_NULL類型, 都會佔用一塊很小的內存空間. 從而, 它就需要一些(CPU)時鐘週期去分配內存空間, 初始化值, 最後認為不再需要它的時候釋放它.
對於很多函數, 在調用空間使用NULL參數只是標記參數不重要, 因此這個處理就沒有意義. 幸運的是zend_parse_parameters()允許參數被標記為"允許NULL", 如果在一個格式描述字元後加上感嘆號(!), 則表示對應參數如果傳遞了NULL, 則將zend_parse_parameters()呼叫時對應的參數設為真正的NULL指標. 考慮下面兩段程式碼, 一個有這個修飾符, 一個沒有:
PHP_FUNCTION(sample_arg_fullnull) { zval *val; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &val) == FAILURE) { RETURN_NULL(); } if (Z_TYPE_P(val) == IS_NULL) { val = php_sample_make_defaultval(TSRMLS_C); } ... PHP_FUNCTION(sample_arg_nullok) { zval *val; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z!", &val) == FAILURE) { RETURN_NULL(); } if (!val) { val = php_sample_make_defaultval(TSRMLS_C); } ...
这两个版本的代码其实没什么不同, 前者表面上看起来需要更多的处理时间. 通常, 这个特性并不是很有用, 但最好还是知道有这么回事.
强制隔离
当一个变量传递到一个函数中后, 无论是否是引用传值, 它的refcount至少是2; 一个是变量自身, 另外一个是传递给函数的拷贝. 因此在修改zval之前(如果直接在参数传递进来的zval上), 将它从它所属的非引用集合中分离出来非常重要.
如果没有"/"格式修饰符, 这将是一个单调重复的工作. 这个格式修饰符自动的隔离所有写时拷贝的引用传值参数, 这样, 你的函数中就可以为所欲为了. 和NULL标记一样, 这个修饰符放在它要修饰的格式描述字符后面. 同样和NULL标记一样, 直到你真的有使用它的地方, 你可能才知道你需要这个特性.
zend_get_arguments()
如果你正在设计的代码计划在非常旧的php版本上工作, 或者你有一个函数, 它只需要zval *, 就可以考虑使用zend_get_parameters()的API调用.
zend_get_parameters()调用与它对应的新版本有一点不同. 首先, 它不会自动执行类型转换; 而是所有期望的参数都是zval *数据类型. 下面是zend_get_parameters()的最简单用法:
PHP_FUNCTION(sample_onearg) { zval *firstarg; if (zend_get_parameters(ZEND_NUM_ARGS(), 1, &firstarg) == FAILURE) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Expected at least 1 parameter."); RETURN_NULL(); } /* Do something with firstarg... */ }
其次, 你可能已经注意到, 它需要手动处理错误消息, zend_get_parameters()并不会在失败的时候输出错误文本. 它对可选参数的处理也很落后. 如果你让它抓取4个参数, 最好至少给它提供4个参数, 否则可能返回FAILURE.
最后, 它不像parse, 这个get变种会自动的隔离所有写时复制的引用集合. 如果你想要跳过自动隔离, 就要使用它的兄弟接口: zend_get_parameters_ex().
除了不隔离写时复制集合, zend_get_parameters_ex()还有一个不同点是返回zval **指针而不是直接的zval *. 它们的差别同样可能直到你使用的时候才知道需要它们, 不过它们最终的使用非常相似:
PHP_FUNCTION(sample_onearg) { zval **firstarg; if (zend_get_parameters_ex(1, &firstarg) == FAILURE) { WRONG_PARAM_COUNT; } /* Do something with firstarg... */ }
要注意的是_ex版本不需要ZEND_NUM_ARGS()参数. 这是因为增加_ex的版本时已经比较晚了, 当时Zend引擎已经不需要这个参数了.
在这个例子中, 你还使用了WRONG_PARAM_COUNT宏, 它用来处理E_WARNING错误消息的显示已经自动的离开函数.
处理任意数目的参数
还有两个zend_get_parameters()族的函数, 用于解出zval *和zval **指针集合, 它们适用于有很多参数或运行时才知道参数个数的引用场景.
考虑var_dump()函数, 它用来展示传递给它的任意数量的变量的内容:
PHP_FUNCTION(var_dump) { int i, argc = ZEND_NUM_ARGS(); zval ***args; args = (zval ***)safe_emalloc(argc, sizeof(zval **), 0); if (ZEND_NUM_ARGS() == 0 || zend_get_parameters_array_ex(argc, args) == FAILURE) { efree(args); WRONG_PARAM_COUNT; } for (i=0; i<argc; i++) { php_var_dump(args[i], 1 TSRMLS_CC); } efree(args); }
这里, var_dump()预分配了一个传给函数的参数个数大小的zval **指针向量. 接着使用zend_get_parameters_array_ex()一次性将参数摊入到该向量中. 你可能会猜到, 存在这个函数的另外一个版本: zend_get_parameters_array(), 它们仅有一点不同: 自动隔离, 返回zval *而不是zval **, 在第一个参数上要求传递ZEND_NUM_ARGS().
参数信息和类型暗示
在上一章已经简短的向你介绍了使用Zend引擎2的 参数信息结构进行类型暗示的概念. 我们应该记得, 这个特性是针对ZE2(Zend引擎2)的, 对于php4的ZE1(Zend引擎1)不可用.
我们重新从ZE2的参数信息结构开始. 每个参数信息都使用ZEND_BEGIN_ARG_INFO()或ZEND_BEGIN_ARG_INFO_EX()宏开始, 接着是0个或多个ZEND_ARG_*INFO()行, 最后以ZEND_END_ARG_INFO()调用结束.
这些宏的定义和基本使用可以在刚刚结束的第6章"返回值"的编译期引用传值一节中找到.
假设你想要重新实现count()函数, 你可能需要创建下面这样一个函数:
PHP_FUNCTION(sample_count_array) { zval *arr; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &arr) == FAILURE) { RETURN_NULL(); } RETURN_LONG(zend_hash_num_elements(Z_ARRVAL_P(arr))); }
zend_parse_parameters()会做很多工作, 以保证传递给你的函数是一个真实的数组. 但是, 如果你使用zend_get_parameters()函数或某个它的兄弟函数, 你就需要自己在函数中做类型检查. 当然, 你也可以使用类型暗示! 通过定义下面这样一个arg_info结构:
static ZEND_BEGIN_ARG_INFO(php_sample_array_arginfo, 0) ZEND_ARG_ARRAY_INFO(0, "arr", 0) ZEND_END_ARG_INFO()
并在你的php_sample_function结构中声明该函数时使用它:
PHP_FE(sample_count_array, php_sample_array_arginfo)
这样就将类型检查的工作交给了Zend引擎. 同时你给了你的参数一个名字(arr), 它可以在产生错误消息时被使用, 这样在使用你API的脚本发生错误时, 更加容易跟踪问题.
在第6章第一次介绍参数信息结构时, 你可能注意到了对象, 也可以使用ARG_INFO宏进行类型暗示. 只需要在参数名后增加一个额外的参数来说明类型名(类名)
static ZEND_BEGIN_ARG_INFO(php_sample_class_arginfo, 0) ZEND_ARG_OBJECT_INFO(1, "obj", "stdClass", 0) ZEND_END_ARG_INFO()
要注意到这里的第一个参数(by_ref)被设置为1. 通常来说这个参数对对象来说并不是很重要, 因为ZE2中所有的对象默认都是引用方式的, 对它们的拷贝必须显式的通过clone来实现. 在函数调用内部强制clone可以做到, 但是它和强制引用完全不同.
因為當設定了zend.ze1_compatiblity_mode標記時, 你可能關心ZEND_ARG_OBJECT_INFO一行的by_ref設定. 這種特殊情況下, 物件可能仍然傳遞的是一個拷貝而不是引用. 因為你在處理物件的時候, 可能需要的是物件可能仍然傳遞的是一個拷貝而不是引用. 因為你在處理物件的時候, 可能需要的是一個真正的引用, 因此設置這個標記你就不用擔心這一方面的影響.
不要忘記了數組和對象的參數信息宏中有一個allow_null選項. 關於允許NULL的細節請參考前一章的編譯期引用傳值一節.
當然, 使用使用參數資訊進行型別暗示只在ZE2中支援, 如果你想讓你看的擴充相容php4, 需要使用zend_get_parameters(), 這樣就只能將型別驗證放到函數內部, 手動的通過測試Z_TYPE_P(value)或使用第2章看到的convert_to_type()方法進行自動類型轉換來完成.
小結
現在你的手頭可能已經有點髒亂了, 和用戶空間通信的功能代碼透過簡單的輸入/輸出函數實現. 已經比較深入的了解了zval的引用計數系統, 並學習了控制變量傳遞到你的內部函數方法和時機.
下一章將開始學習數組數據類型,並了解使用者空間的陣列表示怎樣映射到內部的HashTable實現. 另外還將看到一大批可選的Zend以及php api函數, 它們用於操縱這些複雜的結構體.
以上就是 [翻譯][php擴展開發與嵌入式]第7章-接受參數的內容,更多相關內容請關注PHP中文網(www.php.cn)!