11/08/2018, 21:11

Ruby Internal - Code Ruby của bạn được thực thi như thế nào - Compilation (Phần 3)

Về phần trước Đây là một bài nằm trong chuỗi #hardcore của nhóm Ruby Vietnam Xem phần 2 tại đây Xem phần 1 tại đây Ruby Compilation Như mình đã giới thiệu ở phần 1, Ruby compile code Ruby thành bytecode, còn gọi là YARV (Yet Another Ruby VM) instructions, và được thực thi ở YARV. Ở ...

Về phần trước

Đây là một bài nằm trong chuỗi #hardcore của nhóm Ruby Vietnam

Xem phần 2 tại đây
Xem phần 1 tại đây

Ruby Compilation

Như mình đã giới thiệu ở phần 1, Ruby compile code Ruby thành bytecode, còn gọi là YARV (Yet Another Ruby VM) instructions, và được thực thi ở YARV. Ở phần này chúng ta sẽ tìm hiểu quá trình compile đó diễn ra như thế nào?

Cấu trúc của YARV instruction

YARV bản chất là một Stack-oriented VM, nói một cách khác YARV nó sử dụng một value stack (chứa các args và giá trị trả về) để thực thi "YARV instruction" (từ đây mình sẽ gọi là instruction cho nhanh), nên các instruction được xây dựng tận dụng stack này (push giá trị tính được vào stack hoặc pop giá trị cần tìm ra khỏi stack).

Ruby là một ngôn ngữ OOP hoàn toàn, tất cả các lệnh gọi đều có dạng receiver.method(arguments), vì thế instructions được sinh ra luôn đi theo nguyên tắc sau.

  1. Đẩy receiver vào stack.
  2. Đẩy arguments vào stack.
  3. Đẩy method vào stack.

Ví dụ với lệnh 5 + 4, YARV instructions được sinh ra như sau:

putobject   5
putobject   4
opt_plus    <callinfo!mid:+, argc:1, ARGS_SKIP>

ARGS_SKIP ở đây nhằm giúp YARV biết được tham số được truyền vào là những giá trị đơn giản (không phải là block hay một array các tham số).

Và với lệnh puts 9, YARV instructions được sinh ra như sau:

putself
putobject           9
opt_send_simple     <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>

NOTE: Như ta đã biết puts là một lệnh từ module Kernel mà bất kì một Ruby class nào cũng include, nên receiver ở đây được hiểu là self, đó là lý đó vì sao lệnh putself được đưa vào vị trí receiver ở trên.

Để xuất YARV instruction như trên bạn có thể dùng lệnh

RubyVM::InstructionSequence.compile("your-ruby-code-goes-here").diasam

Local Table

Khi compiler chạy, thông tin về các biến, tham số được Ruby lưu ở một nơi khác gọi là Local Table.

Giá sử ta có một đoạn code Ruby sau:

a = "Viet"
b = "Nam"
puts a + b

Để sử dụng local table Ruby dùng hai lệnh setlocal (dùng để gán) và getlocal (dùng để tham chiếu)

Với đoạn code trên đầu tiên, áp dụng kiến thức ở phần trên và phần này, ta có instruction như sau.

# YARV Instructions                                 # Local Table

putstring   "Viet"
setlocal    3                                       [3] a
# Tương tự Với b
putstring   "Nam"
setlocal    2                                       [2] b
putself
getlocal    3
getlocal    2
send        :+
send        :puts
leave

Scoping

Ở 2 mục trên ta đã tìm hiểu các YARV instruction đơn giản và local table, ở mục này ta sẽ tìm hiểu compiler sẽ làm gì với các lệnh Ruby bao gồm scope (như block với tham số).

10.times do |n|
  puts n
end

Cách Ruby làm là nó sẽ chia đoạn code trên thành 2 block khác nhau (với 2 local table khác nhau), mình tạm gọi là outer block gồm 10.times { ... } và inner block gồm { |n| puts n }

# YARV Instructions                                                     # Local Table
putobject   10
send        <callinfo!mid:times, argc:0, block:block in <compiled>>
# Send đến method :times, không tham số với block (inner block bên dưới)
# YARV Instructions                             # Local Table
#                                               [2] n<Arg>
# putself
# getlocal  2
# send      :puts

<Arg> ở trên để đánh dấu cho YARV biết rằng n là một tham số thông thường.

Ngoài ra còn một số kí hiệu cho các loại tham số khác như:

  • <Arg>: như trên, dành cho tham số thông thường. Ví dụ: def foo(a)
  • <Rest>: dành cho các tham số dạng argument (splat *). Ví dụdef foo(*args)
  • <Post>: dành cho các tham số sau splat arguments. Ví dụ: def foo(*args, a)
  • <Block>: dành cho các tham số block được truyền vào bằng kí hiệu &. Ví dụ def foo(&block)
  • <Opt=i>: dành cho các tham số có giá trị mặc định. Ví dụ def foo(a=1)

Kết luận

Còn khá nhiều điều khá thú vị về các lệnh bytecode của Ruby mà một bài viết như thế này không đủ để cover hết, các bạn có thể tìm hiểu thêm tại trang chủ của YARV nhé.

Các bạn có thể tham gia thảo luận về bài viết ở bên dưới hoặc thông qua chat group của nhóm Ruby Vietnam.

0