Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Ruby C Extensions: สร้าง High-Performance Modules จากศูนย์

在這個 Ruby Magic 版本 我們將向你展示如何從 Ruby 使用 C 寫的代碼 這可用於優化 performance sensitive 部分的代碼 或創建 C library 和 Ruby 之間的 interface 這是通過創建 wrap C libraries 的 extensions 完成的

有很多成熟和 performant 的 libraries 寫在 C 中 我們不必 reinvent the wheel 通過 porting 它們 我們也可以從 Ruby 利用這些 libraries 在 Ruby 不傳統強的領域 在 AppSignal 我們在開發 rdkafka gem 時使用過這種方法

所以讓我們看看如何 approach 這個 如果你想跟著做實驗 看看 example code 為了開始 讓我們拿這段 Ruby code 帶有 string, number 和 boolean (你會 C 為什麼, pun intended) 並 port 它到 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

依次 顯示的方法分別 concatenate 一個字符串 增加一個數字一 並返回 boolean 的相反

Our Library Ported to C

下面 你可以看到代碼 ported 到 C C Standard Library 和 IO Library 被 included 以便我們可以使用 string formatting 我們使用 char* 而不是 Ruby String char* 指向內存中某處字符緩衝區的位置

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

正如你所見 你必須跳過一些 hoops 來做簡單的 string formatting 為了 concatenate 一個字符串 我們首先必須 allocate 一個 buffer 有了這個 sprintf 函數可以將 formatted result 寫入它 最後 我們可以返回 buffer

有了上面的代碼 我們已經引入了一個可能的 crash 或 security issue 如果傳入字符串超過 245 bytes 可怕的 buffer overflow 將發生 你絕對應該小心寫 C 很容易射中自己的腳

下一個是 header file:

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

這個文件描述我們 C library 的 public API 其他程序用它知道 library 中哪些函數可以被調用

The 2018 Way: Use the ffi Gem

所以 我們現在有一個我們想從 Ruby 使用的 C library 有兩種方式 wrap 這個 C code 在 gem 中 現代方式涉及使用 ffi gem 它自動化我們必須跳過的許多 hoops 使用 ffi 配合我們剛寫的 C code 長這樣:

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

為了這篇文章的目的 我們也將解釋如何用 C extension wrap C code 這將給我們更多關於 Ruby 底層如何運作的洞察

Wrapping our Library in a C Extension

所以 我們現在有一個我們想從 Ruby 使用的 C library 下一步是創建一個編譯並 wrap 它的 gem 創建 gem 後 我們首先添加 ext 到 gemspec 中的 require_paths:

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

這通知 Rubygems 有一個 native extension 需要被 built 它將尋找一個叫 extconf.rb 或 Rakefile 的文件 在這種情況下 我們添加了 extconf.rb:

require "mkmf"
create_makefile "extension"

我們 require mkmf 代表 "Make Makefile" 它是包含在 Ruby 中的一組 helpers 消除 C build setup 的 finicky 部分 我們調用 create_makefile 並為 extension 設置名稱 這創建一個包含所有配置和命令來 build C code 的 Makefile

接下來 我們需要寫一些 C code 連接 library 到 Ruby 我們將創建一些函數轉換 C types 如 char* 到 Ruby types 如 String 然後我們將用 C code 創建一個 Ruby class

首先 我們 include 一些來自 Ruby 的 header files 它們將 import 我們需要做 type conversion 的函數 我們也 include 我們之前創建的 library.h header file 以便我們可以調用我們的 library

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

然後我們為我們 library 中的每個函數創建一個 wrapper function 這是 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);
}

我們首先檢查傳入的 Ruby value 是否為 string 因為處理非 string value 可能導致各種 bugs 然後我們用 Ruby 提供的 RSTRING_PTR helper macro 將 Ruby String 轉換為 char* 我們現在可以調用我們的 C library 為了轉換返回的 char* 我們使用 included rb_str_new2 函數 我們將為 number 和 boolean 添加類似的 wrapping functions

對於 numbers 我們用 NUM2INT 和 INT2NUM helpers 做類似的事:

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 版本也類似 注意 C 實際上沒有 boolean type 慣例是改用 0 和 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;
  }
}

最後 我們可以 wire up 一切以便從 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);
}

是的 你沒讀錯: 我們可以在 C 中創建 Ruby modules, classes 和 methods 我們在這裡設置我們的類 然後我們向類添加 Ruby methods 我們必須提供 Ruby method 的名稱 將被調用的 C wrapper function 的名稱 和指示參數數量

經過所有那些工作 我們最終可以調用我們的 C code:

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

Conclusion

我們跳過 hoops 沒有 crash 並讓我們的 C extension 工作 寫 C extensions 不是為膽小鬼 即使使用 ffi gem 你仍然很容易 crash 你的 Ruby process 但它是可行的 並能為你打開一個 performant 和穩定的 C libraries 世界!