Fiddle là một mô-đun ít được biết đến đã được thêm vào thư viện tiêu chuẩn của Ruby trong 1.9.x. Nó cho phép bạn tương tác trực tiếp với các thư viện C từ Ruby. Nó thậm chí còn cho phép bạn kiểm tra và thay đổi trình thông dịch ruby khi nó chạy.
Nó hoạt động bằng cách gói libffi, một thư viện C phổ biến cho phép mã được viết bằng một ngôn ngữ để gọi các phương thức được viết bằng một ngôn ngữ khác. Trong trường hợp bạn chưa nghe nói về nó, "ffi" là viết tắt của "giao diện chức năng nước ngoài". Và bạn không chỉ giới hạn ở C. Khi bạn học Fiddle, bạn có thể sử dụng các thư viện được viết bằng Rust và các ngôn ngữ khác hỗ trợ nó.
Hãy cùng xem qua Fiddle. Chúng ta sẽ bắt đầu với một ví dụ đơn giản và sau đó kết thúc bằng cách truy cập Arduino qua một cổng nối tiếp. Bạn không cần biết nhiều như vậy C. Tôi hứa. :)
Một ví dụ đơn giản
Trong Ruby cũ, bạn luôn phải xác định một phương thức trước khi có thể gọi nó. Với Fiddle cũng vậy. Bạn không thể chỉ gọi một hàm C trực tiếp. Thay vào đó, bạn phải tạo một trình bao bọc cho hàm C, sau đó gọi trình bao bọc.
Trong ví dụ dưới đây, chúng ta đang gói hàm logarit của C. Về cơ bản, chúng tôi đang sao chép Math.log
của Ruby .
require 'fiddle'
# We're going to "open" a library, so we have to tell Fiddle where
# it's located on disk. This example works on OSX Yosemite.
libm = Fiddle.dlopen('/usr/lib/libSystem.dylib')
# Create a wrapper for the c function "log".
log = Fiddle::Function.new(
libm['log'], # Get the function from the math library we opened
[Fiddle::TYPE_DOUBLE], # It has one argument, a double, which is similar to ruby's Float
Fiddle::TYPE_DOUBLE # It returns a double
)
# call the c function via our wrapper
puts log.call(3.14159)
Làm cho nó đẹp
Ví dụ trước hoạt động, nhưng nó hơi dài dòng. Tôi chắc rằng bạn có thể tưởng tượng mọi thứ sẽ lộn xộn như thế nào nếu bạn phải gói 100 chức năng. Đó là lý do tại sao Fiddle cung cấp một DSL đẹp. Nó được hiển thị qua Fiddle::Importer
mixin.
Việc sử dụng mixin này giúp cho việc tạo một mô-đun có đầy đủ các chức năng bên ngoài trở nên ngắn gọn. Trong ví dụ dưới đây, chúng tôi đang tạo một mô-đun có chứa một số phương pháp logarit.
require 'fiddle'
require 'fiddle/import'
module Logs
extend Fiddle::Importer
dlload '/usr/lib/libSystem.dylib'
extern 'double log(double)'
extern 'double log10(double)'
extern 'double log2(double)'
end
# We can call the external functions as if they were ruby methods!
puts Logs.log(10) # 2.302585092994046
puts Logs.log10(10) # 1.0
puts Logs.log2(10) # 3.321928094887362
Điều khiển cổng nối tiếp
Ok, vậy là cuối cùng bạn đã mua được một trong những Arduinos mà bạn đã nghĩ đến việc mua trong nhiều năm. Bạn đã gửi văn bản "hello world" đến máy tính của mình mỗi giây một lần bằng cổng nối tiếp.
Bây giờ, sẽ thực sự tuyệt vời nếu bạn có thể đọc dữ liệu đó bằng Ruby. Và bạn thực sự có thể đọc từ cổng nối tiếp bằng các phương thức IO tiêu chuẩn của Ruby. Nhưng các phương pháp IO này không cho phép chúng tôi định cấu hình đầu vào nối tiếp với mức độ chi tiết mà chúng tôi cần nếu chúng tôi là tin tặc phần cứng hạng nặng.
May mắn thay, thư viện chuẩn C cung cấp tất cả các chức năng chúng ta cần để làm việc với cổng nối tiếp. Và sử dụng Fiddle, chúng ta có thể truy cập các hàm C này trong Ruby.
Tất nhiên, đã có đá quý làm điều này cho bạn. Khi tôi làm việc trên mã này, tôi đã lấy một chút cảm hứng từ viên ngọc rubyserial. Nhưng mục tiêu của chúng tôi là học cách tự thực hiện công việc này.
Kiểm tra termios.h
Trong C, bất kỳ tên tệp nào kết thúc bằng .h đều là tệp tiêu đề. Nó chứa danh sách tất cả các hàm và hằng số mà thư viện cung cấp cho mã của bên thứ ba (mã của chúng tôi). Bước đầu tiên trong việc gói bất kỳ thư viện C nào sẽ là tìm tệp này, mở tệp và xem xung quanh.
Thư viện mà chúng tôi đang sử dụng được gọi là termios. Trên OSX Yosemite, tệp tiêu đề được đặt tại /usr/include/sys/termios.h. Nó sẽ ở một nơi khác trên linux.
Đây là những gì termios.h trông như thế nào. Tôi đã cô đọng lại một chút để rõ ràng.
typedef unsigned long tcflag_t;
typedef unsigned char cc_t;
typedef unsigned long speed_t;
struct termios {
tcflag_t c_iflag; /* input flags */
tcflag_t c_oflag; /* output flags */
tcflag_t c_cflag; /* control flags */
tcflag_t c_lflag; /* local flags */
cc_t c_cc[NCCS]; /* control chars */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};
int tcgetattr(int, struct termios *);
int tcsetattr(int, int, const struct termios *);
int tcflush(int, int);
Có ba điều quan trọng cần lưu ý về mã này. Đầu tiên, chúng tôi có một số typedef. Sau đó, chúng tôi có một cấu trúc dữ liệu được sử dụng để giữ thông tin cấu hình cho kết nối. Cuối cùng, chúng tôi có các phương pháp, chúng tôi sẽ sử dụng.
Sử dụng trình nhập khẩu của fiddle, chúng ta gần như có thể sao chép nguyên văn các phần này vào mã Ruby của mình. Hãy giải quyết từng cái một.
Nhập bí danh và typedefs
Trong C, việc tạo bí danh cho các kiểu dữ liệu khá phổ biến. Ví dụ:tệp termios.h tạo một kiểu mới gọi là speed_t chỉ là một số nguyên dài. Trình nhập Fiddle có thể xử lý những điều này thông qua tính năng typealias của nó.
# equivalent to `typedef unsigned long tcflag_t;`
typealias "tcflag_t", "unsigned long"
Cấu trúc
C không có lớp học hoặc mô-đun. Nếu bạn muốn nhóm một loạt các biến lại với nhau, bạn sử dụng một cấu trúc. Fiddle cung cấp một cơ chế tuyệt vời để làm việc với các cấu trúc trong ruby.
Bước đầu tiên là xác định cấu trúc trong trình nhập Fiddle. Như bạn có thể thấy, chúng tôi gần như chỉ có thể sao chép và dán từ tệp tiêu đề.
Termios = struct [
'tcflag_t c_iflag',
'tcflag_t c_oflag',
'tcflag_t c_cflag',
'tcflag_t c_lflag',
'cc_t c_cc[20]',
'speed_t c_ispeed',
'speed_t c_ospeed',
]
Bây giờ chúng ta có thể tạo một "instance" của struct bằng cách sử dụng phương thức malloc. Chúng ta có thể đặt và truy xuất các giá trị giống như cách chúng ta làm với bất kỳ cá thể lớp Ruby bình thường nào.
s = Termios.malloc
s.c_iflag = 12345
Tập hợp tất cả lại với nhau
Nếu bạn kết hợp những gì chúng ta vừa học được về typedef và cấu trúc với những gì chúng ta đã trình bày với các hàm, chúng ta có thể kết hợp một trình bao bọc hoạt động của thư viện termios.
Trình bao bọc của chúng tôi chỉ chứa các phương thức chúng tôi cần để định cấu hình cổng nối tiếp. Chúng tôi sẽ sử dụng ruby cũ đơn thuần cho phần còn lại.
require 'fiddle'
require 'fiddle/import'
# Everything in this module was pretty much copied directly from
# termios.h. In Yosemite it's at /usr/include/sys/termios.h
module Serial
extend Fiddle::Importer
dlload '/usr/lib/libSystem.dylib'
# Type definitions
typealias "tcflag_t", "unsigned long"
typealias "speed_t", "unsigned long"
typealias "cc_t", "char"
# A structure which will hold configuratin data. Instantiate like
# so: `Serial::Termios.malloc()`
Termios = struct [
'tcflag_t c_iflag',
'tcflag_t c_oflag',
'tcflag_t c_cflag',
'tcflag_t c_lflag',
'cc_t c_cc[20]',
'speed_t c_ispeed',
'speed_t c_ospeed',
]
# Functions for working with a serial device
extern 'int tcgetattr(int, struct termios*)' # get the config for a serial device
extern 'int tcsetattr(int, int, struct termios*)' # set the config for a serial device
extern 'int tcflush(int, int)' # flush all buffers in the device
end
Sử dụng thư viện
Trong ví dụ dưới đây, chúng tôi mở thiết bị nối tiếp của mình trong Ruby, sau đó lấy bộ mô tả tệp của nó. Chúng tôi sử dụng các hàm thuật ngữ của mình để định cấu hình thiết bị để đọc. Sau đó, chúng tôi đọc.
Tôi đang sử dụng một số con số kỳ diệu ở đây. Đây là kết quả của việc kết hợp một loạt các tùy chọn cấu hình bitwise cần thiết để giao tiếp với arduino. Nếu bạn quan tâm đến nguồn gốc của những con số kỳ diệu, hãy xem bài đăng trên blog này.
file = open("/dev/cu.wchusbserial1450", 'r')
fd = file.to_i
# Create a new instance of our config structure
config = Serial::Termios.malloc
# Load the default config options into our struct
Serial.tcgetattr(fd, config);
# Set config options important to the arduino.
# I'm sorry for the magic numbers.
config.c_ispeed = 9600;
config.c_ospeed = 9600;
config.c_cflag = 51968;
config.c_iflag = 0;
config.c_oflag = 0;
# wait for 12 characters to come in before read returns.
config.c_cc[17] = 12;
# no minimum time to wait before read returns
config.c_cc[16] = 0;
# Save our configuration
Serial.tcsetattr(fd, 0, config);
# Wait 1 second for the arduino to reboot
sleep(1)
# Remove any existing serial input waiting to be read
Serial.tcflush(fd, 1)
buffer = file.read(12)
puts "#{ buffer.size } bytes: #{ buffer }"
file.close
Trình đọc nối tiếp đang hoạt động
Nếu bây giờ tôi tải arduino của mình bằng một chương trình liên tục in hello world, tôi có thể sử dụng tập lệnh ruby của mình để đọc nó.
Chương trình arduino này chỉ ghi "hello world" vào nối tiếp, lặp đi lặp lại
Và đây là những gì nó trông như thế nào khi tôi chạy màn hình nối tiếp của mình.