Computer >> Máy Tính >  >> Lập trình >> Ruby

Xây dựng tiện ích mở rộng Ruby C từ Scratch

Trong ấn bản này của Ruby Magic, chúng tôi sẽ hướng dẫn bạn cách sử dụng mã được viết bằng C từ Ruby. Điều này có thể được sử dụng để tối ưu hóa các phần nhạy cảm về hiệu suất trong mã của bạn hoặc để tạo giao diện giữa thư viện C và Ruby. Điều này được thực hiện bằng cách tạo các phần mở rộng bao bọc các thư viện được viết bằng C.

Có rất nhiều thư viện hoàn thiện và hiệu quả được viết bằng C. Thay vì phát minh lại bánh xe bằng cách chuyển chúng, chúng ta cũng có thể tận dụng các thư viện này từ Ruby. Bằng cách này, chúng ta có thể viết mã bằng ngôn ngữ yêu thích của mình, trong khi sử dụng các thư viện C trong các lĩnh vực mà Ruby truyền thống không mạnh. Tại AppSignal, chúng tôi đã sử dụng phương pháp này để phát triển đá quý rdkafka.

Vì vậy, hãy xem làm thế nào một người có thể tiếp cận điều này. Nếu bạn muốn làm theo và tự thử nghiệm, hãy xem mã mẫu. Để bắt đầu, hãy lấy đoạn mã Ruby này với một chuỗi, một số và một boolean (bạn sẽ sử dụng C tại sao, dự định chơi chữ) và chuyển nó vào thư viện C:

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 phương thức được hiển thị sẽ nối một chuỗi, tăng một số lên một và trả về tương ứng với một boolean.

Thư viện của chúng tôi đã được chuyển sang C

Dưới đây, bạn có thể thấy mã được chuyển sang C. Thư viện tiêu chuẩn C và Thư viện IO được bao gồm để chúng tôi có thể sử dụng định dạng chuỗi. Chúng tôi sử dụng char* thay vì một chuỗi String . char* trỏ đến vị trí của bộ đệm các ký tự ở đâu đó trong bộ nhớ.

# 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 có thể thấy, bạn phải nhảy qua một số vòng để thực hiện định dạng chuỗi đơn giản. Để nối một chuỗi, trước tiên chúng ta phải cấp phát một bộ đệm. Với việc này được thực hiện, sprintf sau đó hàm có thể ghi kết quả được định dạng vào nó. Cuối cùng, chúng tôi có thể trả lại bộ đệm.

Với đoạn mã trên, chúng tôi đã giới thiệu một sự cố có thể xảy ra hoặc sự cố bảo mật. Nếu chuỗi đến dài hơn 245 byte, lỗi tràn bộ đệm đáng sợ sẽ xảy ra. Bạn nên cẩn thận khi viết C, rất dễ tự bắn vào chân mình.

Tiếp theo là tệp tiêu đề:

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

Tệp này mô tả API công khai của thư viện C của chúng tôi. Các chương trình khác sử dụng nó để biết hàm nào trong thư viện có thể được gọi.

Cách 2018:Sử dụng ffi Đá quý

Vì vậy, bây giờ chúng ta có một thư viện C mà chúng ta muốn sử dụng từ Ruby. Có hai cách để bọc mã C này trong một viên ngọc. Cách hiện đại liên quan đến việc sử dụng ffi đá quý. Nó tự động hóa nhiều vòng mà chúng ta phải vượt qua. Sử dụng ffi với mã C chúng tôi vừa viết trông như thế này:

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

Với mục đích của bài viết này, chúng tôi cũng sẽ giải thích cách bọc mã C bằng phần mở rộng C. Điều này sẽ cung cấp cho chúng ta cái nhìn sâu sắc hơn về cách hoạt động của tất cả trong Ruby.

Bao bọc Thư viện của chúng tôi trong một Phần mở rộng C

Vì vậy, bây giờ chúng ta có một thư viện C mà chúng ta muốn sử dụng từ Ruby. Bước tiếp theo là tạo một viên đá quý để biên dịch và bao bọc nó. Sau khi tạo gem, trước tiên chúng ta thêm ext tới 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 thông báo cho Rubygems rằng có một phần mở rộng gốc cần được xây dựng. Nó sẽ tìm kiếm một tệp có tên là extconf.rb hoặc một Rakefile . Trong trường hợp này, chúng tôi đã thêm extconf.rb :

require "mkmf"
 
create_makefile "extension"

Chúng tôi yêu cầu mkmf , là viết tắt của "Make Makefile". Đó là một bộ trợ giúp đi kèm với Ruby giúp loại bỏ phần khó khăn trong việc thiết lập bản C. Chúng tôi gọi create_makefile và đặt tên cho phần mở rộng. Điều này tạo ra một Makefile trong đó chứa tất cả cấu hình và lệnh để xây dựng mã C.

Tiếp theo, chúng ta cần viết một số mã C để kết nối thư viện với Ruby. Chúng tôi sẽ tạo một số hàm chuyển đổi các loại C chẳng hạn như char* sang các loại Ruby chẳng hạn như String . Sau đó, chúng ta sẽ tạo một lớp Ruby với mã C.

Trước hết, chúng tôi bao gồm một số tệp tiêu đề từ Ruby. Chúng sẽ nhập các chức năng chúng ta cần để thực hiện chuyển đổi kiểu. Chúng tôi cũng bao gồm library.h tệp tiêu đề mà chúng tôi đã tạo trước đó để chúng tôi có thể gọi thư viện của mình.

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

Sau đó, chúng tôi tạo một hàm để gói từng hàm trong thư viện của chúng tôi. Đây là một cho chuỗi:

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);
}

Trước tiên, chúng tôi kiểm tra xem giá trị Ruby đến có phải là một chuỗi hay không, vì việc xử lý một giá trị không phải là chuỗi có thể gây ra tất cả các loại lỗi. Sau đó, chúng tôi chuyển đổi String của Ruby thành một char* với RSTRING_PTR macro trợ giúp mà Ruby cung cấp. Bây giờ chúng ta có thể gọi thư viện C của chúng ta. Để chuyển đổi char* được trả về , chúng tôi sử dụng bao gồm rb_str_new2 hàm số. Chúng tôi sẽ thêm các hàm gói tương tự cho số và boolean.

Đối với các con số, chúng tôi làm điều gì đó tương tự bằng cách sử dụng NUM2INTINT2NUM người trợ giúp:

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);
}

Phiên bản boolean cũng tương tự. Lưu ý rằng C không thực sự có kiểu boolean. Quy ước là thay vào đó sử 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 tôi có thể kết nối mọi thứ để chúng tôi có thể gọi nó 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);
}

Vâng, bạn đã đọc đúng:chúng tôi có thể tạo các mô-đun, lớp và phương thức Ruby trong C. Chúng tôi thiết lập lớp của mình ở đây. Sau đó, chúng tôi thêm các phương thức Ruby vào lớp. Chúng ta phải cung cấp tên của phương thức Ruby, tên của hàm trình bao bọc C sẽ được gọi và cho biết số lượng đối số.

Sau tất cả các công việc đó, cuối cùng chúng ta có thể gọi mã C của mình:

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

Kết luận

Chúng tôi đã vượt qua vòng lặp, không gặp sự cố và có phần mở rộng C của chúng tôi để hoạt động. Viết C phần mở rộng không dành cho người yếu tim. Ngay cả khi sử dụng ffi bạn vẫn có thể dễ dàng làm hỏng quá trình Ruby của mình. Nhưng nó có thể làm được và có thể mở ra một thế giới thư viện C ổn định và hiệu quả cho bạn!