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

Thành Thức Phân Tích Tham Số Trong Ruby C Extensions: Hướng Dẫn Bước Đi Theo Bước

Ruby là một ngôn ngữ tuyệt vời, được tạo ra cho con người trước và máy tính sau. Nó dễ đọc và viết. Có nhiều cách để viết bất cứ điều gì, và bạn thường có thể đoán được thư viện chuẩn bằng cách gõ tên phương thức bạn đã tự chọn.

Vì lý do này, các tham số của Ruby rất linh hoạt, cho phép chúng ta biểu diễn API rất rõ ràng. Nhưng điều này đi kèm nhược điểm: Ruby khá khó phân tích (parse) cho nhà phát triển C extension!

Trong bài viết này, chúng ta sẽ đi qua hai cách thiết lập API Ruby phức tạp được viết bằng C:

  • với rb_define_method và phân tích nó với rb_scan_args
  • sử dụng Ruby interface

Hãy bắt đầu!

C và Ruby: Giới Thiệu

Như đã đề cập, Ruby khó phân tích cho nhà phát triển C extension.

Ví dụ:


Vẻ đẹp của C, ngôn ngữ Ruby được viết bằng, xuất phát từ sự đơn giản, bao gồm cả tham số hàm:

  • <data-type> <variable-identifier>
  • ... cho tham số variadic.

Những điều này sẽ giúp bạn duy trì codebase không quá khó hiểu.

Đây là cách phức tạp nhất để định nghĩa hàm C:


Khi bạn viết C extension cho codebase Ruby, bạn sẽ bắt đầu hiểu nơi độ phức tạp bắt đầu. Đừng lo — nhà phát triển Ruby MRI đã lo cho chúng ta.

Định Nghĩa Phương Thức Đơn Giản Trong Ruby C Extension

Chúng ta bắt đầu với phương thức bạn sẽ phải dùng tại một thời điểm, rb_define_method.

Bạn cũng có thể làm theo các ví dụ mã trong repo này.

Đây là signature của rb_define_method:


Và theo extension.rdoc của Ruby:

argc là số lượng tham số. Nếu argc là -1, hàm sẽ nhận 3 tham số: argc, argv, và self. Nếu argc là -2, hàm sẽ nhận 2 tham số, self và args, trong đó args là mảng Ruby của các tham số phương thức.

Tóm lại:


Vì vậy nếu API của bạn chỉ bao gồm phương thức với độ dài tham số cố định, hoặc chỉ một tham số variadic (def foo(*bar)), đừng đọc tiếp — bạn xong rồi! Nếu bạn muốn cách gọi API phong phú hơn, mời bạn tiếp tục.

Đầu có, nếu bạn muốn ví dụ sâu hơn về rb_define_method, tôi đề nghị đọc bài viết của Peter Zhu về Defining Methods.

Sử Dụng Ruby C API Internals

Vậy, quay lại use case của chúng ta: phân tích tham số phức tạp. May mắn thay, một số công cụ có thể giúp.

Nhưng trước, hãy xem hạn chế của việc chỉ dùng rb_define_method.

Nhược Điểm Của rb_define_method

Không Nhắc Đến Block Arguments

Một hạn chế là rb_define_method không bao giờ nhắc đến block arguments. Chúng không được xem xét, vì không thực sự quan trọng đối với Ruby nếu bạn truyền block. Bạn vẫn có thể đảm bảo block được truyền bằng cách dùng rb_block_given_p hoặc rb_need_block. Có thêm về chủ đề này trong bài của Peter Zhu.

Args Có Thể Thay Đổi

Một hạn chế quan trọng khác là args có thể thay đổi, nhưng lời gọi phương thức bản thân không bị ràng buộc chặt. Do đó, nếu bạn muốn API như def foo(bar, *baz), bạn sẽ phải phân tích tham số. Có vài phương thức giúp bạn đi đường đó. rb_check_arity là một trong số đó, cùng với phiên bản -1 của rb_define_method.

Đây là function signature:


Keyword Arguments

Một hạn chế cuối chúng ta sẽ đi qua là sử dụng keyword arguments. Và tôi để phần hay nhất cho cuối, vì đó sẽ là cốt lõi của bài viết.

Chúng ta phải lấy keyword arguments trước khi phân tích chúng đúng. May mắn thay, Ruby C API đi kèm phương thức cho việc này, rb_scan_args.

Đây là signature của rb_scan_args:


Bạn truyền rb_scan_args argc và argv cho bởi rb_define_method — một chuỗi nói về cách tham số nên được phân tích (fmt) và receiver cho những tham số đó. Xong, toàn bộ độ phức tạp args của Ruby được phân tích trong một dòng! Gần như.

Bạn có thể tham khảo extension.rdoc cho biểu diễn chính thức cách fmt nên viết, mặc dù chúng ta sẽ bao quát một phần trong các ví dụ dưới đây.

Viết & Phân Tích Hàm: Ví Dụ

Trong phần còn lại của bài, hãy xem xét chúng ta muốn viết hàm này:


Phân tích dùng rb_scan_args sẽ trông như sau:


Chuỗi "1*:" vô nghĩa kia có nghĩa:

  • 1: một tham số positional bắt buộc
  • *: bất kỳ số lượng tham số positional không bắt buộc
  • :: keyword arguments ở cuối

Phân Tích Keyword Arguments

Bây giờ chúng ta đã ràng buộc phương thức, tiếc là chưa xong. API hiện tại là def voronoi_diagram(envelope, *polygons, **kwargs).

Cuối cùng, chúng ta cần phân tích keyword arguments dùng rb_get_kwargs.


Bạn phải chọn một số tham số bắt buộc và tùy chọn. Khi xong, bạn dùng table để báo cho Ruby tên các tham số đó, và lưu kết quả trong mảng (values).


Xong! Một phương thức Ruby phức tạp, được phân tích chỉ dùng C. Tuy nhiên, nếu điều này quá rối rắm cho bạn, có lựa chọn khác.

Sử Dụng Ruby Interface

Cách khác để xử lý vấn đề là thực sự dùng cú pháp Ruby trực tiếp và làm phần phân tích ở stage Ruby.


Với điều này, bạn có thể trực tiếp dùng form thứ ba của rb_define_method, cho hàm C trông như sau:


Và xong — bạn hoàn toàn tránh được vấn đề với giải pháp thanh lịch thực sự được dùng cho một số phương thức trong triển khai Ruby (với lớp Primitive).

Mặc dù lớp dùng bởi MRI khá phức tạp và tự generate C, chúng ta có thể lấy cảm hứng từ nó.

Hãy tạo một đối tượng và gắn phương thức để tránh c_voronoi_diagram hiển thị cho user của API:



Xem use case thực tế của lớp này trong codebase RGeo.

Phân Tích Tham Số: Nên Dùng Phương Pháp Nào?

Repo này trình bày hai cách thiết lập API Ruby phức tạp viết bằng C — tóm lại:

  • với rb_define_method và phân tích nó với rb_scan_args
  • sử dụng Ruby interface

Khi so sánh cả hai giải pháp về hiệu năng, chúng xấp xỉ ngang nhau (Ruby parsing nhanh hơn 1.02 lần trung bình trên M1 của tôi). Điều đó không tạo ra nhiều khác biệt.

Trong lib RGeo, lựa chọn thiết kế đầu tiên là API chỉ dùng variadic length arguments. Không keywords, không blocks. Điều này có thể rất hạn chế, và chúng ta hiện dùng cách Primitive để cho phép tham số phức tạp hơn.

Lợi Ích Của Việc Dùng Ruby Interface

Lời khuyên của tôi là dùng Ruby interface vì nhiều lý do, bao gồm:

  • kích thước mã nhỏ hơn
  • thay đổi dễ dàng hơn

Tổng thể, điều này làm trải nghiệm đọc codebase đơn giản hơn.

Thu hút người dùng Ruby đọc mã nguồn của gem họ dùng quan trọng đối với tôi. Vì Ruby dễ đọc, gem cũng nên dễ đọc.

Lợi Ích Của Việc Dùng rb_define_method

Phiên bản C, tuy nhiên, cho chúng ta chút hương vị của một số phương thức nội bộ rất hữu ích. Ví dụ, rb_check_arity vẫn rất hữu ích. Phương thức xử lý block cũng tuyệt vời, và bạn có thể không cần dùng Ruby facade cho block.

Tất cả là về việc tìm lựa chọn tốt nhất cho use case của bạn.

Kết Luận

Trong bài viết này, chúng ta đã khám phá hai phương pháp phân tích tham số trong Ruby C extension — dùng rb_define_method (cũng nhìn ngắn gọn hạn chế của nó) và phân tích với rb_scan_args và dùng Ruby interface.

Nếu bạn muốn đọc thêm về C extensions, tôi khuyên:

  • Building a Ruby C Extension From Scratch
  • A Rubyist's Walk Along the C-side
  • Working with ruby C extensions on Mac

Và lời khuyên cuối cùng: hãy xem RGeo. Đây là codebase C extension phổ biến trong phát triển tích cực. Hầu hết ví dụ của tôi đến từ đây.

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!

Thành Thức Phân Tích Tham Số Trong Ruby C Extensions: Hướng Dẫn Bước Đi Theo Bước

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