FFI:在PHP程序中用Rust编写

在PHP 7.4中,将显示FFI,即 您可以直接在C(或例如Rust)中连接库,而无需编写整个扩展并了解其许多细微差别。


让我们尝试用Rust编写代码,并在PHP程序中使用它

在PHP 7.4中实现FFI的想法来自LuaJIT和Python,即:语言中内置了一个解析器,该解析器可以理解函数,结构等的声明。 C语言。 实际上,您可以将头文件的全部内容放到那里,然后立即开始使用它。


一个例子:


<?php //    printf    $ffi = FFI::cdef( "int printf(const char *format, ...);", //     "libc.so.6"); //    //  printf $ffi->printf("Hello %s!\n", "world"); 

连接某人的完成品既简单又有趣,但是您也想编写自己的东西。 例如,您需要快速解析文件,并使用php的解析结果。


在三种系统语言(C,C ++,Rust)中,我个人选择了后者。 原因很简单:我没有足够的能力立即使用C或C ++编写内存安全程序。 Rust很复杂,但从这个意义上讲,它看起来更可靠。 编译器会立即告诉您哪里出错了。 实现未定义行为几乎是不可能的。


免责声明:我不是系统程序员,所以其余的后果自负。


让我们从编写完全简单的东西开始,一个简单的加数函数。 只是为了训练。 然后让我们继续执行更困难的任务。


将项目创建为库


cargo new hellofromrust --lib


并在cargo.toml中指出它是一个动态库(dylib)


  …. [lib] name="hellofromrust" crate-type = ["dylib"] …. 

Rast上的函数本身看起来像这样


 #[no_mangle] pub extern "C" fn addNumbers(x: i32, y: i32) -> i32 { x + y } 

好吧 正常功能,仅添加了两个魔术词no_mangle和extern“ C”


接下来,我们进行货物构建以获取so文件(在Linux下)


可以从php使用:


 <?php $ffi = FFI::cdef("int addNumbers(int x, int y);", './libhellofromrust.so'); print "1+2=" . $ffi->addNumbers(1, 2) . "\n"; // 1+2=3 

加数很容易。 该函数按值接受整数参数,然后返回一个新的整数。


但是,如果您需要使用字符串怎么办? 但是,如果函数返回指向元素树的链接怎么办? 以及如何在功能签名中使用Rast的特定构造?


这些问题困扰着我,因此我在Rast上编写了一个数学表达式解析器。 我决定从PHP使用它来研究所有细微差别。


完整的项目代码在这里: simple-rust-arithmetic-parser 。 顺便说一下,我还在其中放置了一个包含PHP(与FFI编译),Rust,Cbindgen等的docker映像。 您需要运行的所有内容。


如果考虑纯Rast语言,则解析器将执行以下操作:


接受格式为“ 100500*(2+35)-2*5 ”的字符串,并将expression.rs转换为树表达式


 pub enum Expression { Add(Box<Expression>, Box<Expression>), Subtract(Box<Expression>, Box<Expression>), Multiply(Box<Expression>, Box<Expression>), Divide(Box<Expression>, Box<Expression>), UnaryMinus(Box<Expression>), Value(i64), } 

它是一个Rast枚举,在Rast中,如您所知,枚举不仅是一组常量,而且您仍然可以将值绑定到它们。 在这里,如果节点类型为Expression :: Value,则将整数写入其中,例如100500。对于类型为Add的节点,我们还将存储两个链接(框)到该加法的操作数表达式。

尽管Rust的知识有限,我还是很快地编写了解析器,但是我不得不用FFI折磨自己。 如果在C中,该字符串是指向char *类型的指针,即 指向以\ 0结尾的字符数组的指针,然后在Rast中是完全不同的类型。 因此,必须将输入字符串转换为&str类型,如下所示:


 CStr::from_ptr(s).to_str() 

有关CStr的更多信息


这是麻烦的一半。 真正的问题是C中没有Rast枚举或安全Box链接。 因此,我必须创建一个单独的ExpressionFfi结构来存储C风格的表达式树,即 通过struct,union和简单指针( ffi.rs )。


 #[repr(C)] pub struct ExpressionFfi { expression_type: ExpressionType, data: ExpressionData, } #[repr(u8)] pub enum ExpressionType { Add = 0, Subtract = 1, Multiply = 2, Divide = 3, UnaryMinus = 4, Value = 5, } #[repr(C)] pub union ExpressionData { pair_operands: PairOperands, single_operand: *mut ExpressionFfi, value: i64, } #[derive(Copy, Clone)] #[repr(C)] pub struct PairOperands { left: *mut ExpressionFfi, right: *mut ExpressionFfi, } 

好了,还有一种转换成它的方法:


 impl Expression { fn convert_to_c(&self) -> *mut ExpressionFfi { let expression_data = match self { Value(value) => ExpressionData { value: *value }, Add(left, right) | Subtract(left, right) | Multiply(left, right) | Divide(left, right) => ExpressionData { pair_operands: PairOperands { left: left.convert_to_c(), right: right.convert_to_c(), }, }, UnaryMinus(operand) => ExpressionData { single_operand: operand.convert_to_c(), }, }; let expression_ffi = match self { Add(_, _) => ExpressionFfi { expression_type: ExpressionType::Add, data: expression_data, }, Subtract(_, _) => ExpressionFfi { expression_type: ExpressionType::Subtract, data: expression_data, }, Multiply(_, _) => ExpressionFfi { expression_type: ExpressionType::Multiply, data: expression_data, }, Divide(_, _) => ExpressionFfi { expression_type: ExpressionType::Multiply, data: expression_data, }, UnaryMinus(_) => ExpressionFfi { expression_type: ExpressionType::UnaryMinus, data: expression_data, }, Value(_) => ExpressionFfi { expression_type: ExpressionType::Value, data: expression_data, }, }; Box::into_raw(Box::new(expression_ffi)) } } 

Box::into_rawBox类型转换为原始指针


结果,我们将导出到PHP的函数如下所示:


 #[no_mangle] pub extern "C" fn parse_arithmetic(s: *const c_char) -> *mut ExpressionFfi { unsafe { // todo: error handling let rust_string = CStr::from_ptr(s).to_str().unwrap(); parse(rust_string).unwrap().convert_to_c() } } 

这是一堆unwrap(),表示“对任何错误都感到恐慌”。 当然,在正常的生产代码中,必须正常处理错误,并将错误作为C函数返回的一部分传递。


好吧,在这里我们看到一个强制的不安全块,没有它,什么都不会编译。 不幸的是,在程序的这一点上,Rust编译器不能对内存安全负责。 这是可以理解和自然的。 在Rust和C的交界处,这将始终如此。 但是,在所有其他地方,一切都受到绝对控制和安全。


Fuf,就像一切都可以编译一样。 但是实际上还有一个细微差别:您仍然需要编写标头构造,以便PHP理解函数和类型的签名。


幸运的是,Rast有一个方便的cbindgen工具。 它会在Rast代码中自动搜索标记为extern“ C”,repr(C)等的构造。 并生成头文件


我不得不忍受cbindgen设置的困扰,结果变成了这样( cbindgen.toml ):


 language = "C" no_includes = true style="tag" [parse] parse_deps = true 

我不确定我是否清楚所有细微之处,但确实可以)


启动示例:


 cbindgen . -o target/testffi.h 

结果将是这样的:


 enum ExpressionType { Add = 0, Subtract = 1, Multiply = 2, Divide = 3, UnaryMinus = 4, Value = 5, }; typedef uint8_t ExpressionType; struct PairOperands { struct ExpressionFfi *left; struct ExpressionFfi *right; }; union ExpressionData { struct PairOperands pair_operands; struct ExpressionFfi *single_operand; int64_t value; }; struct ExpressionFfi { ExpressionType expression_type; union ExpressionData data; }; struct ExpressionFfi *parse_arithmetic(const char *s); 

因此,我们生成了h文件,编译了cargo build库,您可以编写我们的php代码。 该代码仅使用printExpression递归函数在屏幕上显示由Rust库解析的内容。


 <?php $cdef = \FFI::cdef(file_get_contents("target/testffi.h"), "target/debug/libexpr_parser.so"); $expression = $cdef->parse_arithmetic("-6-(4+5)+(5+5)*(4-4)"); printExpression($expression); class ExpressionKind { const Add = 0; const Subtract = 1; const Multiply = 2; const Divide = 3; const UnaryMinus = 4; const Value = 5; } function printExpression($expression) { switch ($expression->expression_type) { case ExpressionKind::Add: case ExpressionKind::Subtract: case ExpressionKind::Multiply: case ExpressionKind::Divide: $operations = ["+", "-", "*", "/"]; print "("; printExpression($expression->data->pair_operands->left); print $operations[$expression->expression_type]; printExpression($expression->data->pair_operands->right); print ")"; break; case ExpressionKind::UnaryMinus: print "-"; printExpression($expression->data->single_operand); break; case ExpressionKind::Value: print $expression->data->value; break; } } 

好,就是这样,谢谢收看。


该死的“一切”。 仍然需要清除内存。 Rast无法在Rast代码之外应用他的魔法。


添加另一个销毁功能


 #[no_mangle] pub extern "C" fn destroy(expression: *mut ExpressionFfi) { unsafe { match (*expression).expression_type { ExpressionType::Add | ExpressionType::Subtract | ExpressionType::Multiply | ExpressionType::Divide => { destroy((*expression).data.pair_operands.right); destroy((*expression).data.pair_operands.left); Box::from_raw(expression); } ExpressionType::UnaryMinus => { destroy((*expression).data.single_operand); Box::from_raw(expression); } ExpressionType::Value => { Box::from_raw(expression); } }; } } 

Box::from_raw(expression); -将原始指针转换为Box类型,并且由于此转换的结果未被任何人使用,因此退出示波器时,内存将自动销毁。


不要忘记构建和生成头文件。


在php中,我们向函数添加了一个调用


 $cdef->destroy($expression); 

现在就这些了。 如果您想在某处添加或告诉我我错了,请随时发表评论。


包含完整示例的存储库位于以下链接:[ https://github.com/anton-okolelov/simple-rust-arithmetic-parser ]


 # build docker-compose build docker-compose run php74 cargo build docker-compose run php74 cbindgen . -o target/testffi.h #run php docker-compose run php74 php testffi.php 

PS我们将在下一期Zinc Prod播客中讨论此问题,因此请确保订阅该播客。

Source: https://habr.com/ru/post/zh-CN455614/


All Articles