当前位置: 首页 > 科技观察

使用Rust创建PHP扩展

时间:2023-03-14 00:21:46 科技观察

去年10月,我和Etsy的同事讨论了如何为PHP、Ruby或Python等解释型语言编写扩展,目前应该比PHP更容易。我们谈到了编写一个成功创建的扩展的障碍是它们通常需要用C编写,但是如果你不擅长C就很难有那个信心。从那时起我就有了写一个的想法在Rust中,并且在过去的几天里一直在尝试。今天早上我终于让它运行起来了。我在C或PHP中使用Rust的基本出发点是将一些可编译的Rust代码写入一个库,并为其编写一些C头文件,并在C中对调用的PHP进行扩展。这不是很简单,但很有趣。RustFFI(外部函数接口)我做的第一件事是摆弄Rust与C接口的外部函数接口。我曾经用一个简单的方法(hello_from_rust)编写了一个灵活的库,只有一个声明(指向C字符的指针,也称为字符串),如下所示:输入后的“HellofromRust”。//hello_from_rust.rs#![crate_type="staticlib"]#![feature(libc)]externcratelibc;usestd::ffi::CStr;#[no_mangle]pubextern"C"fnhello_from_rust(name:*constlibc::c_char){letbuf_name=unsafe{CStr::from_ptr(name).to_bytes()};letstr_name=String::from_utf8(buf_name.to_vec()).unwrap();letc_name=format!("HellofromRust,{}",str_name);println!("{}",c_name);}我将它从一个从C(或其他任何东西!)调用的Rust库中分离出来。这很好地解释了接下来会发生什么。编译它会产生一个.a文件,即libhello_from_rust.a。这是一个静态库,自带所有的依赖,我们在编译C程序的时候链接它,这样我们就可以做后续的事情了。注意:我们编译之后,我们会得到如下输出:note:linkagainstthefollowingnativeartifactswhenlinkingagainstthisstaticlibrarynote:theorderandanyduplicationcanbesignificantononsomeplatforms,andsomayneedtobepreservednote:library:Systemnote:library:pthreadnote:library:cnote:library:m这是Rust编译器在我们不使用时告诉我们的这个依赖需要链接什么。从C调用Rust现在我们有了一个库,我们必须做两件事才能从C调用它。首先,我们需要为它创建一个C头文件hello_from_rust.h。然后我们编译的时候链接到它。这是头文件:注意:链接此静态库时会链接以下本机工件注意:顺序和任何重复在某些平台和某些平台上可能很重要,有些平台可能需要保留注意:库:系统note:library:pthreadnote:library:cnote:library:m这是一个相当基本的头文件,只为一个简单的函数提供签名/定义。然后我们需要编写一个C程序并使用它。//hello.c#include#include#include"hello_from_rust.h"intmain(intargc,char*argv[]){hello_from_rust("Jared!");}让我们运行代码编译它:gcc-Wall-ohello_chello.c-L/Users/jmcfarland/code/rust/php-hello-rust-lhello_from_rust-lSystem-lpthread-lc-lm注意最后的-lSystem-lpthread-lc-lm告诉gcc在编译我们的Rust库时不要链接Rust编译器可以提供的那些“原生古董”。我们可以通过运行以下代码来获取二进制文件:$./hello_cHellofromRust,Jared!美丽的!我们刚刚从C调用了Rust库。现在我们需要了解Rust库如何适应PHP扩展。从php调用c部分花了我一段时间才弄清楚文档并不是这个世界上最好的php扩展。最好的部分来自绑定脚本ext_skel的php源(主要代表“扩展骨架”),它生成您需要的大部分样板代码。为了让代码正常工作,我非常努力地研究了php文档“ExtendedBones”。您可以通过下载未加引号的php源代码开始,将代码写入php目录并运行:$cdext/$./ext_skel--extname=hello_from_rust这将生成创建php扩展所需的基本框架。现在,将文件夹移动到任何你想在本地保留扩展的地方。并将您的.rust源.rust库.c标头移动到同一目录中。所以现在你应该在这样的目录中查找:├──hello_from_rust.rs├──libhello_from_rust.a├──php_hello_from_rust.h└──tests└──001.phpt一个目录,11个文件你可以在上面的phpdocs中看到关于这些文件的很好的描述。创建一个扩展文件。We'llstartbyeditingconfig.m4.不解释,下面就是我的成果:PHP_ARG_WITH(hello_from_rust,forhello_from_rustsupport,[--with-hello_from_rustIncludehello_from_rustsupport])iftest"$PHP_HELLO_FROM_RUST"!="no";thenPHP_SUBST(HELLO_FROM_RUST_SHARED_LIBADD)PHP_ADD_LIBRARY_WITH_PATH(hello_from_rust,.,HELLO_FROM_RUST_SHARED_LIBADD)PHP_NEW_EXTENSION(hello_from_rust,hello_from_rust.c,$ext_shared)fi据我所知,这些是基本的宏命令。但是这些宏的文档非常糟糕(例如:谷歌“PHP_ADD_LIBRARY_WITH_PATH”没有显示PHP团队编写的结果)。我在前一个线程中遇到了这个PHP_ADD_LIBRARY_PATH宏,其中一些人正在谈论在PHP扩展中链接静态库。评论中建议的其他宏是在我运行ext_skel后生成的。现在我们已经有了配置设置,我们需要从我们的PHP脚本中实际调用库。为此,我们必须修改自动生成的文件hello_from_rust.c。首先我们将hello_from_rust.h头文件添加到include命令中。那么我们就要修改一下confirm_hello_from_rust_compiled的定义方法。#include"hello_from_rust.h"//abunchofcommentsandcoderemoved...PHP_FUNCTION(confirm_hello_from_rust_compiled){char*arg=NULL;intarg_len,len;char*strg;if(zend_parse_parameters(ZEND_NUM_ARGS()TSRMLS_CC,"s",&arg,&arg_len)==FAILURE){return;}hello_from_rust("Jared(fromPHP!!)!");len=spprintf(&strg,0,"恭喜!您已成功修改ext/%.78s/config.m4.Module%.78sisnowcompiledintoPHP.","hello_from_rust",参数);RETURN_STRINGL(strg,len,0);}注意:我添加了hello_from_rust("Jared(fromPHP!!)!");。现在,我们可以尝试构建我们的扩展:$phpize$./configure$sudomakeinstall执行它,生成我们的元配置,运行生成的配置命令,并安装扩展。安装时,我必须自己使用sudo,因为我的用户在安装目录中没有php扩展。现在,我们可以运行它了!$phphello_from_rust.phpFunctionsavailableinthetestextension:confirm_hello_from_rustisnowcompiledHellofromRust,Jared(fromPHP!!)!恭喜!你已经成功修改ext/hello_from_rust/config.m4.Modulehello_from_rustisnowcompiledintoPHP.Segmentationfault:11看到我们的应用方法列表在。接下来,rust库和开始打印进入我们的rust库我们的字符串。好好玩!但是……那个错误的结局是怎么回事?正如我提到的,与Rust相关的println!这里使用了宏,但我没有进一步调试它。如果我们从我们的Rust库中移除并返回一个char*而不是,段错误就会消失。这是Rust代码:#![crate_type="staticlib"]#![feature(libc)]externcratelibc;usestd::ffi::{CStr,CString};#[no_mangle]pubextern"C"fnhello_from_rust(name:*constlibc::c_char)->*constlibc::c_char{letbuf_name=unsafe{CStr::from_ptr(name).to_bytes()};letstr_name=String::from_utf8(buf_name.to_vec()).unwrap();letc_name=格式!("HellofromRust,{}",str_name);CString::new(c_name).unwrap().as_ptr()}并更改C头文件:#ifndef__HELLO#define__HELLOconstchar*hello_from_rust(constchar*name);#endif修改C扩展文件:}char*str;str=hello_from_rust("Jared(fromPHP!!)!");printf("%s/n",str);len=spprintf(&strg,0,"恭喜!您已成功修改ext/%.78s/config.m4.Module%.78sisnowcompiledintoPHP.","hello_from_rust",arg);RETURN_STRINGL(strg,len,0);}无用的微基准测试那么你为什么要这样做呢?我还没有真正在现实世界中使用过它,但我真的认为斐波那契数列算法是一个很好的例子,说明了PHP扩展可以多么基本。通常它是直接的(在Ruby中):defib(at)doif(at==1||at==0)returnatelsereturnfib(at-1)+fib(at-2)endend这可以通过不使用来改进递归性能好:defib(at)doif(at==1||at==0)returnatesif(val=@cache[at]).present?returnvalendtotal=1parent=1gp=1(1..at).eachdo|i|total=parent+gpgp=parentparent=totalendreturntotalend所以我们围绕它写了两个例子,一个用PHP,一个用Rust。看看哪个更快。这是PHP版本:defib(at)doif(at==1||at==0)returnatesif(val=@cache[at]).present?returnvalendtotal=1parent=1gp=1(1..at)。eachdo|i|total=parent+gpgp=parentparent=totalendreturntotalend这是它运行的结果:$timephpphp_fib.phpreal0m2.046suser0m1.823ssys0m0.207s现在我们来做Rust版本。这是库资源:#![crate_type="staticlib"]fnfib(at:usize)->usize{ifat==0{return0;}elseifat==1{return1;}letmuttotal=1;letmutparent=1;letmutgp=0;for_in1..at{total=parent+gp;gp=parent;parent=total;}returntotal;}#[no_mangle]pubextern"C"fnrust_fib(at:usize)->usize{fib(at)}注意,我编译的库rustc--orust_lib.rs让编译器优化(因为我们这里是标准的)。这是C扩展源(相关摘录):PHP_FUNCTION(confirm_rust_fib_compiled){longnumber;if(zend_parse_parameters(ZEND_NUM_ARGS()TSRMLS_CC,"l",&number)==FAILURE){return;}RETURN_LONG(rust_fib(number));}运行PHP脚本:";if(!extension_loaded('rust_fib')){dl('rust_fib.'.PHP_SHLIB_SUFFIX);}for($i=0;$i<100000;$i++){confirm_rust_fib_compiled(92);}?>它是这样工作的:$timephprust_fib.phpreal0m0.586suser0m0.342ssys0m0.221s你可以看到它比前三倍!最新的Rust微基准测试!总结这里几乎没有什么结论可以得出。我不确定在Rust上编写PHP扩展是个好主意,但这是花一些时间探索Rust、PHP和C的好方法。如果您想查看所有代码或更改日志,可以访问GitHub回购。