Saya memelihara paket Dart bernama mraa, ini merupakan implementasi dari perpustakaan Intel MRAA Linux menggunakan mekanisme FFI Dart. Paket ini sekarang berusia sekitar 3 tahun dan diimplementasikan dengan membuat pengikatan FFI yang diperlukan oleh Dart ke MRAA C API seperti yang disediakan dalam file header MRAA. Ini berfungsi cukup baik tetapi tidak mudah untuk pemeliharaan, setiap perubahan dalam API MRAA harus diperiksa, dan paket diperbarui sesuai dengan itu, ini memakan waktu dan rawan kesalahan, diperlukan solusi jangka panjang yang jauh lebih baik .

Untungnya, Dart sekarang memiliki paket ffigen. Paket ini memindai file atau file header API berbasis C dan secara otomatis membuat file kelas Dart dengan pengikatan FFI yang diperlukan sudah dibuat. Karena kelas ini dibuat secara otomatis, setiap perubahan di API MRAA diambil secara otomatis, tidak perlu pemeriksaan manual apa pun. Ini merupakan keuntungan besar bagi pemeliharaan, jadi saya memutuskan untuk mengadopsi pengikatan FFI yang diperlukan oleh paket mraa dari file yang dihasilkan ini.

Namun ada beberapa pertimbangan yang perlu dipertimbangkan saat melakukan ini: -

  1. File yang dihasilkan tidak boleh disentuh dengan cara apa pun, yaitu tidak boleh dilakukan pengeditan tangan, tidak ada gunanya menghemat waktu menghapus kode pengikatan FFI buatan tangan untuk kemudian harus menerapkan pengeditan tangan setiap kali file yang dihasilkan dibuat ulang.
  2. Struktur API paket yang ada harus dipertahankan, yaitu bagian-bagian berbeda dari API MRAA diakses melalui kelas Dart yang sesuai, misalnya fungsi GPIO diakses seperti ini 'mraa.gpio.initialise…'. Hal ini memungkinkan penspasian nama fungsionalitas yang lebih baik, kami tidak ingin API dengan mungkin 100 fungsi di dalamnya diberikan kepada pengguna selama daftar ada dalam file yang dihasilkan.
  3. API publik dari paket mraa harus dipertahankan dengan sedikit atau tanpa perubahan, kami tidak ingin membuat pekerjaan yang tidak perlu bagi pengguna yang mengupgrade mraa, oleh karena itu tipe apa pun yang dihasilkan oleh ffigen harus dicangkokkan ke dalam struktur penamaan tipe paket yang ada.

Intinya, kita perlu mengganti kode pengikatan FFI buatan tangan yang ada dengan kode kelas yang dihasilkan dengan dampak minimal.

Salah satu cara untuk melakukan ini adalah dengan menggunakan pendekatan yang umum di dunia C++ yang disebut Pimpl Idiom. Teknik ini pada dasarnya menghilangkan detail implementasi suatu kelas dari representasi objeknya dengan menempatkannya di kelas terpisah, diakses melalui pointer buram. Bagi pecinta pola, ini adalah variasi dari pola Bridge yaitu cara menyembunyikan implementasi, terutama untuk memutus ketergantungan kompilasi, sedangkan pola Bridge lengkap adalah cara mendukung banyak implementasi, meskipun Idiom Pimpl di C++ telah ada sejak lama. bertahun-tahun sebelum pola desain diformalkan.

Kelas Ffigen'd Pimpl

Mari kita mulai dengan membuat kelas ffigen kita, selanjutnya disebut sebagai kelas 'pimpl', ini bukan artikel tentang ffigen atau cara menggunakannya, silakan lihat dokumentasinya untuk ini, jadi di bawah ini adalah sorotan dari ffigen_pubspec saya. file yaml :-

figen:  
  output: 'mraa_impl.dart'  
  name: 'MraaImpl'  
  description: 'Holds ffigen generated implementation bindings to MRAA.'  
  headers:  
    entry-points:  
      - 'mraa/mraa.h'  
    include-directives:  
      - '**mraa/*.h'  
  comments: true  
  preamble: |

Jadi, kami membuat file bernama mraa_impl.dart, yang berisi kelas bernama MraaImpl dari satu file header mraa.h yang berisi file header untuk masing-masing area API (cara standar untuk melakukan ini di C), kami menyimpan komentar dan menambahkan pembukaan ke kelas untuk perizinan dll. ffigen menangani generasi ini tanpa masalah, kelas yang dihasilkan dapat dilihat di sini.

Sekarang mari kita lihat bagaimana kita dapat memperbarui paket mraa untuk menggunakan kelas ini.

Pembaruan Kelas mraa utama

Dimulai dengan kelas mraa utama kita dapat melihat struktur asli bergantung pada perpustakaan bersama mraa yang mendasarinya, sekarang kita perlu menghapus ini dan membuat kelas mraa bergantung pada kelas pimpl, jadi :-

Mraa() {  
  _lib = DynamicLibrary.open('libmraa.so');  
}

menjadi :-

Mraa() {  
  _impl = mraaimpl.MraaImpl(DynamicLibrary.open('libmraa.so'));  
}

dan kami menambahkan :-

// The MRAA Implementation class  
late mraaimpl.MraaImpl _impl;

Konstruksi API individual sekarang berubah dari :-

common = MraaCommon(_lib, noJsonLoading, useGrovePi);

to :-

common = MraaCommon(_impl, noJsonLoading, useGrovePi);

Mengganti ketergantungan perpustakaan bersama mraa ke kelas pimpl kami, perhatikan bahwa tidak ada perubahan API publik yang berdampak.

Pembaruan Kelas API

Kami sekarang perlu memperbarui masing-masing kelas API kami, dengan mengambil API Umum sebagai contoh, konstruksi berubah menjadi: -

MraaCommon(this._impl, this._noJsonLoading, this._useGrovePi) {  
  // Set up the pin offset for grove pi usage.  
  if (_useGrovePi) {  
    _grovePiPinOffset = Mraa.grovePiPinOffset;  
  }  
}  
  
// The MRAA implementation  
final mraaimpl.MraaImpl _impl;

yaitu kita sekarang menggunakan kelas pimpl kita, melihat metode inisialisasi kita melihatnya telah berubah dari :-

MraaReturnCode initialise() => returnCode.fromInt(_initFunc());

to

MraaReturnCode initialise() => MraaReturnCode.
returnCode(_impl.mraa_init());

Perhatikan bahwa tipe pengembalian tidak berubah, jadi tidak ada dampak pada API publik. Panggilan C yang mendasari sekarang dipanggil melalui kelas pimpl, bukan perpustakaan bersama melalui pengikatan FFI buatan tangan kami, ini memungkinkan kami untuk menghapus 100 baris kode yang ada di kelas asli, jumlah baris sekarang menjadi 316, bukan 658. Perhatikan juga sedikit mengutak-atik derivasi kode pengembalian, ini karena kami juga telah meningkatkan enumerasi paket, lebih lanjut tentang ini nanti.

Sekarang mari kita lihat metode yang meneruskan parameter dan mengembalikan tipe :-

String platformVersion(int platformOffset) {  
  final ptr = _platformVersionFunc(platformOffset);  
  if (ptr != nullptr) {  
    ptr.toDartString();  
  }  
  return 'Platform Version is unavailable';  
}

menjadi

String platformVersion(int platformOffset) {  
  final ptr = _impl.mraa_get_platform_version(platformOffset);  
  if (ptr != nullptr) {  
    return ptr.cast<ffi.Utf8>().toDartString();  
  }  
  return 'Platform Version is unavailable';  
}

Perhatikan lagi bahwa tanda tangan metode publik tidak berubah, ada sedikit pengkodean ulang penanganan pointer, tapi ini sudah bisa diduga karena sekarang kita telah menghapus dukungan apa pun untuk ini dari kelas kita yang ada. Pembaruan ini sederhana dan intuitif untuk dilakukan dan yang lebih penting ada dalam metode publik kami.

Semua metode lain di semua kelas API lainnya telah diperbarui, hal ini hanya membutuhkan sedikit waktu dibandingkan dengan penghematan pemeliharaan yang diperoleh.

Pembaruan Tipe Publik

Paket ini menggunakan banyak tipe yang diekspos secara publik, kami tidak ingin mengubah namanya, jadi kami tidak ingin menggunakannya langsung dari kelas pimpl, melainkan kami ingin memetakan nama yang digunakan di kelas pimpl ke nama yang sudah ada , mari kita lihat beberapa contoh.

Banyak panggilan API mengambil parameter 'konteks', ini adalah tipe buram yang diinisialisasi dan kemudian diteruskan ke metode API sesuai kebutuhan, praktik umum di API berbasis C, contohnya adalah tipe konteks kelas GPIO yang didefinisikan sebagai :-

class MraaGpioContext extends Opaque {}

Tipe FFI buram, kita tidak bisa menggunakannya sekarang, kita harus menggunakan definisi yang disediakan oleh kelas pimpl, jadi sekarang menjadi :-

typedef MraaGpioContext = mraaimpl.mraa_gpio_context;

mraa_gpio_context dari kelas pimpl itu sendiri adalah typedef, jadi kita sekarang membuat typedef dari typedef (rapi) dan yang terpenting mempertahankan nama tipe yang ada. Namun hal ini memiliki dampak besar pada API publik, typedef yang dideklarasikan di kelas pimpl adalah sebuah pointer,

ffi.Pointer<_gpio>; 

karena kelas yang ada adalah tipe buram, tidak ada anggota yang dapat mengaksesnya, tidak apa-apa tetapi setiap pengguna menyatakan ini,

Pointer<MraaGpioContext>

sekarang akan melihat kerusakan, karena typedef baru kita sudah menjadi pointer, jadi penggunaan ini sekarang perlu diubah menjadi 'MraaGpioContext'. Ini adalah perubahan yang menyederhanakan dan karena tipe ini diinisialisasi hanya sekali tidak akan menyebabkan terlalu banyak pembaruan, saya yakin ini dapat diterima.

Demikian pula, kita juga harus mengganti tipe berbasis kelas buatan tangan yang ada dengan tipe dari kelas pimpl, dengan mengambil kelas MraaGpioEvent sebagai contoh, kita mengubah yang asli :-

class MraaGpioEvent extends Struct {  
  /// Construction  
  MraaGpioEvent(int id, int timestamp) {  
    id = id;  
    timestamp = timestamp;  
  }  
  
  @Int32()  
  
  /// Id  
  external int id;  
  
  @Int64()  
  
  /// Timestamp  
  external int timestamp;  
}

to

typedef MraaGpioEvent = mraaimpl.mraa_gpio_event;

yaitu kita kembali menggunakan definisi dari kelas pimpl, dalam hal ini mraa_gpio_event adalah kelas, bukan typedef, jadi kita masih bisa menggunakan deklarasi seperti ini dari unit test suite misalnya: -

final events = <MraaGpioEvent>[];

dan masih mengakses anggota kelas, sehingga menyederhanakan kode kita tanpa mengubah kode apa pun yang ada.

Tipe-tipe lainnya telah diperbarui sesuai dengan hal ini. Hal ini menyisakan satu area besar lainnya yang harus ditangani, yaitu enumerasi.

Pembaruan Pencacahan Publik

Deklarasi C API untuk paket seperti MRAA berisi banyak nilai konfigurasi untuk memilih ambang tegangan, arah pin, frekuensi, dll. yang digunakan oleh berbagai fungsi C API. Mengambil petunjuk pin sebagai contoh, kita dapat melihat bahwa header MRAA GPIO C berisi yang berikut :-

typedef enum {  
    MRAA_GPIO_OUT = 0,      /**< Output. A Mode can also be set */  
    MRAA_GPIO_IN = 1,       /**< Input */
    .....

Ada cara lain untuk melakukan ini, dalam beberapa kasus arahan definisi langsung digunakan. Ini direpresentasikan sebagai: -

abstract class mraa_gpio_dir_t {  
  /// < Output. A Mode can also be set  
  static const int MRAA_GPIO_OUT = 0;  
  
  /// < Input  
  static const int MRAA_GPIO_IN = 1;
...

Di kelas pimpl kami oleh ffigen, jadi kami sekarang perlu memasukkan nilai-nilai ini ke dalam paket mraa kami.

Awalnya, paket ini menggunakan enumerasi Dart standar di mana nilai tidak dapat diberikan untuk enumerasi individual, tabel pemetaan pendukung dan fungsi akses disediakan untuk mengonversi enumerasi Dart ke nilai sebenarnya, hal ini rawan kesalahan buatan tangan dan membosankan untuk dipelihara. Anda dapat melihat contohnya pada bagian pembaruan kelas API di atas, lihat fungsi pembantu 'fromInt'.

Sekarang, dengan hadirnya enum yang disempurnakan di Dart, kami dapat menyederhanakan hal ini dan mengurangi upaya pemeliharaan secara besar-besaran. Enum MraaGpioDirection sekarang menjadi: -

enum MraaGpioDirection {  
  /// Out  
  out(mraaimpl.mraa_gpio_dir_t.MRAA_GPIO_OUT),  
  
  /// In  
  inn(mraaimpl.mraa_gpio_dir_t.MRAA_GPIO_IN),
  ....

dengan banyak kode yang dikurangi, bahkan dengan beberapa metode override dan pembantu, tentu saja nama enumerasi publik kami tidak berubah, begitu pula nama individu yang enumerasi, dan kami tidak lagi peduli apa nilai-nilai ini, mereka hanya diambil dari kelas jerawat.

Penghematan pemeliharaan di sini tidak dapat diremehkan, harus memeriksa bahwa tidak satu pun dari nilai-nilai ini yang berubah dari rilis ke rilis MRAA adalah pekerjaan yang menghancurkan jiwa dan berharap yang terbaik bukanlah alternatif yang layak.

Perhatikan enumerasi 'inn' di atas, ini harusnya 'in' sebagai arah, namun kita tidak bisa memiliki 'in', highlight sintaksisnya dengan 'in' tidak dapat digunakan sebagai pengenal karena itu adalah kata kunci. '. Saya kira tidak bisa mendapatkan semuanya!

Hasil

Jadi, apakah kami mencapai pembaruan paket sesuai dengan pertimbangan yang kami nyatakan di atas, yaitu apakah kami memiliki dampak minimal atau tidak sama sekali terhadap pengguna kami? Salah satu cara untuk melihat hal ini dengan cepat adalah dengan memeriksa file tempat kami menemukan pengguna API publik, yang dalam paket ini adalah rangkaian pengujian unit dan direktori contoh.

Kami melihat satu pembaruan dalam contoh mraa_uart_details.dart :-

'${returnCode.asString(ret)}');

menjadi

'$ret)');

penyederhanaan sebagai hasil dari implementasi enum baru.

Di seluruh rangkaian pengujian unit, kami hanya memiliki satu dampak di mraa_common_test.dart :-

mraa.common.resultPrint(returnCode.asInt(MraaReturnCode.success));  
mraa.common  
    .resultPrint(returnCode.asInt(MraaReturnCode.errorInvalidHandle));

menjadi

mraa.common.resultPrint(MraaReturnCode.success.code);  
mraa.common.resultPrint(MraaReturnCode.errorInvalidHandle.code);

sekali lagi penyederhanaan, tidak diperlukan pembaruan lain pada rangkaian pengujian, yang berjalan sepenuhnya tanpa kesalahan.

Secara keseluruhan, saya akan menganggap ini sebagai sebuah kemenangan untuk dipertimbangkan. 3. Menggunakan paket ini di proyek lain mungkin akan menimbulkan beberapa ketidakkonsistenan, namun jika tidak ada dampak sama sekali pada pengguna, itu akan menjadi sebuah keajaiban, kami berusaha untuk 'minimal atau tidak ada perubahan sama sekali. ', tidak ada sama sekali.

Pertimbangan 1 dan 2 di atas nampaknya terpenuhi sepenuhnya.

Kesimpulan

Jika Anda melakukan pekerjaan serius dengan FFI dan API berbasis C, ffigen adalah solusinya, menurut saya ini mudah digunakan dan intuitif, pembuatan kodenya selesai, tidak memerlukan hiasan manual, tidak ada kejutan dan langsung berfungsi.

Namun saya akan merekomendasikan bahwa meskipun Anda bisa, Anda tidak boleh mengekspos kelas ffigen Anda secara langsung kepada pengguna Anda, membungkusnya di bawah lapisan Dart lainnya, itu sangat berharga dalam jangka panjang.

Selamat ffigen'ing!