用C++擴展PHP

用C++擴展PHP - (2)
2006-10-25 17:40:26 / 個人分類:程序設計
用C++擴展PHP - (2)
(接上篇)

6節.將C++類影射到PHP中

 

目錄

MyClass

宏、函數及其它

封裝代碼

PHP 5的類支持很多新的特性。如:權限(protected, public, private),異常,interfaces,等等。在這個簡單的介紹中,我們只做最基本的事情:使PHP可以影射到C++的類。這樣你可以用PHP中使用你的類,之後的事情將會變得很簡單的。在看下面的介紹之前,你可以參考一下Sqlite, SimpleXML 及 cryptopp-php 模塊的代碼。

這裏介紹一下。我們要用一個C++類做爲例子,就叫做MyClass吧。在PHP術語中,把它叫做一個resource(資源)。PHP常使用這類的東西,如數據庫的連接就是resource,它也可能是一個指向實際resource的struct(結構)。在C++中,class 實際上是struct的一個近義詞(struct默認爲public,classe默認爲private --僅這個區別而已)。

在結構化的接口中,我們會用類似以下的PHP代碼來使用resources:

$m = myclass_new();
myclass_set_string($m, 'hello world!');
var_dump(myclass_get_string($m));
?>

在面向對象式的接口中,一樣可以使用PHP resources,不過已經被封裝在一個PHP對象中了,如:

$m = new MyClass;
$m->setString('hello world!');
var_dump($m->getString());
?>

我們不需要關心被封裝的實際的代碼做了些什麼。當我們有一個叫MyClass的C++類。我們可以把這個C++類當成resources並把C++類裏的方法封裝成PHP的函數。然後我們也可以把它封裝成一個PHP的對象,使得可以像一般的C++那樣使用。

在看本文時,記住我們的目的就是:把C++類封裝成PHP可以使用的結構化的函數或對象化的類。也許在一開始有些東西會令你迷惑,但讀下去後就會慢慢明白的了。中間會有很多的宏定義,但當你看明白後,會覺得所有東西都很清淅很容易了。

1  MyClass

 

首先我們需要一個類。下邊是一個只有一個私有屬性和幾個公有方法的簡單的類。

以下是這個類的聲明頭文件 myclass.h

#ifndef __MYCLASS_H__
#define __MYCLASS_H__

#include

using namespace std;

class MyClass {
    private:
        string itsString;

    public:
        MyClass(string s = "default");
        ~MyClass();

        string getString() const;
        bool setString(const string s);
};

#endif

下邊是定義代碼myclass.cpp

#include "myclass.h"

MyClass::MyClass(string s)
{
   itsString = s;
}

MyClass::~MyClass()
{
}

string MyClass::getString() const
{
    return itsString;
}

bool MyClass::setString(const string s)
{
    itsString = s;
    return true;
}

這只是一個做爲例子的類。

然後我們要讓構建系統知道和可以編譯這些文件。把config.m4文件做以下修改:

PHP_NEW_EXTENSION(php5cpp, php5cpp.cpp, $ext_shared)

becomes...

PHP_NEW_EXTENSION(php5cpp, php5cpp.cpp myclass.cpp, $ext_shared)

php5cpp.cpp 文件中增加#include “myclass.h”

extern "C" {
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
}

#include "php_php5cpp.h"
#include "myclass.h"

不要把include php_php5cpp.hmyclass.h 的語句放在 extern "C" 中,否則會出現錯誤。

2  宏、函數及其它

 

爲了讓這個模塊可以同時在PHP 4和PHP 5使用,我們需要使用一些宏去聲明是依賴於PHP 4或是PHP 5的。因爲PHP 4和PHP 5的執行文件是不兼容的,所以你需要爲這兩個PHP的版本分別編譯不同的版本。

通常我會把這些宏的聲明放在一個單獨的文件中。在這個例子裏,就放在objects.h 中吧。

objects.h 中要寫一些PHP 5需要的函數,如:

#ifndef __PHP5CPP_OBJECTS_H__
#define __PHP5CPP_OBJECTS_H__

#if PHP_MAJOR_VERSION == 5

zend_class_entry *php5cpp_ce_myclass;
static zend_object_handlers php5cpp_object_handlers_myclass;

function_entry php5cpp_myclass_methods[] = {
    ZEND_ME_MAPPING(MyClass,    myclass_new,        NULL)
    ZEND_ME_MAPPING(setString, myclass_set_string, NULL)
    ZEND_ME_MAPPING(getString, myclass_get_string, NULL)
    {NULL, NULL, NULL}
};

typedef enum {
    is_myclass
} php5cpp_obj_type;

struct php5cpp_obj {
    zend_object std;
    int rsrc_id;
    php5cpp_obj_type type;
    union {
        MyClass *myclass;
    } u;
};

你可以發現,爲了保持一致,每個聲明都加上了php5cpp_ 前綴。這是習慣上的一種約定。

另外,#if PHP_MAJOR_VERSION == 5表明在下邊的那些宏、函數等都只是在PHP 5下才生效,當我們在PHP 4下編譯時它們會被預處理忽略掉。

php5cpp_ce_myclass 是類 MyClass 的入口。 php5cpp_object_handlers_myclass 是類的內部處理handler(句柄)。

php5cpp_myclass_methods[] 把MyClass中的函數影射成可在PHP使用的標準函數。這樣我們在PHP中就可以使用 myclass_new, myclass_get_string 等來執行這些函數。你會發現這裏並沒有定義myclass_destroy函數,因爲在你對一個類實例使用unset() 時,系統會自動調用它的釋構函數的了。

在結構php5cpp_obj中的枚舉變量 php5cpp_obj_type 聲明瞭對象的類型。如果你想在擴展中再加入一個類,如: AnotherClass,你需要再增加一項,如: is_anotherclass

php5cpp_obj 結構中聲明瞭在PHP中使用的一些基本信息,包括:

一個resource ID:rsrc_id ,它指向PHP內部的一個記錄着C++對象的track的resource(資源)。實際上,我們的類在PHP中的操作是更像是一個PHP resource,它有自己的垃圾回收和引用計數等機制。

一個zend_object 來聲明我們的PHP類,使得它可以像PHP的類那樣。當使用這個PHP類時,實際上就會調用我們的C++類去處理了。(需要記住的是:使用時需要像使用其它PHP類那樣動態的創建它。(譯:使用new))

type 聲明現在處理的對象。在這裏,它只有一個:is_myclass。但正如我前邊說過,你可以再增加其它的類。

聯合類型變量u 保存了指向C++對象實例的指針。如果你在這個擴展中還有其它class,那還需要在這再增加其它的指針,如:AnotherClass *anotherclass

static void php5cpp_object_free_storage(zend_object *object TSRMLS_DC)
{
    php5cpp_obj *obj = (php5cpp_obj *) object;

    zend_hash_destroy(obj->std.properties);
    FREE_HASHTABLE(obj->std.properties);

    if (obj->u.myclass) {
        zend_list_delete(obj->rsrc_id);
    }

    efree(obj);
}

static void php5cpp_object_new(zend_class_entry *class_type, zend_object_handlers *handlers, zend_object_value *retval TSRMLS_DC)
{
    zval *tmp;
    php5cpp_obj *obj = (php5cpp_obj *) emalloc(sizeof(php5cpp_obj));
    memset(obj, 0, sizeof(php5cpp_obj));
    obj->std.ce = class_type;

    ALLOC_HASHTABLE(obj->std.properties);
    zend_hash_init(obj->std.properties, 0, NULL, ZVAL_PTR_DTOR, 0);
    zend_hash_copy(obj->std.properties, &class_type->default_properties, (copy_ctor_func_t) zval_add_ref, (void *) &tmp, sizeof(zval *));
    retval->handle = zend_objects_store_put(obj, NULL, php5cpp_object_free_storage, NULL TSRMLS_CC);
    retval->handlers = handlers;
}

php5cpp_object_free_storage() 可以看成了對象是 php5cpp_obj 的釋構函數,因爲它要做的就是把MyClass的對象釋放掉

php5cpp_object_new() 基本上算是一個構造函數,它負責分配內存空間,分配zend_object結構,及初始化handler(句柄)等等。它負責爲擴展中所有的類的構造,不管是對象MyClass 還是 AnotherClass

php5cpp_object_new_myclass() 通過調用 php5cpp_object_new() 來創建一個PHP MyClsss的實例。如果在擴展中有幾個類的話,你要爲每個類寫一個類似php5cpp_object_new_*()函數。

// Register the class entry..

#define PHP5CPP_REGISTER_CLASS(name, obj_name)
    {
        zend_class_entry ce;
        INIT_CLASS_ENTRY(ce, obj_name, php5cpp_ ## name ##_methods);
        ce.create_object = php5cpp_object_new_ ## name;
        php5cpp_ce_ ## name = zend_register_internal_class_ex(&ce, NULL, NULL TSRMLS_CC); 
        memcpy(&php5cpp_object_handlers_ ## name, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
        php5cpp_object_handlers_ ## name.clone_obj = NULL;
        php5cpp_ce_ ## name->ce_flags |= ZEND_ACC_FINAL_CLASS;
    }

// Register resources. If we're using an object, put it into the object store.

#define PHP5CPP_REGISTER_RESOURCE(obj_type, return_value, res, le)
    {
        int rsrc_id = ZEND_REGISTER_RESOURCE(object ? NULL : return_value, res, le);
        if (object) {
            php5cpp_obj *obj = (php5cpp_obj *) zend_object_store_get_object(object TSRMLS_CC);
            obj->u.obj_type= res;
            obj->rsrc_id = rsrc_id;
            obj->type = is_ ## obj_type;
        }
    }

#define PHP5CPP_REGISTER_MYCLASS_RESOURCE(return_value, res, le)
    PHP5CPP_REGISTER_RESOURCE(myclass, return_value, res, le)

接着我們要在php5cpp.cpp中加上以上的宏。雖然看起來很相似,但不要把它們弄亂,實際上PHP5CPP_REGISTER_CLASS()PHP5CPP_REGISTER_RESOURCE()的處理是不一樣的。

PHP5CPP_REGISTER_CLASS() 登記一個類的實際處理的程序。在後邊介紹模塊初始化函數(PHP_MINIT_FUNCTION())時還會遇到它。

PHP5CPP_REGISTER_RESOURCE() 負責在登記一個resource時取得resource得ID。其實resource就是我們的C++對象的一個實例。當我們處理一個對象時,它會創建一個PHP對象,把實際的對象保存在PHP的對象貯存器中,然後把resource ID記錄在PHP對象中。

PHP5CPP_REGISTER_MYCLASS_RESOURCE() 只是爲了使用 PHP5CPP_REGISTER_RESOURCE()時可以方便一些。

// These are for parsing parameters and getting the actual object from the store.

#define PHP5CPP_GET_THIS()
    zval* ōbject = getThis();

#define PHP5CPP_SET_OBJ(type)
    php5cpp_obj *obj = (php5cpp_obj *) zend_object_store_get_object(object TSRMLS_CC);
    type ## _instance = obj->u.type;

#define PHP5CPP_OBJ_PARAMS(type, params)
   PHP5CPP_GET_THIS();
    if (object) {
        if (params == FAILURE) {
            RETURN_FALSE;
        }
        PHP5CPP_SET_OBJ(type)
    }
    else

#define PHP5CPP_OBJ_NO_PARAMS(type)
    PHP5CPP_GET_THIS();
    if (object) {
        if (ZEND_NUM_ARGS() != 0) {
            php_error(E_WARNING, "didn't expect any arguments in %s()", get_active_function_name(TSRMLS_C));
        }
        PHP5CPP_SET_OBJ(type)
    }
    else

#define PHP5CPP_MYCLASS_OBJ_PARAMS(params) PHP5CPP_OBJ_PARAMS(myclass, params)
#define PHP5CPP_MYCLASS_OBJ_NO_PARAMS()     PHP5CPP_OBJ_NO_PARAMS(myclass)

PHP5CPP_GET_THIS() 會返回當前使用對象的指針。如果當前使用的是一個結構,那麼getThis() 會返回NULL;如果使用的是一個對象,getThis() 返回一個指向當前PHP對象的指針。

PHP5CPP_SET_OBJ() 會從對象貯存器中取得PHP對象實際使用的C++對象實例,然後可以用來做其它處理。在使用PHP函數/方法時,對象會貯存在類似"type ## _instance" 的類型裏,如:在我們的例子中是myclass_instance,即MyClass*類型。

PHP5CPP_*_OBJ_PARAMS()PHP5CPP_*_NO_OBJ_PARAMS() 會在調用我們的函數/方法時被使用,它們會處理從PHP方傳進來的參數。在封裝函數和聲明中,可以通過zend_parse_parameters() 去分析這些參數。

你可以注意到,宏PHP5CPP_*_PARAMS() 是以 else結尾的。這樣的話,當所處理的不是一個對象時,它會試着用結構化方式去處理。這些宏都可以在下邊找到。

PHP 5寫的類處理的部分已經寫完了,下邊的部分是爲PHP 4寫的很簡單易懂的處理。如果我們用的是PHP 4,上邊的那部分代碼在預處理時會被乎略掉,而只處理下邊的這些代碼。如果我們用的是PHP 5則反之。

#else // End of PHP5-specific macros

// This stuff is for PHP 4. They're empty on purpose, obviously.

#define PHP5CPP_GET_THIS()
#define PHP5CPP_MYCLASS_OBJ_PARAMS(params)
#define PHP5CPP_MYCLASS_OBJ_NO_PARAMS()
#define PHP5CPP_REGISTER_CLASS(name, obj_name)

#define PHP5CPP_REGISTER_MYCLASS_RESOURCE(return_value, res, le)
    ZEND_REGISTER_RESOURCE(return_value, res, le);

#endif // End of PHP4-specific macros

非常的簡單,除了PHP5CPP_REGISTER_MYCLASS_RESOURCE()外其它的宏都是空的。PHP5CPP_REGISTER_MYCLASS_RESOURCE()仍會註冊一個resource,但不會爲對象做任何的檢查。好了,完成這些後,就使得代碼在PHP 4和PHP 5中都可以編譯通過了。

下邊的一些宏在PHP 4和PHP 5中都是一樣的,它們是處理結構和非面向對象代碼的。

// These are for both PHP 4 and 5

#define PHP5CPP_MYCLASS_RSRC_PARAMS(params)
    if (params == FAILURE) {
        RETURN_FALSE;
    }
    else {
        ZEND_FETCH_RESOURCE(myclass_instance, MyClass*, &resource, -1, "myclass", le_myclass);
    }

#define PHP5CPP_MYCLASS_RSRC_NO_PARAMS()
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &resource) == FAILURE) {
        RETURN_FALSE;
    }
    else {
        ZEND_FETCH_RESOURCE(myclass_instance, MyClass*, &resource, -1, "myclass", le_myclass);
    }

static ZEND_RSRC_DTOR_FUNC(destroy_myclass)
{
    if (rsrc->ptr) {
        delete (MyClass*) rsrc->ptr;
        rsrc->ptr = NULL;
    }
}

#endif

它們同樣是很簡單的:先試着分析PHP界面傳進來的參數,和取得要處理的resource。

destroy_myclass 負責資源回收。當觸發垃圾回收機制,或destroy 對象/ resource 時,它會把在創建C++對象實例時分配的內存清理/釋放掉。

php5cpp.cpp裏我們會這樣使用這些宏:

PHP5CPP_MYCLASS_OBJ_PARAMS(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &s, &len))
PHP5CPP_MYCLASS_RSRC_PARAMS(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &resource, &s, &len))

PHP 5中,經過預處理後會變成這樣:

zval* ōbject = getThis();
if (object) {
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &s, &len) == FAILURE) {
        RETURN_FALSE;
    }
    php5cpp_obj *obj = (php5cpp_obj *) zend_object_store_get_object(object TSRMLS_CC);
    myclass_instance = obj->u.myclass;
}
else if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &resource, &s, &len) == FAILURE) {
    RETURN_FALSE;
}
else {
    ZEND_FETCH_RESOURCE(myclass_instance, MyClass*, &resource, -1, "myclass", le_myclass);
}

可以看到,在面向對象模式下,它會試着去取得對象,分析參數和取得實際的MyClass對象。如果在非面向對象的模式下,這段代碼會試着用結構的方式去分析參數,取得resource等等。

PHP 4中,經過預處理後會變成了下邊這樣:

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &resource, &s, &len) == FAILURE) {
    RETURN_FALSE;
}
else {
    ZEND_FETCH_RESOURCE(myclass_instance, MyClass*, &resource, -1, "myclass", le_myclass);
}

在這裏(PHP 4中),面向對象的代碼會完全給忽略掉,只留下結構化程序的代碼。

爲了上邊我們做的有意義,現在我們要做一個最簡單的部分,寫我們的封裝代碼,和使得在PHP中可以使用我們的class。

3  封裝代碼

 

我們要在php_php5cpp.h中加入一些封裝函數和方法的定義:

PHP_FUNCTION(myclass_new);
PHP_FUNCTION(myclass_destroy);
PHP_FUNCTION(myclass_set_string);
PHP_FUNCTION(myclass_get_string);

簡單吧。好,那我們在php5cpp.cpp中寫入封裝函數的執行代碼:

PHP_FUNCTION(myclass_new)
{
    MyClass *myclass_instance;
    char* s;
    int len = 0;
    PHP5CPP_GET_THIS();

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|s", &s, &len) == FAILURE) {
        RETURN_FALSE;
    }

    if (len) {
        myclass_instance = new MyClass(string(s, len));
    }
    else {
        myclass_instance = new MyClass;
    }

    if (myclass_instance != NULL) {
        PHP5CPP_REGISTER_MYCLASS_RESOURCE(return_value, myclass_instance, le_myclass);
    }
}

PHP_FUNCTION(myclass_destroy)
{
    zval *resource;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &resource) == FAILURE) {
        RETURN_FALSE;
    }

    zend_list_delete(Z_RESVAL_P(resource));
}

PHP_FUNCTION(myclass_set_string)
{
    zval *resource;
    MyClass *myclass_instance;
    int len = -1;
    char *s;
    PHP5CPP_MYCLASS_OBJ_PARAMS(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &s, &len))
    PHP5CPP_MYCLASS_RSRC_PARAMS(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &resource, &s, &len))

    if (myclass_instance == NULL) {
        php_error(E_WARNING, "can't set string on null resource in %s()", get_active_function_name(TSRMLS_C));
        RETURN_FALSE;
    }
    else {
        myclass_instance->setString(string(s, len));
        RETURN_TRUE;
    }
}

PHP_FUNCTION(myclass_get_string)
{
    zval *resource;
    MyClass *myclass_instance = NULL;
    string retval;
    PHP5CPP_MYCLASS_OBJ_NO_PARAMS()
    PHP5CPP_MYCLASS_RSRC_NO_PARAMS()

    if (myclass_instance == NULL) {
        php_error(E_WARNING, "can't get string from null resource in %s()", get_active_function_name(TSRMLS_C));
        RETURN_FALSE;
    }
    else {
        retval = myclass_instance->getString();
        RETVAL_STRINGL((char*) retval.data(), retval.length(), 1);
    }
}

這裏沒什麼太難的地方,所以我不想每個函數都解釋一次。現在就只簡單看一下myclass_new()myclass_set_string()做爲例子。

myclass_new()中,首先嚐試取得一個對象。再次說明,只在面向對象方式下才會生效,在PHP 4下會被預處理所忽略掉。

因爲C++類的構造函數有默認的參數,所以我們要看看PHP是否有傳一個string類型的參數過來。分析完傳進來的參數後,就爲分配myclass_instance內存空間,然後用PHP5CPP_REGISTER_MYCLASS_RESOURCE把得到的resource貯存起來。在PHP 5的OO模式下,PHP5CPP_REGISTER_MYCLASS_RESOURC會把resource當成對象貯存,在PHP 4的結構化模塊下只會簡單的創建一個resource。

接着說myclass_set_string()...

首先,先定義一個zval結構去處理在PHP用結構化接口傳進來的參數。myclass_instance會被用來指向實際的C++對象。len*s用來保存從PHP方傳來的字串和字串的長度。

PHP 4下,PHP5CPP_MYCLASS_OBJ_PARAMS()會被預處理忽略。在PHP 5它會嘗試取得對象,如果獲取對象失敗,否會返回使用結構化接口的步驟。

在取得myclass_instance後,可以像平時使用類那樣使用C++ MyClass類。先調用MyClass的setString()方法。然後把得到的C++標準string類對象轉成類似C方式的char指針,並傳回給PHP方。

最後,要讓PHP知道這個新class,很簡單,在PHP_MINIT_FUNCTION中加入:

PHP5CPP_REGISTER_CLASS(myclass, "MyClass");

le_myclass = zend_register_list_destructors_ex(destroy_myclass, NULL, "myclass", module_number);
好了,到這裏代碼已經寫完了。:P
 
發佈了41 篇原創文章 · 獲贊 5 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章