PHP扩展开发之对象处理器(Object Handlers)

在前面的博文中,已经介绍过一些 object handlers 了,也特别介绍了如何通过指定 handlers 来创建一个自定义的结构和使用clone_obj来对自定义的结构进行克隆操作。 然而,这只是开始:在 php 中,几乎所有的对象操作,都可以通过 object handlers 来实现,而且所有的魔术方法和魔术接口在内核中都是实现了对应的 object handler。此外, 一些 handlers 并没有开放给用户端的 php,例如,内部类可以自定义类的比较操作,而使用 php 代码是无法实现的。

由于 php 中有很多不同的 object handlers,这里只挑几个来讨论,其它的只给出简单的说明。

下面列举出 php 中主要的 26 个(php5.6 中为 28 个)object handlers(位于 phpsrc/Zend/zend_object_handlers.h),并给出简要的说明。

zval *read_property(zval *object, zval *member, int type, const struct _zend_literal *key TSRMLS_DC)
void write_property(zval *object, zval *member, zval *value, const struct _zend_literal *key TSRMLS_DC)
int has_property(zval *object, zval *member, int has_set_exists, const struct _zend_literal *key TSRMLS_DC)
void unset_property(zval *object, zval *member, const struct _zend_literal *key TSRMLS_DC)
zval **get_property_ptr_ptr(zval *object, zval *member, const struct _zend_literal *key TSRMLS_DC)

上述 handlers 分别表示__get__set__isset__unset方法。get_property_ptr_ptr等同于__get返回一个引用类型。zend_literal *key作为这些函数的参数 起到优化作用,例如它包含了一些将属性名进行 hash 计算的结果。

PHP扩展开发之打造一个简易的ArrayBuffer

ArrayBuffer 又叫二进制数组,是一个用来表示通用的,固定长度的二进制数据缓冲区。你不能直接操纵 ArrayBuffer 的内容, 而是创建一个表示特定格式的 buffer 的类型化数组对象(也叫做数据视图对象)来对 buffer 的内容进行读写操作。

我最早了解 ArrayBuffer 是从 JavaScript 开始的,具体的用法和 api 可以参考JavaScript 标准库--ArrayBuffer

那么接下来,我们就给 PHP 扩展一个简单的 ArrayBuffer,顺便巩固一下php 扩展开发之自定义对象的存储

ArrayBuffer是一个非常简单的对象,它只需要申明并存储一个buffer和它的长度即可:

typedef struct _buffer_object {
    zend_object std;
    void *buffer;
    size_t length;
} buffer_object;

接下来我们来实现它的createfree handlers,有了前面的基础,这个实现也是及其简单的:

PHP扩展开发之自定义对象的存储

对于 php 扩展开发,很多人可能已经不那么陌生了,zend 引擎为了们提供了非常丰富了函数和 macro,来帮助我们很快速的创建一个标准的 php 类,然而,当我们在使用自定义的数据结构(struct), 并想把我们自己定义的数据结构封装成 php 的类的时候可能就会有些困惑,因为我们都知道 php 中的所有变量都是通过 zval 来存储的,而我们自定义的数据结构要怎样才能和 zval 实现完美的对接呢? 以前我通常采用的一种方式就是使用 zend 引擎提供的资源类型,因为资源类型的封装中包含了通用的数据类型,而且有很丰富的函数来操作资源,所以使用起来很简单也很方便。然而,强大的 zend 引擎真的没有其他方式扩展数据结构了吗?当然不是!下面就来介绍一个更加优雅的方式。要弄明白,首要要搞清 php 内核是如何创建对象的。

我们首先来探讨下如何创建一个 PHP 对象。为此我们将会用到object_and_properties_init之类的一些宏。

// 创建一个SomeClass类型的对象,并且把properties_hashtable中的变量作为其属性值
zval *obj;
MAKE_STD_ZVAL(obj);
object_and_properties_init(obj, class_entry_of_SomeClass, properties_hashtable);

// 创建一个没有属性的对象
zval *obj;
MAKE_STD_ZVAL(obj);
object_init_ex(obj, class_entry_of_SomeClass);
// = object_and_properties_init(obj, class_entry_of_SomeClass, NULL)

// 创建一个stdClass的对象
zval *obj;
MAKE_STD_ZVAL(obj);
object_init(obj);
// = object_init_ex(obj, NULL) = object_and_properties_init(obj, NULL, NULL);

在上面的例子中,最后一种情况下,当你创建一个stdClass的对象后,通常将会给它添加属性。这时如果使用 zend_update_property之类的函数,是不起作用的,取而代之的是add_property宏函数:

数据结构之hashtable

哈希表又叫散列表,是实现字典操作的中有效数据结构。通常来说,一个 hash table 包含了一个数据,其中的数据通过 index 来访问。 而 hash table 的基本原理就是通过 hash 函数建立起所有可能的 index 与其对应的位置的联系。一个 hash 函数接收一个 key,返回其 hash code, key 的类型是可变的,而 hash code 是一个整型。

由于计算一个 hash 值和通过 index 访问一个数据都是常量级的时间复杂度,所以我们可以通过这中特性实现常量级时间复杂度的查找。 如果一个 hash 函数能够保证不会有两个不同的 key 生成相同的 hash 值,那么这样的 hash table 就被称为是直接定址。然而,这只是一想法而已, 实际上这种 hash table 在现实中却是不常用的。

链式 hash 表从本质上来讲,就是一个存放了一组链表的数组。每个链表可以看做是一个槽,我们把元素通过 hash 函数找到一个 hash 值,然后把元素的值 放入到数组中与改 hash 值对应的槽中。

chained hashtable
链地址法解决哈希冲突

当有两个 key 被 hash 到了同一个位置,就会产生冲突。链式 hash 表有一种简单的冲突解决办法:当冲突产生时,元素被简单的放在同一个槽里。这样做可能带来的问题就是, 如果在同一个位置上出现很多冲突,这个槽就会变得越来越长,这样当我们访问这个槽中的元素的时候,所花的时间也就会越来越长。

c语言之struct

在 c 语言中,结构体(struct)是一种复合数据类型,用来将一系列相同或不同类型的变量聚集在同一个内存区间内并赋予同一个名字,使得通过一个指针就能访问集合中的所有成员。结构体中可以包含许多简单或符合数据类型,因此从内存分配上看,有点类似数组,而从变量组织上看,又类似于面向对象编程语言中的类。

定义一个 struct 非常简单,使用struct关键字即可。

struct student {
    char *name;
    int age;
};

通常有四种常用的 struct 初始化方式。

Java Native Interface(三)

前面系统研究了 JNI 的相关操作,下面就来小试牛刀,做一个实际的练习。

记得去年我曾经用 C 语言写过一个 PHP 的 md5 扩展函数,那么今天就花一点点时间用 JNI 来实现一遍吧。

不过这里可要提前声明了,虽然是实现 md5 函数,但是这里并不会从头写 md5 算法,而是投机取巧使用到了 linux 内核提供的crypto库。

废话不多说,首先来写一个 Java 类

MyString.java

public class MyString {
    static {
        System.loadLibrary("mymd5");
    }

    private String value;

    public native String md5();

    public MyString(String value) {
        this.value = value;
    }
}

然后生成头文件,并实现 c 代码:

Java Native Interface(二)

JNI 中定义了一下类型来对应到相应的 Java 的数据类型:

1. Java 基本数据类型: jint,jbyte,jshort,jlong,jfloat,jdouble,jchar,jboolean分别对应 Java 中的int,byte,short,long,float,double,charboolean

2. Java 引用类型: jobject对应java.lang.object。同时也定义了下列子类型:

  • jclass对应java.lang.Class
  • jstring对应java.lang.String
  • jthrowable对应java.lang.Throwable
  • jarray对应 Java 中的数组。Java 中的数组由 8 种基本数据类型和一个Object类型派生二来,所以 JNI 中也存在jintArray,jbyteArray,jshortArray,jlongArray,jfloatArray,jdoubleArray, jcharArray,jbooleanArrayjobjectArray

native 函数接收和返回上述的 JNI 类型数据。如果 native 函数需要操作它自己的数据类型(如 c 语言中的 int, char *),那么就需要在 JNI 类型和本地类型之间做相应的转换。

简而言之,native 函数的编写流程大致为:

  1. 通过 Java 程序接收 JNI 类型的参数
  2. 将接收的 JNI 类型转换成本地类型
  3. 完成相应的操作
  4. 创建一个需要返回的 JNI 类型的对象,然后将返回的数据 copy 到要返回的对象中
  5. 返回

从上述流程可以看出,编写 JNI 程序主要的挑战在于数据类型之间的转换,然而 JNI 中提供了很多转换函数来帮助我们完成相应的操作。

JNI 是一个 c 语言的接口,c 语言并不支持 OOP 的特性(严格的说,OOP 是一种理念,这里只是从语言本身来说 c 语言不支持面向对象,实际上用 c 语言也可以写出面向对象风格的程序!),所以他们之间并不是真的通过对象来传递。

彻底掌握malloc

说明:参考文献地址 A Malloc Tutorial

malloc是干什么的?如果你连这个名字都没听过,那么你应该先去了解 Unix 环境下的 c 语言开发,然后再来阅读。对一个程序员而言,malloc是一个在 c 语言中用来分配内存的函数,但是大多数人并不知道它背后真正的原理,甚至有些人认为malloc是 c 语言的关键字或者认为它是系统调用。事实上,malloc是一个再简单不过的函数而已,而且只需要很少的操作系统相关知识就可以让我们彻底理解它的原理。

下面来一步步的实现一个简单的malloc函数,从而帮助我们理解其背后运作的原理。因为仅仅作为说明原理之用,所以这里实现的malloc不会太高效,但是足以说明原理。

什么是 malloc

malloc(3)是一个用来分配内存块的标准的 c 语言库函数。它遵循以下规则:

  • malloc至少分配所需字节数的内存;
  • malloc返回其所分配内存空间(程序可以成功读写的空间)的指针;
  • 一块内存一旦被malloc分配,其他malloc调用不能再分配该内存块的任何部分,除非指向该内存块的指针被释放掉;
  • malloc应该是可控的:他必须能够很快完成分配并返回;
  • malloc同时应该提供重新分配内存块大小和释放内存的功能

malloc函数必须遵循以下原型:

void *malloc(size_t size);

其中size是所需要的内存大小。如果失败(没有足够的内存空间可以分配),应该返回NULL

Java Native Interface(一)

最近在整理学习笔记的时候发现了去年年中记录的 JNI 学习笔记,由于存放在了为知笔记中,而如今为知笔记已经不再免费,于是想到了将其重新整理一遍,一来可以巩固所学,二来能将其迁移到本地

有时候,使用 native code(c/c++)来克服 Java 中的内存管理和性能的局限性是很有必要的。Java 支持 native codes,被称作 Java Native Interface(JNI)。

JNI 非常难,毕竟它牵涉到了两种编程语言。假设聪明的你对 Java 和 C/C++以及 GCC 编译器已经有所了解。那么下面就一起来一步步学习 JNI 吧。

Step1: 创建一个名字为 JNITest.java 的文件

public class JNITest {
    static {
        System.load("/home/ubuntu/workspace/java/jni/mynativelib.so");
    }

    //申明一个无参的native方法,而且返回空
    public native void greet();

    //测试
    public static void main(String[] args) {
        JNITest test = new JNITest();
        test.greet();
    }
}

首先用静态代码块加载本地动态链接库"mynativelib.so"。对于静态代码块,我相信写过 Java 的你应该非常清楚,它只会在类被加载的时候执行一次。这个动态链接库会被添加到 Java 的 library path(保存在 Java 系统变量 java.library.path)中,如果加载失败,就会抛出UnsatisfiedLinkError异常。也可以使用 JVM 启动参数来加载该动态链接库到 Java 的 library path 当中:

基于c语言的编程语言开发

当今世道,各种高级语言百花齐放。然而会有人发出这样的疑问–计算机真的能够识别这么多语言吗?稍微有点常识的人都知道,这显然是不可能滴!在计算机的世界里,他们能够直接识别的只有机器语言。然而,由于机器语言对人类不够友好,所以人们才发明了汇编,c,Java…许许多多的人类易读的编程语言,所以我个人对编程语言的理解一直是其实他们就是机器语言的语法糖,而编程语言的创造过程,就是定义一种合理的,没有二义性的语法规则,然后就是通过直接或间接的方式实现该语法到机器语言的转换过程。既然是这样的话,那么我们就很容易想到,计算机语言是一个自我完善的过程:首先我们定了一种非常简单的 x1(这里只是用来举例说明,有没有 x 语言有待考证)语言,然后用机器语言实现了这个非常简单的 x1 语言的编译器,创造了 x1 语言,实现了非常简单的新特性,然后我们再用 x1 语言(相对于机器语言较高级)实现了另一些新的特性的 x2 语言的编译器,创造了 x2 语言,…,如此下去,人们创造了汇编语言,从而创造了 c 语言,接着创造了世界上最好的语言 PHP(不知道是不是真的,反正大家都习惯这么说)。 在各种高级语言越来越强大的今天,我们可能很难再会去接触最原始的东西,高度封装确实提高了生产力,降低了学习成本,但是也使得现代程序员将太多精力花在了各种说明书上,而不清楚其本质。 毕业一年多,工作了一年多,对于计算机编程有了自己的看法,不再像在大学的时候认识的那样肤浅,反而觉得大学中学习的知识才是真正的干货,不禁感叹曾经浪费掉了大好光阴。好在陶渊明有词云:“悟已往之不谏,知来者之可追”。 闲暇之余,扒开 PHP(这里之所以是 PHP 并不因为他是世界上最好的语言,只是因为我目前从事的是 PHP 开发的工作而已)源码,了解了其内部构造和实现原理,百看不一练。今天就初步学习 yacc/lex 了,记录在我的博客中,以便以后翻阅巩固。

一般来说编程语言的解释执行过程如下:

ONE. 词法分析 将源代码拆分成若干 Token 的过程

TWO.语法分析 将 Token 构建成 Syntax Tree 的过程

THREE.生成执行码 生成可执行文件

下面是 wikipedia 中对 yacc 的描述

Yacc is a computer program for the Unix operating system. It is a Look Ahead Left-to-Right (LALR) parser generator, generating a parser, the part of a compiler that tries to make syntactic sense of the source code, specifically a LALR parser, based on an analytic grammar written in a notation similar to Backus–Naur Form (BNF). Yacc itself used to be available as the default parser generator on most Unix systems, though it has since been supplanted as the default by more recent, largely compatible, programs.