Jump to Navigation

用golang实现PHP扩展

用golang实现PHP扩展

一种方式,完全在一个项目中实现

一种方式,在两个分开的项目中实现

技术要点,

  • 需要用PHP调用golang实现的函数。
  • 需要用golang调用PHP写的函数。

在一个项目中实现

项目完整性比较好,一个项目代码就直接能够生成PHP需要的.so扩展。

当然不好的地方也有,就是go编写的功能代码部分不方便在其他项目中重用。

所以具体使用哪个方式,看go编写的功能代码是否需要在其他的项目重用。

在分开的项目中实现

分开的两部分分别是,php扩展部分,go共享库部分。

至于为什么还是需要一个php扩展部分,是因为即使用go写功能代码,仍旧需要使用C编写桥接代码。

抛出go函数

编写go代码与其他go代码并没有什么不同。

在写完go代码的基础上, 为要在.so共享库中抛出的函数加一行指令,

 package main
 import "C"
 //export hello
 func hello() {}

含义为,要把grpc_call函数抛出,让C语言可以调用到。

注意示例中的import "C"指令,这是//export指令需要的。

在PHP中调用go函数

在上一节中,已经抛出了go函数hello,这个hello就成为了C的一个函数符号地址了。

那么,在PHP扩展中(C实现)可以直接调用该hello函数,像正常的调用C函数一样。

...
hello();
...

在go中调用PHP函数

PHP函数是无法直接抛出一个C函数符号地址。

但是PHP的zend api提供了一个调用PHP函数call_user_function与call_user_function_ex,可以方便的在C中调用一个PHP函数。

需要注意的是,第一,最好能够在go中包装一个call_user_function函数,方便在go中调用。

第二,该函数当然需要参数,涉及到大量把go参数转换为php参数的zval类型参数,工作量还是比较大的。比如在示例中可以简化成只传递某些类型的参数,如字符串,数字,而放弃数组类型,资源类型等。

完整仓库目录结构

. ├── go-hello ├── php-hello ├── inone

代码仓库完整代码示例, https://github.com/kitech/php-ext-in-golang

这种方式实现的扩展,无法同时加载多个扩展吧,因为有运行时,在同一进程内冲突。 (经测试,可以同时加载go产生的多个扩展,而go运行时会启动多份,n倍线程数,好像并不会冲突。还需要更多的实际测试,主要有可能是全局变量部分的冲突。)

回调的处理

需要从PHP中回调到GO中的某个函数/方法。

这里的回调,指的是回调到某个动态的,运行时设置的函数。

比如,有可能在go中传递过来的是一个函数指针,有可能是一个go闭包函数同,所以不能像前面提到的//export方式抛出这些函数。

简单的解决方法是,写一个代理调用函数,类型php的call_user_function,也在go中实现的一个抛出的call_user_function函数,实现调用go任意函数的功能。

这个函数的声明大概是这样子:

//
 // 比较通用的在C中调用go任意函数的方法(但参数是都指针形式的)
 // 该函数直接根据函数指定fp函数指针对应的函数。
 //export call_golang_function_p
 func call_golang_function_p(fp unsafe.Pointer, a0 unsafe.Pointer, a1 unsafe.Pointer, a2 unsafe.Pointer,
          a3 unsafe.Pointer, a4 unsafe.Pointer, a5 unsafe.Pointer, a6 unsafe.Pointer,
     a7 unsafe.Pointer, a8 unsafe.Pointer, a9 unsafe.Pointer) unsafe.Pointer {

     return nil
}

这个函数看上去比较直白,繁索,功能也有限制,就是只支持最大10个函数参数。(当然可以优化实现,让其更加灵活)

当提供了一个可用桥接函数后,其他工作就是做参数的转换了,这还是比较复杂的。

参数的处理

参数处理包括以前三个部分的功能, * 参数个数, * 参数类型, * 参数转换, * 参数值的传递, * 参数类型的传递

返回值的处理

  • 返回值个数,
  • 返回值类型
  • 返回值转换

类型的转换

PHP的类型还不算太多,重要的有NULL, BOOL, LONG, STRING, ARRAY, RESOURCE, DOUBLE

在go中,类型拆分的更细些,不过主要细在整数类型部分。

这时需要一个类型转换的映射表,以及对应的转换函数,实现让php和go的类型互相转换的功能。

这里要说明的一个是,double类型比较特殊,如果要通过uint64_t或者void*来传递比较麻烦,还需要把double类型数据放在一个指针中传递,传递完成再反解析指针读取double数值。

如果在go中的函数是interface{}类型参数,还比较好说,只要按照php的类型转换过去即可。

golang的变长参数

还有一种参数,就是现在函数都支持可变长度参数类型,一般表达方式为args ...。

这个像数组,但又不是数组,实现的时候需要特别注意。

在go中变长参数计算出来是一个slice类型,和普通的slice类型没有区别。因为这是一个函数特征,所以要想知道的话,还需要从函数的原型属性中获取。可以用过reflect.Type的IsVariadic()函数取到。

php handler函数

由于在动态创建扩展函数/方法时,也需要动态的php handler函数符号,并且要外带回调到go所需要的映射标识性数据。

但在C中无法实现这一功能,所以需要预告生成一堆函数符号表,如handler_1,handler_2,handler_3等,实现外带一个回调标识ID的功能。

在这个临时的handler_x中调用到真实的handler中,并且带上这个回调标识ID。

例如,这个一个实现的宏,

void phpgo_function_handler_##cbno(INTERNAL_FUNCTION_PARAMETERS) { 
    phpgo_function_handler(cbno, ......);
}

用于动态添加到php的函数符号表中。

而phpgo实际需要的回调函数原型为:

void phpgo_function_handler(cbno, INTERNAL_FUNCTION_PARAMES); 

这个功能目前比较别扭,主要还是因为zend api有点小弱,回调无法外带数据,也就无法区分回调来源(也不够熟悉zend api,可能有方法但不知道吧)。

总结

从这个项目,体会到了zend api不好用,并且从中产生了一个小想法,把zend api封装成更动态的方式编写PHP扩展。比如add_function, add_class, add_method等。

从这个项目,产生了另一个想法,把go的常用函数,像map/array/slice/chan封装个goapi出来,提供C API,能够方便的在C中操作GO的复杂数据类型,也许很有意思。

目前这两个想法都有一点尝试,但还没有整理完成实现代码,且耐心等待。

这个项目代码以后应该会放在github吧,虽然现在还没放,

GitHub 传送门:https://github.com/kitech/php-go

像这种桥接功能的代码,本来概念上显得乱,实现上也有可能比较乱,需要多refactor让实现更清晰点吧。

FAQ & Sundry

Q: 实现的foo.so不能链接到libphp5.so,因为当foo.so被php加载后,会有符号冲突。 比如,_emalloc,实际上在php命令行执行程序,和libphp5.so中分别有一份。 链接参数,-Wl,--unresolved-symbol=ignore-all

Q: 在go程序启动时,会先启动runtime,worker线程。 GOMAXPROCS如果在程序启动后设置,会出现一种情况,启动时产生的线程不会退出, 那么,后续的程序执行,会在多余的线程之间切换吗?这是个问题。 而如果在命令行环境变量设置GOMAXPROCS值,则go runtime只启动设定的线程数,就不会出现上面的可能切换的问题了。

Q: GO启动的线程有x个,调度线程。

Q: 另外,一个很深奥的现象,好像go runtime启动了两次,生成了两倍数量的线程。 如果很多个使用phpgo创建的扩展,那么许多的go runtime之间有什么冲突呢,如何合并。使用go runtime的动态链接方式吗?

Q: 另一个问题,当阻塞在go runtime中时,PHP的gc没有机会执行,PHP的内存会如何呢,要如何清理。

Q: 还有一种方式,应该可以使用PHP的closure功能,动态创建出PHP类的成员变量方法,即成员指向一个可调用的函数/闭包。

Q: 还有一种方式,为这个类创建一个基类,那么基类的名字,或者成员变量可以设置了。

Q: 还有一种方式,为这个类创建一个静态成员变量,存储要回调用gofuncid。但不适用于普通函数。

Q: 这两种方式的问题是,会保泄漏内部信息了。

Q: 还有一种方式,当然就是直接用C实现闭包了,虽然看懂,不过在学不稳定,限制非常大,不可用。 https://github.com/Akvelog/langtoy/blob/master/closure-gcc/closure_gcc.h

添加新评论

Plain text

  • 不允许HTML标记。
  • 自动将网址与电子邮件地址转变为链接。
  • 自动断行和分段。
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.


Main menu 2

Story | by Dr. Radut