在這個 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 世界!