ฉันรักษาแพ็คเกจ 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 จากไฟล์ที่สร้างขึ้นนี้มาใช้
อย่างไรก็ตาม มีข้อควรพิจารณาบางประการที่ควรคำนึงถึงขณะทำเช่นนี้:-
- ไฟล์ที่สร้างขึ้นจะต้องไม่ถูกแตะต้องใดๆ ทั้งสิ้น กล่าวคือ ไม่ควรใช้การแก้ไขด้วยมือ จึงไม่มีประโยชน์ที่จะประหยัดเวลาในการลบโค้ดการเชื่อมโยง FFI ที่จัดทำขึ้นด้วยมือ จากนั้นจึงต้องใช้การแก้ไขด้วยมือทุกครั้งที่สร้างไฟล์ที่สร้างขึ้นใหม่
- ต้องรักษาโครงสร้าง API ที่มีอยู่ของแพ็คเกจ กล่าวคือ มีการเข้าถึงส่วนต่างๆ ของ MRAA API ผ่านคลาส Dart ที่มีชื่อที่เกี่ยวข้อง เช่น มีการเข้าถึงฟังก์ชัน GPIO เช่น 'mraa.gpio.initialise…' ซึ่งช่วยให้มีการเว้นวรรคชื่อฟังก์ชันการทำงานได้ดีขึ้น เราไม่ต้องการให้ API ที่มีฟังก์ชันมากกว่า 100 รายการอยู่ในนั้นเพื่อส่งมอบให้กับผู้ใช้ตามรายการยาวๆ ที่มีอยู่ในไฟล์ที่สร้างขึ้น
- 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 อีกชั้นหนึ่ง มันจะคุ้มค่าในระยะยาว
มีความสุขนะ!