실제 개발에서 일어날 수 있는 삽질을 방지하거나 팁 따위를 적어둔다.




empty()


empty()는 사실 language construct이지 함수가 아니다. (http://www.php.net/empty 참조)
empty, isset, unset 등이 이런 language construct에 해당한다.
이런 것들은 PHP 소스코드 내에 가져다 쓰기 좋게 정의되어 있는게 아니므로, 따로 처리해줘야 한다.

다음은 empty()의 함수버전이다.
empty() 조건에 맞게 zval을 검사해서 empty()와 동일한 결과값을 반환하는 함수를 직접 만들었다.

zend_bool empty(zval *val)
{
    switch (Z_TYPE_P(val)) {
        case IS_STRING:
            if (Z_STRLEN_P(val)==0 || !strcmp(Z_STRVAL_P(val), "0")) return 1;
            break;
        case IS_LONG:
            if (Z_LVAL_P(val)==0) return 1;
            break;
        case IS_DOUBLE:
            if (Z_DVAL_P(val)==0) return 1;
            break;
        case IS_NULL:
            return 1;
            break;
        case IS_BOOL:
            if (Z_BVAL_P(val)==0) return 1;
            break;
        case IS_ARRAY:
            if (zend_hash_num_elements(Z_ARRVAL_P(val))==0) return 1;
            break;
    }

    return 0;
}











include / require


include 나 require 계열도 따로 함수로 정해져 있지 않다.
이런 것들은 그냥 속편하게 zend_eval_string()을 쓰는 것이 좋은 것 같다.
zend_eval_string()은 PHP 코드를 그대로 실행시키는 함수이다.

zend_eval_string
ZEND_API int zend_eval_string(char *str, zval *retval_ptr, char *string_name TSRMLS_DC);

zend_eval_string의 파라메터
char *str : 실행시킬 PHP 코드
zval *retval_ptr : 실행결과 리턴값을 담을 변수
char *string_name : 그냥 이름...

zend_eval_string_ex() 는 마지막에 예외를 처리할 핸들러를 하나 더 받는다.

다음처럼 사용한다.

1. require_once (반환값 없이 코드만 포함시킴)
void _require_once(char *filename TSRMLS_DC)
{
    char *command = NULL;
    spprintf(&command, 0, "require_once('%s');", filename);
    zend_eval_string(command, NULL, "require_once" TSRMLS_CC);
    efree(command);
}
#define require_once(filename) _require_once(filename TSRMLS_CC)


2. @include (반환값 있음)
zval *_include(char *filename TSRMLS_DC)
{
    char *command = NULL;
    zval *ret_val = NULL;
    MAKE_STD_ZVAL(ret_val);
    spprintf(&command, 0, "@include('%s');", filename);
    zend_eval_string(command, ret_val, "include" TSRMLS_CC);
    efree(command);
    return ret_val;
}
#define include(filename) _include(filename TSRMLS_CC)










함수 후킹


PHP의 함수가 EG(function_table)이라는 HashTable로 저장된다는 점을 이용하여,
built-in 함수를 후킹하는 것이 가능하다.
시점이 중요한데, MINIT에서는 아직 EG(function_table)이 세팅되지 않은 상태라 RINIT에서 해야한다.

MINIT에서 EG(function_table)의 주소를 찍어보면 NULL일 때도 있고, 주소값이 나올 때도 있지만,
주소값이 나올 때에도 유효한 값은 아니다.
RINIT에서 찍어본 주소값과는 확연히 다른 주소값을 나타낸다.


다음은 RINIT에서 built-in 함수인 md5를 내가 만든 함수로 후킹하는 예제이다.
후킹이 성공했을 경우, md5() 함수를 호출하면 단지 'md5'라는 문자열만 반환된다.
(같은 원리로 __autoload 같은 magic method도 후킹이 가능하다.)

// RINIT
PHP_RINIT_FUNCTION(myext)
{
    static zend_function *orig_func;
    if (orig_func==NULL) {
        zend_function *new_func;
        // 내가 만든 함수를 찾아놓고
        zend_hash_find(EG(function_table), "myext_md5", strlen("myext_md5")+1, (void**)&new_func);
        // built-in md5()를 찾아서
        if (zend_hash_find(EG(function_table), "md5", strlen("md5")+1, (void**)&orig_func)==SUCCESS) {
            // 내가 만든 함수와 바꿔치기
            zend_hash_update(EG(function_table), "md5", strlen("md5")+1, new_func, sizeof(zend_function, NULL);
        }
    }

    return SUCCESS;
}

// 바꿔치고자 하는 함수
PHP_FUNCTION(myext_md5)
{
    RETURN_STRING("md5", 1);
}










Constructor


PHP에서 클래스의 생성자는 2가지이다.
하나는 클래스 자기자신의 이름과 같은 method이며, 다른 하나는 __construct() 이다.
PHP Extension에서 어떤 클래스의 생성자를 호출할 경우 어느건지 알고 호출해야 한다.
(호출할 때 매개변수가 '함수이름'이니까...)
클래스의 생성자는 zend_class_entry에 정의되어 있다.
(&zend_class_entry->constructor->common.function_name)

거기다가 생성자든 뭐든 함수를 호출할 때는 함수이름이 전부 소문자로 저장되어 있으므로 strtolower를 해줘야 한다.

이걸 일일이 해주기 귀찮으므로 매크로를 하나 만든다.

#define CONSTRUCTOR(class_entry) strtolower((class_entry)->constructor->common.function_name)

사용예제는 다음과 같다.

myext.c
// 타이핑 한 번이라도 줄이기 위한 매크로
#define strtolower(str) php_strtolower(str, strlen(str))

// 클래스 생성자를 구하는 매크로
#define CONSTRUCTOR(class_entry) strtolower((class_entry)->constructor->common.function_name)

// 클래스를 생성해서 반환하는 함수
PHP_FUNCTION(myext_get_my_class)
{
    char *className;
    int className_len;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &className, &className_len)==FAILURE) {
        RETURN_NULL();
    }

    zend_class_entry *class_p, **class_pp;
    zval *zClass;
    MAKE_STD_ZVAL(zClass);
    // 클래스 찾기
    if (zend_lookup_class(className, className_len, &class_pp TSRMLS_CC)==SUCCESS) {
        class_p = *class_pp;
        // 클래스 초기화
        object_init_ex(zClass, class_p);
        // 생성자 호출 (call_method는 임의로 만든 함수임. 클래스가 정리된 챕터 참조)
        call_method(&zClass, class_p, CONSTRUCTOR(class_p), NULL, 0, NULL);
        RETURN_ZVAL(zClass, 1, 1);
    }

    RETURN_NULL();
}


호출
class aClass
{
    public $prop = null;

    public function __construct()
    {
        $this->prop = 'not null';
        var_dump('aClass constructor');
    }
}

class bClass
{
    public $p = 1;

    public function bClass()
    {
        $this->p += 1;
        var_dump('bClass constructor');
    }
}

$cls = myext_get_my_class('aClass');
var_dump($cls);
$cls = myext_get_my_class('bClass');
var_dump($cls);



결과
string 'aClass constructor' (length=18)

object(aClass)[1]
    public 'prop' => string 'not null' (length=8)

string 'bClass constructor' (length=18)

object(bClass)[2]
    public 'p' => int 2










Call by Reference


zend_parse_parameters로 받는 zval 계열의 파라메터는 전부 call by reference 이다.
그래서 따로 처리해주지 않은 상태에서, 받은 파라메터 자체에 변경을 가하면 user space에 그대로 반영된다.
멍때리며 작업하다 배열을 망치는 일이 없도록 주의해야 한다.

아래에 PHP 함수와 그에 대응되는 C 함수를 나열해서 그 차이를 설명한다.


PHP - call by reference
function f1(&$a)
{
    $a[0] = 3;
}


C - call by reference
PHP_FUNCTION(myext_f1)
{
    zval *p;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &p)==FAILURE) {
        RETURN_NULL();
    }

    zval *e;
    MAKE_STD_ZVAL(e);
    ZVAL_LONG(e, 3);

    zend_hash_index_update(Z_ARRVAL_P(p), 0, &e, sizeof(zval*), NULL);
}





PHP - call by value
function f2($a)
{
    $a[0] = 3;
    return $a;
}


C - call by value
// 1. 명시적으로 zval_copy_ctor 를 해주는 방법
PHP_FUNCTION(myext_f2)
{
    zval *p;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &p)==FAILURE) {
        RETURN_NULL();
    }

    // 받아온 파라메터를 임의의 공간에 복사하여 할당한다.
    zval *tmp;
    MAKE_STD_ZVAL(tmp);
    *tmp = *p;
    zval_copy_ctor(tmp);

    zval *e;
    MAKE_STD_ZVAL(e);
    ZVAL_LONG(e, 3);

    zend_hash_index_update(Z_ARRVAL_P(tmp), 0, &e, sizeof(zval*), NULL);
    RETURN_ZVAL(tmp, 1, 1);
}

// 2. 파라메터를 받을 때 분리시키는 방법
PHP_FUNCTION(myext_f2)
{
    zval *p;
    // "a/" 로 파라메터를 받는다.
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a/", &p)==FAILURE) {
        RETURN_NULL();
    }

    zval *e;
    MAKE_STD_ZVAL(e);
    ZVAL_LONG(e, 3);

    zend_hash_index_update(Z_ARRVAL_P(p), 0, &e, sizeof(zval*), NULL);
    RETURN_ZVAL(p, 1, 1);
}


위에 있는 함수들을 호출해보면 차이를 알 수 있을 것이다.
같은 방식으로 만약 문자열을 call by reference로 받고 싶다면,
zend_parse_parameters() 에서 "s" 로 받지 말고 "z" 로 받아서 처리해주면 된다.










var_args


갯수도 형식도 정해지지 않은 var_args 형태의 파라메터를 받는 함수는 다음과 같이 사용할 수 있다.
아래와 같은 함수를 만들고 아무 파라메터나 몇 개든 넣으면서 테스트 해보자.

PHP_FUNCTION(myext_f)
{
    // 파라메터가 없다면 wrong param count warning
    if (ZEND_NUM_ARGS==0) {
        WRONG_PARAM_COUNT;
    }

    int argc = ZEND_NUM_ARGS();
    zval ***argv;
    // 파라메터를 받아올 공간 확보
    argv = (zval***)emalloc(sizeof(zval**) * argc);
    // 파라메터를 받아옴
    if (zend_get_parameters_array_ex(argc, argv)==FAILURE) {
        efree(argv);
        WRONG_PARAM_COUNT;
    }
    // 받아온 파라메터를 하나씩 var_dump로 출력
    int i;
    for (i=0; i<argc; i++) {
        php_var_dump(argv[i], 1 TSRMLS_CC);
    }
    // 해제
    efree(argv);
}










Error


파라메터가 이상한 게 넘어왔거나 했을 경우,
Extension 에서 에러처리를 해줄 수 있다.
php_error() 함수를 사용한다.

아래 예제의 함수를 PHP 소스에서 호출하면, 그 시점에서 에러가 발생하며 정해놓은 메세지가 출력된다.

PHP_FUNCTION(myext_f)
{
    zval *p;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &p)==FAILURE) {
        RETURN_NULL();
    }

    // convert_to_XXX 시리즈는 해당 zval을 XXX 형태로 강제로 변형시킨다.
    // 시간을 들여 소스를 뒤져서 찾아볼 가치는 있다.
    convert_to_string(p);

    // 에러 처리
    php_error(E_ERROR, "This is E_ERROR :: param = %s", Z_STRVAL_P(p));
}


php_error()의 첫번째 파라메터는 다음과 같은 상수값을 가진다.

 상수  설명
 E_ERROR 에러를 표시하며 스크립트의 실행을 즉각 중지한다.
 E_WARNING 일반 경고를 표시하며 스크립트의 실행은 계속 된다.  
 E_PARSE 파서 에러를 표시한다. 스크립트의 실행은 계속 된다. 
 E_NOTICE notice를 표시하며, 스크립트의 실행은 계속 된다.
notice는 php.ini에서 기본적으로 꺼져있다. 
 E_CORE_ERROR 코어에서 발생하는 internal error를 표시한다.
user space에서는 사용할 수 없다. 
 E_COMPILE_ERROR 컴파일러에서 발생하는 internal error를 표시한다.
user space에서는 사용할 수 없다. 
 E_COMPILE_WARNING 컴파일러에서 발생하는 internal warning.
user space에서는 사용할 수 없다.










Posted by bloodguy
,