FFI: الكتابة في Rust في برنامج PHP

في PHP 7.4 ، ستظهر FFI ، أي يمكنك توصيل المكتبات في C (أو ، على سبيل المثال ، Rust) مباشرة ، دون الحاجة إلى كتابة ملحق كامل وفهم الفروق الدقيقة الكثيرة.


دعونا نحاول كتابة التعليمات البرمجية في Rust ، واستخدامها في برنامج PHP

فكرة تطبيق FFI في PHP 7.4 مأخوذة من 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 ++. الصدأ معقد ، لكن بهذا المعنى يبدو أكثر موثوقية. يخبرك المترجم على الفور أين أنت مخطئ. يكاد يكون من المستحيل تحقيق سلوك غير محدد.


تنويه: أنا لست مبرمج نظام ، لذلك استخدم الباقي على مسؤوليتك الخاصة.


لنبدأ بكتابة شيء بسيط تمامًا ، وظيفة بسيطة لإضافة أرقام. فقط للتدريب. ثم دعنا ننتقل إلى مهمة أكثر صعوبة.


إنشاء مشروع كمكتبة


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" إليها


بعد ذلك ، نقوم ببناء البضائع للحصول على الملف (تحت 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. وقررت استخدامه من PHP لدراسة جميع الفروق الدقيقة.


رمز المشروع الكامل هنا: المحلل اللغوي البسيط الصدأ . بالمناسبة ، أضع أيضًا صورة لرسو السفن تحتوي على PHP (تم تجميعها مع FFI) ، Rust ، Cbindgen ، إلخ. كل ما تحتاجه لتشغيل.


المحلل اللغوي ، إذا اعتبرنا لغة Rast الخالصة ، يقوم بما يلي:


تأخذ سلسلة من النموذج " 100500*(2+35)-2*5 " وتحويل 100500*(2+35)-2*5 إلى تعبير شجرة:


 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 ، سنقوم أيضًا بتخزين ارتباطين (Box) لتعبيرات معاملات هذه الإضافة.

كتبت المحلل اللغوي بسرعة كبيرة ، على الرغم من المعرفة المحدودة لصدأ ، لكنني اضطررت إلى تعذيب نفسي مع FFI. إذا كانت السلسلة في C هي مؤشر لنوع char * ، أي مؤشر إلى مجموعة من الأحرف تنتهي بـ \ 0 ، ثم في Rast هو نوع مختلف تمامًا. لذلك ، يجب عليك تحويل سلسلة الإدخال إلى نوع & str كما يلي:


 CStr::from_ptr(s).to_str() 

المزيد عن CStr


هذا هو كل نصف المتاعب. المشكلة الحقيقية هي أنه لا توجد Rums Enums أو روابط صندوق آمن في C. لذلك ، اضطررت إلى إنشاء بنية منفصلة لـ ExpressionFfi لتخزين شجرة تعبير C-style ، أي عبر مؤشرات البنية والاتحاد والوحدة البسيطة ( 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_raw يتحول نوع Box إلى مؤشر خام


نتيجة لذلك ، تبدو الوظيفة التي سنصدرها إلى 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() } } 

في ما يلي مجموعة من الحواجب () التي تعني "ذعر لأي خطأ". في كود الإنتاج العادي ، بالطبع ، يجب معالجة الأخطاء بشكل طبيعي وتمرير خطأ كجزء من إرجاع الدالة C.


حسنًا ، هنا نرى كتلة غير آمنة مفروضة ، وبدونها ، ما كان يمكن تجميعها. لسوء الحظ ، في هذه المرحلة من البرنامج ، لا يمكن أن يكون برنامج التحويل البرمجي Rust مسؤولاً عن أمان الذاكرة. هذا أمر مفهوم وطبيعي. عند تقاطع Rust و C ، سيكون هذا دائمًا. ومع ذلك ، في كل الأماكن الأخرى يتم التحكم في كل شيء بشكل آمن وآمن.


فوف ، مثل كل شيء يمكن تجميعها. ولكن في الواقع هناك فارق بسيط واحد: لا تزال بحاجة إلى كتابة إنشاءات رأسية بحيث يفهم 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 الخاص بنا. يعرض الكود ببساطة ما يتم تحليله بواسطة مكتبة Rust لدينا على الشاشة باستخدام وظيفة الطباعة العودية printExpression.


 <?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); - يحول المؤشر الخام إلى نوع المربع ، وحيث أن هذا التحويل لا يستخدم من قبل أي شخص ، يتم تدمير الذاكرة تلقائيًا عند الخروج من النطاق.


لا تنس إنشاء وإنشاء ملف الرأس.


وفي 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 

ملاحظة: سنناقش هذا الأمر في العدد التالي من البودكاست من Zinc Prod ، لذا تأكد من الاشتراك في البودكاست.

Source: https://habr.com/ru/post/ar455614/


All Articles