ฉันรักษาแพ็คเกจ Dart ชื่อ mraa ซึ่งเป็นการใช้งานไลบรารี Intel MRAA Linux โดยใช้กลไก FFI ของ Dart แพ็คเกจนี้ปัจจุบันมีอายุประมาณ 3 ปีแล้ว และใช้งานโดยงานฝีมือการผูก FFI ที่ Dart ต้องการกับ MRAA C API ตามที่ให้มาในไฟล์ส่วนหัว MRAA วิธีนี้ทำงานได้ดีเพียงพอแต่ไม่ได้ช่วยให้บำรุงรักษาได้ง่าย การเปลี่ยนแปลงใดๆ ใน MRAA API จะต้องได้รับการตรวจสอบ และแพ็คเกจที่อัปเดตตามนั้น ซึ่งทั้งใช้เวลานานและเกิดข้อผิดพลาดได้ง่าย จำเป็นต้องมีวิธีแก้ปัญหาในระยะยาวที่ดีกว่ามาก .

โชคดีที่ตอนนี้ Dart มีแพ็คเกจ "ffigen" แล้ว แพ็คเกจนี้จะสแกนไฟล์ส่วนหัว API ที่ใช้ C หรือไฟล์และสร้างไฟล์คลาส Dart โดยอัตโนมัติพร้อมการเชื่อมโยง FFI ที่จำเป็นซึ่งสร้างไว้แล้ว เนื่องจากคลาสนี้สร้างขึ้นโดยอัตโนมัติ การเปลี่ยนแปลงใดๆ ใน MRAA API จะถูกรับโดยอัตโนมัติ ไม่จำเป็นต้องตรวจสอบด้วยตนเอง นี่เป็นประโยชน์อย่างยิ่งในการบำรุงรักษา ดังนั้นฉันจึงตัดสินใจนำการผูก FFI ที่จำเป็นสำหรับแพ็คเกจ mraa จากไฟล์ที่สร้างขึ้นนี้มาใช้

อย่างไรก็ตาม มีข้อควรพิจารณาบางประการที่ควรคำนึงถึงขณะทำเช่นนี้:-

  1. ไฟล์ที่สร้างขึ้นจะต้องไม่ถูกแตะต้องใดๆ ทั้งสิ้น กล่าวคือ ไม่ควรใช้การแก้ไขด้วยมือ จึงไม่มีประโยชน์ที่จะประหยัดเวลาในการลบโค้ดการเชื่อมโยง FFI ที่จัดทำขึ้นด้วยมือ จากนั้นจึงต้องใช้การแก้ไขด้วยมือทุกครั้งที่สร้างไฟล์ที่สร้างขึ้นใหม่
  2. ต้องรักษาโครงสร้าง API ที่มีอยู่ของแพ็คเกจ กล่าวคือ มีการเข้าถึงส่วนต่างๆ ของ MRAA API ผ่านคลาส Dart ที่มีชื่อที่เกี่ยวข้อง เช่น มีการเข้าถึงฟังก์ชัน GPIO เช่น 'mraa.gpio.initialise…' ซึ่งช่วยให้มีการเว้นวรรคชื่อฟังก์ชันการทำงานได้ดีขึ้น เราไม่ต้องการให้ API ที่มีฟังก์ชันมากกว่า 100 รายการอยู่ในนั้นเพื่อส่งมอบให้กับผู้ใช้ตามรายการยาวๆ ที่มีอยู่ในไฟล์ที่สร้างขึ้น
  3. API สาธารณะของแพ็คเกจ mraa จะต้องได้รับการดูแลโดยมีการเปลี่ยนแปลงน้อยที่สุดหรือไม่มีเลย เราไม่ต้องการทำงานที่ไม่จำเป็นสำหรับผู้ใช้ที่อัปเกรด mraa ดังนั้นประเภทใดๆ ที่สร้างโดย ffigen จะต้องถูกกราฟต์ลงในโครงสร้างการตั้งชื่อประเภทแพ็คเกจที่มีอยู่

โดยพื้นฐานแล้ว เราจำเป็นต้องแทนที่โค้ดการเชื่อมโยง FFI ที่สร้างขึ้นด้วยมือที่มีอยู่ด้วยโค้ดของคลาสที่สร้างขึ้นโดยมีผลกระทบน้อยที่สุด

วิธีหนึ่งในการทำเช่นนี้คือการใช้แนวทางทั่วไปในโลก C++ ที่เรียกว่า "Pimpl Idiom" เทคนิคนี้จะลบรายละเอียดการใช้งานของคลาสออกจากการแสดงอ็อบเจ็กต์โดยพื้นฐานแล้ววางไว้ในคลาสที่แยกจากกัน ซึ่งเข้าถึงได้ผ่านตัวชี้ทึบแสง สำหรับผู้ชื่นชอบรูปแบบ มันเป็นรูปแบบหนึ่งของรูปแบบ Bridge นั่นคือวิธีการซ่อนการนำไปใช้งาน โดยหลักๆ แล้วเพื่อทำลายการพึ่งพาการคอมไพล์ ในขณะที่รูปแบบ Bridge ที่สมบูรณ์เป็นวิธีหนึ่งในการรองรับการใช้งานที่หลากหลาย แม้ว่า Pimpl Idiom ใน C++ จะมีมาหลายตัวแล้ว หลายปีก่อนที่รูปแบบการออกแบบจะกลายเป็นเรื่องเป็นทางการ

คลาส Pimpl ของ Ffigen

เริ่มต้นด้วยการสร้างคลาส ffigen'd ของเรา ซึ่งเรียกว่าคลาส 'pimpl' นับจากนี้ไป นี่ไม่ใช่บทความเกี่ยวกับ ffigen หรือวิธีใช้งาน โปรดดูเอกสารประกอบสำหรับสิ่งนี้ ดังนั้นด้านล่างนี้คือไฮไลท์จาก ffigen_pubspec ของฉัน ไฟล์ 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: |

ดังนั้นเราจึงสร้างไฟล์ชื่อ mraa_impl.dart ซึ่งมีคลาสชื่อ MraaImpl จากไฟล์ส่วนหัว mraa.h ไฟล์เดียวซึ่งมีไฟล์ส่วนหัวสำหรับแต่ละพื้นที่ API (วิธีมาตรฐานในการดำเนินการนี้ในภาษา C) เราจึง รักษาความคิดเห็นและเพิ่มคำนำในชั้นเรียนเพื่อขอใบอนุญาต ฯลฯ ffigen จัดการรุ่นนี้ได้โดยไม่มีปัญหา สามารถดูคลาสที่สร้างขึ้นได้ "ที่นี่"

ตอนนี้เรามาดูกันว่าเราจะอัปเดตแพ็คเกจ mraa เพื่อใช้คลาสนี้ได้อย่างไร

การอัปเดตคลาส mraa หลัก

เริ่มต้นด้วยคลาส mraa หลัก เราจะเห็นโครงสร้างดั้งเดิมที่ขึ้นอยู่กับไลบรารี่ที่ใช้ร่วมกันของ mraa ตอนนี้เราจำเป็นต้องลบสิ่งนี้ออกและทำให้คลาส mraa ขึ้นอยู่กับคลาส pimpl ดังนั้น:-

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

กลายเป็น :-

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

และเราเพิ่ม:-

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

ตอนนี้โครงสร้าง API แต่ละรายการเปลี่ยนจาก: -

common = MraaCommon(_lib, noJsonLoading, useGrovePi);

to :-

common = MraaCommon(_impl, noJsonLoading, useGrovePi);

การแทนที่การพึ่งพาไลบรารีที่แบ่งใช้ mraa ลงในคลาส pimpl ของเรา โปรดทราบว่าไม่มีผลกระทบต่อการเปลี่ยนแปลง API สาธารณะ

การอัปเดตคลาส API

ตอนนี้เราจำเป็นต้องอัปเดตคลาส API ของเราแต่ละรายการ โดยใช้ Common API เป็นตัวอย่าง การเปลี่ยนแปลงโครงสร้างเป็น:-

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;

คือตอนนี้เราใช้คลาส pimpl ของเรา โดยดูที่วิธีการเริ่มต้นที่เราเห็นว่ามีการเปลี่ยนแปลงจาก:-

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

to

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

โปรดสังเกตว่าประเภทการส่งคืนไม่มีการเปลี่ยนแปลง ดังนั้นจึงไม่มีผลกระทบต่อ API สาธารณะ ขณะนี้การเรียก C พื้นฐานถูกเรียกผ่านคลาส pimpl ไม่ใช่ไลบรารีที่แบ่งใช้ผ่านการผูก FFI ที่ประดิษฐ์ขึ้นด้วยมือของเรา ซึ่งช่วยให้เราลบโค้ด 100 บรรทัดที่มีอยู่ในคลาสดั้งเดิมได้ จำนวนบรรทัดตอนนี้อยู่ที่ 316 แทนที่จะเป็น 658 นอกจากนี้ โปรดสังเกตพบปัญหาเล็กน้อยกับการได้รับโค้ดส่งคืน เนื่องจากเราได้อัปเกรดการแจงนับแพ็กเกจด้วย ซึ่งจะอธิบายเพิ่มเติมในภายหลัง

ตอนนี้เรามาดูวิธีการที่ส่งผ่านพารามิเตอร์และส่งกลับประเภท :-

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

กลายเป็น

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';  
}

โปรดสังเกตอีกครั้งว่าลายเซ็นวิธีการสาธารณะไม่มีการเปลี่ยนแปลง มีการเข้ารหัสการจัดการตัวชี้ใหม่เล็กน้อย แต่ตอนนี้คาดว่าจะได้ลบการสนับสนุนสำหรับสิ่งนี้ออกจากคลาสที่มีอยู่ของเราแล้ว การอัปเดตเหล่านี้ทำได้ง่ายและใช้งานง่าย และที่สำคัญกว่านั้นอยู่ในวิธีการสาธารณะของเรา

วิธีการอื่นๆ ทั้งหมดในคลาส API อื่นๆ ทั้งหมดได้รับการอัปเดตตามนั้น ซึ่งใช้เวลาน้อยมากเมื่อเทียบกับการประหยัดค่าบำรุงรักษาที่ได้รับ

การอัปเดตประเภทสาธารณะ

แพ็คเกจใช้ประเภทที่เปิดเผยต่อสาธารณะหลายประเภท เราไม่ต้องการเปลี่ยนชื่อของสิ่งเหล่านี้ ดังนั้นเราจึงไม่ต้องการใช้สิ่งเหล่านี้โดยตรงจากคลาส pimpl แต่เราต้องการแมปชื่อที่ใช้ในคลาส pimpl กับชื่อที่มีอยู่ มาดูตัวอย่างกัน

การเรียก API จำนวนมากใช้พารามิเตอร์ 'บริบท' ซึ่งเป็นประเภททึบแสงที่เริ่มต้นแล้วส่งต่อไปยังวิธี API ตามความจำเป็น วิธีปฏิบัติทั่วไปใน API ที่ใช้ C ตัวอย่างคือประเภทบริบทของคลาส GPIO ซึ่งถูกกำหนดเป็น :-

class MraaGpioContext extends Opaque {}

ประเภท FFI ทึบแสง เราไม่สามารถใช้สิ่งนี้ได้ในขณะนี้ เราต้องใช้คำจำกัดความสำหรับสิ่งนี้ตามที่คลาส pimpl กำหนดไว้ ดังนั้นตอนนี้จึงกลายเป็น:-

typedef MraaGpioContext = mraaimpl.mraa_gpio_context;

mraa_gpio_context จากคลาส pimpl นั้นเป็น typedef ดังนั้นตอนนี้เรากำลังสร้าง typedef จาก typedef (เรียบร้อย) และที่สำคัญจะคงชื่อประเภทที่มีอยู่ของเราไว้ อย่างไรก็ตาม สิ่งนี้มีผลกระทบอย่างรุนแรงต่อ API สาธารณะ typedef ที่ประกาศในคลาส pimpl เป็นตัวชี้

ffi.Pointer<_gpio>; 

เนื่องจากคลาสที่มีอยู่เป็นประเภททึบแสงจึงไม่มีสมาชิกให้เข้าถึงซึ่งก็โอเค แต่ผู้ใช้รายใดประกาศสิ่งนี้

Pointer<MraaGpioContext>

ตอนนี้จะเห็นการแตกหัก เนื่องจาก typedef ใหม่ของเราเป็นตัวชี้อยู่แล้ว ดังนั้นการใช้งานนี้จะต้องเปลี่ยนเป็น 'MraaGpioContext' นี่เป็นการเปลี่ยนแปลงที่ง่ายขึ้น และเนื่องจากประเภทเหล่านี้เริ่มต้นได้เพียงครั้งเดียวเท่านั้นจึงไม่ควรทำให้เกิดการอัปเดตมากเกินไป ฉันจึงเชื่อว่าสิ่งนี้เป็นที่ยอมรับได้

ในทำนองเดียวกัน เรายังต้องแทนที่ประเภทคลาส handcrafted ที่มีอยู่ด้วยประเภทจากคลาส pimpl โดยใช้คลาส MraaGpioEvent เป็นตัวอย่าง เราจะเปลี่ยนต้นฉบับ:-

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;

กล่าวคือ เราใช้คำจำกัดความจากคลาส pimpl อีกครั้ง ในกรณีนี้ mraa_gpio_event เป็นคลาส ไม่ใช่ typedef ดังนั้นเราจึงยังคงสามารถใช้การประกาศเช่นนี้จากชุดทดสอบหน่วยได้ เช่น: -

final events = <MraaGpioEvent>[];

และยังคงเข้าถึงสมาชิกชั้นเรียนได้ ทำให้โค้ดของเราง่ายขึ้นอย่างมากโดยไม่ต้องเปลี่ยนโค้ดที่มีอยู่

ประเภทที่เหลือได้รับการปรับปรุงอย่างเหมาะสมตามบรรทัดเหล่านี้ นี่ทำให้เหลือพื้นที่ขนาดใหญ่อีกพื้นที่หนึ่งที่ต้องจัดการและแจกแจง

อัปเดตการแจงนับสาธารณะ

การประกาศ C API สำหรับแพ็คเกจเช่น MRAA มีค่าการกำหนดค่ามากมายเพื่อเลือกเกณฑ์แรงดันไฟฟ้า ทิศทางพิน ความถี่ ฯลฯ ที่ใช้โดยฟังก์ชัน C API ต่างๆ จากตัวอย่างการใช้หมุด เราจะเห็นว่าส่วนหัว MRAA GPIO C มีดังต่อไปนี้:-

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

มีวิธีอื่นในการทำเช่นนี้ ในบางกรณี มีการใช้คำสั่งการกำหนดแบบตรง นี่แสดงเป็น:-

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

ในคลาส pimpl ของเราโดย ffigen ดังนั้นตอนนี้เราจำเป็นต้องรวมค่าเหล่านี้ไว้ในแพ็คเกจ mraa ของเรา

เดิมที แพ็คเกจใช้การแจงนับ Dart มาตรฐานโดยที่ไม่สามารถระบุค่าสำหรับการแจกแจงแต่ละรายการได้ มีการจัดหาตารางการแมปที่รองรับและฟังก์ชันการเข้าถึงเพื่อแปลงการแจงนับ Dart ให้เป็นค่าจริง สิ่งเหล่านี้เป็นงานฝีมือที่ผิดพลาดได้ง่ายและน่าเบื่อในการบำรุงรักษา คุณสามารถดูตัวอย่างได้ในส่วนการอัปเดตคลาส API ด้านบน ดูฟังก์ชันตัวช่วย 'fromInt'

ขณะนี้ ด้วยการมาถึงของ enum ที่ปรับปรุงแล้วใน Dart เราจึงสามารถลดความซับซ้อนนี้ลงอย่างมากและช่วยลดความพยายามในการบำรุงรักษาได้อย่างมาก ตอนนี้ MraaGpioDirection enum กลายเป็น:-

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

ด้วยโค้ดที่ลดลงมาก แม้ว่าจะมีวิธีแทนที่และตัวช่วยเพียงไม่กี่วิธี แน่นอนว่าชื่อการแจงนับสาธารณะของเราไม่ได้เปลี่ยนแปลง หรือชื่อของแต่ละแจงนับ และเราไม่สนใจอีกต่อไปว่าค่าเหล่านี้คืออะไร พวกเขาเพิ่งหยิบขึ้นมาจาก คลาสสิว

การประหยัดการบำรุงรักษาที่นี่ไม่สามารถประเมินได้ต่ำเกินไป โดยต้องตรวจสอบด้วยว่าค่าเหล่านี้ไม่ได้เปลี่ยนแปลงจากการปล่อย MRAA เป็นงานทำลายล้างจิตวิญญาณ และการหวังสิ่งที่ดีที่สุดไม่ใช่ทางเลือกที่ทำงานได้

สังเกตการแจกแจง 'inn' ด้านบน ซึ่งควรเป็น 'in' เป็นทิศทาง อย่างไรก็ตาม เราไม่สามารถมี 'in' ได้ ไวยากรณ์ที่เน้นด้วย 'in' ไม่สามารถใช้เป็นตัวระบุได้เนื่องจากเป็นคำหลัก '. มีทุกอย่างไม่ได้ ฉันคิดว่า!

ผล

ดังนั้นเราจึงบรรลุการอัปเดตแพ็คเกจตามข้อพิจารณาที่เราระบุไว้ข้างต้น กล่าวคือ เรามีผลกระทบต่อผู้ใช้ของเราเพียงเล็กน้อยหรือไม่มีเลยหรือไม่? วิธีหนึ่งในการดูสิ่งนี้อย่างรวดเร็วคือการตรวจสอบไฟล์ที่เราค้นหาผู้ใช้ API สาธารณะ ซึ่งในแพ็คเกจนี้คือชุดทดสอบหน่วยและไดเร็กทอรีของตัวอย่าง

เราเห็นการอัปเดตหนึ่งครั้งในตัวอย่าง mraa_uart_details.dart :-

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

กลายเป็น

'$ret)');

ลดความซับซ้อนอันเป็นผลมาจากการใช้งานแจงนับใหม่

ในชุดการทดสอบหน่วยทั้งหมด เรามีผลกระทบเพียงประการเดียวใน mraa_common_test.dart :-

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

กลายเป็น

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

ทำให้ง่ายขึ้นอีกครั้ง โดยไม่จำเป็นต้องอัปเดตอื่นๆ สำหรับชุดทดสอบ ซึ่งทำงานได้อย่างสมบูรณ์โดยไม่มีข้อผิดพลาด

โดยรวมแล้ว ฉันจะถือว่าสิ่งนี้เป็นชัยชนะในการพิจารณา 3. การใช้แพ็คเกจในโครงการอื่นอาจทำให้เกิดความไม่สอดคล้องกันบางประการ แต่การที่จะไม่ได้รับผลกระทบใดๆ ต่อผู้ใช้เลยจะเป็นเรื่องมหัศจรรย์ เรากำลังมุ่งมั่นที่จะ 'เปลี่ยนแปลงน้อยที่สุดหรือไม่มีเลย' ' ไม่ใช่ไม่มีเลย

ข้อพิจารณาที่ 1 และ 2 ข้างต้นดูเหมือนจะเป็นไปตามข้อพิจารณาทั้งหมด

บทสรุป

หากคุณกำลังทำงานจริงจังใดๆ กับ FFI และ API ที่ใช้ C ffigen คือหนทางไป ฉันพบว่ามันใช้งานง่ายและเป็นธรรมชาติ การสร้างโค้ดเสร็จสมบูรณ์ ไม่จำเป็นต้องตกแต่งด้วยตนเอง ไม่ต้องแปลกใจ และใช้งานได้จริง

ฉันอยากจะแนะนำว่าแม้ว่าคุณจะทำได้ แต่คุณไม่ควรเปิดเผยคลาส ffigen ของคุณแก่ผู้ใช้ของคุณโดยตรง และห่อมันไว้ใต้เลเยอร์ Dart อีกชั้นหนึ่ง มันจะคุ้มค่าในระยะยาว

มีความสุขนะ!