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

Truy cập bất kỳ C Library nào từ Ruby với Fiddle: Mở khóa khả năng ẩn của Ruby

Fiddle là module ít người biết, thêm vào thư viện chuẩn Ruby từ 1.9.x. Cho phép tương tác trực tiếp với C library từ Ruby. Thậm chí cho inspect và alter Ruby interpreter khi chạy.

Hoạt động bằng cách wrap `libffi`, C library phổ biến cho phép code ngôn ngữ này gọi method ngôn ngữ khác. Chưa nghe? "ffi" = "foreign function interface". Không chỉ giới hạn C. Học Fiddle, dùng thư viện Rust và ngôn ngữ khác support FFI.

Xem Fiddle. Bắt đầu ví dụ đơn giản, kết thúc bằng truy cập Arduino qua serial port. Không cần biết nhiều C. Hứa. :)

Ví dụ đơn giản

Ruby thuần luôn phải define method trước khi call. Fiddle cũng vậy. Không call C function trực tiếp. Thay vào đó tạo wrapper cho C function, rồi call wrapper.

Ví dụ dưới wrap hàm logarithm của C. Basicaly duplicate `Math.log` của Ruby.


require 'fiddle'
# "Open" library, báo Fiddle vị trí trên disk. Ví dụ work trên OSX Yosemite.
libm = Fiddle.dlopen('/usr/lib/libSystem.dylib')
# Tạo wrapper cho C function "log".
log = Fiddle::Function.new(
  libm['log'],           # Lấy function từ math library đã open
  [Fiddle::TYPE_DOUBLE], # 1 argument, double, tương tự Float Ruby
  Fiddle::TYPE_DOUBLE    # Trả về double
)
# Gọi C function qua wrapper
puts log.call(3.14159)

Làm đẹp hơn

Ví dụ trước work nhưng verbose. Tưởng tượng clutter nếu wrap 100 function. Đó là lý do Fiddle cung cấp DSL đẹp. Exposed qua mixin `Fiddle::Importer`.

Dùng mixin này tạo module đầy external function nhanh. Ví dụ dưới tạo module chứa vài method logarithm.


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

# Gọi external function như Ruby method!
puts Logs.log(10)   # 2.302585092994046
puts Logs.log10(10) # 1.0
puts Logs.log2(10)  # 3.321928094887362

Điều khiển Serial Port

OK, cuối cùng mua Arduino suy nghĩ سنوات. Gửi text "hello world" đến máy tính mỗi giây qua serial port.

Sẽ hay nếu đọc data đó bằng Ruby. Thực tế đọc serial port bằng Ruby standard IO method được. Nhưng IO method không cho config serial input với granularity cần thiết cho hardcore hardware hacker.

May mắn C standard library cung cấp tất cả function cần thiết cho serial port. Với Fiddle, truy cập C function này trong Ruby.

Tất nhiên đã có gem làm việc này. Làm code này tôi tham khảo chút từ gem `rubyserial`. Nhưng mục tiêu: học cách tự làm.

Xem termios.h

Trong C, file kết thúc `.h` là header file. Chứa list function và constant library expose cho third-party code (code chúng ta). Bước đầu wrap C library: tìm file, mở, xem quanh.

Library là `termios`. Trên OSX Yosemite, header tại `/usr/include/sys/termios.h`. Linux ở chỗ khác.

`termios.h` trông như sau. Condensed khá nhiều cho clarity.


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

3 điều quan trọng: typedefs, data structure hold config connection, methods sẽ dùng.

Dùng Fiddle importer, gần như copy verbatim các section vào Ruby. Làm từng phần.

Type aliases và typedefs

C thường tạo alias cho data type. Ví dụ `termios.h` tạo `speed_t` là long integer. Fiddle importer handle qua `typealias`.


# tương đương `typedef unsigned long tcflag_t;`
typealias "tcflag_t", "unsigned long"

Structs

C không có class/module. Muốn group variable, dùng struct. Fiddle cung cấp cơ chế đẹp làm struct trong Ruby.

Bước 1: define struct trong Fiddle importer. Gần như copy-paste từ header file.


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ờ tạo "instance" struct bằng `malloc`. Set/get giá trị như Ruby class instance bình thường.


s = Termios.malloc
s.c_iflag = 12345

Tổng hợp lại

Kết hợp typedefs, structs với function demo trước, build wrapper hoạt động của termios library.

Wrapper chỉ chứa method cần config serial port. Dùng Ruby thuần cho phần còn lại.


require 'fiddle'
require 'fiddle/import'

# Mọi thứ trong module này copy trực tiếp từ termios.h.
# Yosemite: /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"

  # Struct hold config data. Instantiate: `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 cho serial device
  extern 'int tcgetattr(int, struct termios*)' # get config serial device
  extern 'int tcsetattr(int, int, struct termios*)' # set config serial device
  extern 'int tcflush(int, int)' # flush all buffers device
end

Sử dụng thư viện

Ví dụ dưới mở serial device trong Ruby, lấy file descriptor. Dùng termios function config device để đọc. Rồi đọc.

Dùng magic number ở đây. Kết quả combine bitwise config options cần thiết cho Arduino. Tò mò nguồn magic number, xem bài viết này.


file = open("/dev/cu.wchusbserial1450", 'r')
fd = file.to_i

# Tạo instance config struct
config = Serial::Termios.malloc

# Load default config options vào struct
Serial.tcgetattr(fd, config);

# Set config options quan trọng cho arduino
config.c_ispeed = 9600
config.c_ospeed = 9600
config.c_cflag = 51968
config.c_iflag = 0
config.c_oflag = 0

# Chờ 12 char trước khi read return
config.c_cc[17] = 12
# Không minimum time chờ trước khi read return
config.c_cc[16] = 0

# Lưu config
Serial.tcsetattr(fd, 0, config)

# Đợi 1s Arduino reboot
sleep(1)

# Xóa serial input đang chờ đọc
Serial.tcflush(fd, 1)

buffer = file.read(12)
puts "#{ buffer.size } bytes: #{ buffer }"
file.close

Serial reader trong hành động

Nạp Arduino chương trình in hello world liên tục, dùng Ruby script đọc.

Truy cập bất kỳ C Library nào từ Ruby với Fiddle: Mở khóa khả năng ẩn của Ruby Chương trình Arduino chỉ viết "hello world" vào serial, lặp lại.

Chạy serial monitor:

Truy cập bất kỳ C Library nào từ Ruby với Fiddle: Mở khóa khả năng ẩn của Ruby