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

Làm chủ Ruby C Extensions: Xây dựng High-Performance Modules từ Con số Không

Trong edition này của Ruby Magic, chúng ta sẽ show bạn cách dùng code viết bằng C từ Ruby. Điều này có thể dùng để optimize performance sensitive parts của code hoặc tạo interface giữa C library và Ruby. Điều này được thực hiện bằng tạo extensions wrap libraries viết bằng C.

Có nhiều mature và performant libraries viết bằng C. Thay vì reinvent the wheel bằng port chúng ta cũng có thể leverage các libraries này từ Ruby. Cách này, chúng ta get to code in favorite language, trong khi dùng C libraries ở areas Ruby không traditionally strong. Tại AppSignal, chúng ta đã dùng approach này develop rdkafka gem.

Vậy hãy xem cách approach này. Nếu bạn muốn follow along và experiment yourself, check out example code. Để start off, hãy lấy piece of Ruby code với string, number và boolean (you'll C why, pun intended) và port nó sang C library:

module CFromRubyExample class Helpers def self.string(value) "String: '#{value}'" end def self.number(value) value + 1 end def self.boolean(value) !value end end end

Theo thứ tự, các methods shown concatenate string, increase number by one và return opposite của boolean, tương ứng.

Library Của Chúng Ta Ported to C

Dưới đây, bạn thấy code ported to C. C Standard Library và IO Library được include để dùng string formatting. Chúng ta dùng char* thay vì Ruby String. char* trỏ đến location của buffer characters đâu đó trong memory.

# include <stdlib.h> # include <stdio.h> char* string_from_library(char* value) { char* out = (char*)malloc(256 * sizeof(char)); sprintf(out, "String: '%s'", value); return out; } int number_from_library(int value) { return value + 1; } int boolean_from_library(int value) { if (value == 0) { return 1; } else { return 0; } }

Như bạn thấy, bạn phải nhảy qua vài hoops để làm simple string formatting. Để concatenate string, chúng ta trước hết allocate buffer. Với việc này done, function sprintf có thể write formatted result vào nó. Cuối cùng, chúng ta return buffer.

Với code trên, chúng ta đã introduce possible crash hoặc security issue. Nếu incoming string dài hơn 245 bytes, dreaded buffer overflow sẽ xảy ra. Bạn definitely should be careful khi viết C, dễ shoot yourself in the foot.

Tiếp theo là header file:

char* string_from_library(char*); int number_from_library(int); int boolean_from_library(int);

File này mô tả public API của C library của chúng ta. Programs khác dùng nó để biết function nào trong library có thể gọi.

The 2018 Way: Dùng ffi Gem

Vậy bây giờ chúng ta có C library muốn dùng từ Ruby. Có hai cách wrap C code này trong gem. Cách modern involve dùng ffi gem. Nó automate nhiều hoops chúng ta phải nhảy. Dùng ffi với C code chúng ta vừa viết trông như sau:

module CFromRubyExample class Helpers extend FFI::Library ffi_lib File.join(File.dirname(__FILE__), "../../ext/library.so") attach_function :string, [:string], :string attach_function :number, [:int], :int attach_function :boolean, [:int], :int end end

Cho purpose của bài này, chúng ta cũng sẽ giải thích cách wrap C code với C extension. Điều này sẽ give chúng ta more insight vào cách nó all works under the hood trong Ruby.

Wrapping Library của Chúng Ta trong C Extension

Vậy bây giờ chúng ta có C library muốn dùng từ Ruby. Bước tiếp theo là tạo gem compile và wrap nó. Sau khi tạo gem, chúng ta trước hết add ext vào require_paths trong gemspec:

Gem::Specification.new do |spec| spec.name = "c_from_ruby_example" # ... spec.require_paths = ["lib", "ext"] end

Điều này inform Rubygems có native extension cần build. Nó sẽ look for file gọi extconf.rb hoặc Rakefile. Trong case này, chúng ta thêm extconf.rb:

require "mkmf" create_makefile "extension"

Chúng ta require mkmf, stands for "Make Makefile". Nó là set of helpers included with Ruby loại bỏ finicky part của getting C build setup. Chúng ta call create_makefile và set name cho extension. Điều này tạo Makefile chứa tất cả configuration và commands build C code.

Tiếp theo, chúng ta cần viết C code connect library đến Ruby. Chúng ta sẽ tạo functions convert C types như char* sang Ruby types như String. Sau đó tạo Ruby class với C code.

Đầu tiên, include một số header files từ Ruby. Chúng sẽ import functions chúng ta cần để type conversion. Chúng ta cũng include library.h header file chúng ta tạo trước để call library của chúng ta.

#include "ruby/ruby.h" #include "ruby/encoding.h" #include "library.h"

Chúng ta sau đó tạo function wrap mỗi function trong library. Đây là cái cho string:

static VALUE string(VALUE self, VALUE value) { Check_Type(value, T_STRING); char* pointer_in = RSTRING_PTR(value); char* pointer_out = string_from_library(pointer_in); return rb_str_new2(pointer_out); }

Chúng ta trước check nếu Ruby value coming in là string, vì processing non-string value có thể cause all sorts of bugs. Chúng ta sau đó convert Ruby String to char* với helper macro RSTRING_PTR Ruby cung cấp. Chúng ta bây giờ có thể call C library. Để convert returned char*, chúng ta dùng includes rb_str_new2 function. Chúng ta sẽ add similar wrapping functions cho number và boolean.

Cho numbers, chúng ta làm similar dùng helpers NUM2INT và INT2NUM:

static VALUE number(VALUE self, VALUE value) { Check_Type(value, T_FIXNUM); int number_in = NUM2INT(value); int number_out = number_from_library(number_in); return INT2NUM(number_out); }

Boolean version cũng similar. Lưu ý C không actually có boolean type. Convention là dùng 0 và 1.

static VALUE boolean(VALUE self, VALUE value) { int boolean_in = RTEST(value); int boolean_out = boolean_from_library(boolean_in); if (boolean_out == 1) { return Qtrue; } else { return Qfalse; } }

Cuối cùng, chúng ta wire up everything để call từ Ruby:

void Init_extension(void) { VALUE CFromRubyExample = rb_define_module("CFromRubyExample"); VALUE NativeHelpers = rb_define_class_under(CFromRubyExample, "NativeHelpers", rb_cObject); rb_define_singleton_method(NativeHelpers, "string", string, 1); rb_define_singleton_method(NativeHelpers, "number", number, 1); rb_define_singleton_method(NativeHelpers, "boolean", boolean, 1); }

Đúng, bạn đọc đúng: chúng ta có thể tạo Ruby modules, classes và methods trong C. Chúng ta setup class ở đây. Chúng ta sau đó add Ruby methods vào class. Chúng ta phải provide name của Ruby method, name của C wrapper function sẽ được called và indicate number of arguments.

Sau all that work, chúng ta cuối cùng có thể call C code:

CFromRubyExample::NativeHelpers.string("a string")

Kết luận

Chúng ta nhảy qua hoops, không crash và get C extension work. Viết C extensions không dành cho faint of heart. Dù dùng ffi gem bạn vẫn quite easily crash Ruby process. But it is doable và can open up world của performant và stable C libraries cho bạn!