国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

PHP 源碼 — implode 函數(shù)源碼分析

?。?。 / 1318人閱讀

摘要:本文首發(fā)于作者基于中的在中,的作用是將一個一維數(shù)組的值轉化為字符串。為了能通過修改代碼來看效果,將函數(shù)復制到擴展文件中,并將其命名為源碼內容省略在擴展中新增一個擴展函數(shù)因為擴展的編譯以及引入前面的已經提及。

本文首發(fā)于 https://github.com/suhanyujie...*

作者:suhanyujie

基于 PHP 7.3.3

PHP 中的 implode

在 PHP 中,implode 的作用是:將一個一維數(shù)組的值轉化為字符串。記住一維數(shù)組,如果是多維的,會發(fā)生什么呢?在本篇分析中,會有所探討。

事實上,通過官方的文檔可以知道,implode 有兩種用法,通過函數(shù)簽名可以看得出來:

// 方法1
implode ( string $glue , array $pieces ) : string
// 方法2
implode ( array $pieces ) : string

因為,在不傳 glue 的時候,內部實現(xiàn)會默認空字符串。

通過一個簡單的示例可以看出:

$pieces = [
    123,
    ",是一個",
    "number!",
];
$str1 = implode($pieces);
$str2 = implode("", $pieces);

var_dump($str1, $str2);
/*
string(20) "123,是一個number!"
string(20) "123,是一個number!"
*/
implode 源碼實現(xiàn)

通過搜索關鍵字 PHP_FUNCTION(implode) 可以找到,該函數(shù)定義于 extstandardstring.c 文件中的 1288 行

一開始的幾行是參數(shù)聲明相關的信息。其中 *arg2 是用于接收 pieces 參數(shù)的指針。

在下方對 arg2 的判斷中,如果 arg2 為空,則表示沒有傳 pieces 對應的值

if (arg2 == NULL) {
    if (Z_TYPE_P(arg1) != IS_ARRAY) {
        php_error_docref(NULL, E_WARNING, "Argument must be an array");
        return;
    }

    glue = ZSTR_EMPTY_ALLOC();
    tmp_glue = NULL;
    pieces = arg1;
} else {
    if (Z_TYPE_P(arg1) == IS_ARRAY) {
        glue = zval_get_tmp_string(arg2, &tmp_glue);
        pieces = arg1;
    } else if (Z_TYPE_P(arg2) == IS_ARRAY) {
        glue = zval_get_tmp_string(arg1, &tmp_glue);
        pieces = arg2;
    } else {
        php_error_docref(NULL, E_WARNING, "Invalid arguments passed");
        return;
    }
}
不傳遞 pieces 參數(shù)

在不傳遞 pieces 參數(shù)的判斷中,即 arg2 == NULL,主要是對參數(shù)的一些處理

將 glue 初始化為空字符串,并將傳進來的唯一的參數(shù),賦值給 pieces 變量,接著就調用 php_implode(glue, pieces, return_value);

十分關鍵的 php_implode

無論有沒有傳遞 pieces 參數(shù),在處理好參數(shù)后,最終都會調用 PHPAPI 的相關函數(shù) php_implode,可見,關鍵邏輯都是在這個函數(shù)中實現(xiàn)的,那么我們深入其中看一看它

在調用 php_implode 時,出現(xiàn)了一個看起來沒有被聲明的變量 return_value。沒錯,它似乎就是憑空出現(xiàn)的

通過谷歌搜索 PHP源碼中 return_value,找到了答案。

原來,這個變量是伴隨著宏 PHP_FUNCTION 而出現(xiàn)的,而此處 implode 的實現(xiàn)就是通過 PHP_FUNCTION(implode) 來聲明的。而 PHP_FUNCTION 的定義是:

#define PHP_FUNCTION            ZEND_FUNCTION
// 對應的 ZEND_FUNCTION 定義如下
#define ZEND_FUNCTION(name)                ZEND_NAMED_FUNCTION(ZEND_FN(name))
// 對應的 ZEND_NAMED_FUNCTION 定義如下
#define ZEND_NAMED_FUNCTION(name)        void ZEND_FASTCALL name(INTERNAL_FUNCTION_PARAMETERS)
// 對應的 ZEND_FN 定義如下
#define ZEND_FN(name) zif_##name
// 對應的 ZEND_FASTCALL 定義如下
# define ZEND_FASTCALL __attribute__((fastcall))

(關于雙井號,它起連接符的作用,可以參考這里了解)

在被預處理后,它的樣子類似于下方所示:

void zif_implode(int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC)

也就是說 return_value 是作為整個 implode 擴展函數(shù)定義的一個形參

在 php_implode 的定義中,一開始,先定義了一些即將用到的變量,隨后使用 ALLOCA_FLAG(use_heap) 進行標識,如果申請內存,則申請的是堆內存

通過 numelems = zend_hash_num_elements(Z_ARRVAL_P(pieces)); 獲取 pieces 參數(shù)的單元數(shù)量,如果是空數(shù)組,則直接返回空字符串

此處還有判斷,如果數(shù)組單元數(shù)為 1,則直接將唯一的單元作為字符串返回。

最后是處理多數(shù)組單元的情況,因為前面標識過,若申請內存則申請的是堆內存,堆內存相對于棧來講,效率比較低,所以只在非用不可的情形下,才會申請堆內存,那此處的情形就是多單元數(shù)組的情況。

隨后,針對 pieces 循環(huán),獲取其值進行拼接,在源碼中的 foreach 循環(huán)是固定結構,如下:

ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(zend_array), tmp) {
    // ...
} ZEND_HASH_FOREACH_END();

這種常用寫法我覺得,在編寫 PHP 擴展中是必不可少的吧。雖然我還沒有編寫過任何一個可用于生產環(huán)境的 PHP 擴展。但我正努力朝那個方向走呢!

在循環(huán)內,對數(shù)組單元分為三類:

字符串

整形數(shù)據(jù)

其它

事實上,在循環(huán)開始之前,源碼中,先申請了一塊內存,用于存放下面的結構體,并且個數(shù)恰好是 pieces 數(shù)組單元的個數(shù)。

struct {
    zend_string *str;
    zend_long    lval;
} *strings, *ptr;

可以看到,結構體成員包含 zend 字符串以及 zend 整形數(shù)據(jù)。這個結構體的出現(xiàn),恰好是為了存放數(shù)組單元中的 zend 字符串/zend 整形數(shù)據(jù)。

字符串

先假設,pieces 數(shù)組單元中,都是字符串類型,此時循環(huán)中執(zhí)行的邏輯就是:

// tmp 是循環(huán)中的單元值
ptr->str = Z_STR_P(tmp);
len += ZSTR_LEN(ptr->str);
ptr->lval = 0;
ptr++;

其中,tmp 是循環(huán)中的單元值。每經歷一次循環(huán),會將單元值放入結構體中,隨后進行指針 +1 運算,指針就指向存儲下一個結構體數(shù)據(jù)的地址:

并且,在這期間,統(tǒng)計出了字符串的總長度 len += ZSTR_LEN(ptr->str);

整數(shù)類型

以上,討論了數(shù)組單元中是字符串的情況。接下來看看,如果數(shù)組單元的類型是數(shù)值類型時會發(fā)生什么?

判斷一個變量是否是數(shù)值類型(其實是 zend_long),通用方法是:Z_TYPE_P(tmp) == IS_LONG。一旦知道當前的數(shù)據(jù)類型是 zend_long,則將其賦值給 ptr 的 lval 結構體成員。然后 ptr 指針后移一個單位長度。

但是,我們知道我們不能像獲取 zend_string 的長度一樣去獲取 zend_long 的字符長度。如果是 zend_string,則可以通過 len += ZSTR_LEN(val); 的方式獲取其字符長度。對于 zend_long,有什么好的方法呢?

在源碼中是通過對 10 做除法運算,得出結果的一部分,再慢慢的累加其長度:

while (val) {
    val /= 10;
    len++;
}

如果是負數(shù)呢?沒有什么特別的辦法,直接判斷處理:

if (val <= 0) {
    len++;
}
字符串的處理和拷貝

循環(huán)結束后,ptr 就是指向這段內存的尾部的指針。

然后,申請了一段內存:str = zend_string_safe_alloc(numelems - 1, ZSTR_LEN(glue), len, 0);,用于存放單元字符串總長度加上連接字符的總長度,即 (n-1)glue + len。因為 n 個數(shù)組單元,只需要 n-1 個 glue 字符串。然后,將這段內存的尾地址,賦值給 cptr,為什么要指向尾部呢?看下一部分,你就會明白了。

接下來,需要循環(huán)取出存放在 ptr 中的字符。我們知道,ptr 此時是所處內存區(qū)域的尾部,為了能有序展示連接的字符串,源碼中,是從后向前循環(huán)處理。這也就是為什么需要把 cptr 指向所在內存區(qū)域的尾部的原因。

進入循環(huán),先進行 ptr--;,然后針對 ptr->str 的判斷 if (EXPECTED(ptr->str)),看了一下此處的 EXPECTED 的作用,可以參考這里??梢院唵蔚膶⑵淅斫庖环N匯編層面的優(yōu)化,當實際執(zhí)行的情況更偏向于當前條件下的分支而非 else 的分支時,就用 EXPECTED 宏將其包裝起來:EXPECTED(ptr->str)。我敢說,當你調用 implode 傳遞的數(shù)組中都是數(shù)字而非字符串,那么這里的 EXPECTED 作用就會失效。

接下來的兩行是比較核心的:

cptr -= ZSTR_LEN(ptr->str);
memcpy(cptr, ZSTR_VAL(ptr->str), ZSTR_LEN(ptr->str));

cptr 的指針前移一個數(shù)組單元字符的長度,然后將 ptr->str (某數(shù)組單元的值)通過 c 標準庫函數(shù) memcpy 拷貝到 cptr 內存空間中。

ptr == strings 滿足時,意味著 ptr 不再有可被復制的字符串/數(shù)字。因為 strings 是 ptr 所在區(qū)域的首地址。

通過上面,已經成功將一個數(shù)組單元的字符串拷貝到 cptr 對應的內存區(qū)域中,接下來如何處理 glue 呢?

只需要像處理 ptr->str 一樣處理 glue 即可。至少源碼中是這么做的。

代碼中有一段是:*cptr = 0,它的作用相當于賦值空字符串。

cptr 繼續(xù)前移 glue 的長度,然后,將 glue 字符串拷貝到 cptr 對應的內存區(qū)域中。沒錯,還是用 memcpy 函數(shù)。

到這里,第一次循環(huán)結束了。我應該不需要像實際循環(huán)中那樣描述這里的循環(huán)吧?相信優(yōu)秀的你,是完全可以參考上方的描述腦補出來的 ^^

當然,處理返回的兩句還是要提一下:

free_alloca(strings, use_heap);
RETURN_NEW_STR(str);

strings 的那一片內存空間只是存儲臨時值的,因此函數(shù)結束了,就必須跟 strings 說再見。我們知道 c 語言是手動管理內存的,沒有 GC,你要顯示的釋放內存,即 free_alloca(strings, use_heap);

在上面的描述中,我們只講到了 cptr,但這里的返回值卻是 str。

不用懷疑,這里是對的,我們所講的 cptr 那一片內存區(qū)域的首地址就是 str。并通過宏 RETURN_NEW_STR 會將最終的返回值寫入 return_value 中

實踐

為了可能更加清晰 implode 源碼中代碼運行時的情況,接下來,我們通過 PHP 擴展的方式對其進行 debug。在這個過程中的代碼,我都放在 GitHub 的倉庫中,分支名是 debug/implode,可自行下載運行,看看效果。

新建 PHP 擴展模板的操作,可以參考這里。請確保操作完里面描述的步驟。

接下來,主要針對 su_dd.c 文件修改代碼。為了能通過修改代碼來看效果,將 php_implode 函數(shù)復制到擴展文件中,并將其命名為 su_php_implode:

static void su_php_implode(const zend_string *glue, zval *pieces, zval *return_value)
{
    // 源碼內容省略
}

在擴展中新增一個擴展函數(shù) su_test:

PHP_FUNCTION(su_test)
{
    zval tmp;
    zend_string *str, *glue, *tmp_glue;
    zval *arg1, *arg2 = NULL, *pieces;

    ZEND_PARSE_PARAMETERS_START(1, 2)
        Z_PARAM_ZVAL(arg1)
        Z_PARAM_OPTIONAL
        Z_PARAM_ZVAL(arg2)
    ZEND_PARSE_PARAMETERS_END();
    glue = zval_get_tmp_string(arg1, &tmp_glue);
    pieces = arg2;
    su_php_implode(glue, pieces, return_value);
}

因為擴展的編譯以及引入,前面的已經提及。因此,此時只需編寫 PHP 代碼進行調用:

// t1.php
$res = su_test("-", [
    2019, "01", "01",
]);
var_dump($res);

PHP 運行該腳本,輸出:string(10) "2019-01-01",這意味著,你已經成功編寫了一個擴展函數(shù)。別急,這只是邁出了第一步,別忘記我們的目標:通過調試來學習 implode 源碼。

接下來,我們通過 gdb 工具,調試以上 PHP 代碼在源碼層面的運行。為了防止初學者不會用 gdb,這里就繁瑣的寫出這個過程。如果沒有安裝 gdb,請自行谷歌。

先進入 PHP 腳本所在路徑。命令行下:

gdb php
b zval_get_tmp_string
r t1.php

b 即 break,表示打一個斷點

r 即 run,表示運行腳本

s 即 step,表示一步一步調試,遇到方法調用,會進入方法內部單步調試

n 即 next,表示一行一行調試。遇到方法,則調試直接略過直接執(zhí)行返回,調試不會進入其內部。

p 即 print,表示打印當前作用域中的一個變量

當運行完 r t1.php,則會定位到第一個斷點對應的行,顯示如下:

Breakpoint 1, zif_su_test (execute_data=0x7ffff1a1d0c0, 
    return_value=0x7ffff1a1d090)
    at /home/www/clang/php-7.3.3/ext/su_dd/su_dd.c:179
179        glue = zval_get_tmp_string(arg1, &tmp_glue);

此時,按下 n,顯示如下:

184        su_php_implode(glue, pieces, return_value);

此時,當前的作用域中存在變量:glue,pieces,return_value

我們可以通過 gdb 調試,查看 pieces 的值。先使用命令:p pieces,此時在終端會顯示類似于如下內容:

$1 = (zval *) 0x7ffff1a1d120

表明 pieces 是一個 zval 類型的指針,0x7ffff1a1d120 是其地址,當然,你運行的時候對應的也是一個地址,只不過跟我的這個會不太一樣。

我們繼續(xù)使用 p 去打印存儲于改地址的變量內容:p *$1,$1 可以認為是一個臨時變量名,* 是取值運算符。運行完后,此時顯示如下:

(gdb) p *$1
$2 = {value = {lval = 140737247576960, dval = 6.9533439118030153e-310, 
    counted = 0x7ffff1a60380, str = 0x7ffff1a60380, arr = 0x7ffff1a60380, 
    obj = 0x7ffff1a60380, res = 0x7ffff1a60380, ref = 0x7ffff1a60380, 
    ast = 0x7ffff1a60380, zv = 0x7ffff1a60380, ptr = 0x7ffff1a60380, 
    ce = 0x7ffff1a60380, func = 0x7ffff1a60380, ww = {w1 = 4054188928, 
      w2 = 32767}}, u1 = {v = {type = 7 "a", type_flags = 1 "