将使用Rust编写的WP_Query转移到PHP中 – 创建一个Rust PHP扩展

嗨!你好!我是奥斯汀呦。

简·杨邹循极概要在上一篇文章中,我们实现了Rust中的WP_Query功能,而这一次,我们将对其进行PHP扩展并编译,以便在PHP代码中进行使用。

目标 (mù我开始学习Rust,希望能广泛应用它。起初,我选择了Rust作为WebAssembly的编译语言,因为它在WebAssembly上表现出色。但后来我发现它也可以编译成PHP的扩展,这让我意识到Rust的潜力更加广阔。

如果将擅长Web的PHP与保证性能和安全性的Rust结合使用,应该就能开发出各种类型的应用程序吧?

我希望能够通过上一篇文章和这一篇文章来证实一个假设,即通过Rust来缩小与PHP之间的差距,使其成为更强大的工具。

扩展-php-rs用于将Rust源代码编译为PHP扩展的有用工具是名为ext-php-rs的crate。

虽然还有其他的PHP开发者存在,但由于ext-php-rs在文档完善性方面表现出色,我们选择了它。

 

因为资料非常仔细地制作,所以几乎毫无困难地使用了。

编译设置尽管上述的文件箱说明了这一点,但我还是会根据我自己的经历简要解释一下。

需要在本地安装PHP Dev。只安装箱子无法编译。据说它是在安装了php-dev的前提下进行构建的,所以请先进行安装。

在Mac上使用Homebrew很方便。

brew install php

为了不要忘记.cargo/config.toml,请记得。如果在设置阶段非常重要且对作者来说很难理解的情况下,需要在本文件夹创建.cargo/config.toml,然后指定不编译与PHP的Zend框架相关的代码。

[target.'cfg(not(target_os = "windows"))']
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]

如果没有包含此项内容,在构建时会出现以下错误。

Undefined symbols for architecture arm64:
            "___zend_malloc", referenced from:
                _ext_php_rs_zend_string_init in libext_php_rs-74f0decce9f75fdf.rlib(wrapper.o)

据说PHP的扩展不是以拥有自己的Zend框架为目标,而是从PHP借用,所以似乎不需要包含进来。

创建型的设置当您跟踪数据并进行设置时,系统会提供包括创建配置在内的指导,但当作者添加这项功能时,整合测试就无法全部编译通过了。

在文件中,写着要将设置为cdylib,但是如果同时添加rlib也可以同时构建而不破坏测试。

[lib]
crate-type = ["cdylib", "rlib"]

尝试用Rust构建一个简单的PHP函数我想在使用WP_Query的反向导入版本之前,先简单测试一下ext-php-rs的配置是否正确。

让我们将lib.rs中的简单方法介绍给他人,并进行构建试验。

use ext_php_rs::prelude::*;

#[php_function]
pub fn hello_world(name: &str) -> String {
    format!("Hello, {}!", name)
}

#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
    module
}

据说,php_module是必要的函数,用于将PHP注册到系统中。

我将用PHP尝试上述代码。

var_dump(hello_world("Austin"));

让我们通过以下命令进行构建并运行。

cargo build
php -d extension=target/debug/libwp_query_rs_php_ext.dylib php/test.php

然后会有以下输出。

string(14) "Hello, Austin!"

我感觉好像受到了非常热烈的欢迎。

将WP_Query带入到PHP中进入本题,如果将用Rust复现的WP_Query逆向导入到PHP中,这将是一项非常繁琐无用的工作。

我会实现MySQL的连接池。
首先要修改原始qp_query_rs包的代码,但首先要处理的是如何获取MySQL连接。

由于创建MySQL连接对性能的影响很大,所以我想在一个PHP实例中建立连接池,以便能够立即获取连接。

在Rust中实现Singleton使用广泛普及的once_cell crate。事实上,once_cell crate的一部分甚至被认可为Rust的标准库的一部分。

这是Rust标准库的版本,必须实现Sync特性,但是由于笔者对Rust知识还不够了解,因此选择了更简单的方式。

use mysql::{OptsBuilder, Pool, PooledConn};
use once_cell::sync::OnceCell;

static POOL_INSTANCE: OnceCell<Pool> = OnceCell::new();

pub fn get_pool() -> &'static Pool {
    POOL_INSTANCE.get_or_init(|| {
        let env_vars = EnvVars::from_env();
        let opts = build_opts_from_env(env_vars);

        Pool::new(opts).expect("SqlConnectionError")
    })
}

通过这个方法,可以从环境变量中创建连接池,从而在PHP端也能稍微提高性能。

让WP_Post的struct被转换为Zend对象。
PHP的Zend框架采用自己的方式来实现在PHP中使用的对象、数组等,但php-ext-rs可以帮助进行转换。

通过实现在php-ext-rs中的IntoZval和FromZval特征,我们可以明确地创建一个用于在Rust和PHP之间传递信息的Rosetta石。

在这次实现中,我们希望将通过Rust的MySQL查询获取的struct WP_Post单向转换为PHP的Zend值,因此不需要实现FromZval。但是,由于在编译时会出现错误,为了简单处理,我这个作者的坏习性在代码中具体体现如下。

impl<'a> FromZval<'a> for WP_Post {
    const TYPE: ext_php_rs::flags::DataType = DataType::Object(Some("RS_WP_Post"));

    // Do not implement as not used, must satisfy ext-php-rs traits
    fn from_zval(zval: &'a Zval) -> Option<Self> {
        None
    }
}

由于IntoZval需要被正确地执行,所以我进行了以下实现。

impl IntoZval for WP_Post {
    const TYPE: ext_php_rs::flags::DataType = DataType::Object(Some("RS_WP_Post"));

    fn into_zval(self, persistent: bool) -> ext_php_rs::error::Result<ext_php_rs::types::Zval> {
        let zobj = self.build_zobj()?;

        zobj.into_zval(persistent)
    }

    fn set_zval(self, zv: &mut Zval, persistent: bool) -> ext_php_rs::error::Result<()> {
        let mut zobj = self.build_zobj()?;

        zv.set_object(&mut zobj);

        Ok(())
    }
}

将Zval的值转换为名为ZBox<_zend_object>的类型并返回。

有两个函数在这个速率下要求实现,但是set_zval可能让人担心。into_zval当然是正确的,但为什么要设置呢?

我能想到的是以下这些情况吗?

$var_a = ['a' => 1];
$var_a = $var_a;

可能Zval被引用进入了堆栈,其内容被全部放置在堆中吗?由于对PHP知识了解不够,我还不能给出答案,但我觉得此观点很有意思。

顺便提一下,set_object函数中使用的build_zobj函数的内容如下。

impl WP_Post {
    fn build_zobj(self) -> ext_php_rs::error::Result<ZBox<_zend_object>> {
        let mut zobj = ZendObject::new_stdclass();

        zobj.set_property("ID", self.ID)?;
        zobj.set_property("post_status", self.post_status)?;
        zobj.set_property("post_author", self.post_author)?;
        zobj.set_property("post_date", self.post_date.to_string())?;
        zobj.set_property("post_date_gmt", self.post_date_gmt.to_string())?;
        zobj.set_property("post_content", self.post_content)?;
        zobj.set_property("post_title", self.post_title)?;
        zobj.set_property("post_excerpt", self.post_excerpt)?;
        zobj.set_property("comment_status", self.comment_status)?;
        zobj.set_property("ping_status", self.ping_status)?;
        zobj.set_property("post_password", self.post_password)?;
        zobj.set_property("post_name", self.post_name)?;
        zobj.set_property("to_ping", self.to_ping)?;
        zobj.set_property("pinged", self.pinged)?;
        zobj.set_property("post_modified", self.post_modified.to_string())?;
        zobj.set_property("post_modified_gmt", self.post_modified_gmt.to_string())?;
        zobj.set_property("post_content_filtered", self.post_content_filtered)?;
        zobj.set_property("post_parent", self.post_parent)?;
        zobj.set_property("guid", self.guid)?;
        zobj.set_property("menu_order", self.menu_order)?;
        zobj.set_property("post_type", self.post_type)?;
        zobj.set_property("post_mime_type", self.post_mime_type)?;
        zobj.set_property("comment_count", self.comment_count)?;

        Ok(zobj)
    }
}

使用宏使PHP代码被编译为类
我想要将其定义为全局类,就像WordPress一样。

使用php-ext-rs可以轻松实现这一功能,请使用它。

use ext_php_rs::prelude::*;

#[derive(Debug, Clone)]
#[php_class]
#[allow(non_camel_case_types)]
pub struct RS_WP_Query {
    pub posts: Vec<WP_Post>,
}

#[php_impl]
impl RS_WP_Query {
    pub fn __construct(args: Params) -> Self {
        let q = WP_Query::new(args).unwrap();

        Self { posts: q.posts }
    }
}

#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
    module
}

使用#[php_class]指令可以将struct转换为PHP类。

如果要在该类中实现方法,可以在iml上方加上#[php_impl]以Rust的方式进行实现。

在后面我会解释Params的事情,现在先编译上面的代码并确认在PHP中是否可用。

<?php

$rs_wp_query = new RS_WP_Query([]);

var_dump($rs_wp_query);

执行后,会有以下输出结果。

object(RS_WP_Query)#1 (0) {
}

哎呀?帖子呢??

有一个小错误。您需要在struct RS_WP_Query的PHP属性上添加一个名为#[prop]的指令。

#[derive(Debug, Clone)]
#[php_class]
#[allow(non_camel_case_types)]
pub struct RS_WP_Query {
    #[prop]
    pub posts: Vec<WP_Post>,
}

当您再次执行上述代码时,将正确显示。

<?php

$rs_wp_query = new RS_WP_Query([]);

var_dump(count($rs_wp_query->posts));

能力:

int(10)

真不容易呀

从PHP的数组中提取参数
这是最大的难题,作者正在犹豫到底要有多认真地实施。

根据查看WordPress的WP_Query文档,可以看出PHP不关心类型,因此可以通过各种模式来指定$args,并在Rust中正确地规范化所有可能性是一项相当复杂的工作。

然而,我只先介绍其中一部分。

让我们回到 wp_query_rs 的代码中,在 Params 中实现 FromZval 的特性。

impl<'a> FromZval<'a> for Params {
    const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::Array;

    fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {
        if !zval.is_array() {
            return None;
        }

        let mut params = Self::new();

        Some(params)
    }
}

注目的是’a,但是为了让Rust知道Zval的生命周期到什么时候,在这里进行了指定,但实际上PHP是通过GC来进行的,所以似乎Rust无法决定这个对象的malloc和drop。顺便说一句。

只要按照上述的方法先进行设置,如果在PHP中传递一个空数组,它将自动获取默认的配置并返回给你。

让我们实际上反映post_type参数。

原本的WP_Query实现可能以以下两种模式之一传递post_type参数。

$args = ['post_type' => 'page']; // OR
$args = ['post_type' => ['page']];

以一种自由且灵活的语言而言,Rust这样的语言对于内存的使用方式可以被视为对内存的亵渎。

然而,并非没有任何解决方法。您可以使用以下的if let来应对。

impl<'a> FromZval<'a> for Params {
    const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::Array;

    fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {
        let mut params = Self::new();

        if let Some(array) = zval.array() {
            /* Post Type */
            // PHP Version allows for array or string, accounts for both possibilies
            if let Some(post_types) = array.get("post_type").map(|r| r.array()).flatten() {
                let p_types: Vec<String> =
                    post_types.iter().filter_map(|p_type| p_type.2.string()).collect();
                params.post_type = Some(p_types)
            } else if let Some(post_type) = array.get("post_type").map(|v| v.string()).flatten() {
                params.post_type = Some(vec![post_type]);
            }
        }

        Some(params)
    }
}

尽管以上源代码可能看起来很丑,但它能够覆盖来自PHP的可能参数。

首先,我们将检查是否适用于传递String数组的情况。然后,我们检查只有一个String的情况。如果没有明确指定,那么就不添加任何参数,保持默认状态。

Paraphrased option in Chinese:
首先,我们会检查是否适用于传递String数组的情况。然后,我们会检查只有一个String的情况。如果没有明确指定,那就不添加任何参数,保持默认状态。

儘管嵌套 if 會讓人不太舒服,但試一試的話,通常都會成功的。

<?php

$rs_wp_query = new RS_WP_Query(['post_type' => ['guide', 'post']]);

$rs_wp_query2 = new RS_WP_Query(['post_type' => 'guide']);

var_dump(count($rs_wp_query->posts));
var_dump($rs_wp_query->posts[0]->post_type);
var_dump(count($rs_wp_query2->posts));

Result:

int(10)
string(5) "guide"
int(10)

太好了

如果我们能涵盖这个,并且还有其他项目和模式,我们就能做到…对吧?但是我看了WP_Query的文档后感到不安。

因为似乎没有精力,所以让我们在这里结束文章,可以吗?

总结
我试着将用Rust实现的WP_Query重新逆向导入回PHP,结果如何呢?大家是否对我浪费时间和品味糟糕而感到惊讶呢?

请你理解,这只是一个概念验证!因为我成功地让Rust和PHP合作,所以我想证明如果在某些需要高性能的PHP应用场景中,可以部分地使用Rust(对我来说)。

最近,仿佛是在抨击PHP一样,JavaScript框架开始兴起。但是,能够驾驭Next.js和SSR的复杂性的开发团队在日本有多少呢?即使有,投资如此庞大的工程资源来实现SPA,最终结果可能是最差的性能导致用户流失。

PHP才能以稳定且低成本的方式给予我们真正出色的用户体验。PHP是绝佳的服务器端渲染(SSR)工具。实际上,有这么好的工具,我们一直都不知道。

如果能够将Rust和PHP结合起来使用,我觉得开发模式将会非常有趣。

希望这种流行开来,就算是Ruby也好,我祈祷着后端工程师的派遣能够复苏。

bannerAds