Ruby เป็นภาษาที่ยอดเยี่ยม สร้างขึ้นสำหรับมนุษย์ก่อน เครื่องจักรทีสอง มันอ่านง่าย เขียนง่าย มีหลายวิธีในการเขียนอะไรก็ได้ และคุณมักเดา standard library ได้โดยการพิมพ์ชื่อเมธอดที่คุณจะเลือกเอง
เพราะนี้ arguments ของ Ruby ยืดหยุ่นมาก ทำให้เราสามารถแสดง API ของเราได้ชัดเจน แต่สิ่งนี้มาพร้อมข้อเสีย: Ruby ยากมากในการ parse สำหรับนักพัฒนา C extension!
ในบทความนี้ เราจะผ่านสองวิธีในการตั้งค่า Ruby API ที่ซับซ้อนซึ่งเขียนใน C:
- ด้วย
rb_define_methodและ parse ด้วยrb_scan_args - ใช้ Ruby interface
ลองเริ่มกันเถอะ!
C และ Ruby: บทนำ
ตามที่กล่าวไว้ Ruby ยากในการ parse สำหรับนักพัฒนา C extension
ตัวอย่าง:
ความสวยงามของ C ภาษาที่ Ruby เขียนขึ้น มาจากความเรียบง่าย รวมถึงพารามิเตอร์ฟังก์ชัน:
<data-type> <variable-identifier>...สำหรับ variadic arguments
สิ่งเหล่านี้จะช่วยคุณ maintain codebase ที่ไม่ยากเกินไป
นี่คือวิธีที่ซับซ้อนที่สุดในการกำหนด C function:
เมื่อคุณเขียน C extension สำหรับโค้ด Ruby ของคุณ คุณจะเริ่มเข้าใจว่าความซับซ้อนเริ่มที่ไหน แต่别担心 — นักพัฒนา Ruby MRI มีเครื่องมือช่วยเรา
การกำหนดเมธอดง่ายใน Ruby C Extension
เราจะเริ่มด้วยเมธอดที่คุณต้องใช้ในบางจุด rb_define_method
คุณสามารถติดตามตัวอย่างโค้ดใน repo นี้ได้
นี่คือ signature ของ rb_define_method:
และตาม extension.rdoc ของ Ruby:
argc คือจำนวนอาร์กิวเมนต์ หาก argc เป็น -1 ฟังก์ชันจะรับ 3 อาร์กิวเมนต์: argc, argv, และ self หาก argc เป็น -2 ฟังก์ชันจะรับ 2 อาร์กิวเมนต์ self และ args โดย args เป็น Ruby array ของอาร์กิวเมนต์เมธอด
ในข้อความสรุป:
ดังนั้น หาก API ของคุณประกอบด้วยเมธอดที่มีความยาวพารามิเตอร์คงที่ หรือพารามิเตอร์ variadic เพียงตัวเดียว (def foo(*bar)) อ่านต่อไม่จำเป็น — คุณเสร็จแล้ว! หากคุณต้องการวิธีเรียก API ที่豐富กว่า โปรด受邀
โดยนัย หากคุณต้องการตัวอย่างที่เกี่ยวข้องกับ rb_define_method มากขึ้น ฉันแนะนำให้อ่านบทความของ Peter Zhu เรื่อง Defining Methods
การใช้ Ruby C API Internals
ดังนั้น เรากลับไปที่ use case ของเรา: การ parse arguments ที่ซับซ้อน โชคดีที่มีเครื่องมือช่วย
แต่ก่อน เรามาดูข้อจำกัดของการใช้ rb_define_method เพียงอย่างเดียว
ข้อเสียของ rb_define_method
ไม่ได้กล่าวถึง Block Arguments
ข้อจำกัดหนึ่งคือ rb_define_method ไม่ได้กล่าวถึง block arguments เหล่านี้ไม่ได้รับการพิจารณา เนื่องว่าไม่สำคัญกับ Ruby อย่างมากหากคุณส่ง block คุณยังคงสามารถรับรองได้ว่ามี block ถูกส่งโดยใช้ rb_block_given_p หรือ rb_need_block มีรายละเอียดเพิ่มเติมในบทความของ Peter Zhu
Args อาจเปลี่ยนแปลงได้
ข้อจำกัดสำคัญอีกอย่างคือ args อาจเปลี่ยนแปลงได้ แต่การเรียกเมธอดเองไม่ได้ถูก constrained มากนัก ดังนั้น หากคุณต้องการ API ของคุณเป็นแบบ def foo(bar, *baz) คุณจะต้อง parse arguments ของคุณ มีเมธอดบางอย่างช่วยคุณบนเส้นทางนี้ rb_check_arity คือหนึ่งที่คุณสามารถใช้ ร่วมกับเวอร์ชัน -1 ของ rb_define_method
นี่คือ function signature:
Keyword Arguments
ข้อจำกัดสุดท้ายที่เราจะผ่านคือการใช้ keyword arguments และฉันเก็บที่ดีที่สุดไว้ท้ายสุด เพราะนี่จะเป็นหัวใจของบทความนี้
เราต้องดึง keyword arguments ก่อนที่จะ parse พวกเขาได้ถูกต้อง โชคดีที่ Ruby C API มาพร้อมเมธอดสำหรับสิ่งนี้ คือ rb_scan_args
นี่คือ signature ของ rb_scan_args:
คุณส่ง rb_scan_args argc และ argv ที่ได้จาก rb_define_method — string ที่บอกว่าอาร์กิวเมนต์ควร parse ยังไง (fmt) และ receiver สำหรับอาร์กิวเมนต์เหล่านั้น นั่นคือทั้งหมด ความซับซ้อนของ Ruby args ถูก parse ในบรรทัดเดียว! นוּ تقريبًا
คุณสามารถอ้างอิง extension.rdoc สำหรับการแทนที่อย่างเป็นทางการของ fmt ควรเขียนอย่างไร แม้เราจะครอบคลุมบางส่วนในตัวอย่างด้านล่าง
เขียนและ Parse ฟังก์ชัน: ตัวอย่าง
สำหรับส่วนที่เหลือของบทความนี้ ให้พิจารณาว่าเราต้องการเขียนฟังก์ชันนี้:
การ parse นี้ด้วย rb_scan_args จะดูเหมือนนี้:
"1*:" ความหมายคือ:
1: อาร์กิวเมนต์ positional ที่จำเป็นหนึ่งตัว*: จำนวนอาร์กิวเมนต์ positional ที่ไม่จำเป็นเท่าไหร่ก็ได้:: keyword arguments ที่ท้าย
Parse Keyword Arguments
ตอนนี้เราจำกัดเมธอดแล้ว แต่ยังไม่เสร็จ API ปัจจุบันของเราคือ def voronoi_diagram(envelope, *polygons, **kwargs)
สุดท้าย เราต้อง parse keyword arguments นั้นๆ โดยใช้ rb_get_kwargs
คุณต้องเลือก required และ optional arguments บางอย่าง เมื่อทำเสร็จ คุณใช้ table เพื่อบอก Ruby ชื่อของอาร์กิวเมนต์เหล่านั้น และเก็บผลลัพธ์ใน array (values)
นั่นคือแล้ว! Ruby method ที่ซับซ้อน ถูก parse ด้วย C เพียงอย่างเดียว อย่างไรก็ตาม หากสิ่งนี้ซับซ้อนเกินไปสำหรับคุณ มีอีกทางเลือกหนึ่ง
การใช้ Ruby Interface
อีกวิธีหนึ่งในการจัดการปัญหาคือใช้ syntax ของ Ruby โดยตรง และทำ parsing ที่ขั้น Ruby
ด้วยสิ่งนี้ คุณสามารถใช้รูปแบบที่สามของ rb_define_method โดยตรง สำหรับ C method ที่ดูเหมือนนี้:
และนั่นคือแล้ว — คุณหลีกเลี่ยงปัญหาโดยสมบูรณ์ด้วยโซลูชันที่ขลังซึ่งถูกใช้สำหรับเมธอดบางอย่างใน Ruby implementation (ด้วยคลาส Primitive)
แม้ว่าคลาสที่ MRI ใช้จะซับซ้อนมากและสร้าง C เอง เราก็ยังได้แรงบันดาลใจจากมัน
ลองสร้างอ็อบเจกต์และเชื่อมเมธอดของเราเพื่อหลีกเลี่ยง c_voronoi_diagram ที่มองเห็นได้สำหรับผู้ใช้ API ของเรา:
ดู real use case ของคลาสนี้ใน codebase ของ RGeo
การ Parse Arguments: ฉันควรใช้วิธีไหน?
Repo นี้แสดงสองวิธีในการตั้งค่า Ruby API ที่ซับซ้อนซึ่งเขียนใน C — สรุป:
- ด้วย
rb_define_methodและ parse ด้วยrb_scan_args - ใช้ Ruby interface
เมื่อเราเปรียบเทียบทั้งสองโซลูชันในแง่ของ performance พวกเขาเกือบเท่ากัน (Ruby parsing เร็วกว่า 1.02 เท่าในเฉลี่ยบน M1 ของฉัน) นั่นไม่ได้ทำความแตกต่างมาก
ใน RGeo lib ตัวเลือกออกแบบแรกของเราคือมี API ที่ใช้แค่ variadic length arguments ไม่มี keywords ไม่มี blocks นี่อาจจำกัดมาก และเราตอนนี้ใช้วิธี Primitive เพื่ออนุญาต arguments ที่ซับซ้อนมากขึ้น
ประโยชน์ของการใช้ Ruby Interface
คำแนะนำของฉันคือใช้ Ruby interface หลายเหตุผล รวมถึง:
- ขนาดโค้ดเล็กลง
- การเปลี่ยนแปลงง่ายขึ้น
โดยรวม ทำให้ประสบการณ์การอ่าน codebase ของคุณเรียบง่ายขึ้น
การมีส่วนร่วมของผู้ใช้ Ruby ในการอ่าน source code ของ gems ที่พวกเขาใช้ สำคัญกับฉัน เนื่องจาก Ruby อ่านง่าย gems ก็ควรจะง่ายเช่นกัน
ประโยชน์ของการใช้ rb_define_method
เวอร์ชัน C แต่ให้เราลิ้มรสเมธอด internal ที่มีประโยชน์บางอย่าง ตัวอย่างเช่น rb_check_arity ยังคงมีประโยชน์มาก เมธอดสำหรับจัดการ blocks ยอดเยี่ยมเช่นกัน และคุณอาจไม่ต้องใช้ Ruby facade สำหรับ blocks
ทั้งหมดเกี่ยวกับการหาเลือกที่ดีที่สุดสำหรับ use case ของคุณ
สรุป
ในโพสต์นี้ เราสำรวจสองวิธีในการ parse arguments ใน Ruby C extension ของคุณ — ใช้ rb_define_method (ดูข้อจำกัดสั้นๆ) และ parse ด้วย rb_scan_args และใช้ Ruby interface
หากคุณต้องการอ่านเพิ่มเติมเกี่ยวกับ C extensions ฉันแนะนำ:
- Building a Ruby C Extension From Scratch
- A Rubyist's Walk Along the C-side
- Working with ruby C extensions on Mac
และคำแนะนำสุดท้ายของฉันให้คุณ: ดู RGeo มันคือ codebase C extension ที่ใช้กันอย่างกว้างขวางและพัฒนาอยู่ต่อไป ส่วนใหญ่ของตัวอย่างของฉันมาจากที่นี่
ขอให้เขียนโค้ดสนุกสนาน!
P.S. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่เผยแพร่ สมัครรับจดหมายข่าว Ruby Magic และจะไม่พลาดโพสต์ใดๆ!

Ulysse Buonomo
ผู้เขียน אורח Ulysse เป็นนักพัฒนา Ruby อุตสาหกรรมก่อนหน้านี้ที่ทุ่มเวลาส่วนใหญ่ไปกับการเดินทางรอบโลก เวลาว่างของเขา dedicated ให้ RGeo และ Ruby และเขาชอบเล่นกับ internal ของ Ruby
บทความทั้งหมดโดย Ulysse Buonomo