用C/C++擴展你的PHP

字號+ 編輯: 种花家 修訂: 种花家 來源: 惠新宸的博客 2023-09-12 我要說兩句(0)

PHP當年取得成功的一個主要原因之一是其擁有大量的可用擴展。web開發者無論有何種需求,這種需求最有可能在PHP發行包裡找到。PHP發行包包括支持各種數據庫,圖形文档格式,壓縮,XML技術擴展在内的許多擴展。

簡介

擴展API的引入使PHP3取得了巨大的進展,擴展API機制使PHP開發社區很容易的開發出幾十種擴展。現在,兩個版本過去了,API仍然和PHP3時的非常相似。擴展主要的思想是:盡可能的從擴展編寫者那裡隱藏PHP的内部機制和腳本引擎本身,僅僅需要開發者熟悉API。

有兩個理由需要自己編寫PHP擴展。第一個理由是:PHP需要支持一項她還未支持的技術。這通常包括包裹一些現成的C函數庫,以便提供PHP接口。例如,如果一個叫FooBase的數據庫已推出市場,你需要建立一個PHP擴展幫助你從PHP裡調用FooBase的C函數庫。這個工作可能僅由一個人完成,然後被整個PHP社區共享(如果你願意的話)。第二個不是很普遍的理由是:你需要從性能或功能的原因考慮來編寫一些商業邏輯。

如果以上的兩個理由都和你沒什麽關係,同時你感覺自己沒有冒險精神,那麽你可以跳過本章。

本章教你如何編寫相對簡單的PHP擴展,使用一部分擴展API函數。對於大多數打算開發自定義PHP擴展開發者而言,它含概了足夠的資料。學習一門編程課程的最好方法之一就是動手做一些極其簡單的例子,這些例子正是本章的線索。一旦你明白了基礎的東西,你就可以在互聯網上通過閲讀文擋、原代碼或參加郵件列表新聞組討論來豐富自己。因此,本章集中在讓你如何開始的話題。在UNIX下一個叫ext_skel的腳本被用於建立擴展的骨架,骨架信息從一個描述擴展接口的定義文档中取得。因此你需要利用UNIX來建立一個骨架。Windows開發者可以使用Windows ext_skel_win32.php代替ext_skel。

然而,本章關於用你開發的擴展編譯PHP的指導僅涉及UNIX編譯系統。本章中所有的對API的解釋與UNIX和Windows下開發的擴展都有聯繫。

當你閲讀完這章,你能學會如何

1 建立一個簡單的商業邏輯擴展。

2 建議個C函數庫的包裹擴展,尤其是有些標準C文档操作函數比如fopen()

快速開始

本節沒有介紹關於腳本引擎基本構造的一些知識,而是直接進入擴展的編碼講解中,因此不要擔心你無法立刻獲得對擴展整體把握的感覺。假設你正在開發一個網站,需要一個把字符串重複n次的函數。下面是用PHP寫的例子:

function self_concat($string, $n)
{
    $result = "";
    for ($i = 0; $i < $n; $i++) {
        $result .= $string;
    }
    return $result;
}
self_concat("One", 3); // returns "OneOneOne".
self_concat("One", 1); // returns "One".

假設由於一些奇怪的原因,你需要時常調用這個函數,而且還要傳給函數很長的字符串和大值n。這意味著在腳本裡有相當巨大的字符串連接量和内存重新分配過程,以至顯著地降低腳本執行速度。如果有一個函數能夠更快地分配大量且足夠的内存來存放結果字符串,然後把$string重複n次,就不需要在每次循環疊代中分配内存。

爲擴展建立函數的第一步是寫一個函數定義文档,該函數定義文档定義了擴展對外提供的函數原形。該例中,定義函數隻有一行函數原形self_concat() :

string self_concat(string str, int n)

函數定義文档的一般格式是一個函數一行。你可以定義可選參數和使用大量的PHP類型,包括: bool, float, int, array等。
保存爲myfunctions.def文档至PHP原代碼目錄樹下。

該是通過擴展骨架(skeleton)構造器運行函數定義文档的時機了。該構造器腳本叫ext_skel,放在PHP原代碼目錄樹的ext/目錄下(PHP原碼主目錄下的README.EXT_SKEL提供了更多的信息)。假設你把函數定義保存在一個叫做myfunctions.def的文档裡,而且你希望把擴展取名爲myfunctions,運行下面的命令來建立擴展骨架

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

這個命令在ext/目錄下建立了一個myfunctions/目錄。你要做的第一件事情也許就是編譯該骨架,以便編寫和測試實際的C代碼。編譯擴展有兩種方法:

1 作爲一個可裝載模塊或者DSO(動態共享對象)

2 靜態編譯到PHP

因爲第二種方法比較容易上手,所以本章採用靜態編譯。如果你對編譯可裝載擴展模塊感興趣,可以閲讀PHP原代碼根目錄下的README.SELF-CONTAINED_EXTENSIONS文档。爲了使擴展能夠被編譯,需要修改擴展目錄ext/myfunctions/下的config.m4文档。擴展沒有包裹任何外部的C庫,你需要添加支持–enable-myfunctions配置開關到PHP編譯系統裡(–with-extension 開關用於那些需要用戶指定相關C庫路徑的擴展)。可以去掉自動生成的下面兩行的注釋來開啓這個配置。

./ext_skel --extname=myfunctions --proto=myfunctions.defPHP_ARG_ENABLE(myfunctions, whether to enable myfunctions support, [ --enable-myfunctions Include myfunctions support])

現在剩下的事情就是在PHP原代碼樹根目錄下運行./buildconf,該命令會生成一個新的配置腳本。通過查看./configure –help輸出信息,可以檢查新的配置選項是否被包含到配置文档中。現在,打開你喜好的配置選項開關和–enable-myfunctions重新配置一下PHP。最後的但不是最次要的是,用make來重新編譯PHP。

ext_skel應該把兩個PHP函數添加到你的擴展骨架了:打算實現的self_concat()函數和用於檢測myfunctions 是否編譯到PHP的confirm_myfunctions_compiled()函數。完成PHP的擴展開發後,可以把後者去掉。

<?php
print confirm_myfunctions_compiled("myextension");

運行這個腳本會出現類似下面的輸出:

"Congratulations! You have successfully modified ext/myfunctions config.m4. Module myfunctions is now compiled into PHP."

另外,ext_skel腳本生成一個叫myfunctions.php的腳本,你也可以利用它來驗证擴展是否被成功地編譯到PHP。它會列出該擴展所支持的所有函數。
現在你學會如何編譯擴展了,該是真正地研究self_concat()函數的時候了。

下面就是ext_skel腳本生成的骨架結構:

/* {{{ proto string self_concat(string str, int n) */ PHP_FUNCTION(self_concat){ char *str = NULL; int argc = ZEND_NUM_ARGS(); int str_len; long n; if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE) return; php_error(E_WARNING, "self_concat: not yet implemented"); } /* }}} */

自動生成的PHP函數周圍包含了一些注釋,這些注釋用於自動生成代碼文档和vi、Emacs等編輯器的代碼折曡。函數自身的定義使用了宏PHP_FUNCTION(),該宏可以生成一個適合於Zend引擎的函數原型。邏輯本身分成語義各部分,取得調用函數的參數和邏輯本身。
爲了獲得函數傳遞的參數,可以使用zend_parse_parameters()API函數。下面是該函數的原型:

zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, …);

第一個參數是傳遞給函數的參數個數。通常的做法是傳給它ZEND_NUM_ARGS()。這是一個表示傳遞給函數參數總個數的宏。第二個參數是爲了線程安全,總是傳遞TSRMLS_CC宏,後面會講到。第三個參數是一個字符串,指定了函數期望的參數類型,後面緊跟著需要隨參數值更新的變量列表。因爲PHP採用松散的變量定義和動態的類型判斷,這樣做就使得把不同類型的參數轉化爲期望的類型成爲可能。例如,如果用戶傳遞一個整數變量,可函數需要一個浮點數,那麽zend_parse_parameters()就會自動地把整數轉換爲相應的浮點數。如果實際值無法轉換成期望類型(比如整形到數組形),會觸發一個警告。

下表列出了可能指定的類型。我們從完整性考慮也列出了一些沒有討論到的類型。

類型指定符 對應的C類型 描述
l long 符號整數
d double 浮點數
s char *, int 二進制字符串,長度
b zend_bool 邏輯型(1或0)
r zval * 資源(文档指針,數據庫連接等)
a zval * 聯合數組
o zval * 任何類型的對象
O zval * 指定類型的對象。需要提供目標對象的類類型
z zval * 無任何操作的zval

類型指定符對應的C類型描述llong符號整數ddouble浮點數schar *, int二進制字符串,長度bzend_bool邏輯型(1或0)rzval *資源(文档指針,數據庫連接等)azval *聯合數組ozval *任何類型的對象Ozval *指定類型的對象。需要提供目標對象的類類型zzval *無任何操作的zval爲了容易地理解最後幾個選項的含義,你需要知道zval是Zend引擎的值容器[1]。無論這個變量是布爾型,字符串型或者其他任何類型,其信息總會包含在一個zval聯合體中。本章中我們不直接存取zval,而是通過一些附加的宏來操作。下面的是或多或少在C中的zval, 以便我們能更好地理解接下來的代碼。

typedef union _zval{     long lval;     double dval;     struct {          char *val;          int len;     }str;      HashTable *ht;     zend_object_value obj; }zval;

在我們的例子中,我們用基本類型調用zend_parse_parameters(),以本地C類型的方式取得函數參數的值,而不是用zval容器。

爲了讓zend_parse_parameters()能夠改變傳遞給它的參數的值,並返回這個改變值,需要傳遞一個引用。仔細查看一下self_concat():

if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)return;

注意到自動生成的代碼會檢測函數的返回值FAILUER(成功即SUCCESS)來判斷是否成功。如果沒有成功則立即返回,並且由zend_parse_parameters()負責觸發警告信息。因爲函數打算接收一個字符串l和一個整數n,所以指定 ”sl” 作爲其類型指示符。s需要兩個參數,所以我們傳遞參考char * 和 int (str 和 str_len)給zend_parse_parameters()函數。無論什麽時候,記得總是在代碼中使用字符串長度str_len來確保函數工作在二進制安全的環境中。不要使用strlen()和strcpy(),除非你不介意函數在二進制字符串下不能工作。二進制字符串是包含有nulls的字符串。二進制格式包括圖象文档,壓縮文档,可執行文档和更多的其他文档。”l” 只需要一個參數,所以我們傳遞給它n的引用。盡管爲了清晰起見,骨架腳本生成的C變量名與在函數原型定義文档中的參數名一樣;這樣做不是必須的,盡管在實踐中鼓勵這樣做。

回到轉換槼則中來。下面三個對self_concat()函數的調用使str, str_len和n得到同樣的值:

self_concat("321", 5); self_concat(321, "5"); self_concat("321", "5"); str points to the string "321", str_len equals 3, and n equals 5.

str 指向字符串"321",str_len等於3,n等於5。

在我們編寫代碼來實現連接字符串返回給PHP的函數前,還得談談兩個重要的話題:内存管理、從PHP内部返回函數值所使用的API。

内存管理

用於從堆中分配内存的PHP API幾乎和標準C API一樣。在編寫擴展的時候,使用下面與C對應(因此不必再解釋)的API函數:

emalloc(size_t size); efree(void *ptr); ecalloc(size_t nmemb, size_t size); erealloc(void *ptr, size_t size); estrdup(const char *s); estrndup(const char *s, unsigned int length);

在這一點上,任何一位有經驗的C程序員應該象這樣思考一下:“什麽?標準C沒有strndup()?”是的,這是正確的,因爲GNU擴展通常在Linux下可用。estrndup()只是PHP下的一個特殊函數。它的行爲與estrdup()相似,但是可以指定字符串重複的次數(不需要結束空字符),同時是二進制安全的。這是推薦使用estrndup()而不是estrdup()的原因。

在幾乎所有的情況下,你應該使用這些内存分配函數。有一些情況,即擴展需要分配在請求中永久存在的内存,從而不得不使用malloc(),但是除非你知道你在做什麽,你應該始終使用以上的函數。如果沒有使用這些内存函數,而相反使用標準C函數分配的内存返回給腳本引擎,那麽PHP會崩潰。

這些函數的優點是:任何分配的内存在偶然情況下如果沒有被釋放,則會在頁面請求的最後被釋放。因此,真正的内存泄漏不會産生。然而,不要依賴這一機制,從調試和性能兩個原因來考慮,應當確保釋放應該釋放的内存。剩下的優點是在多線程環境下性能的提高,調試模式下檢測内存錯誤等。

還有一個重要的原因,你不需要檢查這些内存分配函數的返回值是否爲null。當内存分配失敗,它們會發出E_ERROR錯誤,從而決不會返回到擴展。

從PHP函數中返回值

擴展API包含豐富的用於從函數中返回值的宏。這些宏有兩種主要風格:第一種是RETVAL_type()形式,它設置了返回值但C代碼繼續執行。這通常使用在把控制交給腳本引擎前還希望做的一些清理工作的時候使用,然後再使用C的返回聲明 ”return” 返回到PHP;後一個宏更加普遍,其形式是RETURN_type(),他設置了返回類型,同時返回控制到PHP。下表解釋了大多數存在的宏。

設置返回值並且結束函數 設置返回值 宏返回類型和參數
RETURN_LONG(l) RETVAL_LONG(l) 整數
RETURN_BOOL(b) RETVAL_BOOL(b) 布爾數(1或0)
RETURN_NULL() RETVAL_NULL() NULL
RETURN_DOUBLE(d) RETVAL_DOUBLE(d) 浮點數
RETURN_STRING(s, dup) RETVAL_STRING(s, dup) 字符串。如果dup爲1,引擎會調用estrdup()重複s,使用拷貝。如果dup爲0,就使用s
RETURN_STRINGL(s, l, dup) RETVAL_STRINGL(s, l, dup) 長度爲l的字符串值。與上一個宏一樣,但因爲s的長度被指定,所以速度更快。
RETURN_TRUE RETVAL_TRUE 返回布爾值true。注意到這個宏沒有括號。
RETURN_FALSE RETVAL_FALSE 返回布爾值false。注意到這個宏沒有括號。
RETURN_RESOURCE(r) RETVAL_RESOURCE(r) 資源句柄。

設置返回值並且結束函數設置返回值宏返回類型和參數RETURN_LONG(l)RETVAL_LONG(l)整數RETURN_BOOL(b)RETVAL_BOOL(b)布爾數(1或0)RETURN_NULL()RETVAL_NULL()NULLRETURN_DOUBLE(d)RETVAL_DOUBLE(d)浮點數RETURN_STRING(s, dup)RETVAL_STRING(s, dup)字符串。如果dup爲1,引擎會調用estrdup()重複s,使用拷貝。如果dup爲0,就使用sRETURN_STRINGL(s, l, dup)RETVAL_STRINGL(s, l, dup)長度爲l的字符串值。與上一個宏一樣,但因爲s的長度被指定,所以速度更快。RETURN_TRUERETVAL_TRUE返回布爾值true。注意到這個宏沒有括號。RETURN_FALSERETVAL_FALSE返回布爾值false。注意到這個宏沒有括號。RETURN_RESOURCE(r)RETVAL_RESOURCE(r)資源句柄。

完成self_concat()

現在你已經學會了如何分配内存和從PHP擴展函數裡返回函數值,那麽我們就能夠完成self_concat()的編碼:

/* {{{ proto string self_concat(string str, int n)*/
PHP_FUNCTION(self_concat)
}
char *str = NULL;
int argc = ZEND_NUM_ARGS();
int str_len;
long n;
char *result;
/* Points to resulting string */
char *ptr;
/* Points at the next location we want to copy to */
int result_length;
/* Length of resulting string */
if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
return;
/* Calculate length of result */
result_length = (str_len * n);
/* Allocate memory for result */
result = (char *) emalloc(result_length + 1);
/* Point at the beginning of the result */
ptr = result;while (n--) {
/* Copy str to the result */
memcpy(ptr, str, str_len);
/* Increment ptr to point at the next position we want to write to */ptr += str_len;}
/* Null terminate the result. Always null-terminate your stringseven if they are binary strings */
*ptr = '\0';
/* Return result to the scripting engine without duplicating it*/
RETURN_STRINGL(result, result_length, 0);}
/* }}} */

現在要做的就是重新編譯一下PHP,這樣就完成了第一個PHP函數。

讓我門檢查函數是否真的工作。在最新編譯過的PHP樹下執行[2]下面的腳本:

<?php
for ($i = 1; $i <= 3; $i++) {
    print self_concat("ThisIsUseless", $i);
    print "\n";
}

你應該得到下面的結果:

ThisIsUseless
ThisIsUselessThisIsUseless
ThisIsUselessThisIsUselessThisIsUseless

實例小結

你已經學會如何編寫一個簡單的PHP函數。回到本章的開頭,我們提到用C編寫PHP功能函數的兩個主要的動機。第一個動機是用C實現一些算法來提高性能和擴展功能。前一個例子應該能夠指導你快速上手這種類型擴展的開發。第二個動機是包裹三方函數庫。我們將在下一步討論。

包裹第三方的擴展

本節中你將學到如何編寫更有用和更完善的擴展。該節的擴展包裹了一個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


閲完此文,您的感想如何?
  • 有用

    0

  • 沒用

    0

  • 開心

    0

  • 憤怒

    0

  • 可憐

    0

1.如文章侵犯了您的版權,請發郵件通知本站,該文章將在24小時内刪除;
2.本站標注原創的文章,轉發時煩請注明來源;
3.交流群: 2702237 13835667

相關課文
  • mac開發接入微信公衆號接口返回報錯 cURL error 56: SSLRead() return error -9806

  • PHP的換行符是什麽

  • pecl安裝程序時報錯Array and string offset access syntax with curly braces is no longer supported

  • 由於商家傳入的H5交易參數有誤,該筆交易暫時無法完成,請聯繫商家解決

我要說說
網上賓友點評