Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Ngăn Chặn Rò Rỉ Bộ Nhớ Khi Gọi Phương Thức Ruby Từ C Extensions

Rò rỉ bộ nhớ (memory leaks) là nỗi đau cho người dùng gem. Chúng khó theo dõi và có thể dẫn đến chi phí cơ sở hạ tầng đắt đỏ.

Rò rỉ bộ nhớ trong C extension còn tệ hơn. Bạn sẽ thấy nhiều công cụ và bài viết về tìm rò rỉ trong Ruby. Tuy nhiên, bạn không có cùng quyền truy cập vào nội bộ trong C.

Việc sử dụng ngây thơ rb_funcall có thể gây rò rỉ bộ nhớ: tốt hơn nhiều là dùng rb_protect thay thế. Vì vậy, nếu bạn là người viết C extension, hãy đọc tiếp vì sự phát triển của các developer sẽ dùng gem của bạn.

Hãy bắt đầu!

Vấn Đề Của rb_funcall Và C

rb_funcall có thể là công cụ tuyệt vời khi bạn cần tương tác giữa Ruby và phần C của thư viện nhưng chỉ cần viết ít C.

Tuy nhiên, khi bạn chạy rb_funcall, bạn không còn ở C nơi mọi thứ đơn giản. Bạn có thể bị छोड़ trong "nước đục" nếu hàm được gọi:

  1. Hoàn toàn thay đổi định nghĩa của nó trong runtime
  2. Ném ra một exception (raise)

Số 1 là dễ bắt nhất. Bạn có thể sẽ gặp segfault, và nếu test suite đủ hoàn chỉnh, bạn nên bắt được trước khi publish.

Tuy nhiên, cái sau có thể gây rò rỉ bộ nhớ và làm codebase của bạn khó đọc hơn nhiều. Hãy xem điều đó ngay bây giờ.

Raise Trong Ruby Gây Rò Rỉ Bộ Nhớ C

Cơ chế raise của Ruby nhảy giữa các phần code từ scope này sang scope cha đầu tiên bắt lỗi. Điều này được triển khai trong MRI dùng longjmp và setjmp.

Nếu bạn quan tâm cách xây dựng, hãy đọc chương Evaluator trong Ruby Hacking Guide. Tóm lại, khi bạn dùng khối begin..ensure, bạn setjmp(), và khi raise trong khối này, bạn longjmp() về vị trí đã lưu.

Vì vậy nếu hàm raise với rb_funcall, mã C được gọi sau nó không bao giờ thực thi.

Ví dụ dưới đây minh họa rò rỉ tiềm ẩn. Nếu json_parse raise, nó sẽ rò rỉ.


Tất nhiên, ví dụ trên hơi ngớ ngẩn — bạn có thể đảo ngược phần freeing và xử lý Ruby. Tuy nhiên, điều này không phải lúc nào cũng khả thi, và thân hàm dài hơn có thể rối ren hơn.

Dùng begin..ensure Trong Ruby

Nếu bạn dùng Ruby, bạn có thể viết ví dụ trên dùng begin..ensure:


API này cũng có sẵn trong C với rb_rescue và rb_ensure:


Tuy nhiên, điều này hơi rườm rà, và nếu bạn muốn thêm khối rescue, nó trở nên kém đọc hơn nhiều. Tôi đề nghị đọc 'A Rubyist's Walk Along the C-side (Part 8): Exceptions & Error Handling' của Peter Zhu nếu bạn muốn dùng API begin..rescue..ensure..end trong C.

Dùng rb_protect Cho C

Có một lựa chọn khác. Đầu tiên, hãy xem nó trông như thế nào trong Ruby:


Điều này trông lạ trong Ruby, nhưng là quy trình làm việc rất phù hợp với C. MRI có API cho điều đó, rb_protect, và hàm C trông như sau:


Phương pháp trên sẽ re-raise (ném lại) lỗi Ruby sau khi đã free mọi thứ.

Lưu ý rằng chúng ta cũng có thể chọn bỏ qua lỗi bằng cách dùng khối rescue rỗng trong Ruby:


Cảnh báo: Nếu bạn không raise lỗi, bước rb_set_errinfo(Qnil) quan trọng để bạn không giữ thông tin về lỗi mà người dùng không nên biết.

Hoặc, bạn có thể có điều kiện chọn raise lỗi, như rescue My::Error:


Bạn thực sự có thể coi rb_errinfo() giống như biến toàn cục $!.

Tất cả điều này tuyệt vời, nhưng khi归结为 chỉ một rb_funcall, chúng ta có thể đơn giản hóa API đó.

Ý tưởng tổng thể đằng sau việc dùng API rb_protect khi có hàm có thể raise là tăng tính đọc được. Bạn không cần kiểm tra xem hàm có thể raise hay không, bạn giả định nó có thể, và dùng state để làm việc với điều đó.

Đề Xuất rb_protect_funcall

Hãy tách biệt rb_funcall, vì đó là phương thức nguy hiểm duy nhất. Đây là API sẽ làm điều đó:


API này giống rb_funcall, với state từ rb_protect. Do đó việc sử dụng khá trực tiếp:


API này chưa có sẵn trong Ruby, và có thể không bao giờ có. Bạn có thể lấy nó từ RGeo (MIT LICENSE).

Ví Dụ Thực Tế

Nếu bạn muốn xem ví dụ thực tế, tôi khuyến khích bạn đọc codebase RGeo vì chúng ta gần đây chuyển sang dùng toàn bộ rb_protect. Chúng ta thậm chí có một số hàm, như rgeo_convert_to_geos_geometry, lan truyền state này để dùng đơn giản hơn. Hàm này là nơi tốt để bắt đầu đào sâu.

Đừng ngần ngại mở issue trên RGeo để thảo luận thêm về các lựa chọn chúng ta đã làm.

Kết Luận

Trong bài viết này, chúng ta đã cảnh báo chống lại việc dùng rb_funcall với C vì nó có thể gây rò rỉ bộ nhớ. Chúng ta đã khám phá việc dùng begin..ensure hoặc rb_protect thay thế.

Chúc bạn viết mã vui vẻ!

P.S. Nếu bạn muốn đọc các bài Ruby Magic ngay khi chúng xuất bản, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ bất kỳ bài viết nào!

Ngăn Chặn Rò Rỉ Bộ Nhớ Khi Gọi Phương Thức Ruby Từ C Extensions

Ulysse Buonomo

Tác giả khách Ulysse là cựu nhà phát triển Ruby trong ngành công nghiệp, dành hầu hết thời gian đi du lịch quanh thế giới. Thời gian rảnh anh dành cho RGeo và Ruby, và thích thú cài vặt với nội bộ Ruby.

Tất cả bài viết của Ulysse Buonomo