FFI: menulis di Rust dalam program PHP

Di PHP 7.4, FFI akan muncul, mis. Anda dapat menghubungkan pustaka di C (atau, misalnya, Karat) secara langsung, tanpa harus menulis seluruh ekstensi dan memahami banyak nuansanya.


Mari kita coba menulis kode dalam Rust , dan menggunakannya dalam program PHP

Gagasan menerapkan FFI di PHP 7.4 diambil dari LuaJIT dan Python, yaitu: parser dibangun ke dalam bahasa yang memahami deklarasi fungsi, struktur, dll. Bahasa C. Bahkan, Anda dapat menyelipkan seluruh konten file header di sana dan segera mulai menggunakannya.


Contoh:


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

Menghubungkan orang yang sudah selesai itu sederhana dan menyenangkan, tetapi Anda juga ingin menulis sesuatu sendiri. Misalnya, Anda perlu mem-parsing file dengan cepat, dan menggunakan hasil parsing dari php.


Dari tiga bahasa sistem (C, C ++, Rust), saya pribadi memilih yang terakhir. Alasannya sederhana: Saya tidak memiliki cukup kompetensi untuk segera menulis program memori-aman dalam C atau C ++. Karat memang rumit, tetapi dalam hal ini terlihat lebih andal. Kompiler segera memberi tahu Anda di mana Anda salah. Hampir mustahil untuk mencapai Perilaku Tidak Terdefinisi.


Penafian: Saya bukan pemrogram sistem, jadi gunakan sisanya dengan risiko Anda sendiri.


Mari kita mulai dengan menulis sesuatu yang sangat sederhana, fungsi sederhana untuk menambahkan angka. Hanya untuk pelatihan. Lalu mari kita beralih ke tugas yang lebih sulit.


Buat proyek sebagai perpustakaan


cargo new hellofromrust --lib


dan tunjukkan dalam cargo.toml bahwa itu adalah pustaka dinamis (dylib)


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

Fungsi itu sendiri pada Rast terlihat seperti ini


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

baik saya fungsi normal, hanya beberapa kata ajaib no_mangle dan extern "C" yang ditambahkan


Selanjutnya, kami melakukan pembangunan kargo untuk mendapatkan file-jadi (di Linux)


Dapat digunakan dari php:


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

Menambahkan angka itu mudah. Fungsi ini mengambil argumen integer berdasarkan nilai, dan mengembalikan integer baru.


Tetapi bagaimana jika Anda perlu menggunakan string? Tetapi bagaimana jika suatu fungsi mengembalikan tautan ke pohon elemen? Dan bagaimana cara menggunakan konstruksi spesifik Rast dalam fungsi tanda tangan?


Pertanyaan-pertanyaan ini menyiksa saya, jadi saya menulis parser ekspresi aritmatika pada Rast. Dan saya memutuskan untuk menggunakannya dari PHP untuk mempelajari semua nuansa.


Kode proyek lengkapnya ada di sini: simple-rust-arithmetic-parser . Omong-omong, saya juga memasukkan gambar buruh pelabuhan di dalamnya yang berisi PHP (dikompilasi dengan FFI), Rust, Cbindgen, dll. Semua yang Anda butuhkan untuk dijalankan.


Pengurai, jika kita menganggap bahasa Rast murni, melakukan hal berikut:


mengambil string dari bentuk " 100500*(2+35)-2*5 " dan mengubah ekspresi.rs menjadi ekspresi pohon:


 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), } 

itu adalah Rast enum, dan di Rast, seperti yang Anda tahu, enum bukan hanya seperangkat konstanta, tetapi Anda masih dapat mengikat nilai kepada mereka. Di sini, jika jenis simpul adalah Ekspresi :: Nilai, maka bilangan bulat ditulis padanya, misalnya 100500. Untuk simpul tipe Tambah, kami juga akan menyimpan dua tautan (Kotak) ke ekspresi operan dari penambahan ini.

Saya menulis parser dengan cepat, meskipun pengetahuan Rust terbatas, tetapi saya harus menyiksa diri sendiri dengan FFI. Jika dalam C string adalah pointer ke tipe char *, mis. pointer ke array karakter yang diakhiri dengan \ 0, kemudian di Rast itu adalah tipe yang sama sekali berbeda. Oleh karena itu, Anda harus mengubah string input ke tipe & str sebagai berikut:


 CStr::from_ptr(s).to_str() 

Lebih lanjut tentang CStr


Ini semua setengah masalah. Masalah sebenarnya adalah bahwa tidak ada Enums Rast atau tautan Kotak aman di C. Oleh karena itu, saya harus membuat struktur ExpressionFfi terpisah untuk menyimpan pohon ekspresi C-style, yaitu melalui struct, union dan pointer sederhana ( 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, } 

Nah, dan metode untuk mengubahnya menjadi:


 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 mengubah tipe Box menjadi pointer mentah


Akibatnya, fungsi yang akan kami ekspor ke PHP terlihat seperti ini:


 #[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() } } 

Berikut adalah sekelompok unwrap (), yang berarti "panik untuk kesalahan apa pun." Dalam kode produksi normal, tentu saja, kesalahan harus ditangani secara normal dan kesalahan dilewatkan sebagai bagian dari kembalinya fungsi-C.


Nah, di sini kita melihat blok paksa yang tidak aman, tanpanya, tidak ada yang bisa dikompilasi. Sayangnya, pada titik ini dalam program, compiler Rust tidak dapat bertanggung jawab atas keamanan memori. Ini bisa dimengerti dan wajar. Di persimpangan Rust dan C, ini akan selalu terjadi. Namun, di semua tempat lain semuanya benar-benar terkontrol dan aman.


Fuf, sepertinya semuanya bisa dikompilasi. Tetapi sebenarnya ada satu nuansa lagi: Anda masih perlu menulis konstruksi header agar PHP memahami tanda tangan fungsi dan tipe.


Untungnya, Rast memiliki alat cbindgen yang nyaman. Secara otomatis mencari dalam kode Rast untuk konstruksi yang diberi label extern "C", repr (C), dll. dan menghasilkan file header


Saya harus menderita sedikit dengan pengaturan cbindgen, mereka ternyata seperti ini ( cbindgen.toml ):


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

Saya tidak yakin bahwa saya mengerti dengan jelas semua nuansa, tetapi berhasil)


Contoh Peluncuran:


 cbindgen . -o target/testffi.h 

Hasilnya akan seperti ini:


 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); 

Jadi, kami membuat file-h, mengkompilasi pustaka cargo build dan Anda dapat menulis kode php kami. Kode hanya menampilkan apa yang diuraikan oleh perpustakaan Rust kami di layar dengan fungsi rekursi 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; } } 

Yah, itu dia, terima kasih sudah menonton.


Persetan ada "segalanya." Memori masih perlu dibersihkan. Rast tidak dapat menerapkan sihirnya di luar kode Rast.


Tambahkan fungsi penghancuran lainnya


 #[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); - Mengonversi penunjuk mentah ke jenis Kotak, dan karena hasil konversi ini tidak digunakan oleh siapa pun, memori secara otomatis dihancurkan ketika Anda keluar dari ruang lingkup.


Jangan lupa untuk membuat dan menghasilkan file header.


dan di php kita menambahkan panggilan ke fungsi kita


 $cdef->destroy($expression); 

Nah, itu saja. Jika Anda ingin menambahkan atau memberi tahu bahwa saya salah, silakan berkomentar.


Repositori dengan contoh lengkap terdapat di tautan: [ 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 

NB. Kita akan membahas ini dalam edisi berikutnya podcast Zinc Prod , jadi pastikan untuk berlangganan podcast.

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


All Articles