Letzten Oktober diskutierten meine Etsy-Kollegen und ich darüber, wie man Erweiterungen für interpretierte Sprachen wie PHP schreibt. Die aktuelle Situation von Ruby oder Python sollte einfacher sein als PHP. Wir haben darüber gesprochen, dass das Hindernis beim Schreiben einer erfolgreichen Erweiterung darin besteht, dass sie normalerweise in C geschrieben werden müssen, aber es ist schwer, dieses Vertrauen zu haben, wenn man nicht gut in C ist.
Seitdem hatte ich die Idee, eines in Rust zu schreiben und probiere es seit ein paar Tagen aus. Heute Morgen habe ich es endlich zum Laufen gebracht.
Rust in C oder PHP
Mein grundlegender Ausgangspunkt besteht darin, kompilierbaren Rust-Code in eine Bibliothek zu schreiben und einige C-Header-Dateien dafür zu schreiben. In C ist es ein Aufruf von PHP eine Erweiterung. Es ist nicht einfach, aber es macht Spaß.
Rust FFI (Foreign Function Interface)
Das erste, was ich gemacht habe, war, mit Rusts Foreign Function Interface herumzuspielen, das Rust mit C verbindet. Ich habe einmal eine flexible Bibliothek mit einer einfachen Methode (hello_from_rust) mit einer einzigen Deklaration (einem Zeiger auf ein C-Zeichen, auch als String bekannt) geschrieben. Hier ist die Ausgabe von „Hello from Rust“ nach der Eingabe.
// hello_from_rust.rs #![crate_type = "staticlib"] #![feature(libc)] extern crate libc; use std::ffi::CStr; #[no_mangle] pub extern "C" fn hello_from_rust(name: *const libc::c_char) { let buf_name = unsafe { CStr::from_ptr(name).to_bytes() }; let str_name = String::from_utf8(buf_name.to_vec()).unwrap(); let c_name = format!("Hello from Rust, {}", str_name); println!("{}", c_name); }
Ich habe es aus einer Rust-Bibliothek aufgeteilt, die von C (oder einem anderen!) aufgerufen wird. Hier ist eine gute Erklärung, was als nächstes kommt.
Beim Kompilieren erhält man eine Datei mit der Datei .a, libhello_from_rust.a. Dies ist eine statische Bibliothek, die alle ihre eigenen Abhängigkeiten enthält. Wir verknüpfen sie beim Kompilieren eines C-Programms, sodass wir nachfolgende Aufgaben ausführen können. Hinweis: Nach dem Kompilieren erhalten wir die folgende Ausgabe:
note: link against the following native artifacts when linking against this static library note: the order and any duplication can be significant on some platforms, and so may need to be preserved note: library: Systemnote: library: pthread note: library: c note: library: m
Dies ist, was der Rust-Compiler uns sagt, dass wir eine Verknüpfung erstellen sollen, wenn wir diese Abhängigkeit nicht verwenden.
Rust von C aus aufrufen
Da wir nun eine Bibliothek haben, müssen wir zwei Dinge tun, um sicherzustellen, dass sie von C aus aufrufbar ist. Zuerst müssen wir eine C-Header-Datei dafür erstellen, hello_from_rust.h. Dann verlinken Sie darauf, wenn wir es kompilieren.
Das Folgende ist die Header-Datei:
// hello_from_rust.h #ifndef __HELLO #define __HELLO void hello_from_rust(const char *name); #endif
Dies ist eine ziemlich einfache Header-Datei, die lediglich eine Signatur/Definition für eine einfache Funktion bereitstellt. Als nächstes müssen wir ein C-Programm schreiben und es verwenden.
// hello.c #include <stdio.h> #include <stdlib.h> #include "hello_from_rust.h" int main(int argc, char *argv[]) { hello_from_rust("Jared!"); }
Wir kompilieren es, indem wir diesen Code ausführen:
gcc -Wall -o hello_c hello.c -L /Users/jmcfarland/code/rust/php-hello-rust -lhello_from_rust -lSystem -lpthread -lc -lm
Beachten Sie, dass -lSystem -lpthread -lc -lm am Ende gcc anweist, diese „lokalen Antiquitäten“ nicht zu verknüpfen. , Damit der Rust-Compiler es beim Kompilieren unserer Rust-Bibliothek bereitstellen kann.
Indem wir den folgenden Code ausführen, können wir eine Binärdatei erhalten:
$ ./hello_c Hello from Rust, Jared!
Wunderschön! Wir haben gerade die Rust-Bibliothek von C aus aufgerufen. Jetzt müssen wir verstehen, wie eine Rust-Bibliothek in eine PHP-Erweiterung gelangt.
C aus PHP aufrufen
Es hat eine Weile gedauert, bis ich diesen Teil herausgefunden habe, und die Dokumentation ist nicht die beste der Welt für PHP-Erweiterungen. Das Beste daran ist, dass die PHP-Quelle aus der Bündelung eines Skripts ext_skel (steht meist für „Extended Skeleton“) stammt, das den größten Teil des benötigten Boilerplate-Codes generiert. Um den Code zum Laufen zu bringen, habe ich sehr hart daran gearbeitet, die PHP-Dokumentation „Extended Bones“ zu lernen.
Sie können beginnen, indem Sie die nicht zitierte PHP-Quelle herunterladen, den Code in das PHP-Verzeichnis schreiben und Folgendes ausführen:
$ cd ext/
$ ./ext_skel –extname=hello_from_rust
Dadurch wird das Grundgerüst generiert, das zum Erstellen einer PHP-Erweiterung erforderlich ist. Verschieben Sie nun die Ordner überall dort, wo Sie Ihre Erweiterungen lokal behalten möchten. Und verschieben Sie Ihre
.rust-Quelle
.rust-Bibliothek
.c-Header
in dasselbe Verzeichnis. Nun sollten Sie sich also ein Verzeichnis wie dieses ansehen:
.
├── CREDITS
├── EXPERIMENTAL
├── config.m4
├── config .w32
├── hello_from_rust.c
├── hello_from_rust.h
├── hello_from_rust.php
├── hello_from_rust.rs
├──. libhello_from_rust.a
├─ ─ php_hello_from_rust.h
└── Tests
└── 001.phpt
Ein Verzeichnis, 11 Dateien
Sie können darüber in den PHP-Dokumenten oben lesen. Die Dateien sind in Ordnung beschrieben. Erstellen Sie eine erweiterte Datei. Wir beginnen mit der Bearbeitung von config.m4.
Ohne Erklärung hier meine Ergebnisse:
PHP_ARG_WITH(hello_from_rust, for hello_from_rust support, [ --with-hello_from_rust Include hello_from_rust support]) if test "$PHP_HELLO_FROM_RUST" != "no"; then PHP_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
正如我所理解的那样,这些是基本的宏命令。但是有关这些宏命令的文档是相当糟糕的(比如:google”PHP_ADD_LIBRARY_WITH_PATH”并没有出现PHP团队所写的结果)。我偶然这个PHP_ADD_LIBRARY_PATH宏命令在有些人所谈论的在一个PHP拓展里链接一个静态库的先前的线程里。在评论中其它的推荐使用的宏命令是在我运行ext_skel后产生的。
既然我们进行了配置设置,我们需要从PHP脚本中实际地调用库。为此我们得修改自动生成的文件,hello_from_rust.c。首先我们添加hello_from_rust.h头文件到包含命令中。然后我们要修改confirm_hello_from_rust_compiled的定义方法。
#include "hello_from_rust.h" // a bunch of comments and code removed... PHP_FUNCTION(confirm_hello_from_rust_compiled) { char *arg = NULL; int arg_len, len; char *strg; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) { return; } hello_from_rust("Jared (from PHP!!)!"); len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "hello_from_rust", arg); RETURN_STRINGL(strg, len, 0); }
注意:我添加了hello_from_rust(“Jared (fromPHP!!)!”);。
现在,我们可以试着建立我们的扩展:
$ phpize
$ ./configure
$ sudo make install
就是它,生成我们的元配置,运行生成的配置命令,然后安装该扩展。安装时,我必须亲自使用sudo,因为我的用户并不拥有安装目录的 php 扩展。
现在,我们可以运行它啦!
$ php hello_from_rust.php
Functions available in the test extension:
confirm_hello_from_rust_compiled
Hello from Rust, Jared (from PHP!!)!
Congratulations! You have successfully modified ext/hello_from_rust/config.m4. Module hello_from_rust is now compiled into PHP.
Segmentation fault: 11
还不错,php 已进入我们的 c 扩展,看到我们的应用方法列表并且调用。接着,c 扩展已进入我们的 rust 库,开始打印我们的字符串。那很有趣!但是……那段错误的结局发生了什么?
正如我所提到的,这里是使用了 Rust 相关的 println! 宏,但是我没有对它做进一步的调试。如果我们从我们的 Rust 库中删除并返回一个 char* 替代,段错误就会消失。
这里是 Rust 的代码:
#![crate_type = "staticlib"] #![feature(libc)] extern crate libc; use std::ffi::{CStr, CString}; #[no_mangle] pub extern "C" fn hello_from_rust(name: *const libc::c_char) -> *const libc::c_char { let buf_name = unsafe { CStr::from_ptr(name).to_bytes() }; let str_name = String::from_utf8(buf_name.to_vec()).unwrap(); let c_name = format!("Hello from Rust, {}", str_name); CString::new(c_name).unwrap().as_ptr() }
并变更 C 头文件:
#ifndef __HELLO #define __HELLO const char * hello_from_rust(const char *name); #endif
还要变更 C 扩展文件:
PHP_FUNCTION(confirm_hello_from_rust_compiled) { char *arg = NULL; int arg_len, len; char *strg; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) { return; } char *str; str = hello_from_rust("Jared (from PHP!!)!"); printf("%s/n", str); len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "hello_from_rust", arg); RETURN_STRINGL(strg, len, 0); }
无用的微基准
那么为什么你还要这样做?我还真的没有在现实世界里使用过这个。但是我真的认为斐波那契序列算法就是一个好的例子来说明一个PHP拓展如何很基本。通常是直截了当(在Ruby中):
def fib(at) do if (at == 1 || at == 0) return at else return fib(at - 1) + fib(at - 2) end end
而且可以通过不使用递归来改善这不好的性能:
def fib(at) do if (at == 1 || at == 0) return at elsif (val = @cache[at]).present? return val end total = 1 parent = 1 gp = 1 (1..at).each do |i| total = parent + gp gp = parent parent = total end return total end
那么我们围绕它来写两个例子,一个在PHP中,一个在Rust中。看看哪个更快。下面是PHP版:
def fib(at) do if (at == 1 || at == 0) return at elsif (val = @cache[at]).present? return val end total = 1 parent = 1 gp = 1 (1..at).each do |i| total = parent + gp gp = parent parent = total end return total end
这是它的运行结果:
$ time php php_fib.php real 0m2.046s user 0m1.823s sys 0m0.207s
现在我们来做Rust版。下面是库资源:
#![crate_type = "staticlib"] fn fib(at: usize) -> usize { if at == 0 { return 0; } else if at == 1 { return 1; } let mut total = 1; let mut parent = 1; let mut gp = 0; for _ in 1 .. at { total = parent + gp; gp = parent; parent = total; } return total; } #[no_mangle] pub extern "C" fn rust_fib(at: usize) -> usize { fib(at) }
注意,我编译的库rustc – O rust_lib.rs使编译器优化(因为我们是这里的标准)。这里是C扩展源(相关摘录):
PHP_FUNCTION(confirm_rust_fib_compiled) { long number; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &number) == FAILURE) { return; } RETURN_LONG(rust_fib(number)); }
运行PHP脚本:
<?php $br = (php_sapi_name() == "cli")? "":"<br>"; if(!extension_loaded('rust_fib')) { dl('rust_fib.' . PHP_SHLIB_SUFFIX); } for ($i = 0; $i < 100000; $i ++) { confirm_rust_fib_compiled(92); } ?>
这就是它的运行结果:
$ time php rust_fib.php real 0m0.586s user 0m0.342s sys 0m0.221s
你可以看见它比前者快了三倍!完美的Rust微基准!
总结
这里几乎没有得出什么结论。我不确定在Rust上写一个PHP的扩展是一个好的想法,但是花费一些时间去研究Rust,PHP和C,这是一个很好的方式。