96000


PHPのFFIを使用して実行結果の格納された FFI\CData オブジェクトのプロパティの存在を確認しようと、property_exists
を使用した所、Cannot use object of type FFI\CData as array というエラーになってしまいました。

FFI\CData のプロパティは、通常のオブジェクトとは何か違うのでしょうか？

<https://www.php.net/manual/en/class.ffi-cdata.php>
には以下のように書かれているので、全ての機能が使えないのかもしれません。

> Note: Notable limitations are that FFI\CData instances do not support
> isset(), empty() and unset(), and that wrapped C structs and unions do not
> implement Traversable.
[code]

    <?php
    
    use FFI\CData ;
    
    $DEF=<<<'EOF'
    
    typedef struct mecab_t  mecab_t;
    
    struct mecab_dictionary_info_t {
      const char                     *filename;
      const char                     *charset;
      unsigned int                    size;
      int                             type;
      unsigned int                    lsize;
      unsigned int                    rsize;
      unsigned short                  version;
      struct mecab_dictionary_info_t *next;
    } ;
    
    mecab_t*      mecab_new2(const char *arg);
    const struct mecab_dictionary_info_t* mecab_dictionary_info(mecab_t *mecab);
    EOF;
    
    try {
    
        echo phpversion() . "\n" ;
    
        $libmecab = FFI::cdef($DEF,'/usr/lib/x86_64-linux-gnu/libmecab.so') ;
    
        $mecab = $libmecab->mecab_new2('') ;
        $rc = $libmecab->mecab_dictionary_info($mecab) ;
    
        $mecab_dictionary_info_t = $rc[0] ;
    
        print_r($mecab_dictionary_info_t) ;
    
        $k = 'filename' ;
    
        printf("[%s]\n",$mecab_dictionary_info_t->filename) ;
        printf("[%s]\n",$mecab_dictionary_info_t->$k) ;
    
        if(property_exists($mecab_dictionary_info_t,$k)){
            echo "存在する\n" ;
        }else{
            echo "存在しない\n" ;
        }
    
    }catch (Exception $e){
        printf("[%s]\n",$e->getMessage()) ;
    }
    
[/code]

実行結果

[code]

    /usr/local/php83/bin/php ffi.php
    8.3.0beta3
    FFI\CData:struct mecab_dictionary_info_t Object
    (
        [filename] => /var/lib/mecab/dic/debian/sys.dic
        [charset] => UTF-8
        [size] => 879221
        [type] => 0
        [lsize] => 15626
        [rsize] => 15388
        [version] => 102
        [next] =>
    )
    [/var/lib/mecab/dic/debian/sys.dic]
    [/var/lib/mecab/dic/debian/sys.dic]
    
    Fatal error: Uncaught Error: Cannot use object of type FFI\CData as array in /usr/local/Berdysh/PHP/SRC/ffi.php:42
    Stack trace:
    #0 /usr/local/Berdysh/PHP/SRC/ffi.php(42): property_exists(Object(FFI\CData:struct mecab_dictionary_info_t), 'filename')
    #1 {main}
      thrown in /usr/local/Berdysh/PHP/SRC/ffi.php on line 42
    
[/code]


CC BY-SA 4.0
2023-08-19T09:11:11.963
95985
2023-08-21T14:18:55.667
2023-08-20T06:41:36.263
3060
59393
question
php
PHP FFI オブジェクトのプロパティが property_exists でエラーになる


php-src についてはそこまで詳しくないため，ご自分で確認されることをおすすめしますが，分かる範囲で説明します．

PHP のオブジェクト情報は， `zend_object` という構造体で定義されています:

[code]

    struct _zend_object {
        zend_refcounted_h gc;
        uint32_t          handle; // TODO: may be removed ???
        zend_class_entry *ce;
        const zend_object_handlers *handlers;
        HashTable        *properties;
        zval              properties_table[1];
    };
    
[/code]

ソース（https://github.com/php/php-
src/blob/1887f02b0b39f1cdb89343a4d9fd9372aedb33b6/Zend/zend_types.h#L547-L554）  
これは，すべての PHP オブジェクトに共通する基本的な情報を持っていて，クラスやプロパティに関する情報を含んでいます．

そして， FFI によって読み込まれた C 言語のソースコードから作られるオブジェクト情報は `zend_ffi_cdata`
という構造体で定義されています：

[code]

    typedef struct _zend_ffi_cdata {
        zend_object            std;
        zend_ffi_type         *type;
        void                  *ptr;
        void                  *ptr_holder;
        zend_ffi_flags         flags;
    } zend_ffi_cdata;
    
[/code]

ソース（https://github.com/php/php-
src/blob/278a57f95a30253d2a462baca43144f4dabad4c6/ext/ffi/ffi.c#L189C1-L195C18）

また，今回エラーの原因となった property_exists() の実装は，以下のようになっています：

[code]

    ZEND_FUNCTION(property_exists)
    {
        zval *object;
        zend_string *property;
        zend_class_entry *ce;
        zend_property_info *property_info;
    
        /* We do not use Z_PARAM_OBJ_OR_STR here to be able to exclude int, float, and bool which are bogus class names */
        if (zend_parse_parameters(ZEND_NUM_ARGS(), "zS", &object, &property) == FAILURE) {
            RETURN_THROWS();
        }
    
        if (Z_TYPE_P(object) == IS_STRING) {
            ce = zend_lookup_class(Z_STR_P(object));
            if (!ce) {
                RETURN_FALSE;
            }
        } else if (Z_TYPE_P(object) == IS_OBJECT) {
            ce = Z_OBJCE_P(object);
        } else {
            zend_argument_type_error(1, "must be of type object|string, %s given", zend_zval_value_name(object));
            RETURN_THROWS();
        }
    
        property_info = zend_hash_find_ptr(&ce->properties_info, property);
        if (property_info != NULL
         && (!(property_info->flags & ZEND_ACC_PRIVATE)
          || property_info->ce == ce)) {
            RETURN_TRUE;
        }
    
        if (Z_TYPE_P(object) ==  IS_OBJECT &&
            Z_OBJ_HANDLER_P(object, has_property)(Z_OBJ_P(object), property, 2, NULL)) {
            RETURN_TRUE;
        }
        RETURN_FALSE;
    }
    
[/code]

ソース（https://github.com/php/php-
src/blob/278a57f95a30253d2a462baca43144f4dabad4c6/Zend/zend_builtin_functions.c#L941-L977）

この関数のなかで実際にプロパティ情報を取りに行っているのは関数後半の以下の部分です：

[code]

    property_info = zend_hash_find_ptr(&ce->properties_info, property);
    if (property_info != NULL
     && (!(property_info->flags & ZEND_ACC_PRIVATE)
      || property_info->ce == ce)) {
      RETURN_TRUE;
    }
    
    if (Z_TYPE_P(object) ==  IS_OBJECT &&
      Z_OBJ_HANDLER_P(object, has_property)(Z_OBJ_P(object), property, 2, NULL)) {
      RETURN_TRUE;
    }
    RETURN_FALSE;
    
[/code]

まず，最初の if について．

PHP 内部では，クラスエントリという，クラスの情報を管理する構造体を持っていて，クラスのメソッドやプロパティ，定数などの情報を持っています．  
このクラスエントリは PHP のプロセス起動後作成され，PHP
のコードが読み込まれるとき（クラスが初めて使用されるときやオートローダで読み込まれるとき）に各クラスの情報が格納されます．  
クラスエントリはハッシュテーブルとして実装されていて，効率よく情報が管理できるようになっています．

`zend_object` が持っている `ce` というのが，クラスエントリへのポインタですね．

該当コードでは，`zend_hash_find_ptr`
を使って渡されたオブジェクト（zend_object）のプロパティ情報にアクセスし，指定されたプロパティ情報が存在するかをチェックしています．  
今回，FFI によって読み込まれた `zend_ffi_cdata` に対して，property_exists
を使ってプロパティの存在確認を行っているわけですね．

FFI では，C 言語で定義された構造体を PHP 上で扱えるように処理しますが，言語構造上完全に対応させることができないため，`zend_object`
や，そのクラスエントリへのポインタが指すクラス情報を埋めきるだけの情報がなさそうです（C 言語 の構造体には，PHP
のオブジェクトがもつようなメタデータやメソッドが存在しないため）．

`zend_ffi_cdata` は内部的に `zend_object` を持っていますが，その `ce` が指すクラスエントリに
property_info が含まれているとは限らないということで，おそらく無いんだと思います．

isset(), empty() and unset() が使えないのもおそらく同様の理由です．

したがって，ここの if で RETURN_TRUE されることはなさそうです．

次に，後半の if についてです．

今回のエラー（Cannot use object of type FFI\CData as array）はおそらくこの部分で起きています．

`Z_OBJ_HANDLER_P(object, has_property)` というマクロが使われている部分で，object （今回は
zend_ffi_cdata）に登録されているハンドラを呼び出しています．

このマクロは，オブジェクトのハンドラを取得して，その中の `has_property` 関数を呼び出しています．

このハンドラは，以下の部分で登録されています（長いので部分的に切り出します）

[code]

    memcpy(&zend_ffi_cdata_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
    zend_ffi_cdata_handlers.get_constructor      = zend_fake_get_constructor;
    zend_ffi_cdata_handlers.free_obj             = zend_ffi_cdata_free_obj;
    zend_ffi_cdata_handlers.clone_obj            = zend_ffi_cdata_clone_obj;
    zend_ffi_cdata_handlers.read_property        = zend_ffi_cdata_read_field;
    zend_ffi_cdata_handlers.write_property       = zend_ffi_cdata_write_field;
    zend_ffi_cdata_handlers.read_dimension       = zend_ffi_cdata_read_dim;
    zend_ffi_cdata_handlers.write_dimension      = zend_ffi_cdata_write_dim;
    zend_ffi_cdata_handlers.get_property_ptr_ptr = zend_fake_get_property_ptr_ptr;
    zend_ffi_cdata_handlers.has_property         = zend_fake_has_property;
    zend_ffi_cdata_handlers.unset_property       = zend_fake_unset_property;
    zend_ffi_cdata_handlers.has_dimension        = zend_fake_has_dimension;
    zend_ffi_cdata_handlers.unset_dimension      = zend_fake_unset_dimension;
    zend_ffi_cdata_handlers.get_method           = zend_fake_get_method;
    zend_ffi_cdata_handlers.get_class_name       = zend_ffi_cdata_get_class_name;
    zend_ffi_cdata_handlers.do_operation         = zend_ffi_cdata_do_operation;
    zend_ffi_cdata_handlers.compare              = zend_ffi_cdata_compare_objects;
    zend_ffi_cdata_handlers.cast_object          = zend_ffi_cdata_cast_object;
    zend_ffi_cdata_handlers.count_elements       = zend_ffi_cdata_count_elements;
    zend_ffi_cdata_handlers.get_debug_info       = zend_ffi_cdata_get_debug_info;
    zend_ffi_cdata_handlers.get_closure          = zend_ffi_cdata_get_closure;
    zend_ffi_cdata_handlers.get_properties       = zend_fake_get_properties;
    zend_ffi_cdata_handlers.get_gc               = zend_fake_get_gc;
    
[/code]

ソース（https://github.com/php/php-
src/blob/1887f02b0b39f1cdb89343a4d9fd9372aedb33b6/ext/ffi/ffi.c#L5356-L5505）

これを見る限り，`has_property` 関数には実際には `zend_fake_has_property`
が登録されていて，これの実装が以下の通りです：

[code]

    static ZEND_COLD int zend_fake_has_property(zend_object *obj, zend_string *member, int has_set_exists, void **cache_slot) /* {{{ */
    {
        zend_bad_array_access(obj->ce);
        return 0;
    }
    
[/code]

ソース（https://github.com/php/php-
src/blob/1887f02b0b39f1cdb89343a4d9fd9372aedb33b6/ext/ffi/ffi.c#L5097-L5101）

また，この内部で呼び出されている `zend_bad_array_access` の実装が以下の通りです：

[code]

    static ZEND_COLD zend_never_inline void zend_bad_array_access(zend_class_entry *ce) /* {{{ */
    {
        zend_throw_error(NULL, "Cannot use object of type %s as array", ZSTR_VAL(ce->name));
    }
    
[/code]

ソース（https://github.com/php/php-
src/blob/1887f02b0b39f1cdb89343a4d9fd9372aedb33b6/ext/ffi/ffi.c#L5045）

やっと，同様のエラー文が出てきました．

いろいろ php-src の実装を追いましたが，結局のところ，FFI によって読み込まれた C 言語の構造体は通常の PHP
のオブジェクトと同様に扱えるように見えて，内部的には違いがあるため，PHP の標準関数が使えないことがあります．

また，内部的には，それを考慮したハンドラが設定されていて，

> Cannot use object of type %s as array

のような直感的ではないエラーが出力される，というわけですね．


CC BY-SA 4.0
2023-08-21T10:22:47.267
96000
2023-08-21T14:18:55.667
2023-08-21T14:18:55.667
58973
58973
95985
answer