Memory leaks เป็นปัญหาสำหรับผู้ใช้ gem พวกมันยากที่จะติดตามและอาจนำไปสู่ต้นทุนโครงสร้างพื้นฐานที่แพง
Memory leaks ภายใน C extension แย่กว่า คุณจะเห็นเครื่องมือและบทความมากมายเกี่ยวกับการหา leaks ใน Ruby แต่ใน C คุณไม่มีการเข้าถึง internal เหมือนกัน
การใช้ rb_funcall แบบ naive อาจทำให้เกิด memory leaks: ใช้ rb_protect แทนจะดีกว่ามาก ดังนั้น หากคุณเป็นนักเขียน C extension โปรดอ่านต่อเพื่อนักพัฒนาที่จะใช้ gem ของคุณ
ลองเริ่มกันเถอะ!
ปัญหาของ rb_funcall และ C
rb_funcall สามารถเป็นเครื่องมือที่ยอดเยี่ยมเมื่อคุณต้องการโต้ตอบระหว่าง Ruby และส่วน C ของไลบรารีของคุณ แต่ต้องการเขียน C เพียงเล็กน้อย
อย่างไรก็ตาม เมื่อคุณรัน rb_funcall คุณจะไม่ได้อยู่ใน C อีกต่อไป ซึ่งทุกอย่างตรงไปตรงมา คุณอาจถูกทิ้งไว้ในน้ำท้องทุ่งหากฟังก์ชันที่เรียก:
- เปลี่ยนแปลงคำนิยามของมันโดยสมบูรณ์ระหว่าง runtime
- Raise ขึ้น
ข้อ 1 เป็นเรื่องง่ายที่สุดในการจับ คุณอาจจบลงกับ segfault และถ้า test suite ของคุณครบถ้วนพอ คุณควรจับได้ก่อนเผยแพร่
อย่างไรก็ตาม ข้อหลังสามารถทำให้เกิด memory leaks และทำให้โค้ดของคุณอ่านยากขึ้น มาดูตอนนี้
Raise ใน Ruby ทำให้เกิด C Memory Leaks
กลไกการ raise ของ Ruby กระโดดระหว่างส่วนของโค้ดจาก scope หนึ่งไปยัง parent แรกที่จับข้อผิดพลาด ได้รับการ implement ใน MRI โดยใช้ longjmp และ setjmp
หากคุณสนใจว่าสิ่งนี้สร้างขึ้นอย่างไร ให้อ่านบท Evaluator ใน Ruby Hacking Guide ในข้อความสรุป เมื่อคุณใช้บล็อก begin..ensure คุณ setjmp() และเมื่อคุณ raise ภายในบล็อกนี้ คุณ longjmp() ไปยังตำแหน่งที่บันทึกไว้
ดังนั้น หากฟังก์ชันถูก raise ด้วย rb_funcall โค้ด C ที่เรียกหลังจากมันจะไม่ถูก execute
ตัวอย่างด้านล่างแสดงให้เห็นถึง potential leak หาก json_parse raise มันจะ leak
แน่นอน ตัวอย่างข้างต้นมี点เล็กน้อย — คุณอาจกลับลำดับการ free และการประมวลผล Ruby แต่สิ่งนี้ไม่ได้เป็นไปได้เสมอไป และ function bodies ที่ยาวขึ้นอาจซับซ้อนมากขึ้น
การใช้ begin..ensure ใน Ruby
หากคุณใช้ Ruby คุณสามารถเขียนตัวอย่างข้างต้นด้วย begin..ensure แทน:
API นี้ยังมีใน C ด้วย rb_rescue และ rb_ensure:
อย่างไรก็ตาม นี้มี点 cumbersome และหากคุณต้องการเพิ่มบล็อก rescue เข้าไป มันจะอ่านยากมากขึ้น ฉันแนะนำให้อ่าน 'A Rubyist's Walk Along the C-side (Part 8): Exceptions & Error Handling' ของ Peter Zhu หากคุณต้องการใช้ API begin..rescue..ensure..end ใน C
การใช้ rb_protect สำหรับ C
มีอีกทางเลือกหนึ่ง ก่อนอื่น เรามาดูว่ามันอาจดูอย่างไรใน Ruby:
สิ่งนี้ดูแปลกใน Ruby แต่เป็น workflow ที่เหมาะกับ C มาก MRI มี API สำหรับสิ่งนี้คือ rb_protect และ C function ดูเหมือนนี้:
เมธอดข้างต้นจะ re-raise Ruby error หลังจาก free ทุกอย่างแล้ว
สังเกตว่าเราสามารถเลือกที่จะ ignore error ได้โดยใช้บล็อก rescue ว่างใน Ruby:
คำเตือน: หากคุณไม่ raise error ขั้นตอน rb_set_errinfo(Qnil) สำคัญมาก เพื่อไม่ให้เก็บข้อมูลเกี่ยวกับข้อผิดพลาดที่ผู้ใช้ไม่ควรทราบ
หรือคุณสามารถเลือกเงื่อนไข raise error ได้ เหมือน rescue My::Error:
คุณสามารถพิจารณา rb_errinfo() ว่าเหมือนกับตัวแปร global $!
ทั้งหมดนี้ดีมาก แต่เมื่อมุ่งเน้นไปที่ rb_funcall เพียงตัวเดียว เราสามารถลด API นี้ลงได้
แนวคิดโดยรวมเบื้องหลังการใช้ API rb_protect เมื่อมีฟังก์ชันที่อาจ raise คือเพื่อเพิ่มความอ่านง่าย คุณไม่ต้องตรวจสอบว่าฟังก์ชันสามารถ raise ได้หรือไม่ คุณสมมติว่ามันทำได้ และใช้ state เพื่อทำงานกับมัน
ข้อเสนอ rb_protect_funcall
ลองแยก rb_funcall ออกมา เนื่องจากมันคือเมธอด อันตราย เพียงตัวเดียวที่จะใช้ นี่คือ API ที่จะทำสิ่งนั้น:
API นี้เหมือนกับ rb_funcall พร้อม state จาก rb_protect ดังนั้นการใช้งานจึงตรงไปตรงมา:
API นี้ยังไม่มีใน Ruby และอาจไม่มีเลย คุณสามารถนำไปใช้จาก RGeo (MIT LICENSE)
ตัวอย่างจากโลกจริง
หากคุณต้องการดูตัวอย่างจากโลกจริง ฉันขอแนะนำให้อ่านโค้ดของ RGeo เนื่องจากเราเปลี่ยนมาใช้ rb_protect เต็มที่ล่าสุด เรามีฟังก์ชันบางอย่าง เช่น rgeo_convert_to_geos_geometry ที่ propagate state นี้เพื่อการใช้งานที่ง่ายขึ้น ฟังก์ชันนี้เป็นจุดเริ่มต้นที่ดีในการขุดค้น
อย่าลังเลเปิด issue บน RGeo เพื่ออภิปรายการเลือกของเราเพิ่มเติม
สรุป
ในโพสต์นี้ เราเตือนเรื่องการใช้ rb_funcall กับ C เนื่องจากอาจทำให้เกิด memory leaks เราสำรวจการใช้ begin..ensure หรือ rb_protect แทน
ขอให้เขียนโค้ดสนุกสนาน!
P.S. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่เผยแพร่ สมัครรับจดหมายข่าว Ruby Magic และจะไม่พลาดโพสต์ใดๆ!

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